🏠 总目录📚 本教程 08 · 反向传播 ← →
📑 本页目录(点开跳转)

08 / 反向传播 · 一段一段看影响

先看误差怎么传回去,
再看矩阵怎么写。

反向传播做的事:算出每个参数稍微变化时,最终损失会怎样变化。

你先带走这句就够了

先算结果,
再把局部影响乘回去。

反向传播负责算梯度;优化器才负责用梯度更新参数。

  1. 1前向

    得到中间值和最终损失。

  2. 2反向

    沿依赖关系计算、复用梯度。

  3. 3更新

    按学习率与梯度修改参数。

01 / 一个能手算的例子

只看一个参数 w = 1

这里用平方运算演示链式法则,不是在推荐网络的激活函数。

参数 w1z = 2w2a = z²4L = ½(a−1)²4.5
先沿箭头算数值,再从损失往回问:前一个量变一点,后一个量变多少?

02 / 链式法则

三个局部比例,乘成一个总影响

$$\frac{\partial L}{\partial w}=\frac{\partial L}{\partial a}\frac{\partial a}{\partial z}\frac{\partial z}{\partial w}$$

在当前数值处:3 × 4 × 2 = 24。参数 w 微增时,损失的一阶变化率是 24。

点完整的分式看解释。它描述当前位置的局部变化,不是任意大步长下的精确差值。

03 / 梯度下降的下一步

有了梯度,才轮到更新

$$w_{\mathrm{new}}=w-\eta\frac{\partial L}{\partial w}$$

学习率取 0.01:1 − 0.01 × 24 = 0.76。这里梯度为正,沿负梯度就减小 w。

步子过大仍可能使损失上升;算对梯度不等于任何学习率都合适。

可以停在这里

你已经能区分“向前算结果”“向后算梯度”和“用梯度更新”。矩阵写法只是把许多这样的局部影响一起计算。

继续看异或实验、形状对照与梯度检验 →
继续探索

原有正文、图解和例子都在下方。按需跳转,不必一次读完。

08 · 反向传播

⏱ 36 分钟 | ⭐ 手写一遍就永远忘不掉 | 🔨 必须动手


🎯 一句话

反向传播 = 链式法则 + 从后往前算,把中间结果复用起来。 它不是什么高深理论,就是求导时别做重复劳动。

输入 x隐层 a¹隐层 a²输出 ŷ前向:算出预测反向:把「责任」δ 一层层传回去δ = ∂J/∂z这一层要担多少责任
前向把数据推到输出算出损失,反向把「这一层要担多少责任」一层层传回去。⭐ 反向传播的全部内容,就是那个 δ 怎么从右边走到左边。

🧠 一、先建立直觉:谁该为错误负责

预测错了,loss = 0.8 「这个错,每个参数该负多少责任?」 反向传播回答的就是这个问题 得到每个参数的梯度 ∂loss/∂w w₁ 责任大 w₂ 责任小 w₃ 责任中 谁责任大,就多改一点:w ← w − lr × 梯度 w₁ 迈一大步 · w₂ 几乎不动 · w₃ 迈一中步
把 loss 想成一次事故,反向传播做的就是追责:先算出每个参数各该担多少责任(条越长=梯度越大),再按责任大小决定各自改多少。⭐ 它和「前向 / 反向两条流」那张图互补 —— 那张讲责任怎么传回去,这张讲责任算出来之后怎么用。

💡 人话:一个公司业绩差了,要追责到每个部门、每个人。 反向传播就是从结果开始,一层层往回追责——而且追责时会复用上一层已经算好的结论,不重复劳动。


⛓️ 二、链式法则:唯一需要的数学

对照

如果 loss ← a ← z ← w (w 影响 z,z 影响 a,a 影响 loss)

那么 ∂loss/∂w = ∂loss/∂a × ∂a/∂z × ∂z/∂w

↑ 一路乘回去

💡 人话:「w 变一点点,z 变多少;z 变一点点,a 变多少;a 变一点点,loss 变多少」——三个比例连乘。

为什么要从后往前算:因为 ∂loss/∂a 这一段,前面所有参数都要用。 从后往前计算并复用中间梯度,适合“许多参数影响一个标量损失”的训练问题。反向计算量通常与前向计算量同阶。 不要把它简化成无条件的 O(n) 对 O(n²):复杂度还取决于计算图、输入输出维度;前向自动微分也不等于逐参数做数值差分。

📊 这个"复用"能省多少

信息关系

朴素做法(数值梯度):
对每个参数 ±ε 各跑一次前向→2P 次完整前向传播
P = 100 万参数→200 万次前向 💀 完全不可行
反向传播:
1 次前向 + 1 次反向:常粗估为约 3 次前向算量
【一次拿到全部参数的梯度】 ⭐

