反向传播不是一个新模型,它是一种算梯度的算法:从损失出发,按链锁律一层一层往回乘。 代价是一次前向加一次反向,复杂度和一次前向同阶。 没有它,每算一个参数的梯度就要重跑一遍前向。 实测:13 个参数逐个和数值微分对拍,最大相对误差 1.21e-08。
4.6 节的梯度法假设梯度是白给的,这一节回答「怎么给」。 4.2 节的激励函数导数在这里全面登场:反向传播每穿一层,乘的就是那一层的导数。 4.4 节那个 2-4-1 的 XOR 网络能训起来,靠的正是这一节的东西。
一、链锁律:一层一层往回乘
复合函数求导只有一条规则:dz/dx = dz/dy 乘 dy/dx。这就是反向传播的全部原理,剩下的都是记账。 网络是多层复合:L 依赖输出 p,p 依赖 z₂,z₂ 依赖隐层 H,H 依赖 z₁,z₁ 依赖 W₁。 要从 L 算到 W₁,就沿这条链把每段的导数依次相乘。 关键在乘的顺序。 从输出往回乘,中间结果可以复用;从前往后算,同一段导数会被重复计算几十万次。 「反向」两个字指的就是这个顺序,不是「把误差送回去」这种拟人说法。
二、四步公式
拿一个 2-3-1 的两层网络当例子:tanh 隐层、sigmoid 输出、交叉熵损失。四个式子就写完了:
dp = (p - y) / n 损失对输出层 z 的偏导
dW2 = H 的转置 乘 dp 输出层权重
dH = dp 乘 W2 再乘 (1 - H*H) 穿过 tanh 往回传
dW1 = X 的转置 乘 dH 隐层权重
第二行那个 dp 值得多说一句: 严格讲它是「损失对 z₂ 的偏导」,不是对 p 的。因为交叉熵的 1/p 和 sigmoid 的 p(1-p) 恰好约掉, 两者长得一模一样。 (1 - HH) 是四个式子里唯一和激励函数有关的地方。 换 sigmoid 就是 H(1-H),换 ReLU 就是「正数处 1、负数处 0」。* 转置的位置最容易记混。 判断办法是「让形状对上」:X 是 4×2、dH 是 4×3,乘出来必须是 2×3,那就只剩 X 的转置乘 dH 一种写法。
三、一次反向,拿到全部梯度
数值微分算一个参数要两次前向。n 个参数就要 2n 次,总代价是 O(n²)。 反向传播是 O(n):一次前向把各层中间值存下来,一次反向把它们乘回去,所有参数的梯度一次到手。 所以深度学习能跑起来,不是因为模型多聪明,是因为梯度算得够便宜。 1986 年那篇论文真正解决的问题,就是这个成本。 代价是内存。反向要用前向的中间值,所以训练比推理吃显存得多。 batch 一大就爆显存,原因在这里——不是模型变大了,是存下来的中间结果变多了。
四、计算图与自动微分
把每一步运算画成节点、把数据流画成边,就是计算图。 反向传播就是在图上从输出往输入走一遍,每个节点套用自己的导数规则。 TensorFlow 和 PyTorch 做的都是这件事,叫自动微分。 它和符号求导(表达式会爆炸)、数值求导(慢且精度差)都不一样,它记住的是前向时算过的数, 反向时直接拿来乘。 所以 Keras 里 fit 那一行,底下跑的就是这个图加这套规则。框架替你写的,就是这一节这四行公式。
五、为什么必须对拍
反向传播的公式有十来行,任何一处转置写错、任何一处漏了导数因子,代码照样能跑, 损失照样会降——只是降得慢,或者降到一个奇怪的地方。 所以标准动作是:拿数值微分当标准答案,逐参数对一遍,相对误差在 1e-7 以内算过。 这个习惯在小网络上是教科书,在大网络上是救命。 大网络的错往往表现为「能训,但比论文差两个点」,这是最难查的一类 bug。
六、动手 Demo:把它算出来的梯度和数值微分对一遍
造一个 2-3-1 的小网络(tanh 隐层、sigmoid 输出、交叉熵损失),用链锁律算出全部梯度, 再拿中心差分逐个对拍。
import numpy as np
X = np.array([0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]).reshape(4, 2)
y = np.array([0.0, 1.0, 1.0, 0.0])
rg = np.random.default_rng(3)
W1, b1 = rg.normal(0, 0.8, (2, 3)), rg.normal(0, 0.3, 3)
W2, b2 = rg.normal(0, 0.8, (3, 1)), rg.normal(0, 0.3, 1)
def forward():
H = np.tanh(X @ W1 + b1)
p = 1.0 / (1.0 + np.exp(-(H @ W2 + b2))).ravel()
L = -np.mean(y * np.log(p + 1e-12) + (1 - y) * np.log(1 - p + 1e-12))
return H, p, L
H, p, L = forward()
dp = (p - y) / len(y) # 损失对 z2 的偏导
dW2 = (H.T @ dp).reshape(-1, 1) # (3,4) 乘 (4,) 得 (3,1)
db2 = np.array([dp.sum()])
dH = np.outer(dp, W2.ravel()) * (1.0 - H * H) # 1-H² 是 tanh 的导数
dW1 = X.T @ dH # (2,4) 乘 (4,3) 得 (2,3)
db1 = dH.sum(0)
h = 1e-6
for arr, ix, bp in ((W1, (1, 2), dW1[1, 2]), (b2, (0,), db2[0])):
old = arr[ix]
arr[ix] = old + h
lp = forward()[2]
arr[ix] = old - h
lm = forward()[2]
arr[ix] = old
num = (lp - lm) / (2 * h)
print("反向 %+.6f 数值 %+.6f 相对误差 %.2e"
% (bp, num, abs(num - bp) / max(1e-12, abs(num) + abs(bp))))
实跑输出:
--------------------------------------------------------------------
反向传播:把它算出来的梯度和数值微分对一遍
--------------------------------------------------------------------
参数 反向传播 数值微分 相对误差
------------------------------------------------
W1[0,0] +0.329950 +0.329950 6.18e-11
W1[1,2] -0.000327 -0.000327 1.21e-08
b1[0] +0.190879 +0.190879 3.38e-10
b1[2] -0.002537 -0.002537 1.06e-08
W2[0,0] +0.220118 +0.220118 9.09e-11
b2[0] -0.023416 -0.023416 8.24e-10
起始损失 1.006563,13 个参数逐一对拍,最大相对误差 1.21e-08。
**相对误差在 1e-9 量级,说明反向传播那几行公式一行都没写错。**
这个对拍是手写神经网络时的标准动作:**先不信自己推的公式,拿数值微分当标准答案。**
反向传播的链条一共四步,每一行都对应一个「外层导数乘内层导数」:
dp = (p - y) / n 损失对输出的偏导
dW2 = H 转置 乘 dp 输出层权重
dH = dp 乘 W2 再乘 (1 - H²) 穿过 tanh 往回传
dW1 = X 转置 乘 dH 隐层权重
**(1 - H²) 这一项就是 tanh 的导数。**换成 sigmoid 就是 H(1-H),换成 ReLU 就是
「正数处为 1、负数处为 0」。
**所以换激励函数,只需要换这一个因子。**
表的四列:参数名、反向传播给的梯度、数值微分的梯度、相对误差。 六行里最大的相对误差是 1.21e-08,在 W1[1,2] 那一行。 1e-8 这个量级不是「差不多」,是「能对上的极限」。 中心差分的截断误差是 h² 量级、舍入误差是 1/h 量级,h=1e-6 时两者折中后最优精度就在 1e-9 附近。 所以看到 1e-8 就该满意,追求更小反而说明 h 取偏了。 起始损失 1.006563,比 ln 2 = 0.693147 大不少。 随机初始化的网络一开始就是「比乱猜还差一点」,这很正常。 四步的对应关系:dp 是损失对输出层 z 的偏导,dW2 是输出层权重,dH 是穿过 tanh 往回传的那一步, dW1 是隐层权重。dH 里那个 (1 - H*H) 就是 tanh 的导数,H 是前向时存下来的隐层输出。 把激励函数换成 ReLU,dH 变成 dp 乘 W2 再乘一个 0 或 1 的掩码。 除了这一个因子,四步公式一个字都不用改。
30 秒实验
- 把 h 从 1e-6 改成 1e-2,相对误差会变大还是变小?为什么?
- 把隐层的 tanh 换成 sigmoid,dH 那一行要改成什么?
- 如果损失换成平方误差,dp 还会是 (p - y)/n 吗?(提示:平方误差没有那个能约掉的分母)
常见误区
- 以为反向传播是「把误差传回去修正」。它是求导,不是通信。 想清这一点,「梯度消失」才讲得通——不是误差信号在路上衰减了,是乘了一串小于 1 的导数。
- 以为反向传播只能用于神经网络。只要是「一串可微运算」就能用。 现在它被用在物理仿真、渲染、甚至线性方程求解器上。
- 以为梯度对拍是学生练习。生产代码里手写反向传播一样要跑对拍。 一个转置写错不会报错,只会让模型差两个点。
自测三问(点开对答案)
1. 反向传播的核心数学是什么? —— 链锁律。把复合函数的导数沿计算图逐段相乘,从输出往回走,每段的中间结果只算一次。 2. 为什么它比数值微分快? —— 数值微分算一个参数要两次前向,n 个参数就是 2n 次,代价 O(n²)。 反向传播一次前向加一次反向拿到全部梯度,代价 O(n)。 3. 换激励函数要改反向传播的哪一处? —— 只改穿过那一层时乘的导数因子:tanh 是 1-H*H,sigmoid 是 H(1-H),ReLU 是 0 或 1 的掩码。 其余步骤不变。到这里,一个能训的两层网络的全套零件齐了:神经元、激励、损失、梯度、反向传播。 下一节换一个像样点的数据集——手写数字。MNIST 是深度学习界的小白鼠。
- ☐ (本节完,下一节 4.8 MNIST)