Skip to content

反向传播与自动微分

本页速览 反向传播是深度学习训练的地基:链式法则 + 梯度回传。本文从"为什么需要梯度"讲起,手推一个小网络逐步求导,再过渡到自动微分的两种模式,最后给出 PyTorch autograd 的正确用法、常见误区与梯度消失/爆炸的根源。

反向传播与自动微分

一句话定义:反向传播(backpropagation)是用链式法则高效计算损失对每个参数梯度的算法——深度学习的一切训练,本质都是"先反向传播算出梯度,再沿梯度下降更新参数"(见优化与梯度下降)。而自动微分(automatic differentiation)是把这套算法系统化、机器化后的工程实现,PyTorch/TensorFlow 的 backward() 背后就是它。

一、为什么需要反向传播

训练神经网络要做两件事:前向传播算出损失,反向传播算出梯度。前向传播我们在神经网络基础里讲过:z = Wx + b → 激活 → … → 损失 L

问题来了:怎么知道每个参数该往哪个方向调?答案是算 ∂L/∂W——损失对每个权重的偏导数,即梯度。有了梯度,参数更新就是:

W ← W − η · ∂L/∂W

其中 η 是学习率。这就是梯度下降,细节见优化与梯度下降

那么梯度怎么算?最笨的办法是对每个参数做数值差分:

∂L/∂Wᵢ ≈ (L(Wᵢ+ε) − L(Wᵢ−ε)) / 2ε

一个 1 亿参数模型就要跑 2 亿次前向传播,完全不现实。反向传播利用链式法则,一次前向 + 一次反向就得到全部梯度——计算量只相当于两次前向传播,这是它能统治深度学习的根本原因。

为什么反向传播这么重要

反向传播 1986 年由 Rumelhart、Hinton、Williams 系统化提出并引爆深度学习,但其核心思想(逆序链式求导)早在 1970 年代由 Seppo Linnainmaa 在自动微分的论文中就已出现。它把"梯度计算"从 O(参数×前向成本) 降到了 O(前向成本),才让"学亿级参数"成为可能。

二、链式法则:数学内核

链式法则(chain rule)是微积分里一个"一句话"定理:复合函数求导等于逐层导数相乘。

L = f(g(h(x))),则:

∂L/∂x = f′(g(h(x))) · g′(h(x)) · h′(x)

注意方向:外层函数先被计算(先有 f),但求导时我们从最外层 L 往回乘。这就是"反向"二字的含义。数学基础可查数学基础速查

把它套在网络上:损失 L 是最后一层输出的函数,最后一层输出是倒数第二层的函数……所以:

∂L/∂W⁽ˡ⁾ = ∂L/∂a⁽ᴸ⁾ · ∂a⁽ᴸ⁾/∂a⁽ᴸ⁻¹⁾ · … · ∂a⁽ˡ⁺¹⁾/∂a⁽ˡ⁾ · ∂a⁽ˡ⁾/∂W⁽ˡ⁾

写成通用形式,每个中间变量 z 保存一个"上游梯度" ∂L/∂z(也叫梯度信号),反向传播就是把上游梯度沿着计算图一层层传回去。

三、计算图:前向图与反向图

任何网络都可以画成一张计算图(computational graph):节点是数据(张量)与运算,边是依赖关系。前向传播按拓扑序从输入算到输出;反向传播按逆拓扑序从输出往回求梯度。

x ──(Linear)──▶ z ──(ReLU)──▶ a ──(Linear)──▶ ŷ ──(MSE)──▶ L
                    ▲                            ▲
                  W1,b1                        W2,b2

前向图存下每个中间结果(za)与各运算的局部导数;反向图用链式法则把这些局部导数串起来,得到 ∂L/∂W1∂L/∂W2

关键工程点:前向传播的内存开销必须保留(因为反向需要 za 的值)。这就是为什么显存占用约等于"激活值"而非只有参数——训练时显存远大于推理时,也解释了梯度检查点(gradient checkpointing)这类省显存技巧的原理。

