- 示例工程
【免费下载链接】tfjs-examples
Examples built with TensorFlow.js
本文围绕 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。整体分为两个阶段:
- Python 端预训练:用 Keras 在 MNIST 数据集前 5 个数字(0~4)上训练一个简单卷积网络,将模型产物(拓扑 JSON + 权重)托管到 URL;
- 浏览器端微调:用
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.json | http://localhost:1235/resources/model.json |
| 微调训练数据 | .../mnist_transfer_cnn_v1/gte5.train.json | http://localhost:1235/resources/gte5.train.json |
| 微调验证数据 | .../mnist_transfer_cnn_v1/gte5.test.json | http://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):
| 参数 | 默认值 | 含义 |
|---|---|---|
--epochs | 5 | Keras 预训练轮数 |
--batch_size | 128 | 训练批大小 |
--filters | 32 | 卷积层滤波器数量 |
--pool_size | 2 | 最大池化窗口大小 |
--kernel_size | 3 | 卷积核尺寸 |
--artifacts_dir | /tmp/mnist.keras | TensorFlow.js 模型产物保存目录 |
--optimizer | adam | 预训练优化器名称 |
--gte5_cutoff | 1024 | 写入 JSON 的 gte5 样本数上限(>0 时截断) |
--gte5_data_path_prefix | /tmp/mnist_transfer_cnn.gte5 | gte5 训练/测试 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()完成三件事:
- 把
[示例数, 28, 28]的tf.tensor3dreshape 为[示例数, 28, 28, 1]; - 用
tf.mul(tf.scalar(1 / 255), xsTensor)将 0~255 灰度归一化到 0~1; - 标签经
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 预训练"到"浏览器微调"的全链路,其工程要点可归纳为:
- 资源分离:模型、训练数据、验证数据分别托管,浏览器端用 HEAD 请求探测可用性;
- 层级冻结:通过逐层设置
trainable = false实现特征层冻结,且冻结编号与 Python 端网络结构严格对齐; - 三模式对照:冻结 / 不冻结 / 重置权重三种训练模式让迁移学习效果可以量化对比;
- 可视化反馈:tfjs-vis 的模型摘要与训练曲线、ASCII 位图编辑框,让训练过程与预测结果一目了然;
- 工具链协同:双端口服务器绕开打包器限制,为"加载大模型 + 开发页面"提供了可复用的工程模式。
对想在浏览器端复用预训练视觉模型的开发者而言,本示例是理解 TensorFlow.js 迁移学习 API(tf.loadLayersModel、model.compile、model.fit与自定义回调)的最佳入门样板。
- 示例工程
【免费下载链接】tfjs-examples
Examples built with TensorFlow.js
相关推荐
OfficeCLI 完全指南:一个二进制,让 AI Agent 读写 Word/Excel/PPT
OfficeCLI 完全指南:一个二进制,让 AI Agent 读写 Word/Excel/PPT 上午 9:40,AI 助手接到任务:出一份季度销售汇总——带
CLIAI 应用MCP 服务安全烧录系统镜像:Balena Etcher的技术实现与生产实践
安全烧录系统镜像:Balena Etcher的技术实现与生产实践 在嵌入式开发和系统部署场景中,误操作导致数据丢失是技术人员面临的常见风险。传统命令行工具如dd
桌面应用开发工具智能硬件手把手用 MuJoCo Python 绑定跑通物理仿真:从 3 行代码到 60 帧视频
手把手用 MuJoCo Python 绑定跑通物理仿真:从 3 行代码到 60 帧视频 写机器人控制或强化学习时,最磨人的往往不是算法,而是环境:装图形依赖、配
物理引擎机器人机器学习图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考