TensorFlow 2.x入门:从自动微分到训练第一个分类模型 去年写基础1的时候我讲的是张量创建、索引、变形、切片这些“算数”。当时评论区就有不少人在问同一个问题这些张量操作学完了怎么打开别人的项目还是看不懂自己写训练代码更是无从下手。这还真不是笨而是基础1到基础2之间有一段非常关键的“肌肉记忆”没建立起来。这篇文章我就把这部分拆开讲透目标读者很明确TensorFlow刚装好、张量操作已经有一点点感觉、但还没完整跑通过一个模型训练的人。你不需要有高数基础但请准备好跟着代码走一遍因为TensorFlow真正难的不是算数而是“自动微分”和“训练流程”这两个抽象概念。TensorFlow 2.x的安装和版本选择基础里其实就一句话pip install tensorflow但落地时会遇到很多琐碎问题。如果你已经装好了那这篇文章可以直接看如果还没装好先提醒你一点不要只装CPU版先用GPU版或者至少用带AVX指令集的版本否则训练速度会慢到你怀疑人生。下面进入正题。1. GradientTape从“会算张量”到“能训练模型”的分水岭1.1 训练不是“跑一遍数据”而是“循环调参数”很多初学者把“训练”理解成把数据送进模型、得到输出完事。这是错的。真正的训练是一个循环前向算出预测值和损失反向求出每个参数的梯度然后用梯度更新参数再拿着新参数重复这个过程。这个循环里前向计算靠的是你基础1学的张量运算可反向求梯度的步骤怎么办神经网络的损失函数是几百层复合函数如果手推导数哪怕是线性层叠个五层我都不想算。TensorFlow 2的解决方案就是tf.GradientTape。它的设计思路很朴素在tape上下文里执行的所有张量操作都被记录下来然后调用tape.gradient()时从结果反推每一步的梯度。1.2 从一次求导到完整的参数更新先看一个最小例子import tensorflow as tf x tf.Variable(3.0) with tf.GradientTape() as tape: y x ** 2 dy_dx tape.gradient(y, x) print(dy_dx.numpy()) # 6.0这里x**2对x求导的结果是2x在x3处就是6。注意一个关键点GradientTape默认只“看”tf.Variable类型的变量普通tf.Tensor不会被追踪。如果你需要在普通张量上求梯度必须手动调用tape.watch(x)。把这个机制套到训练里你就理解了参数更新的写法optimizer tf.keras.optimizers.SGD(learning_rate0.1) with tf.GradientTape() as tape: loss tf.reduce_mean((y_pred - y_true) ** 2) grads tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables))这里有个很容易被忽略的细节tape.gradient(loss, model.trainable_variables)拿到的是模型里所有可训练参数对应的梯度列表顺序和model.trainable_variables完全一致。optimizer.apply_gradients会把梯度和变量一一绑定然后执行w w - learning_rate * grad。所以优化器不是“自动知道”怎么更新参数的它只是在执行你摆好的梯度更新公式。1.3 GradientTape的三个隐蔽槽点我在实际调试时踩过几个坑值得提前说。第一个坑是磁带是一次性的。默认情况下一次tape.gradient()调用之后磁带资源就释放了。如果你需要针对同一个前向传播结果计算多次梯度比如计算二阶导数必须创建persistentTrue的磁带with tf.GradientTape(persistentTrue) as tape: y x ** 3 first tape.gradient(y, x) # 3x^2 second tape.gradient(first, x) # 6x del tape # 手动释放第二个坑是磁带记录的是一连串操作不是操作后的数值。你如果在tape上下文里把loss重新赋值梯度追踪的仍然是最初那个计算链路但访问的又是新变量结果会出现“梯度计算对象不同”的诡异情况。为了安全保持tape里只记录一个与参数相关的损失计算别在里面做太多与训练无关的杂事。第三个坑是有时候你根本不需要手写GradientTape。Keras的model.fit内部已经封装了这套逻辑。那为什么还要学因为你在读论文源码、自定义训练循环、或者想给某一段特殊计算单独指定梯度时没有它就寸步难行。基础阶段理解它的机制比手写每个细节更重要。2. Keras三层封装Sequential、Model与Layer的取舍2.1 三种抽象分别解决什么问题GradientTape解决的是“怎么算梯度”但如果你用原始张量操作去拼一个神经网络代码会非常啰嗦。Keras的高层抽象让你在几行内定义网络model tf.keras.Sequential([ tf.keras.layers.Dense(64, activationrelu, input_shape(4,)), tf.keras.layers.Dense(3, activationsoftmax) ])这段代码里你看到了两个核心概念Sequential是层的容器Dense是一个全连接层。如果你是初学者用Sequential就够了但对于更复杂的网络结构你要理解Model函数式模型和Layer自定义层的分工。tf.keras.Model用起来像搭积木你先用Layer搭出网络的输入和输出再把它包装成Model。它比Sequential更自由可以处理多输入、多输出、共享层等复杂结构。我在做一个小实验时发现Sequential解决不了的残差连接把前面的输出加到后面的输出上用Model接口反而更自然。2.2 input_shape与参数初始化的隐性问题新手最常见的第一个报错是“模型第一层的input_shape没定义”然后summary显示不了参数数量保存模型也报错。Dense层的input_shape只需要在模型第一层指定或者在Sequential外面传入Input(shape(4,))。这个参数决定了网络输入维度后续层的维度会自动推断model tf.keras.Sequential([ tf.keras.layers.Input(shape(4,)), tf.keras.layers.Dense(64, activationrelu), tf.keras.layers.Dense(3, activationsoftmax) ]) model.summary()另一个隐藏细节是激活函数。Dense层默认activationNone也就是纯线性操作。很多新手以为不写激活函数也能用结果模型叠多少层都等价于一层线性变换训练出来效果等于零。常见的做法是隐藏层用relu二分类输出层用sigmoid多分类用softmax。还有一个特别容易被忽略的参数是use_bias。Dense层默认带一个偏置变量bias如果你在上一层用了BatchNormalization之类的归一化层可以考虑设置use_biasFalse减少重复参数。这种细枝末节的调优问题在你模型效果不够好时很有用。2.3 什么时候值得写自定义Layertf.keras.layers内置了几十种层大部分场景你都用不上自定义。但以下时机你会需要比如你要实现论文里一个“把输入除以一个学习的缩放参数”的层或者你要给层内部加一个与梯度无关的debug信息。自定义Layer的核心是重写两个方法build(input_shape)里创建变量call(inputs)里写前向逻辑。下面这个例子里我定义了一个带可学习缩放参数的层class ScaleLayer(tf.keras.layers.Layer): def __init__(self): super().__init__() self.scale tf.Variable(initial_value1.0, trainableTrue) def call(self, inputs): return inputs * self.scale为什么build里创建变量比在__init__里好因为有些变量维度需要根据输入自动推断在build里用input_shape初始化更稳妥同时也避免了一个隐蔽问题模型加载权重时只重建结构变量必须存在且有确定命名。自定义层一开始够用就行别过度设计。3. 手把手训练第一个分类模型完整链路与回调策略3.1 数据准备归一化、reshap与类型转换我们用一个最经典的MNIST手写数字分类来跑通流程。代码很简单但每一步都有讲究(x_train, y_train), (x_test, y_test) tf.keras.datasets.mnist.load_data() # 28x28的灰度图铺平成784维向量 x_train x_train.reshape(-1, 784).astype(float32) x_test x_test.reshape(-1, 784).astype(float32) # 归一化到0~1范围 x_train / 255.0 x_test / 255.0 # 标签转成0~9的整数形式 y_train y_train.astype(int64) y_test y_test.astype(int64)这里两个决定训练成败的细节第一/ 255.0是必须的。你如果忘记归一化像素值在0~255之间经过网络加权和后数值会很大激活函数直接饱和梯度趋近于零训练就锁死了。第二reshape的-1表示自动推导该维度正好是样本数量。你把数据弄成(60000, 784)后模型输入维度就对上了。类型也要注意默认是uint8很多运算会溢出转成float32保险。3.2 compile三要素怎么搭配model.compile里的三个参数优化器、损失函数、评估指标。新手最容易在这里配出“很怪但没报错”的组合。以我们的多分类任务为例model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] )为什么用sparse_categorical_crossentropy而不是categorical_crossentropy因为标签是整数0~9不是One-hot向量。如果是One-hot才用categorical_crossentropy。选错不会报错但会换来莫名其妙的低准确率或者训练起来很别扭。这个坑我见过太多次了。优化器不必纠结adam是大多数入门任务的默认选择。它的自适应学习率机制能容忍较大的初始学习率选择欠调也通常能收敛不像裸SGD那么敏感。3.3 fit训练与回调函数训练不能干等callbacks [ tf.keras.callbacks.EarlyStopping(patience3, restore_best_weightsTrue), tf.keras.callbacks.ModelCheckpoint( best_model.keras, save_best_onlyTrue ) ] model.fit( x_train, y_train, epochs20, batch_size64, validation_split0.2, callbackscallbacks, verbose1 )verbose1是打印进度条verbose0是静默verbose2是只打印一行。训练小模型时怎么都行但模型大了以后我习惯用verbose2避免终端被进度条刷屏。回调函数是训练质量的保障。EarlyStopping的意思很直白连续3个epoch验证集损失不下降就提前停止同时恢复验证集性能最好的权重。ModelCheckpoint则让你不用全训练完再担心断电每个epoch结束都会把最优模型存下来。实战中我几乎次次都带这两个回调。这里还有个容易犯的错误validation_split0.2是从训练集末尾切20%出来如果数据不是随机打乱的比如全是按类别排序的那验证集可能只包含部分类别训练就会“发疯”。所以要在fit前手动tf.random.shuffle数据或者把验证集直接用现成的x_test传进validation_data(x_test, y_test)省去拆分麻烦。3.4 evaluate与predict的区别训练完你自然会想看看模型到底多准。evaluate和predict是不同的test_loss, test_acc model.evaluate(x_test, y_test) print(fTest accuracy: {test_acc:.4f}) probabilities model.predict(x_test[:10]) predicted tf.argmax(probabilities, axis1) print(predicted.numpy())evaluate返回的是你在compile里指定的指标。而predict返回的是每个类别的概率形状是(样本数, 10)你不能直接拿来当标签。需要配合tf.argmax取出最大概率对应的索引。这个转换如果你忘记后面画混淆矩阵、算F1时都会乱套。4. 训练中五个最隐蔽的“无声陷阱”4.1 损失函数和标签格式不匹配前面说了categorical_crossentropy和sparse_categorical_crossentropy的区别但还有一个更微妙的陷阱如果你的标签是float32的0~9整数而损失用了categorical_crossentropy代码不会报错准确率却奇低。反过来标签是One-hot向量却用了sparse版本也一样静默出错。排查时先打印y_train[:5]确认数据长什么样再决定损失函数。4.2 学习率太小或太大adam对学习率不算敏感但也不是万能。学习率设1e-5训练10个epoch损失下降极慢你以为模型坏了其实是步子迈得太小。设成1.0损失曲线忽高忽低像过山车甚至直接NaN。我建议从1e-3起步如果损失下降太慢才考虑调到1e-2如果震荡了回退到3e-4。有一招是最实用的用ReduceLROnPlateau回调让平台期自动把学习率降一半训练后期特别有用。4.3 过拟合与早停的演示“训练损失一直降验证损失却上升”——这是过拟合最经典的样子。MNIST这个小任务也会过拟合尤其是模型容量很大的时候。EarlyStopping在这里正好派上用场。另外更稳定的做法是加入dropout层model.add(tf.keras.layers.Dropout(0.5))Dropout的作用是训练时随机丢弃一部分神经元迫使网络学到更鲁棒的特征。注意它只在训练时生效预测时不生效Keras已经帮你处理好这个细节。4.4 数据形状与dtype的细节MNIST数据本身很干净但真实数据问题就多了。最常见的是numpy数组维度是(60000,)模型期望(60000, 784)于是给你一堆维度报错。另一个坑是数据是int型而模型权重是float32计算时框架会尝试自动转换但某些自定义损失函数里tf.cast(y_true, tf.float32)写漏了梯度传递会莫名中断。建议在进入模型前统一检查x.dtype、x.shape、y.dtype、y.shape。4.5 随机种子与可复现性深度学习到处是随机性参数初始化、数据打乱、Dropout丢弃。你上午跑出的结果和下午跑出的结果不一样不代表代码有问题只是随机种子不同。但如果你要复现实验、向别人汇报结果那就要在开头固定所有随机源import os import random import numpy as np import tensorflow as tf os.environ[TF_DETERMINISTIC_OPS] 1 random.seed(42) np.random.seed(42) tf.random.set_seed(42)注意设置了TensorFlow的种子还不够因为数据shuffle可能由numpy管理Dropout的随机来自TensorFlow但也会跨线程。这种“试了好几次得到不同结果”的问题一多半是随机性造成的先固定种子再排查逻辑。5. 2024年TensorFlow与PyTorch的流行趋势基础学完该怎么选5.1 两个框架的现状对比聊这个热点话题前我得声明我不是为了引战而是给选择困难的人一个参考。2024年的实际情况是PyTorch在学术研究、论文复现中的热度已经非常强势大量新模型的首发实现都是PyTorch版。而TensorFlow的阵地更多在生产部署、移动端、TPU生态和传统企业场景。我做了一个简单的对比维度TensorFlowPyTorch学术论文代码偏少很多要自己转偏多尤其是新模型生产部署TF Serving、FTLite生态成熟也有TorchServe但案例相对少移动端/嵌入式TFLite优势明显生态在追赶学习曲线Keras高层很顺底层略绕更接近Python直觉调试友好动态图灵活性动态图默认但复杂分支仍建议tf.function本身就是动态图随意debug你要是去翻GitHub上2024年的热门项目会发现PyTorch的星标数增长明显更快。但你要想把这个模型部署到手机App或者生产服务器上TensorFlow的整套工程化方案依然能打。这不是一个能靠“谁更强”来回答的问题而是“哪个更适合你的目标”。5.2 API设计差异对初学者到底意味着什么从学习角度说TensorFlow 2的Keras高层API让“训练一个模型”变得非常快。你再看看PyTorch实现同样的MNIST训练通常要手写一个for epoch训练循环中间还要自己写optimizer.zero_grad()、loss.backward()、optimizer.step()。对很多刚入学的人来说Keras的速成感是PyTorch给不了的。但反过来PyTorch的手写训练循环会让你更早地直面“梯度清零、反向传播、参数更新”这些核心概念而这种直面在Debug逼格上帮助非常大。我的个人体感是Keras像一个有护栏的自动挡车新手不容易出事故也能顺利抵达目的地PyTorch更像手动挡学习时手忙脚乱但引擎盖下的原理暴露得更多。如果你的目标只是快速做出一个Demo、跑通流程TensorFlowKeras很舒服如果目标是深入理解深度学习内部机制、追新论文那PyTorch的手动循环会给你更多学习素材。5.3 我的务实建议很多人在“该学谁”上耗了很久结果一个都没学会。我的建议很直白你如果还没确定方向先跟一个框架走到底。如果你所在的公司、学校、导师已经在用PyTorch别犹豫直接PyTorch。如果你是本来就想走工程部署路线或者所在企业技术栈就是TensorFlow系那继续TensorFlow完全没问题。如果你想两条腿走路那也不要同时起步。先啃透一个框架的数据加载、模型构建、训练循环、保存加载、部署思维。这些概念在两个框架里高度相通只不过API名字不同。等你把TensorFlow这套流程跑得滚瓜烂熟切到PyTorch基本就是一个下午的事。反过来也一样。真正可怕的不是选错框架而是每个框架都只学了个开头就换另一个。还有一点要提醒2024年热门的“大模型”相关工具链比如HuggingFace Transformers底层默认走PyTorch。你如果之后的兴趣方向是NLP或大模型微调那从一开始就主攻PyTorch会省下很多折腾。这一点我在2023年末给朋友选型时就看出来了到了2024年更明显。写到这里我想起带新人的时候常说的那句话TensorFlow的基础不是看会的是一遍遍跑通bug练会的。我见过太多人卡在第四章这些“无声陷阱”上卡到怀疑自己是不是不适合深度学习。其实每个坑背后都有一个非常具体的机制原因只要你学会打印shape、打印dtype、打印每层输出大部分问题半小时内都能定位。下一篇文章我准备讲讲如何用tf.data高效准备真实数据集以及如何自定义训练循环去“揭开Keras的面纱”。如果你跟着这篇文章跑通了第一个模型欢迎在评论区告诉我你卡在过哪个坑我们下一篇接着排。