四、手推一个小网络:从零算一遍

让我们手推一个最简单的标量网络:输入 x=1,一个隐藏神经元(权重 w1=2,无偏置),ReLU 激活,输出权重 w2=3,均方误差损失,目标 y=10

前向传播:

z1 = w1·x = 2×1 = 2
a1 = ReLU(z1) = 2
ŷ = w2·a1 = 3×2 = 6
L = ½(ŷ − y)² = ½(6−10)² = 8

反向传播(从 L 往回):

∂L/∂ŷ = ŷ − y = 6 − 10 = −4
∂ŷ/∂w2 = a1 = 2            →  ∂L/∂w2 = (−4)×2 = −8
∂ŷ/∂a1 = w2 = 3            →  ∂L/∂a1 = (−4)×3 = −12
∂a1/∂z1 = 1  (ReLU 正区间导数为 1)
∂z1/∂w1 = x = 1            →  ∂L/∂w1 = (−12)×1×1 = −12

所以两个梯度分别是 ∂L/∂w2 = −8∂L/∂w1 = −12。若学习率 η=0.1,则 w2 ← 3+0.8 = 3.8w1 ← 2+1.2 = 3.2。可以看到损失会下降——这正是神经网络"学会"的最小闭环。把这张表从标量推广到矩阵(每层都有 batch 维),就是真实框架里 backward() 做的一切。

五、梯度消失与梯度爆炸:连乘的宿命

反向传播的每一步都是"乘法"。看链式法则:

∂L/∂W⁽¹⁾ = ∂L/∂a⁽ᴸ⁾ · (Πₗ ∂a⁽ˡ⁺¹⁾/∂a⁽ˡ⁾) · ∂a⁽¹⁾/∂W⁽¹⁾

如果每层导数的范数都小于 1(例如 Sigmoid 导数最大 0.25),连乘 30 层后梯度 ≈ 0.25³⁰,直接下溢为 0——梯度消失,浅层参数几乎收不到更新信号。反过来若每层导数大于 1,连乘指数爆炸——梯度爆炸,参数一步跳到 NaN。

工程要点

训练中 90% 的"梯度爆炸/消失"问题,都能在反向传播的链式乘法里找到根源。缓解手段包括:ReLU 系激活、合适的初始化(初始化与归一化)、归一化层、残差连接、梯度裁剪,排查方法见调试与诊断

梯度消失的经典受害者是早期 RNN——跨时间步的连乘让它几乎记不住长距离依赖,这也是后来 LSTM、GRU 门控设计、以及最终 Transformer 全面取代 RNN 的深层原因,演进脉络见RNN 与序列建模

六、自动微分:前向模式 vs 反向模式

自动微分不是数值差分(近似),也不是符号求导(展开表达式),而是在计算图上精确地应用链式法则,只是把求导过程程序化。它有两种基本模式:

  • 前向模式(forward mode):从输入出发,同时对每个中间变量求"方向导数"。计算 n 个输入的梯度需要 n 次前向。适合"输入少、输出多"(如标量函数对向量参数)的场景,Jacobian-vector product 高效。
  • 反向模式(reverse mode):就是反向传播。一次前向 + 一次反向,得到损失对所有参数的梯度。适合"输入多、输出一"的典型神经网络场景(损失是标量,参数百万计)——这正是反向传播统治深度学习的原因。

两种模式各有适用场景:反向模式省一次算全部参数梯度,但必须保存中间激活,内存贵;前向模式无需保存中间结果、内存友好,常用于梯度本身参与计算(如求 Hessian 向量积)。选择逻辑与代价对比是框架与工具怎么选里可实操的考点。

七、PyTorch autograd 的正确用法

PyTorch 的自动微分由 torch.autograd 驱动。核心规则只有几条:

python
import torch

x = torch.tensor([1.0], requires_grad=True)   # 需要梯度的叶子张量
w = torch.tensor([2.0], requires_grad=True)
b = torch.tensor([0.5], requires_grad=False)  # 不追踪梯度

