☰
TensorFlow.js 浏览器端迁移学习实战:MNIST CNN Transfer Learning 示例全解析
2026/10/15 18:39:53 网站建设 项目流程
  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

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

本文围绕 TensorFlow.js 官方示例仓库 tfjs-examples 中的mnist-transfer-cnn示例展开,系统讲解如何把 Python Keras 预训练好的 CNN 模型转换为 TensorFlow.js 格式后加载进浏览器,再通过冻结特征提取层、只微调全连接层的方式完成"数字 5~9"的分类任务。读完本文,你将掌握tf.loadLayersModel()加载远程模型、浏览器内model.fit()微调、三种迁移学习训练模式的选择,以及如何用双端口 HTTP 服务器同时提供页面与模型资源。

示例概述:在浏览器中完成一次迁移学习

该示例完整演示了TensorFlow.js Layers API 下的迁移学习(Transfer Learning)流程,其设计思路继承自 Keras 官方示例mnist_transfer_cnn,实现细节见 python/mnist_transfer_cnn.py。整体分为两个阶段:

  1. Python 端预训练:用 Keras 在 MNIST 数据集前 5 个数字(0~4)上训练一个简单卷积网络,将模型产物(拓扑 JSON + 权重)托管到 URL;
  2. 浏览器端微调:用tf.loadLayersModel()把模型加载进浏览器,冻结卷积层,仅让后面的全连接层在数字 5~9 的数据上重新训练(fine-tuning)。

该演示验证了一个核心观点(详见 index.html 中的 Description 说明):预训练模型的前几层可以作为特征提取器(feature extractor)复用于新数据,从而显著加快新任务上的训练速度。

启动示例只需两条命令:

yarn yarn watch

第一条安装依赖(@tensorflow/tfjs、@tensorflow/tfjs-vis等,见 package.json),第二条启动开发服务器并在浏览器中打开页面。

端到端工作流:从 Keras 预训练到浏览器微调

整个示例的数据流可以概括为下图所示链路(本文依据 index.js 与 python/mnist_transfer_cnn.py 源码梳理):

MNIST 全量数据 │ ├─ 数字 0~4(lt5)──> Python Keras 训练 CNN ──> tfjs.converters.save_keras_model │ │ │ ▼ │ model.json + 权重(托管 URL) │ │ │ 浏览器 tf.loadLayersModel() 加载 │ └─ 数字 5~9(gte5,标签减 5)──> 导出 gte5.train.json / gte5.test.json │ ▼ 浏览器 fetch 加载 ──> 张量化 ──> model.fit() 微调

关键点在于:预训练用的数据(0~4)与微调用数据(5~9)刻意划分开,这样微调时模型从未见过数字 5~9,才能真实检验迁移学习的效果。

在浏览器端,模型与数据有两套来源,分别定义在 index.js 的HOSTED_URLS与LOCAL_URLS中:

资源托管地址(HOSTED)本地地址(LOCAL,端口 1235)
预训练模型https://storage.googleapis.com/tfjs-models/tfjs/mnist_transfer_cnn_v1/model.jsonhttp://localhost:1235/resources/model.json
微调训练数据.../mnist_transfer_cnn_v1/gte5.train.jsonhttp://localhost:1235/resources/gte5.train.json
微调验证数据.../mnist_transfer_cnn_v1/gte5.test.jsonhttp://localhost:1235/resources/gte5.test.json

页面加载后,index.js 的setupMnistTransferCNN()会先通过 HEAD 请求探测两套 URL 是否可达,把可用的"加载模型"按钮(load-pretrained-remote/load-pretrained-local)显示出来。

Python 端:预训练模型架构与数据切分

模型架构:特征层 + 分类层

train_and_save_model()函数把网络显式拆成两组(见 python/mnist_transfer_cnn.py):

  • 特征提取层(feature_layers,共 7 层):Conv2D(32, 3×3)→ReLU→Conv2D(32, 3×3)→ReLU→MaxPooling2D(2×2)→Dropout(0.25)→Flatten;
  • 分类层(classification_layers,共 5 层):Dense(128)→ReLU→Dropout(0.5)→Dense(5)→Softmax。

输入形状由 python/mnist_transfer_cnn.py 中的INPUT_SHAPE决定:通道在前的环境为(1, 28, 28),否则为(28, 28, 1)。整体 12 层结构之所以重要,是因为浏览器端"冻结前 7 层"的编号正是以此为基准。

命令行参数一览

Python 脚本通过 argparse 暴露了全部训练超参数(python/mnist_transfer_cnn.py):

