Skip to content

理论学习:梯度到底从哪来 ​

第 1 章结尾留了一行黑箱代码:

python
loss.backward()

一行调用,109,386 个参数的梯度全部出现。这一篇的任务,是把这行代码拆开:梯度到底是什么、为什么一次反向就能同时算出所有参数、为什么层数一深它会消失、以及为什么没人用数值差分去训练。

先把结论放前面:梯度不是玄学,它是一串局部导数的乘积。反向传播也不是什么特殊算法,它只是链式法则在计算图上的一种高效执行顺序。所有让人觉得「黑」的部分,都来自这条乘法链太长,人眼算不过来。

一、损失曲面:训练是在地形上走路 ​

网络有参数 θ,损失 L(θ) 是一个把几万个参数映射成一个标量的函数。这个函数的图像在几万维空间里,人看不见,但可以压成二维看一眼。

![损失曲面上的梯度路径](/ai-four-kings/第 2 章:反向传播/assets/ch2_loss_path.svg)

<!-- 配图:椭圆等高线、一条来回横跳的下降轨迹、最优点与三个量的说明面板 -->

图里的等高线是「损失相同」的点连成的线。越靠中心损失越小。梯度 ∇L 是一个向量,指向损失上升最快的方向;负梯度 −∇L 指向下降最快的方向。参数更新就一句话:

text
θ ← θ − η ∇L(θ)

η 是学习率,决定沿这个方向走多远。

先看一个能笔算的例子。设

text
L(w) = (w − 2)² + 1

它的导数:

text
dL/dw = 2(w − 2)

从 w = 5 出发,学习率 η = 0.1:

步wL(w)dL/dw更新后 w
05.00010.0006.0004.400
14.4006.7604.8003.920
23.9204.6863.8403.536
33.5363.3733.0723.229
43.2292.5142.4582.983
52.9831.9661.9662.787

每步都在靠近 w = 2,但步子越来越小,因为越靠近谷底导数越小。这就是梯度下降的全部机制:方向由导数给,步长由学习率给。

图里那条蓝色轨迹走的是另一个病:等高线很扁,窄方向上的曲率很大。学习率按宽方向选,窄方向就会冲过头,在谷底两侧来回跳。第 3 章会讲怎么治,这一章只需要记住:梯度只给方向,不给步长;步长给错了,方向再对也没用。

二、链式法则:一层一层乘回去 ​

神经网络是复合函数。输入 x 经过第一层得到 a₁,再经过第二层得到 a₂,最后经过损失得到 L。要求 L 对第一层权重的导数,就得把中间每一段的导数乘起来。

![链式法则](/ai-four-kings/第 2 章:反向传播/assets/ch2_chain_rule.svg)

<!-- 配图:x → f → g → L 四个节点,蓝色前向箭头,红色虚线反向箭头,底部公式 -->

标量情形最直观。设

text
y = (2x + 1)²

把它拆成两个函数:

text
u = 2x + 1        # 内层
y = u²            # 外层

链式法则说:

text
dy/dx = dy/du · du/dx = 2u · 2 = 4(2x + 1) = 8x + 4

在 x = 1 处,dy/dx = 12。用数值差分验证:取 x = 1.001,y = 3.002² = 9.012004;x = 0.999,y = 2.998² = 8.988004;差商 (9.012004 − 8.988004) / 0.002 = 12.000。对上了。

标量乘法顺序无所谓,矩阵不行。到了多层网络,每一段都是一个雅可比矩阵,链式法则变成矩阵连乘,顺序和转置都必须严格对上。第 1 章那个神经元案例里 ∂L/∂w = ∂L/∂z · x,就是链式法则最底层的一次应用。

把两层网络写开:

text
z₁ = W₁x + b₁
a₁ = ReLU(z₁)
z₂ = W₂a₁ + b₂
L  = CrossEntropy(softmax(z₂), y)

损失对 W₂ 的导数,只需要看最后一段:

