理论学习:梯度到底从哪来
第 1 章结尾留了一行黑箱代码:
loss.backward()一行调用,109,386 个参数的梯度全部出现。这一篇的任务,是把这行代码拆开:梯度到底是什么、为什么一次反向就能同时算出所有参数、为什么层数一深它会消失、以及为什么没人用数值差分去训练。
先把结论放前面:梯度不是玄学,它是一串局部导数的乘积。反向传播也不是什么特殊算法,它只是链式法则在计算图上的一种高效执行顺序。所有让人觉得「黑」的部分,都来自这条乘法链太长,人眼算不过来。
一、损失曲面:训练是在地形上走路
网络有参数 θ,损失 L(θ) 是一个把几万个参数映射成一个标量的函数。这个函数的图像在几万维空间里,人看不见,但可以压成二维看一眼。

<!-- 配图:椭圆等高线、一条来回横跳的下降轨迹、最优点与三个量的说明面板 -->
图里的等高线是「损失相同」的点连成的线。越靠中心损失越小。梯度 ∇L 是一个向量,指向损失上升最快的方向;负梯度 −∇L 指向下降最快的方向。参数更新就一句话:
θ ← θ − η ∇L(θ)η 是学习率,决定沿这个方向走多远。
先看一个能笔算的例子。设
L(w) = (w − 2)² + 1它的导数:
dL/dw = 2(w − 2)从 w = 5 出发,学习率 η = 0.1:
| 步 | w | L(w) | dL/dw | 更新后 w |
|---|---|---|---|---|
| 0 | 5.000 | 10.000 | 6.000 | 4.400 |
| 1 | 4.400 | 6.760 | 4.800 | 3.920 |
| 2 | 3.920 | 4.686 | 3.840 | 3.536 |
| 3 | 3.536 | 3.373 | 3.072 | 3.229 |
| 4 | 3.229 | 2.514 | 2.458 | 2.983 |
| 5 | 2.983 | 1.966 | 1.966 | 2.787 |
每步都在靠近 w = 2,但步子越来越小,因为越靠近谷底导数越小。这就是梯度下降的全部机制:方向由导数给,步长由学习率给。
图里那条蓝色轨迹走的是另一个病:等高线很扁,窄方向上的曲率很大。学习率按宽方向选,窄方向就会冲过头,在谷底两侧来回跳。第 3 章会讲怎么治,这一章只需要记住:梯度只给方向,不给步长;步长给错了,方向再对也没用。
二、链式法则:一层一层乘回去
神经网络是复合函数。输入 x 经过第一层得到 a₁,再经过第二层得到 a₂,最后经过损失得到 L。要求 L 对第一层权重的导数,就得把中间每一段的导数乘起来。

<!-- 配图:x → f → g → L 四个节点,蓝色前向箭头,红色虚线反向箭头,底部公式 -->
标量情形最直观。设
y = (2x + 1)²把它拆成两个函数:
u = 2x + 1 # 内层
y = u² # 外层链式法则说:
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,就是链式法则最底层的一次应用。
把两层网络写开:
z₁ = W₁x + b₁
a₁ = ReLU(z₁)
z₂ = W₂a₁ + b₂
L = CrossEntropy(softmax(z₂), y)损失对 W₂ 的导数,只需要看最后一段:
∂L/∂W₂ = ∂L/∂z₂ · ∂z₂/∂W₂ = dz₂ · a₁ᵀ损失对 W₁ 的导数,要把中间两层都乘上:
∂L/∂W₁ = ∂L/∂z₂ · ∂z₂/∂a₁ · ∂a₁/∂z₁ · ∂z₁/∂W₁
= (dz₂ W₂) * ReLU'(z₁) · xᵀ这里出现了三个东西:dz₂ 是损失对第二层预激活的导数,W₂ 把梯度从第二层传回第一层,ReLU'(z₁) 是激活函数的局部导数。下一层的梯度就是上一层的梯度乘以这一层的雅可比——这就是「反向传播」四个字的全部内容。
三、计算图:前向存值,反向传梯度
把上面这些运算画成一张有向图,每个节点代表一次运算,每条边代表数据的流动。