🔑 所以 1986 年反向传播的普及是个里程碑—— 想法不新(链式法则有几百年了), 它推动了多层网络训练的普及。

💡 一个值得记住的推论: 反向传播的代价大约是前向的 2 倍。 所以「训练一个 epoch 的成本 ≈ 推理 3 次全部数据」—— 这是算量的粗估,不是精确耗时;数据加载、通信、优化器等开销还要另算。


🔨 三、动手:纯 numpy 手写一个能学会异或的网络

这段代码是本章的核心。跑一遍,然后逐行对照下面的注解。

import numpy as np

X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)
y = np.array([[0],[1],[1],[0]], dtype=float)      # XOR

def sigmoid(z): return 1/(1+np.exp(-z))

rng = np.random.RandomState(0)
# 网络结构:2 → 8 → 1
W1 = rng.randn(2, 8) * 0.5;  b1 = np.zeros(8)
W2 = rng.randn(8, 1) * 0.5;  b2 = np.zeros(1)
lr = 0.5

for step in range(3001):
    # ============ 前向传播:算出预测和损失 ============
    z1 = X @ W1 + b1          # 线性
    a1 = np.tanh(z1)          # 非线性(第 7 章)
    z2 = a1 @ W2 + b2         # 线性
    a2 = sigmoid(z2)          # 输出概率
    loss = -np.mean(y*np.log(a2+1e-9) + (1-y)*np.log(1-a2+1e-9))   # 交叉熵

    # ============ 反向传播:从后往前追责 ============
    n = len(X)
    dz2 = (a2 - y) / n        # ⭐ sigmoid+交叉熵的梯度就这么干净:预测−真实
    dW2 = a1.T @ dz2          # z2 = a1@W2 → ∂z2/∂W2 = a1
    db2 = dz2.sum(0)
    da1 = dz2 @ W2.T          # 责任传给上一层
    dz1 = da1 * (1 - a1**2)   # 穿过 tanh:tanh 的导数是 1−tanh²
    dW1 = X.T @ dz1
    db1 = dz1.sum(0)

    # ============ 更新参数:朝梯度反方向走一小步 ============
    W2 -= lr*dW2; b2 -= lr*db2
    W1 -= lr*dW1; b1 -= lr*db1

    if step % 1000 == 0:
        print(f"step {step:<5} loss={loss:.4f}")

print("预测:", (a2>0.5).astype(int).ravel(), " 真实:", y.astype(int).ravel())

实测输出:

异或训练的结果:先看 loss 是否持续下降
训练步损失 loss
00.8049
10000.0038
20000.0016
30000.0009

预测: [0 1 1 0] 真实: [0 1 1 0] ✅ 学会异或了

你刚刚从零实现了一个神经网络的完整训练。 大约 25 行。


🔍 四、五个值得盯着看的细节

① dz2 = (a2 - y) —— 干净得可疑

Sigmoid + 交叉熵组合时,梯度正好就是「预测 − 真实」,所有复杂项全约掉了。

🔗 这就是第 3 章说的「分类为什么不用 MSE」的另一面—— 这个组合是被数学选中的,不是随便配的。

② 反向的每一步都对应前向的一步

信息关系

前向:z1→a1→z2→a2→loss
反向:dz1 ← da1 ← dz2 ← loss
(每一步都在问:上一步的输出变一点,最终 loss 变多少)

③ 穿过激活函数时乘它的导数

对照

tanh: dz = da * (1 - a²)

ReLU: dz = da * (z > 0) ← 负区间直接置 0

sigmoid: dz = da * a * (1-a)

💡 梯度消失的根源就在这里:如果激活函数的导数小于 1(如 sigmoid 最大 0.25), 连乘较小的导数可能导致梯度衰减。ReLU 在正区间导数为 1、负区间为 0,能缓解一类饱和问题;梯度还要经过权重等运算,不能保证完全不消失。

④ .sum(0) 是因为 batch

一个 batch 里每个样本都对偏置有贡献,要加起来。

⑤ 每一行梯度的形状怎么推出来(不用背)

结果对照

⭐ 一条规则走天下:【梯度的形状 = 它对应那个量的形状】
W1 是 (2, 8)→dW1 必须是 (2, 8)
X 是 (4, 2)→X.T 是 (2, 4)
dz1 是 (4, 8)
dW1 = X.T @ dz1 形状是 (2,4)@(4,8) = (2,8) ✅ 对上了

