在浏览器中训练 MNIST:tfjs-examples/mnist 手写数字识别示例源码解析
示例工程【免费下载链接】tfjs-examplesExamples 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-coremnist-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等 APItensorflow/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 雪碧图转换为 TensorMNIST 数据集共有65000 个样本其中55000 个用于训练、10000 个用于测试对应 data.js 中的常量定义。每张图片为28 × 28的灰度图共10 个类别数字 09。数据加载的核心逻辑全部封装在 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通过new Image()加载雪碧图并设置img.crossOrigin 以支持跨域读取在内存中开辟65000 * 28 * 28 * 4字节的ArrayBuffer按每次 5000 张的分块方式用canvas.drawImage()裁剪出对应图块再经ctx.getImageData()读出像素数据由于图片是灰度图四个通道取值相同因此只读取红色通道即可并将像素值除以 255 归一化到[0, 1]区间data.js通过fetch()并行加载标签文件与图片加载通过Promise.all等待完成最后分别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()。卷积网络ConvNetcreateConvModel()使用tf.sequential()搭建了一个典型的 CNN 结构index.js各层配置如下层配置说明Conv2DinputShape: [28, 28, 1]kernelSize: 3filters: 16activation: relu输入层同时承担首次卷积接收 28×28 灰度图MaxPooling2DpoolSize: 2strides: 2以区域最大值做下采样Conv2DkernelSize: 3filters: 32activation: relu加深特征提取MaxPooling2DpoolSize: 2strides: 2再次下采样Conv2DkernelSize: 3filters: 32activation: relu第三层卷积Flatten—将二维特征图展平为一维向量Denseunits: 64activation: relu全连接隐藏层Denseunits: 10activation: softmax输出层softmax 产生 10 个类别的概率分布源码注释中对该结构的每个环节都有通俗解释第一层卷积既充当输入层又执行第一次卷积MaxPooling 用区域内的最大值而非平均值来做下采样最后一层 softmax 的输出向量各元素求和为 1可解释为概率分布。全连接网络DenseNetcreateDenseModel()只包含展平层与两个全连接层index.jsmodel.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.jsconst 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]) 3dataSync()同步地将 GPU 上的张量值下载到普通 JavaScript 数组便于在 CPU 侧渲染 UI非阻塞版本是data()。随后 ui.js 的showTestResults()将每个测试样本绘制到独立 canvas 上并标注pred: N预测正确时背景为绿色pred-correct、错误时为红色pred-incorrect效果一目了然。浏览器交互界面index.html 将整个训练流程组织为四个 sectionTraining Parameters训练参数Model Type下拉框ConvNet / DenseNet、# of training epochs输入框默认 3、以及 Load Data and Train Model 训练按钮Training Progress训练进度状态文本、loss 与 accuracy 两个折线图 canvasInference 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-examplesExamples built with TensorFlow.js项目地址https://gitcode.com/gh_mirrors/tf/tfjs-examples点击查看免费下载相关推荐终极指南如何在浏览器中实现ConvNetJS MNIST手写数字识别终极指南如何在浏览器中实现ConvNetJS MNIST手写数字识别 ConvNetJS是一个强大的JavaScript深度学习库让您能够在浏览器中训练卷积深度学习机器学习RIOT 中的 uTensor MNIST 手写数字识别示例从 TensorFlow 模型训练到 MCU 端推理RIOT 中的 uTensor MNIST 手写数字识别示例从 TensorFlow 模型训练到 MCU 端推理 导读 本文围绕 RIOT 仓库 tests/物联网嵌入式操作系统实时系统Cleanlab MNIST示例基于PyTorch的手写数字识别Cleanlab MNIST示例基于PyTorch的手写数字识别 引言数据质量决定模型上限 在机器学习实践中我们常常面临一个残酷的现实 垃圾进垃圾出人工智能机器学习数据清洗数据质检创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考