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:评估指标。训练过程中会实时显示。
⚠️ 注意: 我曾经犯过一个低级错误——编译时忘了指定 metrics,结果训练完了不知道模型到底准不准。后来养成了习惯,每次编译必加 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() 把输入和输出包装起来。

🔑 关键区别: Sequential 用 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,复杂逻辑用设计模式。工具是死的,人是活的。

下一章咱们会深入聊聊数据预处理。毕竟,模型再牛,喂进去的是垃圾,出来的还是垃圾。数据清洗和特征工程,才是真正的重头戏。