text
∂L/∂W₂ = ∂L/∂z₂ · ∂z₂/∂W₂ = dz₂ · a₁ᵀ

损失对 W₁ 的导数,要把中间两层都乘上:

text
∂L/∂W₁ = ∂L/∂z₂ · ∂z₂/∂a₁ · ∂a₁/∂z₁ · ∂z₁/∂W₁
       = (dz₂ W₂) * ReLU'(z₁) · xᵀ

这里出现了三个东西:dz₂ 是损失对第二层预激活的导数,W₂ 把梯度从第二层传回第一层,ReLU'(z₁) 是激活函数的局部导数。下一层的梯度就是上一层的梯度乘以这一层的雅可比——这就是「反向传播」四个字的全部内容。

三、计算图:前向存值,反向传梯度 ​

把上面这些运算画成一张有向图,每个节点代表一次运算,每条边代表数据的流动。

![两层网络的计算图](/ai-four-kings/第 2 章:反向传播/assets/ch2_comp_graph.svg)

<!-- 配图:x、W₁、b₁、z₁、a₁、W₂、b₂、z₂、p、L、y 十一个节点;蓝色前向、红色反向 -->

前向时,每个节点算出自己的输出,并把结果缓存下来。z₁、a₁、z₂、p 都要留着,因为反向时计算局部导数要用到它们。比如 ReLU 的导数只需要知道 z₁ 的符号,乘法节点的导数需要另一个乘数。

反向时,从 L 开始,按拓扑逆序走一遍。每个节点收到「上游传下来的梯度」,乘以自己的局部导数,再传给下游节点。用「伴随量」的说法:

text
ḡ = ∂L/∂v        对每个节点 v

对加法 z = x + y,∂z/∂x = 1,所以 x̄ = z̄,ȳ = z̄。对乘法 z = x · y,x̄ = z̄ · y,ȳ = z̄ · x。对矩阵乘 z = W x,x̄ = Wᵀ z̄,W̄ = z̄ xᵀ。每个节点的反向规则只有几行,组合起来就能处理任意深的网络。

PyTorch 的 autograd 就是这么做的。每个张量上挂一个 grad_fn,记录它是被哪个运算产生的;backward() 沿着这条链调用每个节点的反向函数。

![autograd 的 backward 链](/ai-four-kings/第 2 章:反向传播/assets/ch2_autograd_engine.svg)

<!-- 配图:上排 x、W₁、z₁、a₁、z₂、L 六个张量节点,下排五个 backward 节点,参数 .grad 面板 -->

图里最后一个节点是 AccumulateGrad。它说明了一个容易踩的坑:W.grad 默认是累加的,不是覆盖的。如果训练循环里忘了 optimizer.zero_grad(),两次 backward 的梯度会加在一起,等效于把学习率翻倍,而且越训越乱。这个坑在第 1 章的 PyTorch 实战里已经出现过一次,手写实现里则表现为「忘记把 dw 清零」。

为什么一次反向就够 ​

关键在成本。数值梯度要对每个参数做两次前向,参数量 N 就是 2N 次前向。反向模式只需要一次前向加一次反向,总计算量约为前向的两倍,和参数量无关。

![数值梯度与反向模式的成本](/ai-four-kings/第 2 章:反向传播/assets/ch2_reverse_cost.svg)

<!-- 配图:数值梯度 2×109,386 = 218,772 次前向;反向模式 1 次前向 + 1 次反向 -->

这张图里的 109,386,就是实战篇那个 784→128→64→10 网络的参数量。用它做一次数值梯度,要跑二十多万次前向;用它做反向传播,两次就够。差了五个数量级。这就是所有深度学习框架都必须实现反向模式的原因。

四、数值梯度:只配当裁判 ​

数值梯度不是一无是处。它慢、精度差,但实现简单、不容易写错,所以适合当「裁判」——用来检查手写的反向传播对不对。