💡 卡住时先检查形状,再核对链式法则:形状能排除一些错误,但对得上形状不代表导数一定正确。

要得到 (2,8),手上有 X(4,2) 和 dz1(4,8)—— 只有 X.T @ dz1 这一种乘法能对上形状。 这个技巧在手写任何层的反向传播时都管用。


🔬 五、梯度检验:怎么确认自己写对了

手写反向传播极易出错,而且错了不报错,只是"学得慢"。 专业做法是数值检验:

# 数值梯度:把某个参数挪动一点点,看 loss 变多少
eps = 1e-5
W1p = W1.copy(); W1p[0,0] += eps
W1m = W1.copy(); W1m[0,0] -= eps
num_grad = (forward_loss(W1p,...) - forward_loss(W1m,...)) / (2*eps)

# 和你的解析梯度对比
print(f"数值梯度 {num_grad:.8f}")
print(f"解析梯度 {dW1[0,0]:.8f}")

实测结果:

要点

数值梯度 -0.00007294

解析梯度 -0.00007294

相对误差 1.49e-08 ✅ 实现正确

📋 直接能跑的完整版(上面那段依赖前面那个 XOR 网络,这里把它带上了)

复制整段到空文件,python 文件名.py 就能跑,只需要 NumPy。

import numpy as np

X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)
y = np.array([[0],[1],[1],[0]], dtype=float)

def sigmoid(z): return 1/(1+np.exp(-z))

rng = np.random.RandomState(0)
W1 = rng.randn(2,8)*0.5; b1 = np.zeros(8)
W2 = rng.randn(8,1)*0.5; b2 = np.zeros(1)
lr = 0.5

for step in range(3001):                      # 和第三节那段训练循环一样
    a1 = np.tanh(X @ W1 + b1); a2 = sigmoid(a1 @ W2 + b2)
    n = len(X); dz2 = (a2 - y) / n
    dW2 = a1.T @ dz2; db2 = dz2.sum(0)
    dz1 = (dz2 @ W2.T) * (1 - a1**2)
    dW1 = X.T @ dz1;  db1 = dz1.sum(0)
    W2 -= lr*dW2; b2 -= lr*db2; W1 -= lr*dW1; b1 -= lr*db1

def loss_at(W1, b1, W2, b2):
    a1 = np.tanh(X @ W1 + b1); a2 = sigmoid(a1 @ W2 + b2)
    return -np.mean(y*np.log(a2+1e-9) + (1-y)*np.log(1-a2+1e-9))

# ⭐ 关键:在【当前这组权重】上重新算一次解析梯度,
#    别直接用训练循环里留下的 dW1 —— 那是更新【之前】那组权重上算的(见下面第 5 个坑)
a1 = np.tanh(X @ W1 + b1); a2 = sigmoid(a1 @ W2 + b2)
n = len(X); dz2 = (a2 - y) / n
dz1 = (dz2 @ W2.T) * (1 - a1**2)
dW1 = X.T @ dz1

eps = 1e-5
Wp = W1.copy(); Wp[0,0] += eps
Wm = W1.copy(); Wm[0,0] -= eps
num_grad = (loss_at(Wp,b1,W2,b2) - loss_at(Wm,b1,W2,b2)) / (2*eps)

rel = abs(num_grad - dW1[0,0]) / (abs(num_grad) + abs(dW1[0,0]) + 1e-12)
print(f"数值梯度 {num_grad:.8f}")
print(f"解析梯度 {dW1[0,0]:.8f}")
print(f"相对误差 {rel:.2e}")

实跑输出就是上面那三行:-0.00007294 / -0.00007294 / 1.49e-08。

🔑 本例参考:双精度小例子可参考相对误差 < 1e-6;阈值需随数值尺度、精度和算子调整,不能替代完整测试。 自己实现任何自定义层/损失函数时,梯度检验是必做的一步——第 18 章挑战 A 会大量用到。

⚠️ 梯度检验的五个坑:

坑 说明
优先用中心差分 光滑函数下,中心差分截断误差是 O(ε²),单边差分为 O(ε);不是固定差两个数量级,还要考虑浮点误差
ε 不能太小 双精度示例可从 1e-5 左右试起,并按尺度调整;太小可能被浮点舍入误差淹没
ReLU 的折点 z≈0 处数值梯度天然不准,这是函数本身不可导,不是 bug
必须关掉随机性 固定随机性与状态:关闭 Dropout;固定 BN 的模式/统计。BN 在固定批次上不等于随机运算
💀 两边必须在同一组权重上算 训练循环跑完之后,dW1 就躺在作用域里,顺手拿它比最自然 —— ⚠️ 可它是「更新前」那组权重上算的,而数值梯度取在「更新后」的权重上。实测相对误差 3.90e-04,⭐ 按本章 < 1e-6 的判据,你会以为自己的反向传播写错了,而它其实是对的。正确做法:检验前用当前权重重算一次前向和反向(见上面折叠的完整版)

