← 返回模块
Q2.6.3.2beta 可读 · 未来付费校验通过内容版本 2026-05-26

反向传播与自动微分

2.6.3 · 神经网络 · 数学与统计能力

反向传播与自动微分

Hook:四分钟一步的梯度

你刚加入一家以沪深300 alpha 为主力的私募(private fund),上手第一件事是把上一课那张 5 层、宽度 128 的多层感知机(multi-layer perceptron, MLP)跑通——目标是用一个标准的 Barra 因子模型(factor model)的截面特征去拟合 r^t+1=hθ(xt)\hat r_{t+1} = h_\theta(x_t),本质上是在学一个条件期望(conditional expectation)E[rt+1xt]E[r_{t+1} \mid x_t] 的非线性逼近。早上你套用了 scipy.optimize.approx_fprime:对约 5×1045 \times 10^4 个参数逐个 +epsilon 跑一次前向(forward pass),做有限差分(finite difference)。结果:单步梯度估计约 4 分钟,单 epoch 约 8 小时,训练彻底卡死,PM 在门口看了你一眼走开。

中午翻 PyTorch 文档把 loss.backward() 一行接上去:同一步梯度掉到不到 1 秒,整整三个数量级。本课要回答这三个数量级从哪里来:为什么反向传播(backpropagation)能把所有参数对损失的偏导塞进一次前向 + 一次反向的代价之内?答案不是工程优化,是一类称作反向模式自动微分(reverse-mode automatic differentiation, AD)的算法——反向传播只是它在前馈网络上的具体形态。

一、把前馈网络重画为计算图

第 1 课里 LL 层 MLP 写作

a(l)=σl(z(l)),z(l)=W(l)a(l1)+b(l),a(0)=x.a^{(l)} = \sigma_l(z^{(l)}), \quad z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}, \quad a^{(0)} = x.

把每一个中间张量(x,z(1),a(1),,z(L),a(L),Lx, z^{(1)}, a^{(1)}, \ldots, z^{(L)}, a^{(L)}, L)视为节点,每一次基本运算(matmul、加偏置、逐元素 σl\sigma_l、末端的标量损失约减)视为有向边,整个前向过程就构成一张有向无环图(directed acyclic graph, DAG),称作计算图(computational graph)。这一改写本身没有引入新的数学——它只是把第 1 课的链式定义画了出来——却带来一个关键观察:损失 LL 是图中唯一的标量出口节点,所有参数 {W(l),b(l)}\{W^{(l)}, b^{(l)}\} 都是入口节点;想算 L/θ\partial L / \partial \theta,就是在这张图上做一次反向遍历。

Interactive Widget

该交互组件的正式实现会随课程交互层一起接入。当前 beta 先保留正文、公式和练习内容。

二、余切与误差项 δ(l)\delta^{(l)}

定义层 ll 预激活上的余切(cotangent,亦称误差项 / 余向量):

δ(l)  :=  Lz(l).\delta^{(l)} \;:=\; \dfrac{\partial L}{\partial z^{(l)}}.

δ(l)\delta^{(l)} 想成「损失对该层净输入的灵敏度」。一旦所有 δ(l)\delta^{(l)} 都拿到了,参数梯度只是简单的外积(outer product)。下面分两步:先在输出层启动,再向前递推。

​输出层​​。LL 通过 a(L)a^{(L)} 间接依赖 z(L)z^{(L)},对 2.4.2 的链式法则(chain rule,此处只引用)套一次即得

δ(L)  =  La(L)σL(z(L)),\delta^{(L)} \;=\; \dfrac{\partial L}{\partial a^{(L)}} \,\odot\, \sigma_L'(z^{(L)}),

其中 \odot 是哈达玛积(Hadamard product, 逐元素积)。第一项依赖损失形态(squared error 时即 a(L)ya^{(L)} - y),第二项依赖输出层激活的导数。

三、向后递推的链式法则

给定 δ(l+1)\delta^{(l+1)},要算 δ(l)\delta^{(l)}。注意 z(l+1)=W(l+1)a(l)+b(l+1)z^{(l+1)} = W^{(l+1)} a^{(l)} + b^{(l+1)}a(l)=σl(z(l))a^{(l)} = \sigma_l(z^{(l)}),两次套链式法则得到反向传播的核心递推:

δ(l)  =  ((W(l+1))δ(l+1))σl(z(l)).\delta^{(l)} \;=\; \Bigl( (W^{(l+1)})^\top \delta^{(l+1)} \Bigr) \,\odot\, \sigma_l'(z^{(l)}).