z = x * w + b          # 记录运算
L = z.square().mean()  # 损失(标量)

L.backward()           # 反向传播,填充 .grad
print(w.grad)          # tensor([2.0]) —— ∂L/∂w

几个必须掌握的语义:

  • requires_grad=True:该张量及其所有衍生张量都被追踪进计算图。默认参数张量需要梯度,数据张量不需要。
  • detach():从计算图中"摘除"该张量——返回一个不追踪梯度、不共享记录的新张量。典型用途:在某个中间特征处切断梯度流(如冻结特征提取器、对比学习中 stop-gradient),或者在 torch.no_grad() 之外单独禁用某条支路的梯度。
  • torch.no_grad():上下文管理器,块内所有运算不建图、不存激活,省显存、省时间。推理时必须用,因为推理不需要梯度。
  • torch.set_grad_enabled(False):以编程方式全局开关,训练脚本里按阶段切换的惯用写法。

detach()no_grad() 的区别一句话:detach() 是"某个张量退出图",no_grad() 是"整段代码不建图"。

八、常见误区

反向传播 + autograd 是训练事故的高发区,这里列四个最常见的坑:

  1. 原地操作(in-place)破坏计算图w += 1x.add_(1) 会改写张量的值,而 autograd 反向时需要读取操作前的旧值,会直接报错 a leaf Variable that requires grad is being used in an in-place operation。正确写法:w = w + 1(创建新张量)或用 with torch.no_grad(): w.add_(1) 更新参数。
  2. no_grad() 块内更新参数with torch.no_grad(): w -= lr * w.grad 是合法的(参数更新本来就不需要梯度),但如果在 no_grad() 里执行了前向 + backward(),梯度根本不会计算,模型永远不更新——这是"训练了一晚上 loss 纹丝不动"的头号原因。
  3. 每步不清零梯度loss.backward() 默认是累加梯度。正确流程是每步先 optimizer.zero_grad()(或 loss.backward() 前手动置零),否则梯度累加导致训练震荡。
  4. 对非标量直接 backwardbackward() 要求输出是标量(或传入与输出同形状的梯度权重)。损失多输出时先 .mean().sum() 再 backward。

更多训练期事故的排查思路见调试与诊断常见陷阱与反模式

九、混合精度与梯度缩放

现代 GPU(如 NVIDIA A100)对 FP16/BF16 运算远快于 FP32,于是有了混合精度训练:参数和主梯度存 FP32,前向/反向用 FP16 加速。但 FP16 可表示范围小,梯度可能下溢到 0。解决方案是梯度缩放(gradient scaling):

loss_scaled = loss × scale          # 放大损失
loss_scaled.backward()              # 反向得到放大后的梯度
grad = grad / scale                 # 反向传播后还原

PyTorch 的 torch.cuda.amp.GradScaler 自动处理"溢出就跳过该步、减小 scale"的流程。BF16 因为与 FP32 指数位相同、几乎不丢范围,成了 LLM 训练的默认选择。这一整套技巧在生产训练里几乎是标配,落地方案见训练配方与调参

十、权衡与取舍

权衡与取舍

反向模式省算力、费显存 vs 前向模式省显存、费算力:标准训练用反向模式;当显存是瓶颈且梯度维数低时,前向模式或梯度检查点更划算。

保存全部激活 vs 检查点/重计算:保存激活反向最快但显存随深度线性增长;梯度检查点每层只存少量中间值、反向时重算,显存显著下降、时间约翻倍——在大模型训练里是"用时间换显存"的标准操作。

autograd 的便利 vs 可控性:自动微分让写模型像写数学公式一样自由,但代价是"魔法感"——出问题难以定位。理解手推示例中的每一步(第四节),是日后调试任何梯度问题的底气。

自动微分是深度学习这座大厦的地基:它决定了我们能训多大模型、多深网络、多长序列。把这一节吃透,配合神经网络基础中的前向传播,你就掌握了训练循环的全部核心;剩下的只是"梯度怎么用",交给优化与梯度下降章节。

延伸阅读

参考资料