- 示例工程
【免费下载链接】tfjs-examples
Examples built with TensorFlow.js
本篇文章围绕 tfjs-examples 仓库中的 abalone-node 示例展开,完整讲解如何在 Node.js 环境中使用 TensorFlow.js 完成一次真实的回归任务:根据鲍鱼的物理测量数据预测其年龄(rings 环数)。读者学完后,将掌握tf.data.csv加载本地 CSV、特征预处理、tf.Sequential构建多层感知机、model.fitDataset()训练、model.save()保存与tf.loadLayersModel()推理的完整工程链路,并了解 CPU/GPU 后端的选择方式。
一、示例概览:在 Node.js 中用物理测量预测鲍鱼年龄
abalone-node是 tfjs-examples 中典型的Node.js 环境下的数值回归示例。它使用的数据集来自 UCI Machine Learning Repository 的 Abalone 数据集(仓库中已随附 abalone.csv,共 4177 行数据)。每行记录一只鲍鱼的 8 个物理测量特征与目标变量rings(环数),任务即是从这些测量值回归预测环数,从而推断鲍鱼年龄。
主仓库 README.md 的示例总览表中对该示例的定位是:输入数据类型为数值(Numeric)、任务类型为"从本地文件加载数据并在 Node.js 中训练"、模型类型为多层感知机(Multilayer perceptron)、训练与推理均在 Node.js 中完成、使用Layers API、支持将模型保存到文件系统并在 Node.js 中重新加载。这正是本示例的教学价值所在——它完整串联了 TensorFlow.js 在 Node.js 侧的数据管道、模型训练与序列化三个环节。
数据集字段结构(对应 abalone.csv 首行表头):
| 字段 | 含义(鲍鱼数据集惯例) | 类型 |
|---|---|---|
sex | 性别:M(雄)、F(雌)、I(幼体) | 类别 |
length | 最长壳长度 | 数值(毫米) |
diameter | 与长度垂直的直径 | 数值(毫米) |
height | 带壳肉的高度 | 数值(毫米) |
weight.w | 整体重量 | 数值(克) |
weight.s | 去壳肉重 | 数值(克) |
weight.v | 内脏重量 | 数值(克) |
weight.sh | 壳重 | 数值(克) |
rings | 环数(目标变量) | 整数(2~16) |
在鲍鱼数据集的惯例中,环数常被用作年龄的代理指标;本示例直接以rings作为回归目标,这一点在训练日志与推理输出中均有体现。
二、快速开始:安装依赖并启动训练
由于仓库中每个示例目录都是独立、可拷贝到其他项目直接运行的,因此进入示例目录后依次执行即可:
cd abalone-node yarn yarn trainyarn:依据 package.json 与yarn.lock安装依赖。核心运行时依赖为@tensorflow/tfjs-node(^2.6.0)与命令行解析库argparse(^1.0.10),开发依赖为jasmine测试框架等。yarn train:等价于执行node train.js(见 package.json 中的scripts配置)。
启动训练脚本 train.js 后,会先通过https.get将数据集从远程地址下载为本地./abalone.csv(仓库已随附该文件,脚本运行时会重新拉取覆盖),随后自动完成训练。默认配置下会训练100 个 epoch,最终输出类似以下日志:
... Epoch 100 / 100 eta=0.0 =================================================> 402ms 57414us/step - loss=7.42 val_loss=5.60 The actual test abalone age is 10, the inference result from the model is 11.929240226745605其中loss与val_loss分别是训练集与验证集上的均方误差(MSE),最后一行展示了模型对测试样本的推理结果:真实年龄为 10,模型预测为约 11.93,属于同一数量级的合理回归结果。
命令行参数
train.js 使用argparse暴露了三个可调参数,便于在不改代码的情况下调节训练配置:
| 参数 | 默认值 | 说明 |
|---|---|---|
--epochs | 100 | 训练的轮数(epoch) |
--batch_size | 500 | 训练时每个 batch 的样本数 |
--savePath | file://trainedModel | 模型保存路径,需带file://前缀 |
例如,想训练 200 轮并把模型保存到自定义目录:
node train.js --epochs 200 --batch_size 256 --savePath file://my_modelCPU 与 GPU 后端的选择
训练默认使用@tensorflow/tfjs-node,它在 Node.js 中通过 libtensorflow 运行,默认跑在 CPU 上。如果你的机器配备支持 CUDA 的 GPU,并且系统已正确安装配置好 CUDA 与 CuDNN 库,则可以将依赖中的tfjs-node替换为tfjs-node-gpu,即可让训练跑在 GPU 上,显著加快大规模迭代场景下的训练速度。注意该替换需要自行调整 package.json 中的依赖项并重新安装,且对 CUDA/CuDNN 版本有环境要求。
三、数据加载与预处理:tf.data.csv 与特征编码
数据加载与预处理逻辑全部封装在 data.js 的createDataset(csvPath)函数中,它展示了 TensorFlow.jstf.data管道的标准用法:读取 → 配置 → 转换。
1. 从本地 CSV 创建 Dataset
const dataset = tf.data.csv( csvPath, {hasHeader: true, columnConfigs: {'rings': {isLabel: true}}}); const numOfColumns = (await dataset.columnNames()).length - 1;tf.data.csv支持file://前缀的本地路径(调用处传入'file://' + csvPath),也支持远程 URL。hasHeader: true表明 CSV 首行是列名;columnConfigs指定rings列为标签列(isLabel: true),其余列自动作为特征xs。- 由于标签列被单独拆分出去,输入特征维度为总列数减 1,即
9 - 1 = 8,这正是numOfColumns的计算方式。
2. 特征与标签的映射转换
tf.data.csv返回的行结构中,特征在row['xs'](对象形式),标签在row['ys']。随后通过dataset.map()完成两件事:
const convertedFeatures = Object.keys(rawFeatures).map(key => { switch (rawFeatures[key]) { case 'F': return 0; case 'M': return 1; case 'I': return 2; default: return Number(rawFeatures[key]); } }); const convertedLabel = [rawLabel['rings']]; return {xs: convertedFeatures, ys: convertedLabel};- 类别特征编码:
sex列是字符串类别,无法直接送入网络,因此按F→0、M→1、I→2映射为数值。 - 数值特征转换:其余 7 个测量值通过
Number()转为浮点数,形成 8 维特征向量。 - 标签整形:
rings被包装成单元素数组[rings],作为回归目标(输出维度为 1)。
最终createDataset返回{dataset, numOfColumns},其中dataset已经过 map 转换、可直接进入训练管道。
3. 数据测试对预处理逻辑的验证
data_test.js 使用仓库随附的 test_data.csv(与主数据集同结构)验证了上述逻辑:
numOfColumns等于8;- 特征向量长度为8,其中
features[0](编码后的sex)取值只能是0、1、2三者之一; - 其余 7 个数值特征均在
(0, 1)区间内; - 标签
label[0]是整数,且落在2~16范围内。
这些断言与 abalone.csv 的实际数据分布一致,可作为理解预处理正确性的可执行依据。
四、模型构建:三层全连接回归网络
模型定义位于 model.js 的createModel(inputShape),它构建了一个标准的多层感知机(MLP)回归模型:
const model = tf.sequential(); model.add(tf.layers.dense({inputShape: inputShape, activation: 'sigmoid', units: 50})); model.add(tf.layers.dense({activation: 'sigmoid', units: 50})); model.add(tf.layers.dense({units: 1})); model.compile({optimizer: tf.train.sgd(0.01), loss: 'meanSquaredError'});| 层 | 单元数 | 激活函数 | 说明 |
|---|---|---|---|
| 隐藏层 1(Dense) | 50 | sigmoid | 输入维度inputShape(= 8) |
| 隐藏层 2(Dense) | 50 | sigmoid | 中间隐藏层 |
| 输出层(Dense) | 1 | 线性(默认) | 回归输出单个连续值 |
- 任务类型是回归,因此输出层只有 1 个单元且不带激活函数(线性输出),损失函数选用
meanSquaredError(均方误差)。 - 优化器为SGD,学习率
0.01。该配置可通过修改 model.js 调整,例如换成tf.train.adam()或更大的学习率以加速收敛。
仓库中已保存的训练产物 trainedModel/model.json 从序列化层面印证了上述结构:模型拓扑为Sequential,包含 3 个 Dense 层,batch_input_shape为[null, 8];权重清单显示dense_Dense1/kernel形状为[8, 50]、dense_Dense2/kernel为[50, 50]、dense_Dense3/kernel为[50, 1],与"8 维输入 → 50 → 50 → 1 输出"的网络完全吻合。
五、训练流程:数据切分、批量化与 fitDataset
训练主流程位于 train.js 的run(epochs, batchSize, savePath)函数中,展示了tf.data管道与fitDataset组合使用的完整模式。
1. 数据集切分:训练集与验证集
数据集共4177 行,示例将其拆为两部分:
const trainBatches = Math.floor(3500 / batchSize); const dataset = datasetObj.dataset.shuffle(1000).batch(batchSize); const trainDataset = dataset.take(trainBatches); const validationDataset = dataset.skip(trainBatches);shuffle(1000):在 1000 个样本的缓冲窗口内打乱数据顺序,保证训练样本分布的随机性。batch(batchSize):按 batch 大小切分,默认500。take(trainBatches):取前Math.floor(3500 / batchSize)个 batch 作为训练集(默认 500 时即 7 个 batch,共 3500 行)。skip(trainBatches):跳过上述 batch,剩余约677 行作为验证集(validationData)。
这种"按 batch 数切分"的方式要求训练数据量是 batch 大小的整数倍,3500 恰好是 500 的整数倍,设计上避免了余数问题。
2. 训练与保存
await model.fitDataset( trainDataset, {epochs: epochs, validationData: validationDataset}); await model.save(savePath);model.fitDataset()接受一个tf.data.Dataset作为输入,无需手动准备xs/ys张量对,这正是tf.data管道与fit系列 API 的结合点;validationData传入验证集,在每轮结束后评估val_loss。model.save(savePath)将模型保存到本地,默认路径为file://trainedModel,输出model.json与weights.bin(对应仓库中的 trainedModel/model.json 与trainedModel/weights.bin)。
3. 模型测试对训练与序列化的验证
model_test.js 用 jasmine 写了两个关键测试:
- 训练可行性测试:创建输入长度为 6 的模型,断言
model.inputs[0].shape为[null, 6]、model.outputs[0].shape为[null, 1],并调用model.fit(ones, ones, {epochs: 2})确认能正常训练且 loss 历史长度为 2。 - 保存-加载往返测试(roundtrip):将模型保存到临时目录后,通过
tf.loadLayersModel(file://.../model.json)重新加载,再对同一输入分别推理,用tf.test_util.expectArraysClose断言两次输出完全一致,从而证明序列化不会损失模型行为。
六、加载模型进行推理
训练完成后,train.js 还会演示标准的"加载 → 推理"流程:
const loadedModel = await tf.loadLayersModel(savePath + '/model.json'); const result = loadedModel.predict( tf.tensor2d([[0, 0.625, 0.495, 0.165, 1.262, 0.507, 0.318, 0.39]]));tf.loadLayersModel()从本地model.json(含weightsManifest指向的weights.bin)恢复完整的 Layers 模型。predict()接收一个形状为[1, 8]的张量:0表示性别编码(F),随后依次是length=0.625、diameter=0.495、height=0.165、weight.w=1.262、weight.s=0.507、weight.v=0.318、weight.sh=0.39。- 输出为该样本的预测年龄(环数),如示例日志中的
11.929240226745605,而该样本真实值为 10。
这一小段代码同时展示了"模型保存到文件系统、在新进程中重新加载并复用"的完整闭环,也是主 README 概览表中"保存到文件系统并在 Node.js 中加载"能力的直接体现。
七、单元测试与工程实践
除训练脚本外,示例还配套了完整的测试体系,体现了 tfjs-examples 仓库"示例即工程"的规范:
- 运行测试:在示例目录执行
yarn test(等价于node run_tests.js)。 - run_tests.js 复用仓库根目录的 test_util.js:后者封装了 jasmine 初始化逻辑,将
./*test.js作为 spec 文件加载执行。 - 测试覆盖两类核心逻辑:数据预处理(data_test.js)与模型构建/训练/序列化(model_test.js)。
这种"训练脚本 + 单元测试 + 数据文件"的目录组织方式,使得示例既可以一键复现训练结果,也可以作为 TensorFlow.js Node.js API 的可靠参考实现。
结语
abalone-node虽是一个小而精的示例,却完整覆盖了 TensorFlow.js 在 Node.js 端从数据到部署的五步闭环:用tf.data.csv加载本地 CSV、通过map()完成类别编码与特征转换、用tf.Sequential从零搭建 MLP 回归模型、借助model.fitDataset()配合验证集切分完成训练、最后通过model.save()/tf.loadLayersModel()实现模型的持久化与复用。读者既可以按上文步骤直接运行yarn && yarn train复现日志中的训练效果,也可以把它作为模板,将同样的数据管道与训练流程迁移到自己的数值回归任务中;若拥有 CUDA 环境,还可将后端切换为tfjs-node-gpu获得训练加速。
- 示例工程
【免费下载链接】tfjs-examples
Examples built with TensorFlow.js
相关推荐
TensorFlow.js Data 数据管道指南:从 CSV 加载到模型训练
TensorFlow.js Data 数据管道指南:从 CSV 加载到模型训练 导读 TensorFlow.js Data( @tensorflow/tfjs
人工智能机器学习深度学习前端后端cnn-text-classification-tf训练流程完全解析:从数据加载到模型保存
cnn text classification tf训练流程完全解析:从数据加载到模型保存 想要掌握TensorFlow中的文本分类技术吗?cnn text c
示例工程NLP深度学习ml-project 最小 ML 训练示例深度解析:用 scikit-learn 训练、保存并交付逻辑回归模型(kitops-mcp 仓库篇)
ml project 最小 ML 训练示例深度解析:用 scikit learn 训练、保存并交付逻辑回归模型(kitops mcp 仓库篇) 本文以 ai e
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考