从去年开始,一直有读者在后台问我同一个问题:TensorFlow 2.0 到底该怎么学?网上的教程分成两种极端,要么只教你把 API 照着敲一遍,跑完一个 Demo 就完事,根本不解释为什么;要么一上来就讲自定义算子、自动微分、分布式策略,把新手往死里吓。我自己的建议一直很明确:如果你不是铁了心要去做底层框架开发,只是想尽快把模型跑起来、把想法验证出结果,那就老老实实从 tf.keras 这条路进去。
这一篇是这个系列的第二节,聚焦 TensorFlow-Keras 实战。我不打算把官方文档翻译一遍,而是按照我自己从零上手到落地部署的真实路径来写:先聊清楚为什么选 Keras 而不是手写底层训练循环,再讲环境搭建里那些容易翻车的细节,然后分别走一遍 Sequential 模型和函数式 API 的实战案例,最后把这几年调试模型时踩过的坑整理成一条能直接照着排错的链路。适合从零开始、但不想只看文档空转的读者,也适合已经跑过几个 Demo、想系统补齐基础认知的朋友。
1. 为什么我坚持让新手从 tf.keras 入门
1.1 它不是第三方封装,而是官方主推的高级接口
很多刚接触 TensorFlow 的人会有个误会,以为 Keras 是某个独立团队做的第三方封装库,跟 TensorFlow 是“插件”关系。实际上从 TensorFlow 2.0 正式发布开始,tf.keras 就已经被收编为官方的高级 API,跟随 TensorFlow 源码一起发布、一起维护。
这一点非常关键。第三方封装库最大的问题是版本滞后:TensorFlow 升级了内部表示,封装层没跟上,代码就跑得莫名其妙。tf.keras 没有这个问题,底层算子、梯度计算、设备分配都是直接打通的。你在 tf.keras 里写一个 Dense 层,底层调用的是 TensorFlow 自己的矩阵运算和反向传播逻辑,中间没有隔一层什么东西。
所以,现阶段的正确认知应该是:tf.keras 不是 TensorFlow 的一个“可选皮肤”,它就是官方提供的标准建模方式。TensorFlow 2.x 里你仍然可以不用 Keras 去写自定义训练循环,自己算梯度、自己写 optimizer.apply_gradients,但对绝大多数应用场景来说,直接用 tf.keras 是更高性价比的选择。
1.2 把精力留给模型设计,而不是样板代码
我先举一个对比的例子,曾经在 TensorFlow 1.x 时代跑一个最简单的线性回归,需要定义 placeholder、定义变量、写 session、手动初始化、手动 feed 数据、手动执行 optimizer。整个流程下来,业务逻辑没写几行,全在跟计算图的“仪式感”搏斗。
到了 tf.keras,同样的线性回归只需要这么几件事:搭一个 Sequential 模型、加一个 Dense 层、compile 指定损失和优化器、fit 传入数据。没有 session,没有 placeholder,没有 feed_dict。对新手来说,这套接口把“深度学习框架”的复杂度挡在外面,让你先专注在数据长什么样、模型怎么设计、效果对不对。
我知道有些工程师会觉得高级 API“太傻瓜”,不够底层。但从实际工程经验来说,Keras 的高级 API 并不是解释执行的脚本层,而是把模型描述编译成计算图再执行,性能上并没有可感知的劣化。真正让训练变慢的,往往是数据管线没写好、迭代次数不够、batch_size 不合理这些问题,API 层反而不是瓶颈。
1.3 什么情况下才需要绕开 Keras
我也说句公道话,不是所有场景都适合用 Keras。如果你要实现的模型包含非常规的动态控制流,比如在每次前向传播中根据中间结果决定是否递归调用同一个子网络,Keras 的静态层组合方式会显得笨拙,这时候自定义训练循环反而更自由。
还有一类场景是做底层算子研究,比如你要实现一个全新的卷积变体,并且要控制它在前向和反向中的计算细节,这需要继承 Layer 类去自定义 call 和 compute_output_shape,本质上还是在和 Keras 打交道,只是深入了一层。
但这些都是进阶话题。对入门者来说,Keras 永远是第一优先级的起点,学完它再去理解底层会轻松很多。
2. 环境搭建里那些最容易翻车的细节
2.1 版本配套比你想的更严格
说实话,TensorFlow 安装本身不难,难的是版本搭配。Python 版本、TensorFlow 版本、CUDA 版本、cuDNN 版本这四者之间是有配对关系的。最常见的报错是装完 TensorFlow 后 import 阶段报了类似“cudart64_xxx.dll not found”的错误,原因基本都是 CUDA 和 cuDNN 对不上。
我建议的做法是走虚拟环境加 pip 安装,不要直接在系统环境里乱装。下面这组是我在项目里用得比较稳的组合:
conda create -n tf2 python=3.9 -y conda activate tf2 pip install tensorflow如果你需要 GPU 版本,先确认自己的显卡驱动支持哪个 CUDA 版本,然后按照官方版本矩阵去选择 TensorFlow 版本。这里有个经验可以先记住:不是越新的版本越好,而是越稳定的组合越好。社区里验证比较多的组合,性能未必比最新版本差,但踩坑概率低得多。
另一个容易忽略的问题是不要在一个环境里同时安装多个深度学习框架的 GPU 版。不同框架依赖的 CUDA 版本可能冲突,把它们混在同一个环境里,最后往往是这个框架的库被另一个覆盖,运行时报一堆不明所以的错误。各装各的环境,是能省下大量调试时间的。
2.2 验证环境的标配脚本
安装完成后不要急着跑模型,先跑一个最小验证脚本,确认 TensorFlow 和 GPU 都被正常识别:
import tensorflow as tf print("TensorFlow 版本:", tf.__version__) print("可用的 GPU 设备:", tf.config.list_physical_devices('GPU')) # 一个简单的张量运算,验证基本功能 a = tf.constant([[1.0, 2.0], [3.0, 4.0]]) b = tf.constant([[5.0, 6.0], [7.0, 8.0]]) print("矩阵乘法结果:\n", tf.matmul(a, b))如果tf.config.list_physical_devices('GPU')返回的是空列表,那就说明 TensorFlow 没找到 GPU。这时候先别急着重装,依次检查显卡驱动、CUDA、cuDNN 三个环节,大概率是其中某个没配好。
还有一个我反复遇到的坑:很多人在 Windows 上用 GPU 版本跑模型,有一个很常见的报错是“Could not load dynamic library 'cudnn64_8.dll'”。网上很多建议是去官网下载 cuDNN 然后手动复制到系统目录,但最靠谱的做法还是直接使用官方 Docker 镜像,或者严格安装官方指定的 CUDA、cuDNN 版本。
2.3 显存按需增长必须提前设置
TensorFlow 默认行为是一旦启动就尽量占满所有可用的显存,哪怕当前模型根本用不了那么多。这在单卡环境中问题不大,但如果你机器上有别的进程也在用 GPU,或者你想在同一个卡上并行跑多个实验,就会直接 OOM。
显存按需增长的设置在代码里其实很简单:
import tensorflow as tf physical_gpus = tf.config.list_physical_devices('GPU') for gpu in physical_gpus: tf.config.experimental.set_memory_growth(gpu, True)注意这段代码要放在所有 TensorFlow 操作开始之前,最好放在 import 之后立刻执行。如果模型已经构建了、已经分配了显存,再设置就不生效了。我用一台双卡工作站跑实验时深有体会,一开始没设置这个,两个实验一起跑就有一个崩掉,设置了 growth 之后,显存才真正做到了按需分配。
2.4 训练日志里那些看似吓人的警告
第一次跑 TensorFlow 的人,看到控制台刷出一堆 WARNING 日志往往会慌,比如“oneDNN custom operations are on”,比如“TF-TRT Warning: Could not find TensorRT”。这些大多不需要处理。
真正要关注的警告是涉及 cuDNN、CUDA、设备不可用这几类。如果只是提示你的 CPU 支持某些加速指令,那对训练结果没有任何影响。掌握这个判断标准,可以帮你省掉很多不必要的百度时间。
3. 第一个实战:用 Sequential 十几行代码跑通图像分类
3.1 数据准备里的两个关键细节
这节我用一个公开的手写数字图像数据集来做演示。它足够小、足够经典,不需要下载几百 GB 的图片,加载速度也很快,适合用来跑通整套流程。第一次接触的人不用去纠结数据集的背景,把它当成一个“28 乘 28 像素的灰度手写数字图片,一共 10 个类别”的分类任务就行。
加载数据的代码非常简单:
from tensorflow.keras.datasets import mnist (x_train, y_train), (x_test, y_test) = mnist.load_data()但数据准备阶段有两个细节非常容易被忽略。
第一个是归一化。原始图像的像素值范围是 0 到 255,如果不做处理直接丢给神经网络,输入数值范围偏大,初始的加权求和很容易得到一个很大的值,经过后续激活函数后梯度容易变得不稳定。归一化的标准做法就是除以 255,把像素值压缩到 0 到 1 之间:
x_train = x_train.astype('float32') / 255.0 x_test = x_test.astype('float32') / 255.0这个操作对收敛速度的影响非常显著,尤其是使用梯度下降类优化器的时候。你可以做个对比实验,不归一化直接训练,loss 下降明显更慢,甚至可能出现数值振荡。
第二个细节是标签的两种表达方式。y_train 里存的是 0 到 9 的整数标签,这种叫稀疏标签。还有一种做法是把标签转成 one-hot 向量,比如数字 3 变成[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]。两种方式分别对应两个不同的损失函数,这个选择直接影响到你 compile 阶段怎么写。
3.2 模型结构:为什么是 Flatten 加 Dense
对这张 28 乘 28 的图片,最简单的处理方式是先用 Flatten 层把二维矩阵拉平成 784 维的向量,然后再接若干层 Dense。
Flatten 本身没有可学习的参数,它的作用就是把数据从形状为(batch_size, 28, 28)的张量变成(batch_size, 784)的张量。为什么要这么做?因为 Dense 层也就是全连接层,它的输入约定是二维矩阵,每一行是一个样本的特征向量。图片本来是二维排布的,如果不拉平,Dense 不知道该怎么处理空间结构。
模型结构我通常这么写:
from tensorflow.keras import layers, models model = models.Sequential([ layers.Input(shape=(28, 28)), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.2), layers.Dense(10, activation='softmax') ])中间隐藏层用 relu 激活函数,是因为它计算简单、梯度不容易饱和,能有效缓解深层网络里的梯度消失问题。最后一层是分类输出层,用 softmax 激活函数,因为我们要输出 10 个类别的概率分布,softmax 能保证输出值都在 0 到 1 之间,并且加起来等于 1。
中间加了一个 Dropout 层,作用是随机关闭一部分神经元的输出,减少过拟合。在入门阶段,我会建议直接把 Dropout 当成标配加在 Dense 层后面,哪怕数据简单,也不会有什么副作用。
3.3 训练前的最后一公里:compile 与 fit
模型构建完成后要经过 compile 配置训练方式,再用 fit 执行训练。下面这行代码是常用配置:
model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] )这里要解释一下为什么是sparse_categorical_crossentropy。因为我们刚才保留了整数标签,没有做 one-hot,它对应的就是稀疏交叉熵损失。如果你在数据准备阶段把标签转成了 one-hot,那这里就要写成categorical_crossentropy。两者计算的本质相同,只是标签形式不同,这个对应关系搞混了,最常见的报错就是维度对不上,或者训练过程出现诡异的 loss 值。
fit 阶段最基础的配置是下面这样:
history = model.fit( x_train, y_train, batch_size=32, epochs=10, validation_split=0.2 )validation_split=0.2表示从训练集里抽出 20% 的数据作为验证集,训练过程中同时输出验证集上的 loss 和准确率。验证集不参与梯度更新,只用来观察模型是否泛化到没见过的新数据。每训练完一轮,控制台会输出一行日志:loss、accuracy是训练集上的指标,val_loss、val_accuracy是验证集上的指标。这两组数据之间的差距,就是你判断模型是否过拟合的核心依据。
第一个模型跑完,准确率通常能到 97% 以上。跑通之后,你再回头看看 history 这个变量,它是一个 History 对象,里面记录了每一轮的完整指标变化,后续可以用来画曲线,也可以用来分析训练过程是否健康。
4. compile 与 fit 里的隐藏细节:很多人只会照抄
4.1 优化器选择的经验
optimizer 可能是 Keras 里最容易被忽略但也最值得花时间理解的一个参数。我的习惯是起步阶段一律用adam,没有例外。原因很简单,Adam 同时结合了动量法对梯度方向的稳定性和自适应学习率对单参数步长的调整能力,在绝大多数任务是都能比较快地收敛,不需要手动调节学习率衰减策略。
但这不意味着 Adam 是万能的。到了训练后期,模型接近收敛时,Adam 因为自适应的特性,可能在一些平坦区域反复震荡,这时候换用带动量项的 SGD 并调低学习率,反而更容易收敛到更好的极值点。我自己的经验是,先用 Adam 快速跑出一个能work的 baseline,后期要追精度的时候,再把优化器切成 SGD。
学习率是比优化器更重要的参数。Adam 的默认学习率是 0.001,这个值在多数场景下表现不错。如果你发现 loss 一开始就不下降,或者干脆变成 NaN,先查学习率。学习率太大的典型表现是 loss 剧烈震荡,学习率太小的表现则是 loss 下降得像蜗牛。这里没有什么捷径,多跑几个数量级去对比,很快就能找到适合当前任务的区间。
4.2 损失函数、输出层激活函数、标签形式的对应关系
很多人写模型的时候只是照抄代码,对于为什么用这个损失函数完全没有概念。我给出一张常用的对照表,建议直接存下来:
| 任务类型 | 输出层激活 | 标签形式 | 推荐损失函数 | 常用评估指标 |
|---|---|---|---|---|
| 二分类 | sigmoid | 0/1 | binary_crossentropy | accuracy |
| 多分类 | softmax | one-hot | categorical_crossentropy | accuracy |
| 多分类 | softmax | 整数标签 | sparse_categorical_crossentropy | accuracy |
| 回归 | 无激活 | 连续值 | mse / mae | mae |
这张表的逻辑实际上是一个系统:输出层的激活函数决定了模型输出值的形式,损失函数则衡量“模型输出”和“真实标签”之间的差距。三者必须匹配。
以二分类为例,输出层用 sigmoid,模型输出的就是一个介于 0 到 1 之间的概率值,配合 binary_crossentropy 计算交叉熵损失。如果你用 MSE 来做分类,不是不行,但收敛通常更慢,而且容易优化到不那么理想的区域。这是因为交叉熵对概率输出的梯度更有指导意义,MSE 在分类场景下容易造成梯度饱和。
4.3 回调函数是让训练效果稳定的法宝
如果只调用 fit 不做任何其他设置,模型在最后一轮 epoch 结束时会返回它在该时刻的权重,但这几乎肯定不是整个训练过程中最好的状态。解决这个问题需要引入回调函数,我通常会在项目里同时加三个:
from tensorflow.keras import callbacks callbacks = [ callbacks.EarlyStopping( monitor='val_loss', patience=3, restore_best_weights=True ), callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.2, patience=2 ), callbacks.ModelCheckpoint( filepath='best_model.keras', monitor='val_loss', save_best_only=True ) ]EarlyStopping 的逻辑是,如果连续若干轮验证损失都没有改善,就主动停止训练。这里特别要提一下restore_best_weights=True这个参数。不加它,训练停止后模型权重停留在最后一轮,而最后一轮往往不是最优的;加了它,模型会自动回滚到验证损失最小的那一轮权重。这个参数极大影响最终模型的精度,很多人不知道,白白让训练结果打了折扣。
ReduceLROnPlateau 的作用是当训练进入平台期,验证损失不再下降时,自动把学习率降低一个量级,让优化器能够用更细的步长继续搜索。ModelCheckpoint 则负责保存历史最优模型,防止训练中途崩溃导致所有工作白费。
这三个回调是搭在一起的组合拳,几乎可以无脑用到所有入门项目里。fit 里只需要简单传入:
history = model.fit( x_train, y_train, batch_size=32, epochs=50, validation_split=0.2, callbacks=callbacks )注意 epochs 可以设置得大一些,比如 50 甚至 100,因为反正有 EarlyStopping 在兜底,模型会在合适的时机自动停止,不需要你手动盯着。
4.4 batch_size 的选择逻辑
batch_size 决定了一次参数更新用多少个样本来计算梯度。太小,比如 1,相当于每个样本都做一次梯度更新,梯度方向波动大,训练不稳定,而且 GPU 的并行能力也发挥不出来;太大,比如 512,一次更新虽然是更准确的梯度,但计算量和显存占用都会增大。对入门任务来说,32 或 64 是我最常用的起点,后续再根据显存和收敛情况调整。
还有一个容易被忽略的问题是数据打乱。fit 函数默认会在训练时对数据做随机打乱,但如果你用的是 tf.data 构造的自定义数据集,就需要自己设置.shuffle(buffer_size)。数据不打乱,模型会先学完一类样本再学另一类,训练曲线会出现周期性震荡,泛化效果也会变差。
5. 函数式 API:当模型不再是单入单出
5.1 Sequential 模型的边界在哪
Sequential 模型最大的约束就是“线性堆叠”:一层接着一层,中间没有分支,没有跳跃连接,没有多输入多输出。很多真实业务模型是没法用 Sequential 表达的。
我拿一个典型的场景举例:假设要做一个商品推荐模型,输入包含两类特征,一类是用户的数字化特征,比如年龄、历史点击次数;另一类是商品特征,比如商品类别、价格。这两类特征可以先分别经过各自的 Dense 层处理,然后拼接到一起,再输入到后续网络。这样的结构在 Sequential 里根本没法写,因为输入不是一个张量,而是两个张量。
这就是函数式 API 登场的时候。函数式 API 的核心思路是把每一层都当成一个函数,以张量作为输入,经过函数映射后输出一个新的张量,再用tf.keras.Model把输入张量和输出张量显式关联成模型。
5.2 一个双输入多输出的实战示例
下面用伪代码展示一个“双输入,双输出”的模型结构:两个输入分别经过处理后被拼接,然后分支出两个输出头。这种情况在真实项目中非常普遍,比如同时预测用户是否点击,以及用户可能给出的评分。
from tensorflow.keras import layers, models input_user = layers.Input(shape=(20,), name='user_features') input_item = layers.Input(shape=(15,), name='item_features') dense_user = layers.Dense(64, activation='relu')(input_user) dense_item = layers.Dense(64, activation='relu')(input_item) concat = layers.Concatenate()([dense_user, dense_item]) shared = layers.Dense(128, activation='relu')(concat) click_output = layers.Dense(1, activation='sigmoid', name='click_output')(shared) score_output = layers.Dense(1, name='score_output')(shared) model = models.Model( inputs=[input_user, input_item], outputs=[click_output, score_output] )这种结构下,compile 需要对每个输出分别指定损失函数:
model.compile( optimizer='adam', loss={ 'click_output': 'binary_crossentropy', 'score_output': 'mse' }, loss_weights={ 'click_output': 1.0, 'score_output': 0.5 }, metrics={ 'click_output': 'accuracy' } )注意这里 loss 字典的 key 必须跟输出层的 name 完全一致,否则直接报错。这个点很容易踩,很多人明明写的 loss 没问题,却因为输出层没有加 name 或者名字对不上而报错,卡了半天。
loss_weights参数可以调整两个损失在总损失中的权重。比如在这个场景里,评分预测可能比点击预测更难学,但你更在意点击率,就可以把点击损失的权重调高。这种灵活的控制能力,是函数式 API 在工程落地中不可替代的原因。
5.3 关于层复用的一个重要概念
函数式 API 里你会经常看到这样的写法:
shared_layer = layers.Dense(64, activation='relu') output_a = shared_layer(input_a) output_b = shared_layer(input_b)这里同一个 Dense 层实例被调用了两次,但注意,这两次调用共享的是同一组权重。这种模式在孪生网络里非常有用,比如两个输入本质上应该用完全相同的特征提取逻辑来处理。如果你想让两个分支各自拥有独立的参数,就必须创建两个 Dense 层实例。这个区别是新手上路时最容易糊涂的地方。
理解和掌握了层复用,函数式 API 的威力才真正释放出来。ResNet 里的跳跃连接、Transformer 里的多头注意力分支、多模态模型里的特征融合,本质上都是这一套写法的延伸。
6. 准确率上不去的通用排查链路
6.1 从三个层面入手
曾经有位读者给我发来他的训练截图,loss 在 2.3 左右死活不动,问是不是模型结构有问题。我让他先把第一个 batch 的数据打印出来看一眼,结果发现标签和图片错位了。数据读取阶段的一个 shuffle 没写对,后面所有训练都是白费。
我把自己的排查顺序固定成三层:先查数据,再查模型,最后查训练配置。这个顺序基本能覆盖九成的问题。
数据层:标签是否与样本一一对应,类别是否均衡,数值是否归一化,验证集有没有混入训练集的数据。如果一个分类任务某个类别样本极少,模型会倾向于把所有样本预测成多数类,准确率虚高但实际无效。
模型层:输入 shape 对不对,最后一层输出维度跟类别数是否一致,激活函数和损失函数是否匹配。最常见的报错标题是“logits and labels must have the same first dimension”,也就是模型输出维度跟标签维度对不上。这种情况几乎都是最后一个 Dense 层的神经元数量写错了。
训练层:学习率是否过高或过低,batch_size 是否合理,epochs 是否足够。loss 变成 NaN,大概率是学习率太大导致梯度更新步长超出合理范围;loss 下降极慢,先考虑是不是输入没有归一化。
6.2 一张检查清单直接照做
我把排查步骤整理成一张表,每次训练结果不对劲的时候,按顺序逐项核对:
| 症状 | 可能原因 | 快速验证方法 |
|---|---|---|
| loss 一直是同一个值不下降 | 数据标签错位 / 学习率太小 | 打印一个 batch 的数据和标签做人工检查 |
| loss 直接变为 NaN | 学习率太大 / 数据里有 NaN 值 | 调低学习率一个数量级再试 |
| 训练准确率高但验证准确率低 | 过拟合 | 增加 Dropout / 数据增强 / 正则化 |
| 验证准确率忽高忽低 | 学习率太高 / batch_size 太小 | 调低学习率 / 增大 batch_size |
| 前期正常后期 loss 反弹 | 训练时间过长 | 加 EarlyStopping |
| 模型输出维度与标签不匹配 | 最后一层神经元数量错了 | 检查 model.summary() 的输出形状 |
其中model.summary()方法是排查模型层问题最有效的工具。它会打印每一层的输出 shape 和参数量,一眼就能看出哪一层接错了。
6.3 小规模试跑是最高效的调试方式
我自己的习惯是,任何模型先不要直接拿全部数据去训练。先用少量数据比如每个类别抽几十张,做一个小训练集,跑几个 epoch,确认 loss 确实在下降、准确率确实在上升,再放大到全量数据。
小规模试跑有几个好处:训练速度快,方便你频繁调整;问题暴露早,不会等几个小时后发现方向从一开始就是错的;还能顺便验证整套数据管线是否通了。如果你在小数据集上训练到 100% 准确率做得非常快,那说明模型和数据的连接没有问题,接下来要关注的无非是过拟合和训练策略。
另外一个很多人忽视的点是过拟合的识别。训练集准确率接近 100%,验证集准确率只有 80%,这个差距本身不是 bug,而是模型容量过大、训练时间过长。先尝试增加 Dropout 系数,再尝试加入 L2 正则化,给 Dense 层加上参数惩罚:
from tensorflow.keras import regularizers layers.Dense(128, activation='relu', kernel_regularizer=regularizers.l2(1e-4))L2 正则化的本质是在损失函数里加一项权重的平方和,惩罚那些很大的权重,让模型变得“平滑”。系数1e-4是一个比较温和的起点,如果过拟合依然严重,可以逐步调大到1e-3。
最后,如果你追求实验结果的可复现,就要在脚本开头固定随机种子:
import numpy as np import tensorflow as tf np.random.seed(42) tf.random.set_seed(42)这里要提醒一句,固定种子能复现大部分流程,但如果数据加载过程里涉及多线程或 GPU 上的某些非确定性算子,结果仍然可能存在微小差异,这属于深度学习框架的已知现象,不用过于纠结。
7. 模型落地时一定要留好的后手
7.1 模型保存与加载的正确姿势
训练结束不是终点,把模型保存下来并能够在其他环境里加载推理才是。Keras 的保存方式非常直接:
model.save('best_model.keras')然后用 load_model 一行加载:
from tensorflow.keras import models loaded_model = models.load_model('best_model.keras') predictions = loaded_model.predict(x_test)这个.keras格式同时保存了模型结构、权重、训练配置,加载之后可以直接继续训练,也可以直接做推理。如果项目需要部署到服务器,通常还会导出成 SavedModel 格式,这属于部署层面的内容,后续文章再展开。
要注意的一点是,如果你当时在 compile 阶段挂了自定义损失函数或者自定义层,加载模型时可能需要额外传入 custom_objects 参数,否则会报“Unknown loss function”的错误。这个坑在复用别人代码时尤其常见。
7.2 TensorBoard 和模型可视化
fit 里只需要加一个回调,就能在浏览器里实时观察训练曲线:
callbacks.append(callbacks.TensorBoard(log_dir='./logs'))训练完之后在终端执行tensorboard --logdir=logs,打开浏览器就能看到 loss、accuracy 的完整曲线,也可以查看模型结构图。很多人只盯着训练日志里的最终数字,错过 TensorBoard 里的信息量,非常可惜。
model.summary()还应该作为每次实验的固定动作。它输出的每一层参数量,能帮你快速判断模型是不是过参数化了。举个例子,一个全连接层如果输入是 784 维、输出是 512 个单位,那么仅仅这一层的参数量就有 784 乘 512 再加 512 个偏置,大约 40 万。模型参数越多,越需要更多的数据来做约束,否则过拟合几乎是必然的。
7.3 接下来该往哪个方向走
跑通 Sequential 模型和函数式 API 之后,你其实已经掌握了 Keras 建模的主干。下一步要深入的,我建议按这个顺序来:先学着用 tf.data 构造高效的数据管线,解决大数据集加载和预处理的问题;再学自定义层和自定义损失函数,遇到标准接口表达不了的模型时能自己扩展;最后是自定义训练循环,理解每一步 optimizer 和梯度到底做了什么。
这些内容属于“脱离新手村”之后的进阶路线,也是从“会调 API”走向“理解框架”的关键分水岭。这也是整个系列后续文章要覆盖的主线。
写到这里,想起我最初学 Keras 的时候,总想把每个参数都背下来,后来才明白,真正重要的是理解数据在模型里的流动方式:数据怎么被预处理,怎么进入输入层,怎么经过层层变换,最后怎么被损失函数衡量和梯度更新修正。把这个主链路想清楚,Keras 里的各种 API 只是这条链路在不同环节上的具体工具。
最后再分享一个小技巧:如果你用的是多卡工作站,记得把显存按需增长的配置代码写在所有模型构建和训练调用之前,放在 import 之后就执行。这个顺序一旦搞错,显存还是会被提前占满,我因为这个顺序问题浪费过将近一个下午,后来把配置挪到最前面,问题再没出现过。这篇建议先把代码都跑通,下一节我会用文本分类作为例子,带你继续往前走。