读法:把下游误差用 W(l+1)W^{(l+1)} 的​​转置​​「打回」上游,再被该层激活的导数「门控」。用转置而非逆,是因为算的是梯度(gradient)而非反解——这一区别让反向传播沿矩阵列方向坍缩、计算量与前向同量级,是后面效率论断的代数根源。

四、参数梯度

有了所有 δ(l)\delta^{(l)},再用一次链式:z(l)/W(l)=a(l1)\partial z^{(l)} / \partial W^{(l)} = a^{(l-1)\,\top}z(l)/b(l)=I\partial z^{(l)} / \partial b^{(l)} = I,立刻读出

LW(l)  =  δ(l)(a(l1)),Lb(l)  =  δ(l).\dfrac{\partial L}{\partial W^{(l)}} \;=\; \delta^{(l)} \bigl(a^{(l-1)}\bigr)^\top, \qquad \dfrac{\partial L}{\partial b^{(l)}} \;=\; \delta^{(l)}.

整套算法因此是:先做一次前向并缓存所有 {z(l),a(l)}\{z^{(l)}, a^{(l)}\},再从 δ(L)\delta^{(L)} 倒推到 δ(1)\delta^{(1)},每层顺手用上式取出 L/W(l)\partial L / \partial W^{(l)}L/b(l)\partial L / \partial b^{(l)}。两遍 DAG 遍历、共用同一份激活缓存。

五、效率论断:为什么深度学习是可行的

对函数 f:RnRf : \mathbb{R}^n \to \mathbb{R},反向模式 AD 算梯度 f\nabla f 的时间与内存均为 O(cost(forward))O(\mathrm{cost}(\text{forward})),​​与 nn 无关​​;相对地,有限差分需 nn 次前向,正向模式 AD(forward-mode AD)同样需 nn 次。回到 Hook:当 p5×104p \approx 5 \times 10^4,反向传播节省 5×104\sim 5 \times 10^4 倍;现代量化网络若 p[106,109]p \in [10^6,\, 10^9],节省同等级。没有反向模式 AD,今天所有 PyTorch 训练的私募 alpha 网络在数学上一步都迈不过去。

不对称从何而来?对 f:RnRf : \mathbb{R}^n \to \mathbb{R}(多输入、单输出),反向模式让一个标量误差信号往回扫一次即可;正向模式得在 nn 个输入方向各做一次切向传播。反过来对 g:RRmg : \mathbb{R} \to \mathbb{R}^m(敏感度分析的典型形态),正向模式更省。规则因此简洁:​​反向模式用于训练,正向模式用于敏感度。​

与符号微分(symbolic differentiation)相比,AD 不展开成巨大表达式树,而是沿图逐边数值推进、共享中间结果;与数值微分相比,AD 不依赖步长 ϵ\epsilon,没有舍入—截断的两难。

六、手算一例:2-2-1 网络 + 有限差分校验

W(1)=(0.50.30.20.4)W^{(1)} = \begin{pmatrix} 0.5 & -0.3 \\ 0.2 & 0.4 \end{pmatrix}b(1)=(0.1,0.1)b^{(1)} = (0.1, -0.1)^\topW(2)=(0.6,0.7)W^{(2)} = (0.6, -0.7)b(2)=0.2b^{(2)} = 0.2,隐藏层 ReLU,输出层恒等,损失 L=12(a(2)y)2L = \tfrac{1}{2}(a^{(2)} - y)^2,输入 x=(1,1)x = (1, 1)^\top、标签 y=0.5y = 0.5

​前向​​。z(1)=W(1)x+b(1)=(0.3,0.5)z^{(1)} = W^{(1)} x + b^{(1)} = (0.3, 0.5)^\top,两分量均正故 a(1)=(0.3,0.5)a^{(1)} = (0.3, 0.5)^\topz(2)=0.60.30.70.5+0.2=0.03=a(2)z^{(2)} = 0.6 \cdot 0.3 - 0.7 \cdot 0.5 + 0.2 = 0.03 = a^{(2)}L=12(0.030.5)2=0.11045L = \tfrac{1}{2}(0.03 - 0.5)^2 = 0.11045

​反向​​。δ(2)=a(2)y=0.47\delta^{(2)} = a^{(2)} - y = -0.47。层 2 参数梯度:L/W(2)=δ(2)(a(1))=(0.141,0.235)\partial L / \partial W^{(2)} = \delta^{(2)} (a^{(1)})^\top = (-0.141,\, -0.235)L/b(2)=0.47\partial L / \partial b^{(2)} = -0.47。回传到层 1:δ(1)=(W(2))δ(2)1z(1)>0=(0.6,0.7)(0.47)(1,1)=(0.282,0.329)\delta^{(1)} = (W^{(2)})^\top \delta^{(2)} \odot \mathbf{1}_{z^{(1)} > 0} = (0.6,\, -0.7)^\top \cdot (-0.47) \odot (1, 1) = (-0.282,\, 0.329)^\top。再外积一次:

LW(1)=δ(1)x=(0.2820.2820.3290.329),Lb(1)=(0.282,0.329).\dfrac{\partial L}{\partial W^{(1)}} = \delta^{(1)} x^\top = \begin{pmatrix} -0.282 & -0.282 \\ 0.329 & 0.329 \end{pmatrix}, \quad \dfrac{\partial L}{\partial b^{(1)}} = (-0.282,\, 0.329)^\top.

​有限差分校验​​。把 W11(1)W^{(1)}_{11}ϵ=103\epsilon = 10^{-3} 重做前向:z1(1)=0.301z^{(1)}_1 = 0.301z(2)=0.0306z^{(2)} = 0.0306L=12(0.4694)20.110168L' = \tfrac{1}{2}(-0.4694)^2 \approx 0.110168。则 (LL)/ϵ0.282(L' - L)/\epsilon \approx -0.282,与解析梯度 L/W11(1)=0.282\partial L / \partial W^{(1)}_{11} = -0.282 在第三位小数内吻合。这一校验是你将来在 PyTorch 里实现自定义算子(custom op)的 VJP 时几乎唯一的工具。

Formula Explorer

0.5 * (0.3 * w1 + 0.5 * w2 + 0.2 - 0.5)^2

拉动上图的 w1,w2w_1, w_2(对应 W(2)W^{(2)} 的两个分量、隐藏层激活固定在 (0.3,0.5)(0.3, 0.5)):在 w1=0.6,w2=0.7w_1 = 0.6, w_2 = -0.7 处斜率分别约为 0.141-0.1410.235-0.235,正好是上面手算给出的两个参数梯度。

七、向量—雅可比积:autograd 的真身

把视角放大。任何基本算子 ff 都自带一个雅可比矩阵 JfJ_f;反向模式 AD 真正需要的不是 JfJ_f 本身,而是雅可比向量积(vector-Jacobian product, VJP)vvJfv \mapsto v^\top J_f——给定上游余切 vv,返回下游余切。每个算子注册一个 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)W^{(2)}_10.60.6 改为 0.6+ϵ0.6 + \epsilonϵ=103\epsilon = 10^{-3})。

  1. 用反向传播的解析公式给出 L/W1(2)\partial L / \partial W^{(2)}_1
  2. 用一次额外的前向计算做有限差分校验。
  3. 简述若把隐藏层激活换为 sigmoid,公式中哪一项会改变、δ(1)\delta^{(1)} 的数值大小如何变。
提示
解析答案直接读上面手算结果:L/W1(2)=δ(2)a1(1)=0.47×0.3=0.141\partial L / \partial W^{(2)}_1 = \delta^{(2)} \cdot a^{(1)}_1 = -0.47 \times 0.3 = -0.141。有限差分只需把 W1(2)W^{(2)}_1ϵ\epsilon、重新算一次 z(2)z^{(2)}LL,看 (LL)/ϵ(L' - L)/\epsilon 是否在第三位小数内匹配。
提示
第 (iii) 问的关键在于 ReLU 的导数是 0/1 门控、sigmoid 的导数是 σ(z)(1σ(z))(0,0.25]\sigma(z)(1 - \sigma(z)) \in (0,\, 0.25]。在 z(1)=(0.3,0.5)z^{(1)} = (0.3, 0.5) 处 sigmoid 导数约为 0.2460.2460.2350.235,故 δ(1)\delta^{(1)} 各分量会被压到大约原值的四分之一——这就是「梯度消失」(vanishing gradient)的种子,下一课的初始化与归一化要处理的就是它。

九、通向下一课

到这里你已经能在任意前馈网络上以 O(cost(forward))O(\mathrm{cost}(\text{forward})) 的代价拿到 θR^\nabla_\theta \widehat R——梯度是「免费」的了。下一课的问题随之而来:拿着这个梯度怎么走?深度网络的损失曲面是非凸的,朴素梯度下降会撞上鞍点、平台与梯度爆炸/消失三种典型病态,纯堆 GPU 救不了。第 3 课把训练栈补齐:SGD 与小批量、动量与 Adam、Glorot / He 初始化、批归一化、Dropout、权重衰减、学习率调度——本质上都是在用容量控制(capacity control)的语言驯服非凸优化。梯度算得到只是第一步;让它走得稳,是下一课的事。