TensorFlow.js 的“可训练模型”这几个字,我这些年反复跟人解释过很多次。很多人一听到“前端跑模型”就觉得是调用现成的推理接口,实际上真正有价值的是在浏览器里从零训练或者微调一个模型,让模型根据你的数据、你的场景长出适合你的能力。这篇文章我会把 5 类最核心的可训练模型逐一拆开,从线性回归到迁移学习,每类都配一个能直接跑的示例,参数怎么调、哪里容易踩坑都会交代清楚。适合刚接触前端机器学习的人,也适合已经会调用接口、想进一步把手头数据变成页面模型的人。
1. 先理清楚:这5类模型分别解决什么问题
1.1 可训练模型到底“可训练”在哪里
可训练模型的关键在于模型的权重参数是随着训练数据不断迭代更新的。TensorFlow.js 中,model.fit()这个方法跑起来之后,模型内部的权重会根据损失函数算出的梯度一步步调整。你不需要手动写任何反向传播代码,框架已经把梯度计算、权重更新、动量调节这些底层细节都封装好了。
对比一下,如果你只是加载一个已训练好的模型做推理,模型的权重是固定的,输入数据经过网络算出结果,这个过程叫“前向传播”。而可训练模型多了一个“反向传播”的过程:预测结果和真实标签之间的差异被计算出来,梯度从输出层逐层回传,每一层的权重按优化器规则更新。理解这个差异很重要,因为后面 5 类模型本质上都是同一套训练逻辑,区别只在网络结构和数据形态。
1.2 5类核心模型的选型地图
下面这张表是我在实际项目中常用的选型参考,可以帮你快速判断手上的任务该用哪类模型:
| 模型类型 | 典型任务 | 数据结构 | 适合场景 | 浏览器端表现 |
|---|---|---|---|---|
| 线性回归 | 数值预测 | 一个或多个数值特征 | 趋势预测、价格估算、参数拟合 | 极快,动画级实时更新 |
| 逻辑回归 | 二分类 | 数值特征 | 分类判断、风险评分、漏斗筛选 | 极快,适合决策边界可视化 |
| 多层感知机 | 多分类/回归 | 结构化特征 | 表格数据分类、用户分群、特征交叉 | 很快,小模型秒级训练 |
| 卷积神经网络 | 图像分类/检测 | 图片像素张量 | 手写识别、物体分类、视觉质检 | 中等,小网络可在移动端跑 |
| 迁移学习 | 图像/文本/音频定制 | 预训练模型特征 | 个性化分类、教学演示、小样本适配 | 快,因为只训练最后一层 |
这5类模型呈递进关系。线性回归是一切的基础,逻辑回归把回归思想扩展到分类问题,多层感知机引入隐藏层和激活函数,卷积神经网络处理二维图像结构,迁移学习则站在预训练模型的肩膀上解决小样本问题。你把这5类吃透,市面上 90% 的前端 AI 需求都能找到对应方案。
2. 动手前必须搞定:环境引入和数据规范
2.1 三种引入TensorFlow.js的方式怎么选
我见过不少人在环境这块浪费了时间。引入 TensorFlow.js 有三种常见方式,各有利弊。
第一种是 script 标签直接引入,适合原型验证和单页面 Demo。写完 HTML 就能跑,不需要任何构建工具,浏览器全局会挂一个tf对象。缺点是模块化管理不方便,代码多了容易乱。
第二种是 npm 安装,在工程化项目里用import * as tf from '@tensorflow/tfjs'引入。这是生产项目最推荐的方式,配合 webpack、vite 或者任何现代打包工具都能正常使用,而且可以做 tree shaking 减小体积。缺点是需要熟悉前端工程化流程。
第三种是在线代码编辑器里直接跑,适合快速试错和分享。初次学习建议先用这种方式跑通,再切到工程化环境。
我个人的建议是:别在环境上纠结太久。只要核心 API 能跑起来,后面所有示例代码都是通用的。初始化完成后,建议先执行一次console.log(tf.getBackend()),确认后端是什么。WebGL 后端通常比 CPU 后端快 10 倍以上,尤其是训练卷积神经网络的时候,后端选错会严重影响速度。
2.2 数据归一化和张量化的标准流程
TensorFlow.js 处理的数据必须是张量,最常见的坑是把普通 JavaScript 数组直接传给模型。数据进入模型之前,必须完成两个标准化操作:转成张量、进行归一化。
转张量这一步很简单,tf.tensor2d(data, [shape])就能把二维数组转成张量。但 shape 写错是高频问题,报错信息里的[undefined undefined]大多数时候就是 shape 和实际数据对不上。
归一化这一步很多初学者会忽略。假设特征范围是 0 到 1000,模型初始权重接近 0,计算梯度的时候数值几乎全部溢出,loss 直接变成 NaN。归一化最常用的方法是 min-max 归一化:将每个特征值减去最小值、除以最大值减最小值,把所有数据压缩到 0 到 1 之间。
// 归一化示例 function normalize(tensor) { const min = tensor.min(); const max = tensor.max(); const range = max.sub(min); return { data: tensor.sub(min).div(range), min, range }; }这里要特别注意,训练时用到的min和range必须保存下来,预测的时候要用同一组参数做反归一化,否则预测结果没有任何意义。我踩过好几次这个坑,训练得很漂亮的模型,预测出来的数字大到离谱,就是因为预测阶段的输入没有归一化,或者输出没有反归一化。这是初学者最容易犯的错误之一,所以在数据处理环节单独强调。
3. 五类核心模型实操示例逐项拆解
3.1 线性回归:最精悍的可训练模型模板
线性回归是所有神经网络的基础形态。一个不带激活函数的全连接层,本质上就是一个线性回归模型。它的训练流程足够简单,非常适合作为理解model.fit()的入门案例。
下面这个例子生成一组带噪声的线性数据,然后用 TensorFlow.js 训练出回归系数。
import * as tf from '@tensorflow/tfjs'; // 生成 100 个样本,x 范围 0~2 const xs = tf.tensor2d( Array.from({ length: 100 }, (_, i) => i / 50), [100, 1] ); // y = 2x + 1 + 噪声 const ys = xs.mul(2).add(1).add(tf.randomNormal([100, 1], 0, 0.1)); // 构建模型:单个全连接层,无激活函数 const model = tf.sequential(); model.add(tf.layers.dense({ units: 1, inputShape: [1] })); model.compile({ optimizer: 'sgd', loss: 'meanSquaredError' }); await model.fit(xs, ys, { epochs: 100, batchSize: 32, callbacks: { onEpochEnd: (epoch, logs) => { if (epoch % 20 === 0) { console.log(`epoch ${epoch}: loss = ${logs.loss}`); } } } }); // 预测 x=3 时 y 的值 const pred = model.predict(tf.tensor2d([[3]], [1, 1])); pred.print();这段代码有几个关键点值得展开。
units: 1表示输出维度是 1,因为我们要预测单个数值。inputShape: [1]表示输入是一个维度为 1 的向量,注意这里不要写成[null, 1],fit时的输入张量会自动补上 batch 维度。
meanSquaredError是回归任务最常用的损失函数。它计算预测值与真实值差值的平方均值,特点是差值大的样本产生远大于差值小的样本的惩罚,所以模型会优先优化那些错误幅度大的样本。这就好比考试扣分,错得离谱的题扣分多,模型就会花更多力气把明显离谱的预测修正过来。
optimizer: 'sgd'是随机梯度下降。在这个简单的任务里,SGD 表现很好,而且参数少、训练快。如果训练时发现 loss 下降缓慢,可以把 optimizer 换成'adam',Adam 优化器自带自适应学习率,在绝大多数场景下都比 SGD 更容易收敛。
训练完成后,打印出来的第 1 个 epoch 的 loss 如果大于 10,不用慌,这是正常现象,因为初始权重是随机值。关键是看 loss 是否持续下降,以及最终是否稳定在一个较小的值。
3.2 逻辑回归:从“回归数值”到“判断类别”的跨越
线性回归输出的是连续数值,而很多业务场景需要的是“是或否”的判断。逻辑回归通过一个 sigmoid 激活函数,把线性输出的任意实数压缩到 0 到 1 之间,然后以 0.5 为界限划分类别。
下面这个示例生成两个中心点不同的二维高斯分布数据,分别代表 A 类和 B 类,然后训练一个逻辑回归模型完成分类。
import * as tf from '@tensorflow/tfjs'; // 生成两类数据:一类中心在 (2,2),一类中心在 (4,4) const classA = tf.randomNormal([50, 2]).add(tf.tensor2d([[2, 2]])); const classB = tf.randomNormal([50, 2]).add(tf.tensor2d([[4, 4]])); const data = tf.concat([classA, classB], 0); // 标签:A 类为 0,B 类为 1 const labels = tf.tensor1d([ ...Array.from({ length: 50 }, () => 0), ...Array.from({ length: 50 }, () => 1) ]); const model = tf.sequential(); model.add(tf.layers.dense({ units: 1, inputShape: [2], activation: 'sigmoid' })); model.compile({ optimizer: 'adam', loss: 'binaryCrossentropy', metrics: ['accuracy'] }); await model.fit(data, labels, { epochs: 100, batchSize: 16, validationSplit: 0.2 });逻辑回归的损失函数换成了binaryCrossentropy,这是二分类问题的标准选择。交叉熵衡量的是预测概率分布和真实类别之间的差异,它比均方误差更适合分类任务。你可以这样理解:均方误差关注预测值和真实值的“距离”,交叉熵关注预测概率的“信心”是否正确。当模型用 90% 的概率预测正确类别时,交叉熵损失很小;当模型用 90% 的概率预测错误类别时,损失会非常大。
metrics: ['accuracy']是训练时额外计算准确率,这是分类任务非常常用的指标,能直观看到训练过程中的表现变化。validationSplit: 0.2表示从训练数据中抽出 20% 用作验证集,验证集不参与权重更新。这样可以观察模型在未见过的数据上的表现,避免只知道训练集上的成绩。
sigmoid 输出的预测结果是一个概率值,比如 0.82。业务判断的时候可以直接拿这个概率做阈值控制,比如 0.8 以上才判为 A,中间段落入待人工确认。这是逻辑回归在实际应用里很好用的一个点,它不只是给一个类别判断,还能给出置信度。
3.3 多层感知机:处理多分类和复杂特征交叉
当数据不是线性可分的时候,逻辑回归就不够用了。多层感知机通过引入隐藏层和非线性激活函数,可以学习特征之间的复杂组合关系。这里我用一个三分类的二维数据来演示。
import * as tf from '@tensorflow/tfjs'; // 生成三簇数据 const groupA = tf.randomNormal([30, 2]).add(tf.tensor2d([[0, 0]])); const groupB = tf.randomNormal([30, 2]).add(tf.tensor2d([[3, 0]])); const groupC = tf.randomNormal([30, 2]).add(tf.tensor2d([[0, 3]])); const data = tf.concat([groupA, groupB, groupC], 0); // 三类标签,使用 one-hot 编码 const labels = tf.tensor2d([ ...Array.from({ length: 30 }, () => [1, 0, 0]), ...Array.from({ length: 30 }, () => [0, 1, 0]), ...Array.from({ length: 30 }, () => [0, 0, 1]) ]); const model = tf.sequential(); model.add(tf.layers.dense({ units: 8, inputShape: [2], activation: 'relu' })); model.add(tf.layers.dense({ units: 3, activation: 'softmax' })); model.compile({ optimizer: 'adam', loss: 'categoricalCrossentropy', metrics: ['accuracy'] }); await model.fit(data, labels, { epochs: 200, batchSize: 16, validationSplit: 0.2, callbacks: { onEpochEnd: (epoch, logs) => { if (epoch % 50 === 0) { console.log(`epoch ${epoch}: loss=${logs.loss.toFixed(4)} acc=${logs.acc.toFixed(4)}`); } } } });多分类和二分类有几个重要区别。
第一,输出层维度从 1 变成 3,对应三个类别。第二,输出层激活函数换成 softmax,它把网络的输出转换成一组和为 1 的概率,可以当作“属于各个类别的置信度”。第三,标签必须使用 one-hot 编码,也就是用一个长度为类别数的向量表示类别,属于第几类就在第几位写 1,其余写 0。这也是为什么标签用[1,0,0]的形式而不是直接用数字 0、1、2。第四,损失函数换成categoricalCrossentropy,对应多分类交叉熵。
隐藏层选 8 个神经元是一个比较保守的选择。对于两个特征的简单数据,8 个神经元足够表达决策边界了。隐藏层神经元不是越多越好,神经元太多容易过拟合,模型把训练数据的特征学得太彻底,反而泛化能力下降。可以这样记:模型容量要和任务复杂度匹配,小任务不需要大网络。
这里我用relu激活函数。ReLU 已经是隐藏层激活函数的默认选择,因为它的梯度计算简单,在正向区梯度恒为 1,不容易出现“梯度消失”问题,收敛速度通常优于 sigmoid 或者 tanh。
3.4 卷积神经网络:从结构化数据到图像数据的跨越
假设你拿到一批手写数字图片,每张是 28×28 像素的灰度图。如果用多层感知机处理,需要先把图片展平成 784 维向量,这会丢掉二维空间结构信息——相邻像素的关联关系消失了。卷积神经网络用卷积核在图片上滑动,能保留局部空间特征。
下面是一个典型的卷积网络结构:
import * as tf from '@tensorflow/tfjs'; const model = tf.sequential(); model.add(tf.layers.conv2d({ filters: 8, kernelSize: 5, inputShape: [28, 28, 1], activation: 'relu' })); model.add(tf.layers.maxPooling2d({ poolSize: [2, 2] })); model.add(tf.layers.conv2d({ filters: 16, kernelSize: 5, activation: 'relu' })); model.add(tf.layers.maxPooling2d({ poolSize: [2, 2] })); model.add(tf.layers.flatten()); model.add(tf.layers.dense({ units: 10, activation: 'softmax' })); model.compile({ optimizer: 'adam', loss: 'categoricalCrossentropy', metrics: ['accuracy'] }); // 输入数据格式:[样本数, 高度, 宽度, 通道数] // 灰度图通道数为 1,RGB 图为 3这个网络结构是经典的卷积+池化堆叠模式。第一层卷积用 8 个卷积核,每个卷积核尺寸 5×5,输出 8 张特征图。inputShape必须写成[28, 28, 1],这里的顺序是高度、宽度、通道数,和很多后端框架不同,初学者经常在这个位置踩坑。如果用的是 RGB 图片,通道数是 3。
maxPooling2d的作用是下采样。poolSize: [2, 2]表示每 2×2 的区域取最大值,把特征图的尺寸减半。这样做既减少了计算量,又让模型对位置的微小偏移有一定容忍能力。两个池化层把 28×28 的输入逐步降维,最终经过 flatten 展开成一维向量,再交给全连接层做分类。
在实际训练中,图像数据量通常比较大,一个 epoch 可能要几秒甚至更久。建议先用少量数据跑通流程,确认网络结构正确、loss 确实在下降,再加大训练轮数。另外,图像数据的归一化尤其重要,像素值 0 到 255 直接喂给网络,初始 loss 很容易爆炸。归一化到 0 到 1 区间,训练会稳定很多。
卷积核的尺寸和数量是一个需要经验积累的调参项。小卷积核(比如 3×3)在现代网络中更流行,因为两层 3×3 堆叠的感受野等同于一层 5×5,但参数更少、非线性更强。但我这个示例用了 5×5,是因为在 28×28 这样的小图上效果足够,网络也更简单。
3.5 迁移学习:小数据量图片分类的最优解
从头训练一个图像分类网络需要大量数据和较长训练时间,在浏览器里很不现实。迁移学习思路是:用已经训练好的大规模视觉模型提取通用特征,然后只训练一个新的分类层。
以常见的视觉特征提取器为例,思路是这样的:
// 加载预训练的特征提取模型 // 输入图片经过前向传播,得到特征向量 const featureModel = await tf.loadLayersModel(MODEL_URL); featureModel.trainable = false; // 冻结所有层 // 用特征向量构造新的分类器 const classifier = tf.sequential(); classifier.add(tf.layers.inputLayer({ inputShape: [FEATURE_DIM] })); classifier.add(tf.layers.dense({ units: 32, activation: 'relu' })); classifier.add(tf.layers.dense({ units: CLASS_COUNT, activation: 'softmax' })); classifier.compile({ optimizer: 'adam', loss: 'categoricalCrossentropy', metrics: ['accuracy'] }); // 先对输入图片调用特征提取器的 infer 得到特征 // 再把特征喂给 classifier 训练 await classifier.fit(features, labels);这段代码展示了核心思路:预训练模型负责把图片编码成向量,新分类层负责从向量映射到你的自定义类别。因为特征提取器所有的权重都是冻结状态,反向传播只会更新 classifier 那几十个权重参数,训练量比从头训练整个网络小了至少两个数量级。
实际操作中可以更进一步,把特征提取和分类器组合成一个完整的模型。先添加输入层,再手动把特征提取器的前若干层加进去,追加自定义的分类层。要注意的是,设置trainable = false之后,一定要重新compile(),否则设置不会生效。这是一个很容易忽略的坑。
迁移学习在浏览器端特别实用,原因有三点:样本需求少,每类只需要几十张图片就能达到不错的效果;训练时间短,几秒到几十秒就完成;可以做增量学习,随时往模型里加入新类别的样本,重新训练一下分类层就能更新能力。
很多带摄像头的交互应用,就是靠迁移学习在浏览器里直接完成模型训练的。用户体验是:打开页面,对着摄像头拍几组样例,点一下训练,模型马上能用。这种即时反馈体验,服务端训练很难做到。
4. 训练过程的调参思路与性能优化心得
4.1 三个核心超参数:学习率、批次大小、训练轮数
学习率也许是全部超参数中最值得先调的。学习率决定每一步权重更新迈多大的步子。步子太大,权重在最优解附近反复震荡甚至发散,loss 曲线表现为剧烈抖动不下降;步子太小,训练进展缓慢,几百个 epoch 后 loss 还挂在高位。
TensorFlow.js 里指定学习率的方式是传入优化器实例:
model.compile({ optimizer: tf.train.sgd(0.05), // 显式指定学习率 loss: 'meanSquaredError' });简单任务从 0.01 或 0.05 开始试,复杂任务从 0.001 开始试。如果 loss 变成 NaN,最快的排查方式就是把学习率调低一个数量级。
batchSize 指每次权重更新使用的样本数。批量大小影响训练速度和稳定性。batchSize 太小,梯度噪音大,loss 曲线不光滑;batchSize 太大,单次迭代计算时间长,收敛轮数可能变多。常见的做法是取 16、32、64 这样的 2 的幂。结构化数据模型训练时 16 或 32 是不错起点。
epochs 代表完整遍历训练数据的次数。不要一开始就盲目设几千。我的习惯是先用 100 个 epoch 观察 loss 下降趋势,当 loss 下降变得非常缓慢时,再增量增加 epoch。用 callbacks 里的onEpochEnd打印每个 epoch 的 loss,比训练完再看结果要直观得多。
4.2 过拟合:新手最容易忽视的拦路虎
模型在训练集上表现很好,验证集上却一塌糊涂,这就是过拟合。典型表现是训练 loss 持续下降,验证 loss 反而上升。
一个最简单的检测方法是:训练时设置validationSplit: 0.2,每个 epoch 结束后同时打印训练和验证两个 loss。如果两条曲线逐渐分离,而且分离程度越来越大,基本可以判定过拟合在发生。
应对过拟合有几种常用手段。
增加数据量是治本的方法。图像任务中做数据增强,随机翻转、旋转、缩放图片,相当于免费扩充了数据集。结构化数据很难做增强,可以尝试添加轻微噪声。
降低模型容量。把隐藏层神经元数量减半,或者去掉一层,让模型没有那么多参数去“死记硬背”训练数据。
加入 Dropout 层。Dropout 在训练时随机让一部分神经元不工作,强迫网络学习到更鲁棒的特征。TensorFlow.js 中可以用tf.layers.dropout({ rate: 0.5 })。注意 Dropout 层只应在训练阶段生效,TensorFlow.js 里预测时会自动关闭 Dropout,不需要手动处理。
早停法是实用技巧。当验证 loss 连续多个 epoch 不再改善时,停止训练并回退到最佳验证 loss 对应的模型,而不是等所有 epoch 跑完。TensorFlow.js 里可以自己在 callbacks 中记录最优状态,顺便把模型保存下来。
4.3 浏览器端的性能三大件:后端、内存、线程
浏览器环境比服务端多了几个特殊的性能瓶颈。
WebGL 后端的利用率直接影响训练速度。TensorFlow.js 在浏览器里默认尝试 WebGL 后端,如果执行环境不支持,自动回退到 CPU 后端。CPU 上训一个小型卷积网络可能都要等很久,体验极差。写代码时可以用await tf.setBackend('webgl')主动设置。设置失败时打印警告,提示用户使用支持 WebGL 的浏览器才能获得更好性能。
内存泄漏是前端训练模型最容易出现的隐藏问题。每次tf.tensor、model.predict都会产生新的张量占用显存,如果不及时释放,页面用着用着就变卡,最终崩溃。
最稳的写法是在代码里手动释放不再使用的张量:tensor.dispose()。批量操作时用tf.tidy()包裹,里面创建的所有中间张量都会自动释放:
const result = tf.tidy(() => { const a = tf.tensor2d([1, 2, 3, 4], [2, 2]); const b = a.square(); return b.sum(); // 返回值会保留,中间张量自动释放 });训练循环中尤其注意,每个 epoch 产生的中间结果都在积累,如果发现显存占用不断增加,优先排查是否忘了 dispose。
主线程阻塞是另一个体验杀手。训练过程是 CPU 密集和 GPU 密集任务的混合,如果所有计算都放在浏览器主线程,页面会直接卡住,连滚动都费劲。常见方案是把训练放到 Web Worker 中执行,主线程只负责接收训练进度回调。TensorFlow.js 的官方迁移学习示例就是这样的架构。不过 Worker 中用 WebGL 后端需要额外配置,CPU 后端相对简单,需要酌情取舍。
模型体积和加载速度也不容忽视。model.save('downloads://my-model')保存出来的模型,一个简单多层感知机可能几百 KB,大型模型动辄几十 MB。浏览器端场景建议优先选择小模型或使用量化压缩。
4.4 训练小模型时的浏览器端内存与WebGL限制
我们在 4.3 里提到 WebGL 后端快,但它也有明显限制。浏览器对 WebGL 纹理尺寸有上限,超出纹理最大值的数据没法塞进一层纹理。具体表现为训练时抛出类似“Texture size limit exceeded”的错误。
解决思路有两种。第一种是缩小输入尺寸,比如把图片从 224×224 缩到 96×96,大部分情况下对精度影响可控。第二种是减少 batchSize,让每次进入 GPU 的数据量变小。我自己实测下来,一张普通消费级显卡的浏览器环境,WebGL 纹理上限通常能撑住 256×256×3 的输入,但超过之后就很容易报错。
另外,浏览器里的 WebGL 上下文是共享资源,同页面开多个 TensorFlow.js 实例会互相争抢。如果页面里同时跑多个模型,建议把推理和训练错开,或者复用一个模型实例。
5. 常见问题与排查技巧实录
5.1 高频报错排查速查表
我整理的这些问题,每一个都是我在实战中亲自踩过或者帮别人排查过的。按频率排序:
| 现象 | 最常见原因 | 解决思路 |
|---|---|---|
| loss 一直是 NaN | 学习率过大 / 数据未归一化 / 数据含 NaN | 降低学习率,检查数据,先归一化再训练 |
| shape 报错,提示维度对不上 | inputShape 与实际张量 shape 不一致 | 打印张量.shape核对,注意图片的 [高, 宽, 通道] 顺序 |
| 训练速度极慢 | 后端错误、数据集过大、batchSize 过大 | 确认tf.getBackend(),减小 batchSize |
| 验证 loss 持续上升 | 过拟合 | 增加数据/增强、加 Dropout、减少模型容量、早停 |
| 预测结果全是同一个值 | 数据泄漏导致训练和预测分布不一致 | 预测阶段做同样的归一化,检查数据边界 |
| 页面内存持续增长 | 张量没有 dispose | 用tf.tidy包裹、手动 dispose、检查训练循环 |
| 训练中途浏览器崩溃 | WebGL 纹理超限 | 缩小输入尺寸,减小 batchSize |
5.2 训练结果不理想的定位思路
结果不好,先别急着调模型结构,按照顺序排查更高效。
第一步看数据。把训练数据直接可视化出来,如果是分类任务,画个散点图看类别是否明显可分。数据本身就是混成一团,换再大的网络也白搭。第二步看 loss 曲线。如果 loss 没有下降,先调学习率,通常是把学习率调低一个数量级再试。第三步看验证 loss。训练 loss 低而验证 loss 高,才进入过拟合的处理流程。
我见过很多人在模型结构上反复尝试,其实是数据出了问题。数据质量永远优先于模型复杂度。一个很小的网络在干净数据上效果,往往好过一个很大的网络在脏数据上。
5.3 模型保存与加载的三种方式
训练好的模型要保存下来,不然页面一刷新就全部归零。TensorFlow.js 提供三种保存目标。
// 保存到浏览器本地下载 await model.save('downloads://my-model'); // 保存到浏览器 IndexedDB await model.save('indexeddb://my-model'); // 保存到服务器 await model.save('http://my-server.com/save-model');downloads://会触发浏览器下载两个文件:模型结构和权重文件。indexeddb://适合把模型存在用户本地,下次访问直接加载,离线也能用。保存到服务器则适合把模型分享给其他用户。
加载时对应使用:
const model = await tf.loadLayersModel('indexeddb://my-model');有两个坑需要提醒。第一,IndexedDB 是浏览器隔离的,不同域名无法互相访问模型。第二,如果页面被清理浏览器缓存,IndexedDB 里的模型也可能被清除。长期保存重要模型还是应该下载到本地备份。
结尾
我实际用 TensorFlow.js 做模型的体会是,它的价值并不在于取代服务端训练,而在于把模型训练、评估、调试、部署全部塞进同一个交互环境。前端开发者不需要额外搭建训练环境,就能把手上的数据快速变成可交互的模型。如果你正在学习阶段,建议先把线性回归那个示例完整跑通,重点理解model.fit()和model.predict()这两个核心方法,以及张量生命周期管理。这 5 类模型之间是阶梯式递进的,每突破一类,你对“模型是怎么学习”的理解就更深一层。最后再分享一个小技巧:每次训练完模型,第一时间执行model.save()保存一份。我最初练习的时候吃过好几次亏,辛苦调好的参数因为忘了保存,页面一关全部回到原点。养成保存的习惯,你的每一次实验都不会白费。