ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

在浏览器中训练 MNIST:tfjs-examples/mnist 手写数字识别示例源码解析

在浏览器中训练 MNIST:tfjs-examples/mnist 手写数字识别示例源码解析
  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

项目地址:https://gitcode.com/gh_mirrors/tf/tfjs-examples
点击查看免费下载

本文围绕 TensorFlow.js 官方示例仓库 tfjs-examples 中的 mnist 示例展开,深入讲解如何使用 TensorFlow.js 的 Layers API 在浏览器中完成 MNIST 手写数字识别模型的训练、评估与推理。读完本文,你将掌握基于tf.sequential()搭建卷积神经网络(ConvNet)与全连接网络(DenseNet)的完整流程、MNIST 雪碧图数据集的加载与张量化方法,以及利用model.fit()回调在训练过程中实时绘制 loss/accuracy 曲线的实战技巧。

示例概览:一个浏览器端的数字识别训练器

本示例的核心目标是:使用 TensorFlow.js 的 Layers API(即tf.layers高层 API)在浏览器页面中训练 MNIST 手写数字识别模型。这是 tfjs-examples 仓库中最基础的图像分类示例之一,对应的页面标题为 "TensorFlow.js: Digit Recognizer with Layers"(见 index.html)。

按照 mnist/README.md 的说明,示例在训练过程中会每 5 步(steps)在 1000 个随机测试集样本上计算准确率,并随训练进度实时绘制 loss 与 accuracy 曲线。同时文档也给出了一条实用的工程建议:可以通过减少参与准确率评估的样本数、降低评估频率来缩短训练时间——因为测试集评估本身也会消耗计算资源。

需要注意的一点是,当前版本中整个 MNIST 数据集被预先拼合存储在一张 PNG 雪碧图(sprite image)中,data.js 负责将它解析并转换为Tensor。README 中注明这种加载方式"在不久的将来会变得更加简单",事实上仓库后续也衍生出了mnist-core(mnist-core/README.md,使用底层 tfjs-core API 而非 Layers API)等其他实现,感兴趣的读者可以对比阅读。

项目结构与运行方式

mnist/目录是一个完全独立、可拷贝到其他项目中直接使用的示例:

文件职责
index.html页面骨架:训练参数表单、训练进度区、推理结果展示区
index.js核心逻辑:模型定义、编译、训练、评估与推理
data.js数据层:MnistData类,负责加载并张量化 MNIST 数据
ui.jsUI 层:状态提示、loss/accuracy 曲线绘制、测试结果渲染
package.json依赖与脚本配置(Parcel 打包)

在 package.json 中可以看到,该示例仅依赖两个运行时包:

  • @tensorflow/tfjs(版本 2.0.0):核心库,提供tf.sequential、tf.layers等 API;
  • @tensorflow/tfjs-vis(^1.1.0):用于在页面上渲染 loss/accuracy 折线图。

运行方式同样在 package.json 的 scripts 中定义:

# 开发模式:Parcel 启动本地服务器并自动打开浏览器 yarn watch # 生产构建:输出到 dist 目录 yarn build

其中watch实际执行parcel index.html --no-hmr --open,由 Parcel 直接以 index.html 为入口完成 ES Module 的打包与热更新。

数据加载:将 MNIST 雪碧图转换为 Tensor

MNIST 数据集共有65000 个样本,其中55000 个用于训练、10000 个用于测试(对应 data.js 中的常量定义)。每张图片为28 × 28的灰度图,共10 个类别(数字 0~9)。

数据加载的核心逻辑全部封装在 data.js 的MnistData类中,其数据来源是两处远端资源:

const MNIST_IMAGES_SPRITE_PATH = 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png'; const MNIST_LABELS_PATH = 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8';
  • 雪碧图:所有 65000 张图片纵向拼接成一张大 PNG,每行 28 像素、每段 5000 张;
  • 标签文件:一个Uint8Array格式的二进制文件,每个标签为 one-hot 编码、长度 10。

load()方法的处理流程如下(data.js):

  1. 通过new Image()加载雪碧图,并设置img.crossOrigin = ''以支持跨域读取;
  2. 在内存中开辟65000 * 28 * 28 * 4字节的ArrayBuffer,按每次 5000 张的分块方式用canvas.drawImage()裁剪出对应图块,再经ctx.getImageData()读出像素数据;
  3. 由于图片是灰度图,四个通道取值相同,因此只读取红色通道即可,并将像素值除以 255 归一化到[0, 1]区间(data.js);
  4. 通过fetch()并行加载标签文件,与图片加载通过Promise.all等待完成;
  5. 最后分别slice出训练集(前 55000 个)与测试集(后 10000 个)的图片与标签。

