首页 / 资讯中心 / 文章详情

Keras API 实战指南:高频层、编译训练与回调详解

Keras API 实战指南:高频层、编译训练与回调详解 ★ FEATURED ARTICLE
Keras API 是 TensorFlow 里用得最频繁的那一层接口做深度学习的人应该都有体会不管是几层全连接跑个手写数字识别还是搞一整套图像分类、文本分类的流程最后落到代码上基本都是 tf.keras 这套高层 API 在干活。它最大的价值就是把反向传播、梯度更新、设备分配这些底层细节封装起来你只需要把模型结构描述清楚指定优化器和损失函数一个 compile 一个 fit训练流程就跑起来了。这篇东西适合两类人看一类是刚接触 TensorFlow、想系统搞明白 Keras 常用 API 到底有哪些、怎么搭配使用的初学者另一类是已经写过不少模型、但对某些 API 的细节参数和背后的设计逻辑还没完全摸透的开发者。我会按实际使用频率和踩坑经验来聊尽量讲清楚每个 API 为什么这么设计、参数该怎么选、什么地方容易翻车。1. 先搞清楚 Keras API 的设计逻辑1.1 为什么是 KerasKeras 最早是独立的开源项目后来被集成进 TensorFlow 作为官方的高层接口。它的核心思路是描述式建模你不用一行一行手写前向传播和梯度更新而是用 Layer 对象把网络结构拼接起来剩下的交给框架处理。这跟底层 API 的区别有点像装修底层 API 是自己买水泥、抹墙、铺瓷砖Keras 是给你预制好的墙板和地砖你只需要按图纸拼装。对绝大多数工程场景来说拼装已经完全够用而且快得多。我见过不少新人一上来就去啃底层 API结果在梯度裁剪、变量初始化这些地方卡了好几天模型还没跑起来。这其实没必要。Keras 这套接口在灵活性和易用性之间做了很好的平衡官方模型库、预训练模型、部署工具链全都围绕它来构建。你先把 Keras 用熟等真遇到需要自定义训练循环的场景再去触碰底层也不迟。1.2 三种建模方式的适用场景Keras 提供了三种建模方式很多文章会一笔带过但选错方式后面会非常痛苦。Sequential 顺序模型最简单一层接一层往中间加。适合线性堆叠的网络结构比如多层感知机、简单的 CNN。函数式 APIFunctional通过显式连接张量来定义模型支持多输入多输出、分支结构、共享层。这是我最推荐的主力方式复杂结构全靠它。Model 子类化Subclassing继承 tf.keras.Model在 call 方法里自己写前向逻辑。灵活度最高适合研究型代码但调试成本也高序列化和部署会遇到额外麻烦。三种方式的选择标准其实很简单能用 Sequential 就用 Sequential结构稍复杂就上函数式 API只有需要动态控制流、复杂共享逻辑时才考虑子类化。我把它们的特点整理成一张表建模方式适用场景优点缺点Sequential线性堆叠的简单网络代码最少、可读性高无法处理分支、共享层函数式 API多输入多输出、残差结构、共享层结构清晰、易于序列化代码比 Sequential 稍长Model 子类化动态逻辑、研究型原型灵活度最高调试难、部署麻烦、可读性一般举个函数式 API 的例子一个带残差连接的简单模块import tensorflow as tf inputs tf.keras.Input(shape(28, 28)) x tf.keras.layers.Flatten()(inputs) x tf.keras.layers.Dense(128, activationrelu)(x) shortcut tf.keras.layers.Dense(128, activationrelu)(inputs) ...等等这个例子其实有问题Input 的 shape 是 (28, 28)Flatten 之后是 784 维跟 shortcut 的维度对不上。我故意留了这个坑就是想强调使用函数式 API 时分支合并前必须保证张量 shape 一致。这是这块最常踩的坑维度对不上add 操作直接报错。正确写法是让 shortcut 也从 Flatten 之后接出去或者干脆不要让 shortcut 从原始输入走。函数式 API 的画图能力很强tf.keras.utils.plot_model能直接输出结构图调试网络连接关系非常方便。2. 高频层 API 逐一拆解2.1 Dense 全连接层Dense 是所有层里出现频率最高的它的核心参数就几个units神经元数、activation激活函数、use_bias是否用偏置、kernel_regularizer权重正则等。很多人只关注 units 和 activation容易忽略一个细节默认情况下 Dense 是带偏置的如果你在批归一化之后接 Dense可以考虑把 use_bias 设为 False因为批归一化本身已经做了平移再加偏置是冗余的。关于激活函数的选择中规中矩的答案是隐藏层用 relu二分类输出层用 sigmoid多分类输出层用 softmax。但实际工程里有几个常见替代方案# 隐藏层激活 tf.keras.layers.Dense(256, activationrelu) tf.keras.layers.Dense(256, activationswish) # 深层网络有时更稳 tf.keras.layers.Dense(256, activationgelu) # Transformer 风格 # 输出层激活 tf.keras.layers.Dense(1, activationsigmoid) # 二分类 tf.keras.layers.Dense(10, activationsoftmax) # 多分类 tf.keras.layers.Dense(4, activationNone) # 回归任务无激活这里有个注意点relu 在负数区间的梯度是 0如果网络初始化不好或者学习率太大很容易出现神经元死亡也就是大量神经元输出恒为 0、梯度永远传不过去。遇到这种情况可以换成 leaky_relu 或者 swish也可以稍微调低学习率。2.2 卷积层与池化层图像任务里 Conv2D 是绝对主力。除了 filters、kernel_size、strides、padding 这些基本参数有两个点值得单独讲。第一个是 padding 的取值。paddingsame表示输出尺寸不变或按公式近似变paddingvalid表示不填充卷积后尺寸会缩小。很多人以为 same 就是不加填充其实是加了而且是在输入四周补零。选择原则是想保留边缘信息就用 same想逐步缩小特征图就配合池化层用 valid。第二个是 kernel_size 的奇偶选择。实际工程里几乎都用奇数卷积核3x3、5x5因为奇数卷积核有天然的对称中心同时 same 模式下的 padding 量是对称的方便计算。Erdős 风格的规则是优先堆叠两个 3x3 卷积而不是直接用一个 5x5 卷积前者参数量更少感受野却一样而且非线性更强。池化层常用 MaxPooling2D 和 GlobalAveragePooling2D。MaxPooling2D 用来逐步压缩空间尺寸GlobalAveragePooling2D 一般用在卷积网络的最后替代 Flatten 接 Dense。全局平均池化直接把每个通道的特征图池化成一个数参数量为 0而且天然带正则效果比 Flatten 好使model tf.keras.Sequential([ tf.keras.layers.Conv2D(32, 3, activationrelu, paddingsame, input_shape(32, 32, 3)), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Conv2D(64, 3, activationrelu, paddingsame), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Conv2D(128, 3, activationrelu, paddingsame), tf.keras.layers.GlobalAveragePooling2D(), tf.keras.layers.Dense(10, activationsoftmax) ])2.3 Embedding 与循环层自然语言处理任务里Embedding 层是入口。它做的事情本质上是一个可训练的查表操作把 token id 映射成稠密向量。两个关键参数是 input_dim词表大小和 output_dim向量维度。input_dim 记得要设成词表大小 1因为要留出一个位置给填充符或未知字符。这是我见过翻车最多的地方词表按 10000 建的Embedding 里也写成 10000结果模型一加载就报越界。循环层方面LSTM 和 GRU 是主力。它们的参数很多但真正影响行为的就几个return_sequences是否返回每个时间步的输出、return_state是否返回隐藏状态、dropout 和 recurrent_dropout。return_sequences 只有在堆叠多层 RNN或者后面要接 Attention 时才需要设为 True只取最后一步输出的话保持默认 False 就行。stack 两层 LSTM 时的典型写法tf.keras.layers.LSTM(128, return_sequencesTrue, dropout0.2) tf.keras.layers.LSTM(128, dropout0.2)第一层返回完整序列给第二层第二层只输出最后一步的向量。这里有个经验recurrent_dropout 会显著拖慢训练速度因为它要用变分法做 dropout无法走固定矩阵乘的优化路径。如果训练数据量不大我更推荐用 dropout 而不是 recurrent_dropout效果接近但速度快很多。2.4 Dropout 与 BatchNormalization这两个层是所有训练技巧里用得最多的但位置摆错了会适得其反。Dropout 的作用是训练时随机丢弃一部分神经元输出防止神经元之间过度依赖。一个常见的配置原则是只在大的全连接层后面用卷积层后一般不用因为卷积层有空间共享特性本身正则效果就强。Dropout 层在推理时会自动关闭不需要你手动处理。rate 参数的默认值是 0.5但实务中 0.3 到 0.5 都可以rate 太大信息丢失严重太小没效果。BatchNormalization 的作用是把每层的输入分布拉回均值为 0、方差为 1 的范围内。它解决的核心问题是网络层数变深后的内部协变量偏移也就是每层的输入分布一直在变导致训练不稳定。BN 层的常见位置有两种流派激活之前或激活之后。我个人的经验是放在卷积或全连接之后、激活函数之前配合 relu 用效果最稳。还有一个容易忽略的点model.fit时 BN 在 batch 维度上算统计量所以 batch_size 不能太小否则 BN 的均值方差估计不稳定训练集上表现很好、验证集上却很差。tf.keras.layers.Conv2D(64, 3, paddingsame, use_biasFalse) tf.keras.layers.BatchNormalization() tf.keras.layers.ReLU() # 显式激活放在 BN 之后注意这里把 Conv2D 的 use_bias 设成了 False。原因前面提过BN 自己就有平移参数卷积层再带 bias 就是重复省掉还能减少参数量、避免一个已知的相互作用问题。3. 编译与训练compile / fit 的核心参数3.1 compile 三要素model.compile只有三个关键东西优化器、损失函数、评估指标。但每个都值得细说。优化器方面以前大家无脑用 Adam现在我会稍微区分场景快速验证和多数 NLP 任务用 Adam 没问题图像分类这类任务用 SGD 配合动量momentum0.9虽然收敛慢但最终精度往往更高。Adam 家族里还有一个重要参数叫做 learning rate默认是 0.001。这个默认值对很多小数据集是能用的但数据量大的时候通常要调小比如 1e-4 甚至 1e-5。如果发现训练 loss 震荡得很厉害第一反应就应该是把学习率降一个数量级。损失函数的选择是另一个高频翻车点。多分类任务经常有人把categorical_crossentropy和sparse_categorical_crossentropy混着用。区别在于标签是 one-hot 编码的用 categorical标签是整数 id 的用 sparse。两者数学上等价但 integer 标签用 categorical 会直接报错或者算错。二分类用binary_crossentropy回归用mse或mae。还有一个技巧如果你发现损失曲线在下降但指标曲线不动优先检查是不是损失函数选错了类别。评估指标里多分类别用 accuracy二分类可以直接用 accuracy不平衡数据集建议改用AUC或者Precision/Recallmodel.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), losstf.keras.losses.SparseCategoricalCrossentropy(from_logitsFalse), metrics[accuracy, tf.keras.metrics.AUC()] )from_logits这个参数也容易让人迷惑。它表示模型的输出是不是没有经过 softmax。如果你输出层已经用了 softmax 激活这里就设 False 或者不设如果输出层没有激活logits就必须设 True否则概率值会算错。经验法则输出层带了 softmaxfrom_logits 就设 False没带就设 True。3.2 fit 的常用参数与训练细节model.fit的参数看起来多真正每次都要斟酌的就几个batch_size、epochs、validation_split、callbacks、shuffle。batch_size 的选择直接受显存和 BN 层的约束。前面说了BN 对 batch_size 敏感太小不行。反过来batch_size 太大也会让每个 step 的梯度估计过于稳定容易收敛到尖锐的极小值泛化性能反而下降。一个实际经验是显存允许的情况下图像任务从 32 开始试文本任务从 64 开始试然后根据显存和收敛情况上下调整。epochs 不要贪多配合早停EarlyStopping比硬跑固定轮数科学得多。validation_split0.2 是常用的数据划分方式它会在训练集尾部切出 20% 作为验证集。注意这里有个隐含的顺序问题如果你的数据是按类别顺序排列的直接 validation_split 会导致验证集里只有少数类。这种情况下应该先手动 shuffle 数据或者用tf.keras.utils.train_test_split风格的方式自己划分。fit 返回的是一个 History 对象里面记录了每个 epoch 的 loss 和指标。很多人训练完就把它扔了其实这里藏着很多诊断信息history model.fit(x_train, y_train, epochs50, validation_split0.2, verbose2) train_loss history.history[loss] val_loss history.history[val_loss]对比 train_loss 和 val_loss 的走势是判断过拟合最直观的方法。训练 loss 一直降、验证 loss 开始回升就是过拟合开始了这时候再硬训只会更糟。3.3 evaluate 与 predict 的注意点model.evaluate用于在测试集上计算损失和指标model.predict用于输出预测结果。这两个 API 看起来简单实际用的时候有两个高频问题。第一个是 evaluate 和 predict 的数据格式。它们都接受 NumPy 数组或者tf.data.Dataset但如果用 Dataset要注意每个 batch 的产出必须和模型的输入输出严格对应尤其是当你有多个输入或多个输出时。Dataset 里的元组结构错了evaluate 会报一个很晦涩的拆包错误。第二个是 predict 在 batch_size 参数上的误区。很多人以为model.predict(x, batch_size32)会影响结果其实不会它只是决定一次喂多少数据只是影响速度和内存占用。推理结果在数学上是一样的。但 BatchNormalization 在 predict 阶段使用的是训练时积累的滑动平均统计量而不是当前 batch 的统计量所以结果稳定。这个设计经常被忽略但恰恰是 BN 能用于推理的原因。4. 回调函数训练提效的关键工具4.1 ModelCheckpoint自动保存最优模型训练深度学习模型最怕的不是收敛慢而是训到一半崩了或者最后一轮正好过拟合把之前最好的权重覆盖了。ModelCheckpoint 就是干这个用的。它允许你在每个 epoch 结束后按条件保存模型常用的模式是保存验证集指标最好的那次cp_callback tf.keras.callbacks.ModelCheckpoint( filepathbest_model.keras, monitorval_loss, save_best_onlyTrue, modemin, verbose1 )monitor 参数决定观察哪个指标mode 决定什么算好。min 表示越小越好lossmax 表示越大越好accuracy。这里很容易翻车monitor 写的指标跟你 compile 里的 metrics 名称不一致回调就永远不触发保存。如果你用了自定义指标或者没给指标起名最好先打印history.history看一眼实际的 key 名。4.2 EarlyStopping 与 ReduceLROnPlateauEarlyStopping 是防止无效训练的核心工具。它在监控指标不再改善时提前终止训练。关键参数是 patience也就是连续多少个 epoch 没改善才停。patience 设小了容易被暂时的波动误导设大了又白白浪费计算资源。我的习惯是小数据集 patience5大数据集 patience10 起步。ReduceLROnPlateau 和 EarlyStopping 是黄金搭档。它的逻辑是验证指标停滞不前时把学习率自动调低一个数量级factor 参数让模型在一个更精细的尺度上继续优化lr_scheduler tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue ) history model.fit( x_train, y_train, validation_split0.2, epochs200, callbacks[cp_callback, lr_scheduler, early_stop], verbose2 )这里有个被低估的参数restore_best_weights。设成 True 之后EarlyStopping 触发时会自动恢复到验证集上表现最好的那组权重而不是停在最后一轮。我见过太多人跑完训练之后还要手动去翻 checkpoint 文件其实一个参数就解决了。学习率调整的信息会出现在日志里注意观察 lr 字段如果发现 lr 从来没变过说明 ReduceLROnPlateau 的 monitor 或者 patience 可能配置不合理。4.3 TensorBoard 与自定义回调TensorBoard 回调是最直观的训练可视化工具。接法很简单tb_callback tf.keras.callbacks.TensorBoard( log_dirlogs/fit, histogram_freq1, update_freqepoch )histogram_freq 设置为 1 可以记录权重分布变化对排查梯度消失、梯度爆炸很有帮助。但注意它会在每个 epoch 时做额外的计算会拖慢训练调试完记得关掉。自定义回调的需求很常见比如你想在训练中途打印当前的学习率或者检测 loss 变成 NaN 后自动停止。做法是继承tf.keras.callbacks.Callback重写on_epoch_end之类的方法class LossLogger(tf.keras.callbacks.Callback): def on_epoch_end(self, epoch, logsNone): lr float(self.model.optimizer.lr.numpy()) print(fEpoch {epoch}: lr {lr:.6f}, loss {logs.get(loss):.4f})自定义回调的核心是能拿到self.model和logs。logs 是一个 dict里面存了当前 epoch 的所有指标。用这种方式可以完成很多框架不内置的功能比如在验证 loss 连续多少轮不降时给自己发个通知或者按自定义规则动态调整某个超参。这块自由度很大但注意不要在里面塞太重的计算逻辑回调是跑在训练主线程里的塞重活会拖慢训练。5. 模型保存、加载与持久化5.1 选择保存格式.keras 还是 .h5TensorFlow 的模型保存格式经历过几次变化。老教程里常见的.h5是 HDF5 格式现在官方主推的是.keras格式。两者的核心区别在于.keras格式能保存更多 Keras 特有的信息包括自定义层、自定义损失、完整的优化器状态序列化更稳定。.h5则兼容性更好适合需要跟旧代码或者别的工具链打交道的时候。我的原则很简单新写的代码统一用 .keras只有遇到旧项目迁移才用 .h5。保存方式model.save(mymodel.keras) model.save(mymodel.h5) # 加载 model tf.keras.models.load_model(mymodel.keras)加载模型的时候有个坑如果你在模型里用了自定义层或自定义损失函数直接 load_model 会报错需要在加载时提供编译时对应的自定义对象model tf.keras.models.load_model( mymodel.keras, custom_objects{MyLayer: MyLayer, my_loss: my_loss} )5.2 只保存权重的场景并不是所有时候都需要保存完整模型。如果模型的架构代码在项目里始终存在只保存权重就够了文件体积更小加载也更快model.save_weights(weights.weights.h5) new_model build_same_architecture() new_model.load_weights(weights.weights.h5)这里要求两次构建的模型架构完全一致包括层的顺序、名称、参数。层名称不一致时 load_weights 会静默跳过或者报错。如果只改了某层的 units 数量就去 load 旧权重最常见的报错就是 Shape mismatch。想避免这个可以在构建层时统一加上 name 参数让层身份完全可控。5.3 加载后继续训练与推理部署加载模型继续训练是个常见需求做法很简单load_model 之后直接再 fit 一次就行。但要注意一点加载的模型如果原封不动带优化器状态继续训练时会从保存时的优化器状态往下走如果只想用加载的权重、重新开始优化器可以在加载后重新 compile 一次。推理部署方面Keras 模型可以导出为 SavedModel 格式用 TensorFlow Serving 来跑model.export(export_dir)SavedModel 格式的特点是自包含里面同时有模型结构、权重和推理用到的函数服务端不依赖你的 Python 代码也能执行。这里要强调一个实践导出之前一定先跑一遍 predict 对比导出前后的输出确保数值一致。我碰到过几次导出后结果对不上的情况最后发现是模型里用了某些自定义前处理逻辑导出时没被包含进去。6. 常见问题与排查技巧实录6.1 维度不匹配最常见的报错Keras 的维度报错几乎是每个人都会撞上的墙。刚上手时我把错误分成两类一类是数据维度跟输入层不匹配另一类是层与层之间张量 shape 对不上。数据维度不匹配的解法是搞清楚你的输入格式。图像数据通常是 (样本数, 高, 宽, 通道数)文本序列是 (样本数, 序列长度)表格数据是 (样本数, 特征数)。写代码前先打印x_train.shape确认一下别想当然。层与层之间的 shape 问题我用一个习惯性检查手段创建好模型后立刻调用model.summary()看每一层输出 shape。如果用了函数式 APItf.keras.utils.plot_model的图里也会标出每层的输出维度。训练前先做这个检查能省掉一大半调试时间。6.2 过拟合与欠拟合的判断判断过拟合不能只看训练集一定要对比训练集和验证集的 loss 曲线。我建议把 loss 曲线画出来不用 fancy 的工具matplotlib 就够import matplotlib.pyplot as plt history model.fit(..., verbose0) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.show()如果你发现 train_loss 一直下降val_loss 升到某个点开始反弹那就是典型的过拟合。处理方法按优先级排序加 EarlyStopping 和 ReduceLROnPlateau加 Dropout 或正则化减小模型容量最后再考虑数据增强。反过来如果训练集 loss 都下不去那就不是正则化的问题而是模型容量不够、学习率不合适或者数据质量有问题应该先加层数、加宽度、调学习率。6.3 训练不稳定的排查思路训练过程中 loss 变成 NaN 是另一个高频噩梦。常见原因有四个学习率太大导致梯度爆炸、数据里有 NaN 值、损失函数计算过程中出现除零、某些层的权重初始化不当。排查顺序我一般是这样先检查数据里有没有 NaN再试调低学习率两个数量级然后看是不是损失函数的问题最后检查模型在第一个 batch 上的前向输出是不是正常的。如果你给卷积层和全连接层乱设了很大的权重初始化范围或者 Activation 用了不稳定的函数也会导致数值溢出。稳妥的组合是卷积层默认初始化全连接层用tf.keras.initializers.HeNormal()配合 relu 族输出层用tf.keras.initializers.GlorotUniform()或默认。这些初始化策略的细节不用死记只要记住不同激活函数配不同初始化器这个原则。BatchNormalization 也会偶发 NaN。一种已知情况是训练时尝试用非常小的 batch_size导致 BN 统计量出现极端值。另一种是学习率太大BN 的平移缩放参数在前期震荡过大。这两种都能通过增大 batch_size 或降低学习率缓解。6.4 一个实用排查清单最后送大家一张速查表是我自己排查问题时固定过一遍的现象优先排查项常用解法loss 不下降学习率过小/过大、数据未归一化调整 learning_rate检查数据 scaleloss 变成 NaN数据含 NaN、梯度爆炸检查数据、调低 lr、加梯度裁剪训练好但验证差过拟合、数据分布差异加正则化、检查 validation_split验证集没变化指标名写错、数据泄露打印 history 字典检查数据切分模型加载报错自定义层缺失、格式不匹配提供 custom_objects确认保存格式显存不足batch_size 过大调小 batch_size或用生成器喂数据我在实际工程中体会最深的一点是Keras API 的简单是文档化的简单真正上手总会遇到各种边界情况。遇到问题不要急着改代码先打印 shape、打印 history、打印一条 predict 的输出大多数问题都会在打印出来的结果里现出原形。另外每次写完模型先拿一小批数据跑通一遍再做全量训练这个习惯帮我避掉了无数个深夜调 bug 的坑。如果你刚开始接触这套 API不用贪多从 Sequential 加两个 Dense 层开始再把函数式 API 和回调用熟基本就能覆盖日常绝大多数任务了。
阅读完成 · 觉得有帮助?
咨询建站