参数默认值含义
--epochs5Keras 预训练轮数
--batch_size128训练批大小
--filters32卷积层滤波器数量
--pool_size2最大池化窗口大小
--kernel_size3卷积核尺寸
--artifacts_dir/tmp/mnist.kerasTensorFlow.js 模型产物保存目录
--optimizeradam预训练优化器名称
--gte5_cutoff1024写入 JSON 的 gte5 样本数上限(>0 时截断)
--gte5_data_path_prefix/tmp/mnist_transfer_cnn.gte5gte5 训练/测试 JSON 文件路径前缀

数据切分与序列化

load_mnist_data()(python/mnist_transfer_cnn.py)把 MNIST 切分成两组:

  • lt5组:标签< 5,即数字 0~4,用于预训练;
  • gte5组:标签>= 5,且标签统一减 5(数字 5 的标签变为 0,数字 9 的标签变为 4),用于浏览器端微调。

write_mnist_examples_to_json_file()(python/mnist_transfer_cnn.py)把每个样本序列化为{'x': [28×28 灰度值二维数组], 'y': 整数标签}的 JSON 数组,分别写出*.train.json与*.test.json。对应单测testWriteGte5DataToJsFiles与testTrainWithFakeDataAndSave在 python/mnist_transfer_cnn_test.py 中验证了该序列化格式与模型保存流程。

浏览器端模型加载:tf.loadLayersModel()

