4、构建第一个模型:顺序模型(Sequential)与函数式 API 入门
好,咱们开始动手写模型了。
说实话,很多初学者一上来就被各种模型架构吓住了。其实没那么玄乎。在 TensorFlow.js 里,构建模型就两种主流方式:顺序模型(Sequential) 和 函数式 API。我个人习惯把 Sequential 叫做「搭积木」,把函数式 API 叫做「画流程图」。
今天咱们就把这两种方式都走一遍。你跟着敲一遍代码,基本就入门了。
4.1 顺序模型:最直观的搭积木方式
顺序模型,说白了就是一层一层往上堆。数据从输入层进去,经过隐藏层,最后从输出层出来。就像流水线一样,一条道走到黑。
我在项目中遇到过很多场景,比如简单的分类任务、回归预测,用 Sequential 就足够了。它简单、清晰、不容易出错。
4.1.1 创建 Sequential 模型的两种写法
第一种写法:先创建实例,再逐层添加。
const model = tf.sequential();
model.add(tf.layers.dense({units: 64, activation: 'relu', inputShape: [10]}));
model.add(tf.layers.dense({units: 32, activation: 'relu'}));
model.add(tf.layers.dense({units: 1, activation: 'sigmoid'}));
第二种写法:创建时直接传入层数组。
const model = tf.sequential({
layers: [
tf.layers.dense({units: 64, activation: 'relu', inputShape: [10]}),
tf.layers.dense({units: 32, activation: 'relu'}),
tf.layers.dense({units: 1, activation: 'sigmoid'})
]
});
两种写法效果完全一样。我个人偏好第一种,因为调试的时候可以随时注释掉某层,方便排查问题。
inputShape,后面的层会自动推断输入形状。你想想看,这省了多少事。
4.1.2 配置模型与编译
模型搭好了,还得告诉它怎么学。这就用到 compile 方法。
model.compile({
optimizer: 'adam',
loss: 'binaryCrossentropy',
metrics: ['accuracy']
});
这里三个参数:
- optimizer:优化器。Adam 是现在最常用的,自适应学习率,省心。
- loss:损失函数。二分类用 binaryCrossentropy,多分类用 categoricalCrossentropy,回归用 meanSquaredError。
- metrics:评估指标。训练过程中会实时显示。
4.1.3 训练与预测
训练用 fit 方法,预测用 predict 方法。看代码:
// 生成一些假数据
const xs = tf.randomNormal([100, 10]);
const ys = tf.randomNormal([100, 1]);
// 训练
async function trainModel() {
await model.fit(xs, ys, {
epochs: 50,
batchSize: 32,
validationSplit: 0.2,
callbacks: {
onEpochEnd: (epoch, logs) => {
console.log(`Epoch ${epoch}: loss = ${logs.loss.toFixed(4)}`);
}
}
});
// 预测
const testInput = tf.randomNormal([5, 10]);
const predictions = model.predict(testInput);
predictions.print();
}
嗯,这里要注意:fit 是异步的,记得用 await 或者 .then()。我第一次写的时候忘了加 await,结果模型还没训练完就开始预测了,报了一堆错。
4.2 函数式 API:更灵活的模型构建方式
Sequential 虽然简单,但有个硬伤——它只能处理单输入单输出的线性结构。如果你需要多输入、多输出、或者层之间有分支、共享层,那就得用函数式 API 了。
函数式 API 的核心思想是:把层当作函数来调用。每一层的输出作为下一层的输入。说白了就是「层与层之间手动连线」。
4.2.1 一个简单的函数式模型
// 定义输入
const input = tf.input({shape: [10]});
// 定义中间层
const dense1 = tf.layers.dense({units: 64, activation: 'relu'}).apply(input);
const dense2 = tf.layers.dense({units: 32, activation: 'relu'}).apply(dense1);
// 定义输出层
const output = tf.layers.dense({units: 1, activation: 'sigmoid'}).apply(dense2);
// 创建模型
const model = tf.model({inputs: input, outputs: output});
你看,每一层都用 .apply() 方法连接到上一层的输出。最后用 tf.model() 把输入和输出包装起来。
tf.sequential(),函数式 API 用 tf.model()。前者是「搭积木」,后者是「画流程图」。
4.2.2 多输入多输出模型
这才是函数式 API 真正发力的地方。举个例子,假设我们要做一个模型,输入两张图片,输出两个结果——一个分类结果,一个回归结果。
// 两个输入
const inputA = tf.input({shape: [28, 28, 1]});
const inputB = tf.input({shape: [28, 28, 1]});
// 分别处理
const convA = tf.layers.conv2d({filters: 32, kernelSize: 3, activation: 'relu'}).apply(inputA);
const convB = tf.layers.conv2d({filters: 32, kernelSize: 3, activation: 'relu'}).apply(inputB);
// 合并
const concat = tf.layers.concatenate().apply([convA, convB]);
const flatten = tf.layers.flatten().apply(concat);
// 两个输出分支
const output1 = tf.layers.dense({units: 10, activation: 'softmax'}).apply(flatten);
const output2 = tf.layers.dense({units: 1, activation: 'linear'}).apply(flatten);
// 创建模型
const model = tf.model({inputs: [inputA, inputB], outputs: [output1, output2]});
编译时也要对应指定损失函数:
model.compile({
optimizer: 'adam',
loss: ['categoricalCrossentropy', 'meanSquaredError'],
metrics: [['accuracy'], ['mse']]
});
lossWeights 参数设置,比如 lossWeights: [0.7, 0.3]。我做过一个项目,分类任务比回归任务重要,就把分类的权重设高了。
4.3 Sequential vs 函数式 API:怎么选?
| 对比维度 | Sequential | 函数式 API |
|---|---|---|
| 适用场景 | 单输入单输出,线性堆叠 | 多输入多输出,分支结构,共享层 |
| 代码复杂度 | 简单,易读 | 稍复杂,但灵活 |
| 调试难度 | 低,一眼看穿 | 中等,需要理清连接关系 |
| 性能 | 两者底层一样,无差异 | 两者底层一样,无差异 |
我的建议是:能用 Sequential 就用 Sequential。别为了炫技硬上函数式 API。等遇到多输入多输出的需求时,再自然过渡到函数式 API。我在早期项目中就吃过这个亏——明明一个 Sequential 就能搞定的事,非要写成函数式,结果代码又长又难维护。
4.4 模型保存与加载
模型训练好了,得保存下来吧?不然每次用都要重新训练,那也太傻了。
// 保存模型到浏览器本地存储
await model.save('localstorage://my-model');
// 加载模型
const loadedModel = await tf.loadLayersModel('localstorage://my-model');
也可以保存到文件:
// 保存为文件(会下载两个文件:model.json 和权重文件)
await model.save('downloads://my-model');
// 从文件加载(需要用户选择文件)
const uploadedModel = await tf.loadLayersModel(tf.io.browserFiles([modelJsonFile, weightsFile]));
model.json 存结构,权重文件存参数。两个文件缺一不可。我曾经只保存了 model.json,加载时直接报错,折腾了半天才发现权重文件没下载下来。
4.5 小结
今天咱们把 TensorFlow.js 里构建模型的两种方式都过了一遍:
- Sequential:搭积木,简单直观,适合线性结构
- 函数式 API:画流程图,灵活强大,适合复杂结构
你想想看,其实这两种方式并不冲突。小项目用 Sequential,大项目用函数式 API。就像写代码一样,简单逻辑用 if-else,复杂逻辑用设计模式。工具是死的,人是活的。
下一章咱们会深入聊聊数据预处理。毕竟,模型再牛,喂进去的是垃圾,出来的还是垃圾。数据清洗和特征工程,才是真正的重头戏。