加载完成后,getTrainData()与getTestData(numExamples)负责将扁平数组转换为张量:

  • 图片数据重塑为[numExamples, 28, 28, 1]的四维张量(tf.tensor4d),最后一维 1 表示单通道灰度;
  • 标签数据重塑为[numExamples, 10]的二维张量(tf.tensor2d),即 one-hot 编码。

getTestData()还支持传入numExamples参数,通过slice截取部分测试样本——这正是页面"推理示例"板块与训练中定期评估所依赖的能力。

从源码结构看,MnistData的设计具有很好的通用性:它把"远端数据 → 浏览器内存 → Tensor"的完整链路封装成一个可复用的数据管理类。官方注释也明确提示:如果你有自定义数据,完全可以仿照这个类来编写自己的数据加载器(index.js)。

构建模型:ConvNet 与 DenseNet 的对比

页面允许在两种模型间切换(Model Type下拉框),对应的构建函数分别是createConvModel()与createDenseModel()。

卷积网络(ConvNet)

createConvModel()使用tf.sequential()搭建了一个典型的 CNN 结构(index.js),各层配置如下:

层配置说明
Conv2DinputShape: [28, 28, 1],kernelSize: 3,filters: 16,activation: 'relu'输入层同时承担首次卷积,接收 28×28 灰度图
MaxPooling2DpoolSize: 2,strides: 2以区域最大值做下采样
Conv2DkernelSize: 3,filters: 32,activation: 'relu'加深特征提取
MaxPooling2DpoolSize: 2,strides: 2再次下采样
Conv2DkernelSize: 3,filters: 32,activation: 'relu'第三层卷积
Flatten—将二维特征图展平为一维向量
Denseunits: 64,activation: 'relu'全连接隐藏层
Denseunits: 10,activation: 'softmax'输出层,softmax 产生 10 个类别的概率分布

源码注释中对该结构的每个环节都有通俗解释:第一层卷积既充当输入层又执行第一次卷积;MaxPooling 用区域内的最大值而非平均值来做下采样;最后一层 softmax 的输出向量各元素求和为 1,可解释为概率分布。

全连接网络(DenseNet)

createDenseModel()只包含展平层与两个全连接层(index.js):

model.add(tf.layers.flatten({inputShape: [IMAGE_H, IMAGE_W, 1]})); model.add(tf.layers.dense({units: 42, activation: 'relu'})); model.add(tf.layers.dense({units: 10, activation: 'softmax'}));

源码注释明确指出:该模型拥有约31000(~31k)个参数,与上面的卷积网络参数量大致相当,但因为它没有利用图像的空间结构信息,预期训练后准确率会明显低于 ConvNet。这种"参数量相同、结构不同"的对照实验设计,是理解卷积为何有效的绝佳教材——同一页面上即可直观看到两种模型的准确率差异。

编译与训练:关键超参数解读

train()函数集中展示了模型编译与训练的全部配置(index.js),这些超参数也是 README 所述训练行为的具体落地:

model.compile({ optimizer, // 'rmsprop' loss: 'categoricalCrossentropy', // 多分类标准损失 metrics: ['accuracy'], // 人类可读的评估指标 }); const batchSize = 320; // 每次权重更新前分组的样本数 const validationSplit = 0.15; // 留出 15% 训练数据做验证 const trainEpochs = ui.getTrainEpochs(); // 从页面输入读取,默认 3
  • 优化器rmsprop:一种迭代式最小化损失函数的方法,通过在训练中逐步调整权重参数来逼近损失极小值;
  • 损失函数categoricalCrossentropy:多分类任务的标准选择,可微、适合梯度下降训练,但不易被人直观解读;
  • 指标accuracy:直接衡量"分类正确的样本占比",但它不可微、不能作为损失函数,因此单独作为评估指标;
  • batchSize = 320:源码注释给出权衡原则——过小的 batch 每次仅用极少样本更新权重、泛化能力差;过大的 batch 占用更多内存且准确率并不保证更高;
  • validationSplit = 0.15:从训练集中留出最后 15% 作为验证集,用于在训练期间监控过拟合。

训练轮数trainEpochs由 UI 读取(ui.js 中的getTrainEpochs()),页面默认值为 3(见 index.html)。

训练过程中的实时可视化回调

model.fit()通过callbacks对象接入训练事件循环(index.js),这是本示例"边训练边出图"的关键机制:

  • onBatchEnd(batch, logs):每个 batch 结束时触发。这里用totalNumBatches(由训练样本数、validationSplit、batchSize 与 epochs 推算得出)计算并显示训练完成百分比;调用 ui.js 的plotLoss与 ui.js 的plotAccuracy实时绘制训练集曲线;每 10 个 batch 触发一次onIteration回调以刷新页面下方的推理示例;最后await tf.nextFrame()让出主线程,保证页面不冻结;
  • onEpochEnd(epoch, logs):每个 epoch 结束时,将logs.val_acc作为验证准确率记录,并绘制验证集的 loss/accuracy 曲线点。

底层曲线绘制由@tensorflow/tfjs-vis的tfvis.render.linechart()完成(ui.js),同时展示训练集与验证集两条序列,x 轴为 batch 编号,y 轴分别为 Loss 与 Accuracy。

这里可以对照 README 的表述理解设计意图:文档中描述的是"每 5 步在 1000 个随机测试样本上评估准确率"这一目标行为;而在当前 index.js 的实现中,验证集准确率由validationSplit划分的验证数据在每个 epoch 结束时计算,推理示例面板则每 10 个 batch 用 100 个测试样本刷新一次。两种方式目的一致——在训练过程中持续获得"模型当前表现如何"的反馈。

评估与推理:argMax 与 tf.tidy

训练完成后的整体评估

model.fit()返回后,代码调用model.evaluate(testData.xs, testData.labels)在全部 10000 个测试样本上计算最终指标(index.js):

const testResult = model.evaluate(testData.xs, testData.labels); const testAccPercent = testResult[1].dataSync()[0] * 100; const finalValAccPercent = valAcc * 100; ui.logStatus( `Final validation accuracy: ${finalValAccPercent.toFixed(1)}%; ` + `Final test accuracy: ${testAccPercent.toFixed(1)}%`);

最终会在页面状态栏打印验证准确率与测试准确率两个数字。

推理示例面板

showPredictions()取 100 个测试样本进行预测(index.js),是整个推理链路的浓缩:

tf.tidy(() => { const output = model.predict(examples.xs); const labels = Array.from(examples.labels.argMax(axis).dataSync()); const predictions = Array.from(output.argMax(axis).dataSync()); ui.showTestResults(examples, predictions, labels); });

其中三个 API 的用途值得展开:

  • tf.tidy():回调中创建的所有中间张量会在执行完毕后自动释放显存/内存,免去手动dispose()的负担,避免长时间训练后内存泄漏;
  • tf.argMax(axis):one-hot 编码下模型输出是一个概率分布向量,argMax取概率最大的下标即为预测类别(如argmax([0.07, 0.1, 0.03, 0.75, 0.05]) == 3);
  • dataSync():同步地将 GPU 上的张量值下载到普通 JavaScript 数组,便于在 CPU 侧渲染 UI(非阻塞版本是data())。

随后 ui.js 的showTestResults()将每个测试样本绘制到独立 canvas 上,并标注pred: N,预测正确时背景为绿色(pred-correct)、错误时为红色(pred-incorrect),效果一目了然。

浏览器交互界面

index.html 将整个训练流程组织为四个 section:

  1. Training Parameters(训练参数):Model Type下拉框(ConvNet / DenseNet)、# of training epochs输入框(默认 3)、以及 "Load Data and Train Model" 训练按钮;
  2. Training Progress(训练进度):状态文本、loss 与 accuracy 两个折线图 canvas;
  3. Inference Examples(推理示例):动态生成 100 个测试样本的图片与预测标签。

按钮点击后,ui.js 的setTrainButtonCallback()会立即禁用按钮和模型类型下拉框,防止训练过程中重复触发,随后执行 index.js 中注册的主流程回调:

ui.setTrainButtonCallback(async () => { ui.logStatus('Loading MNIST data...'); await load(); // 1. 加载 MNIST 数据 ui.logStatus('Creating model...'); const model = createModel(); // 2. 按所选类型创建模型 model.summary(); // 打印模型结构到控制台 ui.logStatus('Starting model training...'); await train(model, () => showPredictions(model)); // 3. 训练 + 定期刷新推理示例 });

整个流程严格遵循"加载数据 → 构建模型 → 训练 → 评估推理"的经典链路,任何一步出现问题都能通过页面状态栏的logStatus文案与浏览器控制台定位。

结语与延伸方向

tfjs-examples/mnist 是理解"在浏览器中完成完整深度学习训练流程"的最佳起点:它在一个页面内集成了数据加载、模型构建、超参数配置、训练可视化与推理展示,且每一环节都配有清晰的源码注释。在此基础上,仓库还提供了多种进阶方向可供继续研究:

  • mnist-core:不使用 Layers API,改用底层 tfjs-core 手写训练过程,适合理解梯度与权重更新的底层原理;
  • mnist-transfer-cnn:基于预训练 CNN 的迁移学习示例;
  • mnist-acgan:对抗生成网络(ACGAN)在 MNIST 上的应用。

读者可以把 mnist 目录整体拷贝到自己的项目中,替换 data.js 的数据加载逻辑与 index.js 的模型定义,即可快速改造为任意自定义图像分类任务的浏览器端训练器。

  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

项目地址:https://gitcode.com/gh_mirrors/tf/tfjs-examples
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进