浏览器端的加载逻辑封装在 loader.js 中:

  • urlExists(url):对资源做fetch(url, {method: 'HEAD'})探测,返回response.ok,决定页面显示哪个加载按钮;
  • loadHostedPretrainedModel(url):核心一行await tf.loadLayersModel(url),返回tf.Model实例(含拓扑与权重)。加载成功后通过ui.disableLoadModelButtons()隐藏加载按钮,防止重复加载(源码注释提到受 tfjs issue #34 限制,同一模型不能重复加载);
  • loadHostedData(url, numClasses):fetchJSON 后调用util.convertDataToTensors()转为张量。

在 index.js 的init()中,模型加载完成后会打印model.summary()并通过tfVis.show.modelSummary()在 tfjs-vis 面板中可视化;this.imageSize从model.layers[0].batchInputShape[1]读取(即 28),this.numClasses固定为 5。

张量转换与 ASCII 位图交互

JSON 数据 → 训练张量

util.js 的convertDataToTensors()完成三件事:

  1. 把[示例数, 28, 28]的tf.tensor3dreshape 为[示例数, 28, 28, 1];
  2. 用tf.mul(tf.scalar(1 / 255), xsTensor)将 0~255 灰度归一化到 0~1;
  3. 标签经indexToOneHot转成 5 类 one-hot 向量。

像素编辑与预测

util.js 的textToImageArray(text, imageSize)把文本编辑框中的数字矩阵解析为像素值并除以 255,最终构造[1, 28, 28, 1]的tf.tensor4d。预测过程在 index.js 的predict()中完成,并整体包在tf.tidy()中以保证中间张量被自动回收:

const image = util.textToImageArray(imageText, this.imageSize); const predictOut = this.model.predict(image); const winner = predictOut.argMax(1); ui.setPredictResults(predictOut.dataSync(), winner.dataSync()[0] + 5);

由于微调模型输出的是"标签 0~4"(对应数字 5~9),所以最终类别要加 5 还原为真实数字。

浏览器内微调:三种训练模式对比

retrainModel()(index.js)是迁移学习的核心,根据用户在界面选择的training-mode走三条不同路径:

训练模式源码实现适用场景
freeze-feature-layers(冻结特征层)遍历this.model.layers前 7 层,置trainable = false最典型的迁移学习:特征提取器不动,只训最后 5 层分类器
no-freezing(不冻结)不做任何设置,所有层均可训练对照组:观察全部层训练时的收敛速度与损失波动
reinitialize-weights(重置权重)通过tf.models.modelFromJSON({modelTopology: this.model.toJSON(null, false)})重建同拓扑、随机初始化权重的模型等效于从零训练,用于对照迁移学习收益

随后统一compile与fit:

this.model.compile({ loss: 'categoricalCrossentropy', optimizer: tf.train.adam(0.01), metrics: ['acc'], }); const batchSize = 128; const epochs = ui.getEpochs(); // 界面输入,默认 10 await this.model.fit(this.gte5TrainData.x, this.gte5TrainData.y, { batchSize: batchSize, epochs: epochs, validationData: [this.gte5TestData.x, this.gte5TestData.y], callbacks: [ ui.getProgressBarCallbackConfig(epochs), // 进度条回调 tfVis.show.fitCallbacks(..., ['val_loss', 'val_acc'], ...), // tfjs-vis 曲线 ] });

值得注意的工程细节:冻结前 7 层对应 Python 端 12 层结构中前 7 层(两层 Conv2D + 两层 ReLU + 池化 + Dropout + Flatten,见 index.html 的 Retrain Model 说明),这样"只允许最后 5 层参与训练"与 Python 端网络定义严格对应。compile后再次model.summary()打印,即可在控制台看到大量参数变为非可训练(non-trainable)。训练完成后,验证集上的val_acc与val_loss会实时绘制在 tfjs-vis 的 "Transfer Learning" 标签页中。

交互界面:像素级编辑与概率输出

页面布局见 index.html,ui.js 负责交互逻辑:

  • 测试示例选择器:预置数字 5~9 各 10 个(5_1、5_2……9_10),由 index.js 的prepTestExamples()从测试数据中按类别收集(每类 10 个);
  • ASCII 位图编辑框:28×28 的灰度值文本矩阵,可在keyup事件下实时触发重新预测(ui.js),用于直观观察"修改像素 → 类别概率变化";
  • 模型输出表:setPredictResults()(ui.js)以固定表头5 6 7 8 9展示五类概率(保留 6 位小数),并在下方标注Output class(最大概率对应的数字);
  • 进度条:getProgressBarCallbackConfig()注册onTrainBegin/onEpochEnd/onTrainEnd回调,更新#trainProg进度条并统计微调耗时,训练期间禁用 Retrain 按钮防止并发训练。

运行与部署:双端口服务器的由来

开发环境启动

yarn watch实际执行 serve.sh,该脚本同时启动两个 HTTP 服务器:

  • 端口 1234:parcel index.html提供页面与 JS 代码(自动打开浏览器);
  • 端口 1235:node_modules/http-server/bin/http-server dist --cors -p 1235提供预训练模型与数据资源。

之所以需要两个端口,脚本注释给出了明确原因:Parcel 打包器当时存在限制,无法直接伺服预训练模型资源(关联 parcel 仓库 issue #1098)。因此页面代码与模型资源被拆到两个服务上,LOCAL_URLS中localhost:1235正是为了对接后者。当 Parcel 服务退出时,脚本会杀掉 http-server。

生产构建

如需产出静态产物,可执行 package.json 中的build脚本:

yarn build

它调用cross-env NODE_ENV=production parcel build --no-scope-hoist index.html --public-url ./,将页面打包为相对路径可部署的dist/目录。

测试与验证

仓库为 Python 端提供了基于unittest的测试 python/mnist_transfer_cnn_test.py,覆盖两条关键链路:

  • testWriteGte5DataToJsFiles:用随机伪造数据验证 gte5 数据能被正确写成*.train.json/*.test.json,且每个样本的x为 28×28 结构;
  • testTrainWithFakeDataAndSave:用小规模参数(filters/kernel/pool/batch 均为 2,epochs=1)跑通"训练 + 保存"流程,并校验model.json产物可被正常解析。

浏览器端则通过可视界面验证:加载模型 → 选中某个 5~9 的测试示例 → 微调 → 观察概率表与验证曲线。从源码结构看,index.js 中adam(0.01)的学习率与批大小 128 与 Python 端默认参数保持了一致的设计思路(Python 端默认adam、batch_size=128),便于对照两套环境下的训练行为。

小结

mnist-transfer-cnn示例完整覆盖了迁移学习从"Python 预训练"到"浏览器微调"的全链路,其工程要点可归纳为:

  1. 资源分离:模型、训练数据、验证数据分别托管,浏览器端用 HEAD 请求探测可用性;
  2. 层级冻结:通过逐层设置trainable = false实现特征层冻结,且冻结编号与 Python 端网络结构严格对齐;
  3. 三模式对照:冻结 / 不冻结 / 重置权重三种训练模式让迁移学习效果可以量化对比;
  4. 可视化反馈:tfjs-vis 的模型摘要与训练曲线、ASCII 位图编辑框,让训练过程与预测结果一目了然;
  5. 工具链协同:双端口服务器绕开打包器限制,为"加载大模型 + 开发页面"提供了可复用的工程模式。

对想在浏览器端复用预训练视觉模型的开发者而言,本示例是理解 TensorFlow.js 迁移学习 API(tf.loadLayersModel、model.compile、model.fit与自定义回调)的最佳入门样板。

  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

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

相关推荐

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询