9.2 计算图、反向传播与 Autograd:梯度在分支处相加,在链路上相乘
数学塔的链式法则终于接上模型工坊的生产线。一次 forward 可能有百万个中间值;若对每个参数分别从头求导,计算会大量重复。Backpropagation 保存局部依赖,再从 scalar loss 反向复用结果。
它不是新的微积分定理,而是 reverse-mode differentiation 在有向无环计算图上的执行算法。
本课目标
- 把 forward 表达成计算图与局部 Jacobian;
- 手算一个两层标量网络的反向过程;
- 解释共享/分支节点为什么累加梯度;
- 正确使用 PyTorch
.backward()、.grad与 graph control; - 用 finite difference/
gradcheck验证自定义梯度。
1. 计算图记录依赖关系
标量网络:
$$ z_1=w_1x+b_1,quad a_1=\operatorname{ReLU}(z_1), $$
$$ z_2=w_2a_1+b_2,quad L=\frac12(z_2-y)^2. $$
Forward 按拓扑顺序算值。Reverse 从 $\bar L=\partial L/\partial L=1$ 出发,把上游 adjoint 乘局部导数:
$$ \bar z_2=z_2-y,qquad \bar w_2=\bar z_2a_1,qquad \bar b_2=\bar z_2, $$
$$ \bar a_1=\bar z_2w_2,qquad \bar z_1=\bar a_1\mathbf1[z_1>0], $$
$$ \bar w_1=\bar z_1x,qquad \bar b_1=\bar z_1. $$
横线记号只是“最终 loss 对这个中间值的导数”。每条边做 vector–Jacobian product,不必显式构造完整巨大 Jacobian。
2. Reverse Mode 为什么适合训练
网络通常有:
- 一个 scalar/小维 loss;
- 数百万到数十亿 parameters。
Reverse mode 一次反向可得到 scalar loss 对所有上游 parameters 的梯度,成本通常与若干次 forward 同阶。Forward mode 更适合输入方向少、输出多的 Jacobian-vector 场景;现代框架也支持组合二者处理高阶导数/Jacobian。
“Backward 只算每个中间梯度一次”需要限定:共享节点会接收多个下游贡献,框架必须先累加这些贡献,再继续向上游传播。
3. 分支处为什么相加
若:
$$ u=x^2,qquad v=3x,qquad L=u+v, $$
则 $x$ 有两条下游路径:
$$ \frac{dL}{dx} =\frac{dL}{du}\frac{du}{dx} +\frac{dL}{dv}\frac{dv}{dx} =2x+3. $$
Residual connection、parameter sharing、同一 embedding 多次使用都依赖这条规则。若手写 backward 忘记累加,会得到形状正确但数值错误的梯度。
广播也会影响反向:bias 在 batch 维被广播,bias gradient 就要沿被广播维求和。
4. Matrix Layer 的反向形状
设 batch forward:
$$ Z=XW^T+b,qquad G=\frac{\partial L}{\partial Z}. $$
则:
$$ \frac{\partial L}{\partial X}=GW,qquad \frac{\partial L}{\partial W}=G^TX,qquad \frac{\partial L}{\partial b}=\sum_{i=1}^{B}G_i. $$
Shape check:
G: [B, out]
W: [out, in]
dX = G @ W: [B, in]
dW = G.T @ X: [out, in]
db = G.sum(dim=0): [out]Shape 对只是必要条件;transpose 放错仍可能在方阵中静默通过,所以还要 numerical gradient check。
5. 用 PyTorch 核对手算梯度
import torch
torch.set_default_dtype(torch.float64)
x = torch.tensor(2.0)
y = torch.tensor(1.0)
w1 = torch.tensor(0.5, requires_grad=True)
b1 = torch.tensor(0.1, requires_grad=True)
w2 = torch.tensor(-1.2, requires_grad=True)
b2 = torch.tensor(0.3, requires_grad=True)
z1 = w1 * x + b1
a1 = torch.relu(z1)
z2 = w2 * a1 + b2
loss = 0.5 * (z2 - y) ** 2
loss.backward()
print({
"loss": loss.item(),
"dw1": w1.grad.item(),
"db1": b1.grad.item(),
"dw2": w2.grad.item(),
"db2": b2.grad.item(),
})只有 requires_grad=True 且参与 graph 的 tensors 才被追踪。Parameter 通常是 leaf tensor,其 gradient 累加到 .grad。中间 tensor 默认不一定保留 .grad;调试时可 retain_grad(),但会增加内存。
6. .grad 默认累加
连续调用 backward:
loss1.backward()
loss2.backward()parameter .grad 会累加两次贡献。这对 gradient accumulation 有用,也意味着普通训练每次更新前要清空:
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()set_to_none=True 与填零在内存/optimizer 行为上可能有细微差异,使用自定义优化逻辑时要检查 grad is None。
默认 graph 在 backward 后释放部分 saved tensors;要对同一 graph 再 backward 可能需要 retain_graph=True,但反复依赖它常提示 graph 生命周期或 loss 组织有问题。
7. Graph Control:detach、no_grad、inference_mode
tensor.detach():返回与原 storage 关联但不追踪当前 history 的 tensor;in-place 修改仍需小心。torch.no_grad():上下文内不记录需要反向的运算,常用于 validation/inference。torch.inference_mode():更强的推理优化与限制;生成的 tensors 后续参与 autograd 有额外约束。
错误 detach 会截断本该训练的路径;忘记禁用 gradient 会在验证时浪费内存。不要用 .data 绕过 autograd 的版本检查。
8. In-place Operation 与 Saved Tensor
Backward 可能需要 forward 保存的输入/输出。若在原地改写这些 tensors,框架可能报 version mismatch;更危险的自定义代码可能静默破坏数学语义。
ReLU inplace=True、参数原地更新、view/shared storage 要在内存收益与可验证性间权衡。初学/调试阶段优先 out-of-place 表达。
9. 不可导点与数值精度
ReLU 在 0、absolute value 在 0、max ties 处不可唯一求导。Framework 采用某种 subgradient/约定,因此有限差分在这些点可能与 autograd 不一致。
Float32 finite difference 还会受 cancellation/rounding 影响;gradient check 通常使用 float64、适当 $\epsilon$,并避开 kink。
10. Finite Difference Gradient Check
对 scalar parameter $\theta$:
$$ g_{num}=\frac{L(\theta+\epsilon)-L(\theta-\epsilon)}{2\epsilon}. $$
相对误差可写为:
$$ \frac{|g_{ana}-g_{num}|} {\max(1,|g_{ana}|,|g_{num}|)}. $$
检查小模型/少量参数,固定随机性,关闭 dropout,避免 batch statistics 变化。PyTorch torch.autograd.gradcheck 会用 numerical approximation 检查自定义 function,输入通常需 double precision 且 requires_grad=True。
Gradient check 通过不能证明 forward 目标正确;forward 和 backward 可能一致地实现了错误公式,所以还需要 reference cases 和 invariants。
11. Vanishing 与 Exploding Gradient
深层链式法则包含 Jacobian 乘积:
$$ \frac{\partial L}{\partial h_0} =J_1^TJ_2^T\cdots J_L^T \frac{\partial L}{\partial h_L}. $$
若典型 singular values 长期小于 1,梯度衰减;大于 1,梯度放大。影响因素包括权重初始化、activation derivative、depth、normalization、residual path 和数据尺度。
ReLU 去掉正区的 sigmoid saturation,却没有约束权重 Jacobian。Residual connection 给梯度增加 identity-like 路径,normalization 和初始化控制尺度,gradient clipping 限制爆炸的 update;它们各解决不同部分。
12. 调试梯度流
记录而不是猜:
- 每层 parameter/gradient norm;
- activation mean/std、zero/saturation fraction;
- loss 是否 finite;
- update-to-weight ratio;
- NaN/Inf 首次出现的 operation;
- train/eval mode 与 dtype/device。
可用 hooks、anomaly detection 和 profiler 定位,但 hooks 会改变性能/graph 生命周期。先在极小 batch 上 overfit、检查 loss 是否下降,再扩展训练。
常见误区
- Backprop 是把错误平均分给参数:它计算 loss 对每个参数的偏导/VJP。
- 共享节点梯度只算一次就结束:多个下游路径贡献必须相加。
- 调用两次 backward 会覆盖
.grad:默认累加。 - Autograd 通过就说明模型正确:只能说明框架按 graph 求了导。
- ReLU 后没有梯度问题:Jacobian 链仍可能消失或爆炸。
练习
- 手算标量网络所有 forward values 与 gradients,再与 PyTorch 比较。
- 为 $L=x^2+3x$ 画分支图,观察
x.grad的累加。 - 推导 batch affine layer 的
dX/dW/db并核对 shape。 - 故意不清
.grad做两次 backward,解释结果。 - 对自定义 activation 做 float64 central-difference check。
小结
Backpropagation 从 scalar loss 逆拓扑遍历计算图:链路贡献相乘,分支贡献相加。Autograd 自动记录与执行这些局部规则,但 graph 生命周期、梯度累加、不可导点和数值精度仍需开发者理解。
下一课把正确梯度变成稳定训练:初始化控制信号尺度,optimizer 决定 update,normalization 与 regularization 约束训练和泛化。