反向传播与自动微分
Hook:四分钟一步的梯度
你刚加入一家以沪深300 alpha 为主力的私募(private fund),上手第一件事是把上一课那张 5 层、宽度 128 的多层感知机(multi-layer perceptron, MLP)跑通——目标是用一个标准的 Barra 因子模型(factor model)的截面特征去拟合 r^t+1=hθ(xt),本质上是在学一个条件期望(conditional expectation)E[rt+1∣xt] 的非线性逼近。早上你套用了 scipy.optimize.approx_fprime:对约 5×104 个参数逐个 +epsilon 跑一次前向(forward pass),做有限差分(finite difference)。结果:单步梯度估计约 4 分钟,单 epoch 约 8 小时,训练彻底卡死,PM 在门口看了你一眼走开。
中午翻 PyTorch 文档把 loss.backward() 一行接上去:同一步梯度掉到不到 1 秒,整整三个数量级。本课要回答这三个数量级从哪里来:为什么反向传播(backpropagation)能把所有参数对损失的偏导塞进一次前向 + 一次反向的代价之内?答案不是工程优化,是一类称作反向模式自动微分(reverse-mode automatic differentiation, AD)的算法——反向传播只是它在前馈网络上的具体形态。
一、把前馈网络重画为计算图
第 1 课里 L 层 MLP 写作
a(l)=σl(z(l)),z(l)=W(l)a(l−1)+b(l),a(0)=x.
把每一个中间张量(x,z(1),a(1),…,z(L),a(L),L)视为节点,每一次基本运算(matmul、加偏置、逐元素 σl、末端的标量损失约减)视为有向边,整个前向过程就构成一张有向无环图(directed acyclic graph, DAG),称作计算图(computational graph)。这一改写本身没有引入新的数学——它只是把第 1 课的链式定义画了出来——却带来一个关键观察:损失 L 是图中唯一的标量出口节点,所有参数 {W(l),b(l)} 都是入口节点;想算 ∂L/∂θ,就是在这张图上做一次反向遍历。
Interactive Widget
该交互组件的正式实现会随课程交互层一起接入。当前 beta 先保留正文、公式和练习内容。
二、余切与误差项 δ(l)
定义层 l 预激活上的余切(cotangent,亦称误差项 / 余向量):
δ(l):=∂z(l)∂L.
把 δ(l) 想成「损失对该层净输入的灵敏度」。一旦所有 δ(l) 都拿到了,参数梯度只是简单的外积(outer product)。下面分两步:先在输出层启动,再向前递推。
输出层。L 通过 a(L) 间接依赖 z(L),对 2.4.2 的链式法则(chain rule,此处只引用)套一次即得
δ(L)=∂a(L)∂L⊙σL′(z(L)),
其中 ⊙ 是哈达玛积(Hadamard product, 逐元素积)。第一项依赖损失形态(squared error 时即 a(L)−y),第二项依赖输出层激活的导数。
三、向后递推的链式法则
给定 δ(l+1),要算 δ(l)。注意 z(l+1)=W(l+1)a(l)+b(l+1)、a(l)=σl(z(l)),两次套链式法则得到反向传播的核心递推:
δ(l)=((W(l+1))⊤δ(l+1))⊙σl′(z(l)).
读法:把下游误差用 W(l+1) 的转置「打回」上游,再被该层激活的导数「门控」。用转置而非逆,是因为算的是梯度(gradient)而非反解——这一区别让反向传播沿矩阵列方向坍缩、计算量与前向同量级,是后面效率论断的代数根源。
四、参数梯度
有了所有 δ(l),再用一次链式:∂z(l)/∂W(l)=a(l−1)⊤、∂z(l)/∂b(l)=I,立刻读出
∂W(l)∂L=δ(l)(a(l−1))⊤,∂b(l)∂L=δ(l).
整套算法因此是:先做一次前向并缓存所有 {z(l),a(l)},再从 δ(L) 倒推到 δ(1),每层顺手用上式取出 ∂L/∂W(l) 与 ∂L/∂b(l)。两遍 DAG 遍历、共用同一份激活缓存。
五、效率论断:为什么深度学习是可行的
对函数 f:Rn→R,反向模式 AD 算梯度 ∇f 的时间与内存均为 O(cost(forward)),与 n 无关;相对地,有限差分需 n 次前向,正向模式 AD(forward-mode AD)同样需 n 次。回到 Hook:当 p≈5×104,反向传播节省 ∼5×104 倍;现代量化网络若 p∈[106,109],节省同等级。没有反向模式 AD,今天所有 PyTorch 训练的私募 alpha 网络在数学上一步都迈不过去。
不对称从何而来?对 f:Rn→R(多输入、单输出),反向模式让一个标量误差信号往回扫一次即可;正向模式得在 n 个输入方向各做一次切向传播。反过来对 g:R→Rm(敏感度分析的典型形态),正向模式更省。规则因此简洁:反向模式用于训练,正向模式用于敏感度。
与符号微分(symbolic differentiation)相比,AD 不展开成巨大表达式树,而是沿图逐边数值推进、共享中间结果;与数值微分相比,AD 不依赖步长 ϵ,没有舍入—截断的两难。
六、手算一例:2-2-1 网络 + 有限差分校验
取 W(1)=(0.50.2−0.30.4)、b(1)=(0.1,−0.1)⊤、W(2)=(0.6,−0.7)、b(2)=0.2,隐藏层 ReLU,输出层恒等,损失 L=21(a(2)−y)2,输入 x=(1,1)⊤、标签 y=0.5。
前向。z(1)=W(1)x+b(1)=(0.3,0.5)⊤,两分量均正故 a(1)=(0.3,0.5)⊤;z(2)=0.6⋅0.3−0.7⋅0.5+0.2=0.03=a(2);L=21(0.03−0.5)2=0.11045。
反向。δ(2)=a(2)−y=−0.47。层 2 参数梯度:∂L/∂W(2)=δ(2)(a(1))⊤=(−0.141,−0.235);∂L/∂b(2)=−0.47。回传到层 1:δ(1)=(W(2))⊤δ(2)⊙1z(1)>0=(0.6,−0.7)⊤⋅(−0.47)⊙(1,1)=(−0.282,0.329)⊤。再外积一次:
∂W(1)∂L=δ(1)x⊤=(−0.2820.329−0.2820.329),∂b(1)∂L=(−0.282,0.329)⊤.
有限差分校验。把 W11(1) 加 ϵ=10−3 重做前向:z1(1)=0.301,z(2)=0.0306,L′=21(−0.4694)2≈0.110168。则 (L′−L)/ϵ≈−0.282,与解析梯度 ∂L/∂W11(1)=−0.282 在第三位小数内吻合。这一校验是你将来在 PyTorch 里实现自定义算子(custom op)的 VJP 时几乎唯一的工具。
Formula Explorer
0.5 * (0.3 * w1 + 0.5 * w2 + 0.2 - 0.5)^2
拉动上图的 w1,w2(对应 W(2) 的两个分量、隐藏层激活固定在 (0.3,0.5)):在 w1=0.6,w2=−0.7 处斜率分别约为 −0.141 与 −0.235,正好是上面手算给出的两个参数梯度。
七、向量—雅可比积:autograd 的真身
把视角放大。任何基本算子 f 都自带一个雅可比矩阵 Jf;反向模式 AD 真正需要的不是 Jf 本身,而是雅可比向量积(vector-Jacobian product, VJP)v↦v⊤Jf——给定上游余切 v,返回下游余切。每个算子注册一个 VJP 函数,反向遍历就是按反向拓扑序把这些 VJP 串起来。这就是 PyTorch 的 torch.autograd.Function.backward、JAX 的 jax.vjp 在干的事;MLP 上的反向传播只是把 matmul 与逐元素激活的 VJP 拼出来的一个特例。换句话说,loss.backward() 工作于任意可微 Python 程序——只要计算图的每一条边都登记过自己的 VJP。一个直接推论:RNN 的 backprop through time 不是新算法,只是把循环图按时间展开后做的同一套反向模式 AD。
八、练习
Exercise
仍取本课的 2-2-1 网络与同样输入,把 W1(2) 从 0.6 改为 0.6+ϵ(ϵ=10−3)。
- 用反向传播的解析公式给出 ∂L/∂W1(2)。
- 用一次额外的前向计算做有限差分校验。
- 简述若把隐藏层激活换为 sigmoid,公式中哪一项会改变、δ(1) 的数值大小如何变。
提示
解析答案直接读上面手算结果:
∂L/∂W1(2)=δ(2)⋅a1(1)=−0.47×0.3=−0.141。有限差分只需把
W1(2) 加
ϵ、重新算一次
z(2) 与
L,看
(L′−L)/ϵ 是否在第三位小数内匹配。
提示
第 (iii) 问的关键在于 ReLU 的导数是 0/1 门控、sigmoid 的导数是
σ(z)(1−σ(z))∈(0,0.25]。在
z(1)=(0.3,0.5) 处 sigmoid 导数约为
0.246 与
0.235,故
δ(1) 各分量会被压到大约原值的四分之一——这就是「梯度消失」(vanishing gradient)的种子,下一课的初始化与归一化要处理的就是它。
九、通向下一课
到这里你已经能在任意前馈网络上以 O(cost(forward)) 的代价拿到 ∇θR——梯度是「免费」的了。下一课的问题随之而来:拿着这个梯度怎么走?深度网络的损失曲面是非凸的,朴素梯度下降会撞上鞍点、平台与梯度爆炸/消失三种典型病态,纯堆 GPU 救不了。第 3 课把训练栈补齐:SGD 与小批量、动量与 Adam、Glorot / He 初始化、批归一化、Dropout、权重衰减、学习率调度——本质上都是在用容量控制(capacity control)的语言驯服非凸优化。梯度算得到只是第一步;让它走得稳,是下一课的事。