![有限差分:用割线代替切线](/ai-four-kings/第 2 章:反向传播/assets/ch2_finite_diff.svg)

<!-- 配图:L(w)=w² 曲线、w−ε 与 w+ε 两点、割线与真切线、两条误差面板 -->

中心差分的公式:

text
∂L/∂w ≈ (L(w + ε) − L(w − ε)) / (2ε)

对 L(w) = w²,代入得:

text
((w+ε)² − (w−ε)²) / (2ε) = (4wε) / (2ε) = 2w

恰好就是真实导数。这不是巧合:中心差分把一阶误差项消掉了,截断误差是 O(ε²)。

但 ε 不能太小。float64 只有约 16 位有效数字,L(w+ε) 和 L(w−ε) 相差很小,两个相近的数相减会发生灾难性抵消,舍入误差按 O(1/ε) 放大。于是总误差是「截断误差」和「舍入误差」的和,呈 U 型。

实战篇在 8→5→4→3 的小网络上实测了七个 ε:

ε最大相对误差
1e−27.569e−4
1e−37.578e−6
1e−47.578e−8
1e−51.036e−9
1e−62.056e−8
1e−72.723e−7
1e−82.301e−6

最优点在 ε = 1e−5,误差 1.036e−9,比验收线 1e−6 低三个数量级。ε 再大,截断误差抬头;ε 再小,舍入误差抬头。这条 U 型曲线是数值方法课上的经典形状,在这里它有一个很实际的用途:确定 gradient check 的 ε。

结论很明确:数值梯度可以用来验证,不能用来训练。一次训练 step 要算 109,386 个参数,每个参数两次前向,成本不可接受;而且它的相对误差最好也只有 1e−9,远比反向传播的 1e−16 差。

五、深层网络:雅可比连乘 ​

反向传播的公式本身没有错,但它的结构有一个副作用:梯度是连乘出来的。

![五层连乘](/ai-four-kings/第 2 章:反向传播/assets/ch2_jacobian_chain.svg)

<!-- 配图:六层依次乘 0.25,从 1.00 降到 0.001,下方对数柱状图 -->

假设每一层的局部导数最大只有 0.25(Sigmoid 的导数上限就是 0.25),五层之后:

text
0.25⁵ = 0.0009765625

八层之后:

text
0.25⁸ ≈ 1.526e−5

越靠前的层,收到的梯度越小。参数更新量正比于梯度,所以前面的层几乎不动——网络看似很深,实际只有最后几层在学。

![Sigmoid 深层梯度流](/ai-four-kings/第 2 章:反向传播/assets/ch2_grad_flow.svg)

<!-- 配图:2 层、4 层、8 层三组梯度柱,柱高按对数压缩,首层/末层比逐组变小 -->

实测数字更直接。实战篇用同样的初始化策略,分别搭 Sigmoid 和 ReLU 网络,测「首层梯度范数 / 末层梯度范数」:

深度SigmoidReLU
20.3142.846
30.0712.193
40.0212.823
60.001242.158
85.50e−52.246

Sigmoid 的比值随深度指数下降,8 层只剩 5.5e−5;ReLU 的比值稳定在 2 附近,几乎不随深度变化。这不是初始化调得好不好的问题,是激活函数导数上限决定的:Sigmoid 每层最多乘 0.25,ReLU 的正半轴导数是 1。

ReLU 也不是没有问题。它在 z = 0 处不可导,框架取次梯度 0。

![ReLU 的不可导点](/ai-four-kings/第 2 章:反向传播/assets/ch2_relu_subgrad.svg)

<!-- 配图:ReLU 曲线、0 点虚线、反向取值面板 z>0→1、z<0→0、z=0→0 -->

这个约定很实用,但有一个后果:如果一个单元长期落在负半轴,它的梯度恒为 0,权重永远不更新,就「死」了。实战篇测到三层网络的零激活比例在 56%–70%,也就是说一大半容量被浪费。第 3 章会讲怎么缓解,这一章先把它当成 ReLU 的代价记下来。