<!-- 配图:x、W₁、b₁、z₁、a₁、W₂、b₂、z₂、p、L、y 十一个节点;蓝色前向、红色反向 -->
前向时,每个节点算出自己的输出,并把结果缓存下来。z₁、a₁、z₂、p 都要留着,因为反向时计算局部导数要用到它们。比如 ReLU 的导数只需要知道 z₁ 的符号,乘法节点的导数需要另一个乘数。
反向时,从 L 开始,按拓扑逆序走一遍。每个节点收到「上游传下来的梯度」,乘以自己的局部导数,再传给下游节点。用「伴随量」的说法:
ḡ = ∂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() 沿着这条链调用每个节点的反向函数。

<!-- 配图:上排 x、W₁、z₁、a₁、z₂、L 六个张量节点,下排五个 backward 节点,参数 .grad 面板 -->
图里最后一个节点是 AccumulateGrad。它说明了一个容易踩的坑:W.grad 默认是累加的,不是覆盖的。如果训练循环里忘了 optimizer.zero_grad(),两次 backward 的梯度会加在一起,等效于把学习率翻倍,而且越训越乱。这个坑在第 1 章的 PyTorch 实战里已经出现过一次,手写实现里则表现为「忘记把 dw 清零」。
为什么一次反向就够
关键在成本。数值梯度要对每个参数做两次前向,参数量 N 就是 2N 次前向。反向模式只需要一次前向加一次反向,总计算量约为前向的两倍,和参数量无关。

<!-- 配图:数值梯度 2×109,386 = 218,772 次前向;反向模式 1 次前向 + 1 次反向 -->
这张图里的 109,386,就是实战篇那个 784→128→64→10 网络的参数量。用它做一次数值梯度,要跑二十多万次前向;用它做反向传播,两次就够。差了五个数量级。这就是所有深度学习框架都必须实现反向模式的原因。
四、数值梯度:只配当裁判
数值梯度不是一无是处。它慢、精度差,但实现简单、不容易写错,所以适合当「裁判」——用来检查手写的反向传播对不对。

<!-- 配图:L(w)=w² 曲线、w−ε 与 w+ε 两点、割线与真切线、两条误差面板 -->
中心差分的公式:
∂L/∂w ≈ (L(w + ε) − L(w − ε)) / (2ε)对 L(w) = w²,代入得:
((w+ε)² − (w−ε)²) / (2ε) = (4wε) / (2ε) = 2w恰好就是真实导数。这不是巧合:中心差分把一阶误差项消掉了,截断误差是 O(ε²)。
但 ε 不能太小。float64 只有约 16 位有效数字,L(w+ε) 和 L(w−ε) 相差很小,两个相近的数相减会发生灾难性抵消,舍入误差按 O(1/ε) 放大。于是总误差是「截断误差」和「舍入误差」的和,呈 U 型。
实战篇在 8→5→4→3 的小网络上实测了七个 ε:
| ε | 最大相对误差 |
|---|---|
| 1e−2 | 7.569e−4 |
| 1e−3 | 7.578e−6 |
| 1e−4 | 7.578e−8 |
| 1e−5 | 1.036e−9 |
| 1e−6 | 2.056e−8 |
| 1e−7 | 2.723e−7 |
| 1e−8 | 2.301e−6 |
最优点在 ε = 1e−5,误差 1.036e−9,比验收线 1e−6 低三个数量级。ε 再大,截断误差抬头;ε 再小,舍入误差抬头。这条 U 型曲线是数值方法课上的经典形状,在这里它有一个很实际的用途:确定 gradient check 的 ε。
结论很明确:数值梯度可以用来验证,不能用来训练。一次训练 step 要算 109,386 个参数,每个参数两次前向,成本不可接受;而且它的相对误差最好也只有 1e−9,远比反向传播的 1e−16 差。
五、深层网络:雅可比连乘
反向传播的公式本身没有错,但它的结构有一个副作用:梯度是连乘出来的。

<!-- 配图:六层依次乘 0.25,从 1.00 降到 0.001,下方对数柱状图 -->
假设每一层的局部导数最大只有 0.25(Sigmoid 的导数上限就是 0.25),五层之后:
0.25⁵ = 0.0009765625八层之后:
0.25⁸ ≈ 1.526e−5越靠前的层,收到的梯度越小。参数更新量正比于梯度,所以前面的层几乎不动——网络看似很深,实际只有最后几层在学。

