1. 从张量到模型TensorFlow 2.0 到底改了什么这个系列到第二篇了。上一篇把 TensorFlow 2.0 的基本运行机制、张量操作和自动求导原理过了一遍今天直接进入真正干活的部分——用 Keras 把模型搭出来、训练起来、调出效果。这是整个系列里最实用、也最容易被忽视的一篇因为很多人以为 Keras 就是个快速建模的玩具真正上手做项目才发现问题一堆分布策略怎么配、回调函数怎么用、模型保存格式怎么选、自定义损失函数踩了什么坑这些才是实战里每天都要面对的东西。先说清楚一个概念TensorFlow 2.0 和 Keras 是什么关系。TensorFlow 2.0 把 Keras 正式纳入了官方高层 API也就是说你不再需要单独安装 Keras直接from tensorflow import keras就行。这背后是谷歌在 2.0 版本里做的一次重大战略调整——把原来 TensorFlow 1.x 那套繁琐的Session、placeholder、graph机制彻底废弃换成了动态图模式Eager Execution让用户可以像写普通 Python 一样写模型逻辑调试的时候直接打断点看中间结果。对初学者来说最直观的感受就是代码量至少少了一半。原来用 1.x 写一个线性回归你需要定义 placeholder、定义变量、初始化全局变量、创建 Session、run 两次一次初始化一次计算大约 40 行代码。2.0 时代用 Keras10 行内搞定训练流程。但这不意味着 2.0 是给新手玩的简化玩具——恰恰相反Keras 的灵活性和扩展能力足以承载工业级项目前提是你真正理解了它的设计逻辑。我见过太多人卡在同一个误区把 Keras 当成黑盒model.fit()一调就等结果出了 NaN、梯度爆炸、loss 不下降这些问题完全无从下手。这篇文章的核心目的就是把这层黑盒拆开给你看——从建模方式的选型逻辑到训练流程的完整掌控再到分布式训练和部署把每个环节背后的“为什么”说清楚。适合刚学完 Python 基础、想进入深度学习实战的初学者也适合已经用过 TensorFlow 1.x、想尽快迁移到 2.0 做项目的开发者。2. 两种建模方式的选型与实战场景Keras 提供了三种建模方式Sequential 模型、Functional API、以及 Subclassing模型子类化。很多新手不知道该怎么选我的建议非常简单能用 Sequential 就用 Sequential用不了就上 FunctionalSubclassing 只在特殊需求下才考虑。2.1 Sequential 顺序模型的适用边界Sequential 是最直白的一种方式它对应的是“输入经过一系列层依次变换到输出”的线性堆叠结构。import tensorflow as tf from tensorflow.keras import layers model tf.keras.Sequential([ layers.Dense(128, activationrelu, input_shape(784,)), layers.Dropout(0.2), layers.Dense(10, activationsoftmax) ])这段代码就定义了一个结构最简单的全连接网络适合做手写数字识别这类标准化任务。Sequential 的本质是Layer的线性容器它的内部维护了一个层的列表数据按顺序流过每一层。适用场景很明确单输入、单输出、数据流向没有分叉。如果模型结构是一根直线就用它。但它的局限也非常明显无法实现多输入比如文本分类同时输入标题和正文、多输出比如目标检测同时输出类别和坐标、层之间的跳跃连接ResNet 那种结构。一旦遇到这些情况强制用 Sequential 会让代码变得极其别扭。2.2 Functional API 的灵活架构能力Functional API 是实战里面最常用、也最值得学习的建模方式。它通过“张量在层之间传递”的方式来构建网络你可以把层之间的连接关系画成一张图这张图可以是有分支的、有汇合的甚至是有环的某些循环结构。inputs tf.keras.Input(shape(32,)) x layers.Dense(64, activationrelu)(inputs) x layers.Dense(64, activationrelu)(x) branch_a layers.Dense(32, activationrelu)(x) branch_b layers.Dense(16, activationrelu)(x) concatenated layers.Concatenate()([branch_a, branch_b]) outputs layers.Dense(1, activationsigmoid)(concatenated) model tf.keras.Model(inputsinputs, outputsoutputs)注意这里每个层对象的调用方式layers.Dense(64)(inputs)这行代码第一对括号是构造层实例第二对括号是把张量传入层中得到新的输出张量。这种“函数式”的调用方式让模型结构变得非常直观——每一行代码都在描述一个张量变换步骤你可以随时打印中间某个张量的 shape 来确认数据形态是否正确。Functional 模型还有一个较大的优势模型是可序列化的保存为文件后再加载结构会完整还原。因为它本质是一个“结构描述”而不是一团执行代码。这一点在模型部署和迁移时非常关键。2.3 Subclassing 自定义模型的适用判断第三种方式是继承tf.keras.Model在call方法里自己写前向传播逻辑。class MyModel(tf.keras.Model): def __init__(self): super().__init__() self.dense1 layers.Dense(64, activationrelu) self.dense2 layers.Dense(10, activationsoftmax) def call(self, inputs): x self.dense1(inputs) return self.dense2(x)这种方式最灵活——你可以写任意复杂的张量操作比如条件判断、循环、甚至自定义的反向传播逻辑。但这把双刃剑的另一面是你失去了 Keras 对模型的静态结构分析能力模型的summary()无法正确显示中间层的输出形状保存模型时结构信息可能丢失分布式策略在某些情况下也会失效。我的经验准则是如果模型结构可以用 Functional API 表达就优先用 Functional。那些必须用 Subclassing 的场景通常是模型包含动态的控制流比如循环次数取决于输入长度、需要精细控制每一层的前向逻辑或者要做一个全新的研究型网络结构无法用现成层组合出来。常规生产项目里Functional 已经覆盖了至少 80% 的需求。3. 训练流程的完整掌控从 fit 到自定义训练循环模型定义完之后接下来是最核心的部分——训练。Keras 提供了从高到低三个抽象层级的训练方式我的建议是项目初期用model.fit()快速验证思路出问题时降到自定义训练循环精调。3.1 model.compile 的参数选择逻辑compile这一步决定了模型怎么学它有三个关键参数优化器、损失函数、评估指标。model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losstf.keras.losses.SparseCategoricalCrossentropy(), metrics[accuracy] )优化器的选择直接关系到收敛速度和最终效果。实战里最常见的选择是 Adam因为它自适应调整每个参数的学习率对新手来说容错率很高默认参数就能在大多数任务上有不错的表现。但如果你做的是计算机视觉任务或者需要精细控制训练节奏SGD 配合动量、配合学习率衰减往往能得到更好的泛化性能——本质原因是 Adam 的自适应机制在某些情况下会导致模型记住训练集而非学到通用特征。损失函数则需要根据任务来定二分类用BinaryCrossentropy多分类用CategoricalCrossentropy或SparseCategoricalCrossentropy回归用MeanSquaredError或MeanAbsoluteError。这里有一个容易踩的坑如果你的标签是整数索引形式比如每个样本的类别号 0-9就必须用SparseCategoricalCrossentropy如果标签是 one-hot 编码的向量就要用CategoricalCrossentropy。用错的话会直接报错或者得到的损失值完全走样。3.2 回调函数训练过程的定时器与裁判员回调是 Keras 里最被低估的功能之一。model.fit()里传一个回调列表就能在训练的不同节点自动执行特定操作。实战里最常用的几个callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3 ), tf.keras.callbacks.ModelCheckpoint( filepathbest_model.keras, monitorval_acc, save_best_onlyTrue ) ]EarlyStopping是防过拟合的第一道防线——它监控验证集指标如果在指定轮数内没有改善就提前结束训练并且理论上可以通过restore_best_weightsTrue自动恢复到验证集表现最好的那个权重。ReduceLROnPlateau是调参利器——当模型停滞时自动降低学习率相当于给下山的人减小步长帮助它在谷底附近精细搜索。ModelCheckpoint则确保你任何时刻中断训练都不会丢失效果最好的那一组权重。这几个回调组合起来就能让训练过程在没有人工干预的情况下自动应对大多数问题。新手经常犯的一个错误是不用ModelCheckpoint等训练了几十轮之后发现过拟合了前面的权重又没保存只能从头再来——这种时间成本其实才是训练过程里最大的浪费。3.3 自定义训练循环的适用范围model.fit()虽然好用但遇到输入输出之间有复杂逻辑比如多任务学习里每个任务需要独立计算损失、或者需要精细控制梯度更新过程比如梯度裁剪、对抗训练里的同时优化两个网络时就得解包到自定义训练循环。optimizer tf.keras.optimizers.Adam(0.001) loss_fn tf.keras.losses.SparseCategoricalCrossentropy() tf.function def train_step(images, labels): with tf.GradientTape() as tape: predictions model(images, trainingTrue) loss loss_fn(labels, predictions) gradients tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(gradients, model.trainable_variables))这里关键的机制是GradientTape它会记录这段代码块里所有张量操作然后调用tape.gradient()自动计算损失对模型参数的梯度。这也是 TensorFlow 2.0 自动微分的核心——你只要写清楚前向传播过程反向传播的梯度计算交给它自动搞定。自定义训练循环最大的好处是每一步都在你的控制之下可以在梯度更新之前插入梯度裁剪、梯度噪声、梯度累积等操作。但必须承认大多数时候用model.fit()就够了。我见过很多人从入门教程里学了自定义训练循环就一直手写循环不用 fit结果代码量多了好几倍还容易在梯度累积、分布策略这些细节上出错。我的建议是默认用 fit有问题降到自定义不要为了炫技增加复杂度。4. 进阶实战损失函数、混合精度与迁移学习这部分是让模型从“跑得通”迈向“效果好的”关键环节。纯堆层数和数据不如在训练策略上做文章这里展开讲几个我实际项目里用过的进阶手段。4.1 自定义损失函数的两种实现路径内置损失函数能覆盖大多数场景但有些任务需要针对性设计。比如目标检测任务里一张图里大部分区域是背景如果直接用交叉熵模型会倾向把所有区域都预测成背景——这时就需要用带权重调节的损失函数减少背景区域对梯度的主导作用。Keras 实现自定义损失函数有两种方式一个是用函数定义一个是用类定义。# 函数方式 def weighted_crossentropy(y_true, y_pred): weights tf.where(y_true 1, 2.0, 1.0) return tf.reduce_mean( tf.keras.losses.binary_crossentropy(y_true, y_pred) * weights ) # 类方式适合带状态或更复杂的逻辑 class FocalLoss(tf.keras.losses.Loss): def __init__(self, alpha0.25, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def call(self, y_true, y_pred): epsilon 1e-7 y_pred tf.clip_by_value(y_pred, epsilon, 1.0 - epsilon) loss -y_true * tf.math.pow(1 - y_pred, self.gamma) * \ tf.math.log(y_pred) - (1 - y_true) * \ tf.math.pow(y_pred, self.gamma) * tf.math.log(1 - y_pred) return tf.reduce_mean(loss)函数方式的返回值必须是标量或者与 batch 维度一致的张量Keras 会自动对 batch 维度求平均。类方式的好处是可以保存额外的超参数比如 Focal Loss 的 alpha 和 gamma而且看起来更清晰。我个人的经验是项目里如果只有一两个自定义损失函数用函数方式就够了如果损失函数逻辑复杂且要复用写成类继承tf.keras.losses.Loss更合适。需要注意y_true 和 y_pred 的形状必须处理好不要等 Keras 报错了才发现维度不匹配。4.2 混合精度加速的收益与限制混合精度是 2.0 之后非常实用的一个加速手段。原理很简单在 GPU 上大部分张量操作使用 float16 计算而涉及更新权重的关键操作保持 float32这样就减少了数据读取量和计算量让训练速度提升明显。tf.keras.mixed_precision.set_global_policy(mixed_float16)这一行代码就能开启混合精度训练。在最新的 GPU比如 Ampere 架构及以后上实测能带来 2-3 倍的加速效果。但这里有几个实用的细节开启后模型内部某些层的输出会被强制转成 float16所以模型最后一层如果是损失敏感的比如 softmax 输出概率可能需要手动加一行tf.cast(output, tf.float32)再输入损失函数。另外mixed_float16策略下梯度会放大以避免下溢但优化器参数比如 Adam 的一阶和二阶动量仍然以 float32 精度保存。如果你用的 GPU 不支持 bfloat16 或者 float16 计算这个加速策略不会生效反而可能因为精度转换增加额外开销。所以实践前先确认一下 GPU 的算力版本。4.3 迁移学习站在巨人的肩膀上迁移学习在实战里的价值怎么强调都不过分。从头训练一个图像分类模型需要大量数据和长时间 GPU 训练但用预训练模型做迁移学习小规模数据集也能获得不错的效果。Keras 加载预训练模型的方式非常简单base_model tf.keras.applications.ResNet50( weightsimagenet, include_topFalse, input_shape(224, 224, 3) ) base_model.trainable False inputs tf.keras.Input(shape(224, 224, 3)) x base_model(inputs, trainingFalse) x layers.GlobalAveragePooling2D()(x) x layers.Dropout(0.2)(x) outputs layers.Dense(10, activationsoftmax)(x) model tf.keras.Model(inputsinputs, outputsoutputs)这里的关键在于冻结基座网络trainable False只训练新加的全连接层。为什么这样设置因为预训练模型在大规模数据集上已经学到了通用的特征提取能力比如边缘、纹理、形状等这些底层特征对绝大多数视觉任务都有用。冻结它们可以防止小数据集的噪声破坏这些已经学好的特征。训练一段时间后可以考虑解冻部分顶层比如最后几十层用较低的学习率微调——这个阶段的学习率通常是原来的十分之一甚至更低否则会破坏预训练的权重。迁移学习领域有一个经验参数是否微调预训练模型取决于新数据和源数据之间的相似度。如果数据高度相似比如都是自然场景图片可以大幅微调如果数据分布差异很大比如卫星遥感图建议只微调高层特征或者干脆冻结所有前层只训分类头。这些实践判断比盲目增加数据量更有效。5. 模型保存、加载与多卡训练的实战方案模型训练达到目标之后紧接着就是部署环节。怎么把训练好的模型保存下来、加载回来、甚至跨设备运行这里面的坑不下于训练环节。5.1 SaveModel 与 Keras 原生格式的选择TensorFlow 2.0 里面保存模型有几种格式本质区别在于是保存完整模型结构权重还是只保存权重。早期用的.h5格式是 Keras 的序列化格式能保存结构、权重和优化器状态加载后直接恢复训练。新版推荐用的是.keras格式它是 Keras 3 之后的官方推荐格式格式更规范与新版本的兼容性也更好。# 推荐完整模型保存 model.save(my_model.keras) loaded_model tf.keras.models.load_model(my_model.keras)还有一种情况是导出为 SavedModel 格式它的用途偏重部署——进一步转化为 TensorFlow Lite 或 TensorFlow Serving 的格式时SavedModel 是官方推荐中的第一个环节。SavedModel 最大的特点是自包含模型结构、权重、计算图、额外资源都被打包到同一个目录里任何加载了 TensorFlow 的环境都能直接用。相比之下.keras文件需要同一版本的 Keras 才能无痛加载在版本升级后可能会遇到兼容性警告。这里有一个实践要点如果要把模型部署到生产环境做推理比如 API 服务导出成 SavedModel 格式然后用 TensorFlow Serving 或直接用 Python 加载服务方式是兼容的。如果只是本地继续训练、调参就存.keras就行占用空间小、读写快。# 部署推荐格式 model.save(saved_model_dir, save_formattf)5.2 多卡训练的三个层次多 GPU 训练是另一个容易踩坑的领域。TensorFlow 2.0 提供了从简单到复杂的不同并行策略。最直观的是“数据并行”即每个 GPU 拿一部分 batch 计算梯度然后汇总更新参数。用 Keras 实现起来非常简洁strategy tf.distribute.MirroredStrategy() with strategy.scope(): model tf.keras.Sequential([...]) model.compile(optimizeradam, losssparse_categorical_crossentropy)MirroredStrategy的底层逻辑是每张 GPU 都有一份模型副本训练时每张卡独立处理各自的数据分片通过 All-Reduce 通信同步梯度。strategy.scope()这一行非常关键——模型必须在分布策略上下文里创建才会自动获得“分布式模型”的身份。如果模型在scope外定义即使后面进入scope也不会启用分布式训练。多卡训练时还有两个细节需要留意batch_size是“每张卡”的批大小如果你写了batch_size64且使用两张卡全局批大小实际上是 128开学习率时要有对应调整。model.fit()的steps_per_epoch在分布式环境里也容易被忽视——它默认取训练集样本数除以“全局批大小”但如果系统自动给的数值不准确可能出现一个 epoch 只训练了部分数据的情况。这些细节不处理干净训练结果会莫名其妙变差。5.3 排查疑难杂症的经验技巧长期做深度学习项目的人大都会积攒一簿“疑难杂症”笔记。这里列举几个我在实际项目中踩过、并最常看到的坑。loss 为 NaN 的问题根因通常是学习率太大尤其是使用 Adam 时初始学习率设为 0.01 或更大、输入数据里有 NaN 值、模型存在数值稳定性风险比如 log 函数收到 0。排查顺序建议是先检查输入数据tf.debugging.check_numerics可以帮你在训练中定位第一个非数值的位置、然后调低学习率、再考虑损失函数内部是否有 log(0) 之类的操作。维度不匹配Keras 在批处理阶段对张量形状的要求很严格最常见的是最后的全连接层输入维度没有对上。排查方法是打印model.output_shape和中间层的形状。或者直接写一个(batch_size, 原输入维度)的哑数据传入模型测试到哪个层形状坍塌了。过拟合和欠拟合的特征判断如果你看到训练准确率越来越高、验证准确率提升后开始下降这就是典型过拟合标志。应对手段按优先级排列是增加数据扩增、降低模型容量、加大 Dropout、提前结束EarlyStopping。欠拟合的标志是训练和验证的准确率都“卡住不动”这往往说明模型容量不足或学习率太低。需要增大网络容量或者换一个更复杂的模型。这些排查思路看起来零碎但它们是实战里每天都会依赖的东西。我自己从 TensorFlow 1.x 迁移来 2.0 的经历是一开始吐槽过“为什么又要学一遍新语法”后面真正用它做完整项目才发现动态图和 Keras 带来的效率提升是肉眼可见的——数次排查 bug 的耗时减少了大概一半左右。许多从 1.x 时代过来的人难以想象不用打印每个变量的 shape 挨个盯也不用担心 Session 里Placeholder维度的预定义。动态图下直接在模型里写print(x.shape)打断点调试体验已经和写普通 Python 程序没有太大区别。最后再分享一个我在实际项目里摸索出来的小经验多卡环境和混合精度不要在第一版模型上同时启用因为两个都开出了问题很难定位是谁造成的。先跑通单卡、纯 float32 模型确认逻辑正确再逐渐叠加混合精度、多卡以及更高级的回调策略。先求稳再求快。这个系列后续还会继续深入实战卷积网络怎么设计、数据增强怎么做、模型如何部署到移动端都会逐篇展开。我自己在学习过程中最大的感受是深度学习框架的更新速度远比你想象的快但核心的建模思维、训练策略、排查逻辑不会变。把今天讲的这些基础打牢后面无论框架变成什么样你都能快速迁移。