六、随机梯度、学习率、动量 ​

真实训练不会用全量数据算梯度。60,000 张图一次前向太贵,所以每次抽一个 batch,用 batch 梯度近似全量梯度。

![batch 梯度是全量梯度的带噪估计](/ai-four-kings/第 2 章:反向传播/assets/ch2_batch_noise.svg)

<!-- 配图:全量梯度绿色箭头与 batch=1/32/512 三个估计箭头,右侧余弦相似度面板 -->

batch 梯度是全量梯度的无偏估计,但有方差。batch 越小,方向越抖。实战篇在同一个网络上采样 20 次,测 batch 梯度与全量梯度的余弦相似度:

batch余弦相似度相对误差
10.206 ± 0.1553.459
80.595 ± 0.0621.399
320.832 ± 0.0260.660
1280.954 ± 0.0060.324
5120.993 ± 0.0010.123

batch=1 时方向几乎随机,batch=512 时已经非常接近全量。但 batch 从 64 加到 512,每轮步数少 8 倍,同样的 epoch 数下参数更新次数少 8 倍。训练速度和梯度质量在这里做一个折中,64–256 是常见区间。

学习率决定步子大小。

![学习率与曲率](/ai-four-kings/第 2 章:反向传播/assets/ch2_update_geometry.svg)

<!-- 配图:L(θ)=(θ−2)² 曲线,太小、合适、太大三个位置 -->

太小:每步只挪一点,收敛慢。合适:稳定滑到谷底。太大:一步跨过谷底,在两侧来回跳,损失不降反升。学习率没有通用值,它和曲率、batch、优化器都有关。

动量是对「来回跳」的一个修正:不只看当前梯度,还把历史方向带上。

![动量](/ai-four-kings/第 2 章:反向传播/assets/ch2_momentum.svg)

<!-- 配图:SGD 红色折线与动量蓝色平滑线,更新式 v ← βv − η∇L,θ ← θ + v -->

text
v ← βv − η∇L
θ ← θ + v

β 通常取 0.9。在窄方向上,前后两次梯度方向相反,动量把它们抵消,减小震荡;在一致的方向上,动量累加,加速前进。第 3 章会展开 Momentum、RMSProp、Adam 的差别,这一章只需要知道:梯度给方向,优化器决定怎么走。

七、这一章没解决的问题 ​

现在我们知道梯度是一串局部导数的乘积,也知道反向传播按计算图逆序执行。但还有三件事没有落地:

  1. 一个真实网络里,那六个梯度张量的形状到底怎么对?
  2. 手写的反向传播和 PyTorch 的 autograd,数值上能不能逐位对齐?
  3. 既然梯度会消失、会抖动,为什么实战篇的网络还能训到 97.5%?

下一篇先拿一个三层小网络,把每个梯度手算一遍;实战篇再把同样的逻辑写成 NumPy 代码,和 autograd 逐数值对照。

这一章解决不了的问题,留到第 3 章:梯度方向对了,步子怎么迈?SGD、Momentum、Adam 各自在什么地形上更好用?学习率为什么要衰减?这些问题都建立在「梯度已经能算对」之上,所以先把这一章的手推做扎实。

自检 ​

合上笔记,试着回答:

  • 写出 L(w) = (w−2)²+1 的导数和一次梯度下降更新。
  • 用链式法则求 y = (2x+1)² 在 x=1 处的导数,并用中心差分验证。
  • 为什么反向模式的计算成本与参数量无关?数值梯度为什么是 2N 次前向?
  • 中心差分的误差为什么呈 U 型?最优 ε 大概在什么量级?
  • Sigmoid 深层网络为什么梯度消失?ReLU 为什么没有这个问题,它的代价是什么?
  • batch 从 1 加到 512,梯度方向会怎么变?代价是什么?

答不上来的那一条,回到对应小节,把公式和数字重新推一遍。下一篇开始手推三层网络。