💡 相对误差怎么算:|数值−解析| / (|数值|+|解析|+1e-12)。 用相对误差而不是绝对误差,是因为梯度本身的量级可能很小或很大。


🚀 六、现代框架帮你做了什么

# 🧩 骨架:`loss` 来自你自己的代码,这一段只看写法
# 上面 25 行的反向传播,在 PyTorch 里是这样:
loss.backward()      # 就这一行
optimizer.step()

框架做的事叫「自动微分」: - 前向计算时,悄悄记录下一张计算图(谁是谁算出来的) - .backward() 时沿着图从后往前,自动套用链式法则

🔬 想看严格的推导(任意层数、矩阵形式的四个方程)? 见《机器学习的数学原理》第 12 章—— 那里还解释了残差连接在公式上到底多了什么。

💡 所以你以后不用手写反向传播 —— 但手写过一遍,你就知道: - 梯度爆炸/消失是怎么发生的(连乘) - 为什么要 optimizer.zero_grad()(梯度默认累加) - 自定义损失函数为什么可能不收敛(可导性)

第 18 章挑战 A 会让你从零造一个 mini-torch,把这套自动微分自己实现出来。


🔗 和站内其他章的关系

相关的地方 根因在这
推荐算法第 5 章手写 MF 的 SGD 更新 同一套梯度下降,只是网络更简单
全景导论第 15 章「求导交给框架」 框架做的就是本章的自动微分
梯度消失/爆炸 反向连乘导致的指数衰减/增长

✅ 检查点

  1. 为什么反向传播要"从后往前"算?复杂度差别是多少?
  2. 反向传播的代价大约是前向的几倍?这个数字有什么实用价值?
  3. Sigmoid + 交叉熵的梯度是什么?为什么这么干净?
  4. 手写时梯度的形状怎么快速推出来?
  5. 梯度穿过 ReLU 时怎么变?穿过 sigmoid 呢?
  6. 梯度消失的根源是什么?
  7. 梯度检验怎么做?判断标准是什么?有哪五个坑?
  8. 为什么用相对误差而不是绝对误差?
👀 答案
  1. 在标量损失的计算图中,靠后的中间梯度可被前面的参数复用。逐参数做中心差分要 2P 次前向;反向传播只需一次前向和一次反向,计算量通常与前向同阶。
  2. 反向计算常粗估为前向约 2 倍,因此前向加反向约 3 倍;这是算量的粗估,不是包含加载、优化器与通信等开销的精确耗时。
  3. 正好是 (预测 − 真实)。因为这个组合在数学上互相约掉了复杂项——它们是被设计成配对使用的。
  4. 梯度的形状 = 它对应那个量的形状。X.T @ dz1 的结果是 (2,8),与 W1 一致;这是形状自检,还需要由链式法则确认,并用数值梯度检验。
  5. ReLU:dz = da * (z>0),负区间直接置 0;sigmoid:dz = da * a * (1-a),最大值只有 0.25。
  6. 小导数连乘可能衰减;sigmoid 的导数最大为 0.25。ReLU 正区间为 1、负区间为 0,梯度仍受权重等因素影响,并不保证不消失。
  7. 用中心差分 (L(+ε)−L(−ε))/(2ε) 对比解析梯度。本页双精度小例子可参考相对误差 1e-6,并检查:①中心差分;②ε 与数值尺度适配;③避开不可导折点;④固定随机性和状态,如 Dropout 与 BN 的运行统计;⑤两边在同一组权重上重算。
  8. 因为梯度本身的量级可能很小或很大,绝对误差没有统一的判断标准;相对误差 |数值−解析|/(|数值|+|解析|) 才是尺度无关的。

🛑 可以停在这里

⚡ 走神救援

反向传播用链式法则,从后向前计算并复用梯度;优化器再用梯度更新参数。

  • 先看一条短链,再看异或网络;梯度形状是自检线索,不是推导正确的证明。
  • 激活导数和权重共同影响梯度,ReLU 不能保证梯度永不消失。
  • 检验时用中心差分、合适的 ε,避开折点、固定状态,并在同一组权重上重算解析梯度。
  • 正文保留了五个细节、五个检验陷阱与可展开的完整检验示例,忘了就回到对应卡片。

下一节 👉 09-优化器与学习率.md

打卡记录保存在你的浏览器里,首页能看到总进度