<!-- 配图:2 层、4 层、8 层三组梯度柱,柱高按对数压缩,首层/末层比逐组变小 -->
实测数字更直接。实战篇用同样的初始化策略,分别搭 Sigmoid 和 ReLU 网络,测「首层梯度范数 / 末层梯度范数」:
| 深度 | Sigmoid | ReLU |
|---|---|---|
| 2 | 0.314 | 2.846 |
| 3 | 0.071 | 2.193 |
| 4 | 0.021 | 2.823 |
| 6 | 0.00124 | 2.158 |
| 8 | 5.50e−5 | 2.246 |
Sigmoid 的比值随深度指数下降,8 层只剩 5.5e−5;ReLU 的比值稳定在 2 附近,几乎不随深度变化。这不是初始化调得好不好的问题,是激活函数导数上限决定的:Sigmoid 每层最多乘 0.25,ReLU 的正半轴导数是 1。
ReLU 也不是没有问题。它在 z = 0 处不可导,框架取次梯度 0。

<!-- 配图:ReLU 曲线、0 点虚线、反向取值面板 z>0→1、z<0→0、z=0→0 -->
这个约定很实用,但有一个后果:如果一个单元长期落在负半轴,它的梯度恒为 0,权重永远不更新,就「死」了。实战篇测到三层网络的零激活比例在 56%–70%,也就是说一大半容量被浪费。第 3 章会讲怎么缓解,这一章先把它当成 ReLU 的代价记下来。
六、随机梯度、学习率、动量
真实训练不会用全量数据算梯度。60,000 张图一次前向太贵,所以每次抽一个 batch,用 batch 梯度近似全量梯度。

<!-- 配图:全量梯度绿色箭头与 batch=1/32/512 三个估计箭头,右侧余弦相似度面板 -->
batch 梯度是全量梯度的无偏估计,但有方差。batch 越小,方向越抖。实战篇在同一个网络上采样 20 次,测 batch 梯度与全量梯度的余弦相似度:
| batch | 余弦相似度 | 相对误差 |
|---|---|---|
| 1 | 0.206 ± 0.155 | 3.459 |
| 8 | 0.595 ± 0.062 | 1.399 |
| 32 | 0.832 ± 0.026 | 0.660 |
| 128 | 0.954 ± 0.006 | 0.324 |
| 512 | 0.993 ± 0.001 | 0.123 |
batch=1 时方向几乎随机,batch=512 时已经非常接近全量。但 batch 从 64 加到 512,每轮步数少 8 倍,同样的 epoch 数下参数更新次数少 8 倍。训练速度和梯度质量在这里做一个折中,64–256 是常见区间。
学习率决定步子大小。

<!-- 配图:L(θ)=(θ−2)² 曲线,太小、合适、太大三个位置 -->
太小:每步只挪一点,收敛慢。合适:稳定滑到谷底。太大:一步跨过谷底,在两侧来回跳,损失不降反升。学习率没有通用值,它和曲率、batch、优化器都有关。
动量是对「来回跳」的一个修正:不只看当前梯度,还把历史方向带上。

<!-- 配图:SGD 红色折线与动量蓝色平滑线,更新式 v ← βv − η∇L,θ ← θ + v -->
v ← βv − η∇L
θ ← θ + vβ 通常取 0.9。在窄方向上,前后两次梯度方向相反,动量把它们抵消,减小震荡;在一致的方向上,动量累加,加速前进。第 3 章会展开 Momentum、RMSProp、Adam 的差别,这一章只需要知道:梯度给方向,优化器决定怎么走。
七、这一章没解决的问题
现在我们知道梯度是一串局部导数的乘积,也知道反向传播按计算图逆序执行。但还有三件事没有落地:
- 一个真实网络里,那六个梯度张量的形状到底怎么对?
- 手写的反向传播和 PyTorch 的 autograd,数值上能不能逐位对齐?
- 既然梯度会消失、会抖动,为什么实战篇的网络还能训到 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,梯度方向会怎么变?代价是什么?
答不上来的那一条,回到对应小节,把公式和数字重新推一遍。下一篇开始手推三层网络。