跳到主要内容

第 04 章说,神经网络是主角的唯一理由是它能解决贡献度分配。这一章就是那个解法:一次前向、一次反向,每个参数各自认领自己的那份责任。

反向传播与自动微分

1. 这一章讲什么

三件事: 为什么「逐个参数求导」这条路在工程上是死的; 反向传播怎么用一个叫「误差项」的中间量,把所有梯度一次算清; 以及更一般的图景——任何计算拆成计算图之后,导数可以由机器自动累积, 我们手推的反向传播,其实只是其中一个模式。

它在全书链条里的位置: 这是全书承重的一章。 第 04 章把贡献度分配问题称为深度学习的核心难题,并说神经网络能当主角, 唯一的理由是它有反向传播——这一章就是那个理由本身。 后面每一章(卷积——第 13 章的主角、循环、Transformer)的训练,用的都是这里的同一个算法。

需要第 10 章和第 02 章第 4 节(链式法则)。

2. 顶层全景

前向(第 10 章走完的):
x ─▶ z⁽¹⁾ ─▶ a⁽¹⁾ ─▶ z⁽²⁾ ─▶ a⁽²⁾ ─▶ 损失 L

反向(这一章): ▼
∂L/∂W⁽¹⁾ ◂─ δ⁽¹⁾ ◂─── δ⁽²⁾ ◂──────── δ 的起点
∂L/∂W⁽²⁾ = δ⁽²⁾·a⁽¹⁾ = f′(z⁽¹⁾)⊙(W⁽²⁾ᵀδ⁽²⁾) = ŷ − y
(Softmax+交叉熵时)

每个参数的梯度 = 它所在层的误差项 × 它的输入
── 一次前向 + 一次反向,全部梯度到手

一句话链条: 逐个试方向太贵 → 每层只算一个「损失对净输入的偏导」(误差项)→ 误差项能从后往前递推 → 参数梯度 = 误差项 × 输入 → 同一台机器换个名字叫反向模式自动微分 → 代价:中间结果都得存着。

3. 逐个参数求导,有多贵

先量一量最笨的办法,才知道省了多少。

想知道某个参数该往哪挪,最直接的思路是:把它轻轻拨动一点, 看损失变多少——扰动 ÷ 损失变化,就是导数的近似。这叫数值微分1。 它好写,但有两个病:扰动太小会被浮点舍入误差淹没,太大又引入截断误差1

真正的死刑在复杂度上: 每个参数都要单独扰动一次、单独跑一次前向。 N 个参数、一次前向的代价是 O(N),总复杂度就是 O(N²)2。 几千亿个参数的网络,算一遍梯度要跑几千亿次前向——这条路在工程上是死的。

另一条学院派的路是符号微分:像做代数题一样,把梯度解析式整个推出来再代值。 它能编译期算好、平台无关,但编译慢、要专门的语言、程序没法调试3—— 网络里随便一个循环就够它受的。

于是问题被钉死在这:能不能既精确(不是近似)、又不用把表达式整个推出来、 还只花一次前向的常数倍? 反向传播就是对这三个「既要又要」的回答。

4. 一个关键的中间量:误差项

反向传播的全部聪明,在于选对了中间量。

不看「损失对参数 w_ij 的偏导」(参数太多),先看损失对第 l 层净输入 z⁽ˡ⁾ 的偏导。 这个量叫误差项,记作 δ⁽ˡ⁾4:

δ⁽ˡ⁾ ≜ ∂L/∂z⁽ˡ⁾
── 这一层的净输入如果动一动,最终损失会怎么动
── 也就是「这层神经元的敏感程度」

为什么是它?两个理由。

理由一:参数梯度从它一步就到。 因为 z⁽ˡ⁾ = W⁽ˡ⁾a⁽ˡ⁻¹⁾ + b⁽ˡ⁾, 链式法则一推到底,参数梯度就是误差项乘上「自己连着的那份输入」5:

∂L/∂W⁽ˡ⁾ = δ⁽ˡ⁾ · (a⁽ˡ⁻¹⁾)ᵀ 误差项 × 上一层的活性值
∂L/∂b⁽ˡ⁾ = δ⁽ˡ⁾ 偏置的梯度就是误差项本身

理由二:它就是贡献度分配问题的答案。 书里把话说得很白:误差项「反映了不同神经元对网络能力的贡献程度, 从而比较好地解决了贡献度分配问题」4第 04 章那个「几百层里哪一步做对了」的问题,δ 就是每层领到的那份答案。

5. 误差怎么往回传

问题: δ⁽ˡ⁾ 怎么算?难道每层都重新推一遍?

不用——它能从下一层递推回来。 损失对 z⁽ˡ⁾ 的影响, 必须经过「z⁽ˡ⁾ → a⁽ˡ⁾ → z⁽ˡ⁺¹⁾ → 损失」这条路,链式法则拆开是三段6:

δ⁽ˡ⁾ = f′_l(z⁽ˡ⁾) ⊙ (W⁽ˡ⁺¹⁾)ᵀ δ⁽ˡ⁺¹⁾
↑ ↑
本层激活函数的导数 下一层的误差按权重加回来
(弯前对弯后的坡度) (⊙ 是逐元素相乘,即 Hadamard 积)

读法(书里的原话压缩版):第 l 层一个神经元的误差项, 等于所有与它相连的第 l+1 层神经元的误差项的加权和, 再乘上它自己激活函数的导数7

注意这个公式里三个数的来源:δ⁽ˡ⁺¹⁾ 是反向传回来的; W⁽ˡ⁺¹⁾ 是参数;f′_l(z⁽ˡ⁾) 要用到前向时存的净输入—— 第 8 节会回来收这笔账。

于是整个算法只剩三步8:前向算完所有 z、a 和损失; 从输出层开始,逐层把 δ 往回递推;每层顺手算参数梯度、更新。 一次前向 + 一次反向,所有参数的梯度全部到手—— 代价是前向的一个小常数倍,不是 O(N²)。

6. 输出层的起点,特别干净

递推需要一个起点:输出层的 δ⁽ᴸ⁾。它通常很好算, 而最常用的搭配干脆好算到不像话9:

Softmax + 交叉熵(或 Logistic + 交叉熵):
δ⁽ᴸ⁾ = ŷ − y
── 就是「预测减真实」,一个减法

这个结果第 09 章已经见过一次:那里推 Logistic 回归的梯度, 化简到最后正是「真实 − 预测」乘输入10所以输出层的起点不需要任何求导,直接写出来—— 书里也说,这两个结果「直接给出了反向传播的起点」9

连起来看这一节的极简版: 起点是「预测减真实」; 每往上一层,按权重加回来、乘一道激活的导数; 每层的参数梯度 = 误差项 × 该层输入。这就是反向传播的全部。

7. 让机器自己求导:计算图与自动微分

退一步看,反向传播其实是一个更一般的机器的特例。

那台机器叫自动微分:它既不拿扰动近似(不是数值微分), 也不操作解析(写成闭式公式的)表达式(不是符号微分),它的处理对象是一段具体的数值计算过程11

原理只有一句:任何数值计算都能分解为基本操作(+, −, ×, /, exp, log …), 在计算过程中沿链式法则递归(层层套用同一规则)地累积导数11

装这些基本操作的容器叫计算图:每个非叶子节点是一个基本操作, 每个叶子节点是一个输入或常量12。书里把第 02 章那个函数 f(x; w, b) = 1/(exp(−(wx+b)) + 1) 拆成 6 个基本操作 h₁…h₆, 在 x=1、w=0、b=0 处,沿路径把各段导数连乘,得到 ∂f/∂w = 0.2513—— 和第 02 章手推的答案一字不差。参数到输出有多条路径时, 把各条路径的贡献相加即可14

在图上累积导数有两个方向15:

前向模式 沿计算方向,从输入往输出累积
── 适合输入少、输出多的函数

反向模式 逆着计算方向,从输出往输入累积
── 适合输入多、输出少的函数

神经网络的损失恰好是「输入(参数)极多、输出是一个标量」—— 反向模式的天选之形:一次反向遍历,就能得到这个标量对全部参数的梯度15。 所以书里下了那句判语:误差反向传播算法, 也可以看作神经网络上的反向模式自动微分16

这套东西离你有多近? 一个能跑反向传播的最小实现,只有 154 行 Python: 一个 94 行的自动微分引擎(每个数记住「我是谁算出来的、怎么把梯度传回去」), 加一个 60 行的神经元库17。现代框架(PyTorch、TensorFlow、JAX) 做的都是同一件事,只是换成了大规模张量18

8. 代价:中间结果必须存着

反向模式不是白捡的,账单在存储上。

回看第 5 节的递推公式:每一层都要用到前向时的净输入 z⁽ˡ⁾ (算激活的导数)和活性值 a⁽ˡ⁻¹⁾(算参数梯度)。 所以反向模式必须把前向计算的中间结果全部保存下来19—— 网络多深、批量多大,就要存多少份。

显存就是这么被吃掉的:几千亿参数的网络, 中间激活值的体积常常比参数本身还大。 书里点名了一条缓解路线:重计算——不存,反向时用前向重算一遍, 拿时间换空间19。这件事怎么在大模型里做,第 17 章和第 22 章各有一段。

顺带一提工程形态:计算图分静态(先建图、优化、再执行) 和动态(边跑边建图,灵活好调试)两种20—— 今天的框架大多两者混用,这已经只是实现偏好了。

9. 主走查:2-2-1 网络的返程

接着第 10 章那个网络。 前向已经走完:z₁ = [0.6, 0.0]、a₁ = [0.6457, 0.5]、 z₂ = 0.4020、ŷ = 0.5991、损失 0.5123,真实标签 y = 1。(数值保留四位小数。)

第一步:输出层起点(第 6 节那个减法)。

δ₂ = ŷ − y = 0.5991 − 1 = −0.4009

第二步:输出层的参数梯度。

∂L/∂W₂ = δ₂ · a₁ = −0.4009 × [0.6457, 0.5] = [−0.2589, −0.2005]
∂L/∂b₂ = δ₂ = −0.4009

第三步:误差传回隐藏层(第 5 节的递推)。

(W₂)ᵀδ₂ = [0.7, −0.4] × (−0.4009) = [−0.2806, 0.1604]
σ′(z₁) = a₁⊙(1−a₁) = [0.2288, 0.2500]
δ₁ = [0.2288, 0.25] ⊙ [−0.2806, 0.1604] = [−0.0642, 0.0401]

第四步:隐藏层的参数梯度(x = [1, 0],所以第二列全是 0)。

∂L/∂W₁ = δ₁·xᵀ = [ −0.0642 0 ] ∂L/∂b₁ = [−0.0642, 0.0401]
[ 0.0401 0 ]

第五步:更新(学习率 α = 0.5,为演示取的),再跑一次前向看效果。

W₂ = [0.8295, −0.2998] b₂ = 0.3505
W₁ = [0.5321 −0.3] b₁ = [0.1321, −0.2201]
[0.1799 0.8]

新前向:z₁ = [0.6642, −0.0402] a₁ = [0.6602, 0.4900]
z₂ = 0.7512 ŷ = 0.6794

损失:0.5123 → 0.3865

一次前向 + 一次反向,6 个参数各自认领了自己的方向,损失降了 25%。 如果改用第 3 节的数值微分,同样的梯度要再跑 6 次前向—— 参数变成几千亿个时,这就是「能训」和「不能训」的区别。

10. 作者的判断与证据

书里给了完整推导的: 误差项的递推公式、参数梯度公式、 输出层「预测减真实」的起点,全部从链式法则逐步推出695

书里给了明确判语的: 「反向传播算法的含义是: 第 l 层的一个神经元的误差项是所有与它相连的第 l+1 层神经元的误差项的权重和, 再乘上该神经元激活函数的梯度」7;「误差反向传播算法 也可以看作神经网络上的反向模式自动微分」16—— 两句判语,一句管算法,一句管它在世界里的位置。

书里给了复杂度证据的: 数值微分 O(N²) 的论证2—— 每个参数一次扰动、一次前向,这是它被排除的直接原因,不是口味问题。

书里坦白的: 反向模式「需要保存前向计算中的中间结果, 因此会带来额外的存储开销」19——省下来的算力(训练要烧的计算量),有一部分是拿显存换的。

我们的补充(不是书里的): 主走查里的权重、学习率是为演示定的; micrograd 的行数统计来自我们书架上的源码(engine.py 94 行 + nn.py 60 行)17, 书里只讲了自动微分的原理,没有给实现。

11. 边界与局限

只讲了全连接(每个神经元连到下一层全部)网络。 同一个算法搬到卷积、循环网络上, 递推公式要按结构改写——第 13 章和第 15 章各有一段「反向传播在这里怎么走」。

梯度正确 ≠ 训练顺利。 反向传播只保证把梯度算对; 地形是非凸的,梯度本身还会消失或爆炸——这是第 12 章和第 17 章的战场。

自动微分的框架细节没展开。 静态图/动态图只给了一段对比20, 具体的图优化、算子融合不在这本书的范围。

中间结果的存储只点了「重计算」一条路19, 混合精度(高低两种数值格式混用)在本书范围之外。

分页(内存按页调度)存储同理。

12. 可带走的

  1. 数值微分是 O(N²),几千亿参数时是死刑——不是慢,是不可能;
  2. 误差项 δ⁽ˡ⁾ = 损失对该层净输入的偏导,是贡献度分配问题的答案;
  3. 参数梯度 = 误差项 × 该层输入,偏置的梯度就是误差项本身;
  4. 误差往回传:下一层误差按权重加回来,再乘本层激活的导数;
  5. Softmax + 交叉熵的输出层,起点就是「预测减真实」;
  6. 一次前向 + 一次反向 = 全部梯度,代价是前向的小常数倍;
  7. 任何计算拆成计算图,导数都能沿链式法则自动累积——这是自动微分;
  8. 输入多输出少用反向模式,神经网络的标量损失正是天选之形;
  9. 反向传播 = 神经网络上的反向模式自动微分;
  10. 代价是存住全部中间结果——显存的大头往往不在参数,在激活值。

13. 原文地图

主题原书章原文位置
数值微分与 O(N²)第4章 前馈神经网络text/05-ch04.txt:539(搜「合适的扰动」) · text/05-ch04.txt:554(搜「总体时间复杂度」)
符号微分第4章 前馈神经网络text/05-ch04.txt:573(搜「在编译时就计算梯度」)
误差项的定义第4章 前馈神经网络text/05-ch04.txt:394(搜「敏感程度」) · text/05-ch04.txt:401(搜「贡献度分配问题」)
误差递推公式第4章 前馈神经网络text/05-ch04.txt:433(搜「这就是误差的反向传播」) · text/05-ch04.txt:435(搜「误差项的权重和」)
输出层起点第4章 前馈神经网络text/05-ch04.txt:441(搜「𝛿 (𝐿) = 𝑦 ̂ − 𝑦」) · text/05-ch04.txt:448(搜「反向传播的起点」)
参数梯度公式第4章 前馈神经网络text/05-ch04.txt:472(搜「= 𝛿 (𝑙) (𝒂(𝑙−1) )T」) · text/05-ch04.txt:478(搜「= 𝛿 (𝑙)」)
训练三步第4章 前馈神经网络text/05-ch04.txt:483(搜「前馈计算每一层的净输入」)
自动微分的定义第4章 前馈神经网络text/05-ch04.txt:583(搜「自动计算导数的方法」) · text/05-ch04.txt:589(搜「分解为一些基本操作」)
计算图与 0.25 的例子第4章 前馈神经网络text/05-ch04.txt:605(搜「每个非叶子节点表示一个基本操作」) · text/05-ch04.txt:667(搜「= 0.25」)
前向与反向模式第4章 前馈神经网络text/05-ch04.txt:672(搜「前向模式和反向模式」) · text/05-ch04.txt:718(搜「输入维度很高、输出维度较小」)
反向传播即反向模式第4章 前馈神经网络text/05-ch04.txt:721(搜「反向模式自动微分」)
中间结果的存储代价第4章 前馈神经网络text/05-ch04.txt:726(搜「保存前向计算中的中间结果」)
静态与动态计算图第4章 前馈神经网络text/05-ch04.txt:732(搜「静态计算图」)

Footnotes

  1. 出处:「第4章 前馈神经网络」第 539 段(text/05-ch04.txt:539,搜「合适的扰动」)。 原文:「数值微分方法非常容易实现,但找到一个合适的扰动 Δx 却十分困难。 如果 Δx 过小,会引起数值计算问题,比如舍入误差;如果 Δx 过大, 会增加截断误差,使得导数计算不准确。」 2

  2. 出处:「第4章 前馈神经网络」第 554 段(text/05-ch04.txt:554,搜「总体时间复杂度」)。 原文:「假设参数数量为 N,则每个参数都需要单独施加扰动,并计算梯度。 假设每次正向传播的计算复杂度为 O(N),则计算数值微分的总体时间复杂度为 O(N²)。」 2

  3. 出处:「第4章 前馈神经网络」第 573 段(text/05-ch04.txt:573,搜「在编译时就计算梯度」)。 原文列出的不足:「1)编译时间较长,特别是对于循环;2)一般需要设计一种专门的语言 来表示数学表达式,并且要对变量(符号)进行预先声明;3)很难对程序进行调试。」

  4. 出处:「第4章 前馈神经网络」第 394 段(text/05-ch04.txt:394,搜「敏感程度」) 与第 401 段(text/05-ch04.txt:401,搜「贡献度分配问题」)。 原文:「偏导数 ∂L/∂z⁽ˡ⁾ 表示第 l 层神经元对最终损失的影响,也反映了最终损失对 第 l 层神经元的敏感程度,因此一般称为第 l 层神经元的误差项…… 误差项 δ⁽ˡ⁾ 也间接反映了不同神经元对网络能力的贡献程度, 从而比较好地解决了贡献度分配问题。」 2

  5. 出处:「第4章 前馈神经网络」第 472 段(text/05-ch04.txt:472,搜「= 𝛿 (𝑙) (𝒂(𝑙−1) )T」) 与第 478 段(text/05-ch04.txt:478,搜「= 𝛿 (𝑙)」)。 见公式(4.42)与(4.43)。 2

  6. 出处:「第4章 前馈神经网络」第 427 段(text/05-ch04.txt:427,搜「= 𝑓𝑙′ (𝒛(𝑙) ) ⊙」)。 见公式(4.35)。推导用的两个局部导数(∂z⁽ˡ⁺¹⁾/∂a⁽ˡ⁾ = (W⁽ˡ⁺¹⁾)ᵀ、 ∂a⁽ˡ⁾/∂z⁽ˡ⁾ = diag(f′(z⁽ˡ⁾)))见公式(4.29)~(4.31),同页。 2

  7. 出处:「第4章 前馈神经网络」第 435 段(text/05-ch04.txt:435,搜「误差项的权重和」)。 原文:「反向传播算法的含义是:第 l 层的一个神经元的误差项(或敏感性) 是所有与该神经元相连的第 l+1 层的神经元的误差项的权重和。 然后,再乘上该神经元激活函数的梯度。」 2

  8. 出处:「第4章 前馈神经网络」第 483 段(text/05-ch04.txt:483,搜「前馈计算每一层的净输入」)。 完整过程见算法 4.1(随机梯度下降版),含正则化项的更新式。

  9. 出处:「第4章 前馈神经网络」第 441 段(text/05-ch04.txt:441,搜「𝛿 (𝐿) = 𝑦 ̂ − 𝑦」) 与第 448 段(text/05-ch04.txt:448,搜「反向传播的起点」)。 原文:「若第 L 层为二分类输出层,并采用 Logistic 函数和交叉熵损失……δ⁽ᴸ⁾ = ŷ − y。 若第 L 层为多分类输出层,并采用 Softmax 函数和交叉熵损失……δ⁽ᴸ⁾ = ŷ − y。 这两个结果在实际实现中十分常用,因为它们直接给出了反向传播的起点。」 2 3

  10. 出处:「第3章 线性模型」第 347 段(text/04-ch03.txt:347,搜「=− ∑ 𝒙(𝑛) (𝑦(𝑛) − 𝑦(𝑛)」)。 Logistic 回归的梯度 ∂R/∂w = −(1/N)Σx(y − ŷ),与本章输出层结论一致—— 原书在第 3 章总结里也点明了这层关系(text/04-ch03.txt:1268,搜「输出层的处理完全一致」)。

  11. 出处:「第4章 前馈神经网络」第 583 段(text/05-ch04.txt:583,搜「自动计算导数的方法」) 与第 589 段(text/05-ch04.txt:589,搜「分解为一些基本操作」)。 原文:「自动微分既不是通过有限差分来近似导数的数值微分, 也不是直接操作解析表达式的符号微分;它的处理对象是一段具体的数值计算过程。」 2

  12. 出处:「第4章 前馈神经网络」第 605 段(text/05-ch04.txt:605,搜「每个非叶子节点表示一个基本操作」)。

  13. 出处:「第4章 前馈神经网络」第 666 段(text/05-ch04.txt:666,搜「1 × −0.25 × 1 × 1 × −1 × 1 × 1」) 与第 667 段(text/05-ch04.txt:667,搜「= 0.25」)。 6 个基本函数及其导数见表 4.2,计算图见图 4.3。

  14. 出处:「第4章 前馈神经网络」第 669 段(text/05-ch04.txt:669,搜「多条路径」)。

  15. 出处:「第4章 前馈神经网络」第 672 段(text/05-ch04.txt:672,搜「前向模式和反向模式」) 与第 718 段(text/05-ch04.txt:718,搜「输入维度很高、输出维度较小」)。 原文:「前向模式更适合输入维度较小、输出维度较大的情形; 反向模式更适合输入维度很高、输出维度较小的情形。特别地,当输出是标量损失函数时, 反向模式可以在一次反向遍历中同时得到该标量关于大量参数的梯度。」 2

  16. 出处:「第4章 前馈神经网络」第 721 段(text/05-ch04.txt:721,搜「反向模式自动微分」)。 原文:「从更抽象的角度看,前向模式对应的是雅可比向量积,反向模式对应的是向量雅可比积…… 误差反向传播算法也可以看作神经网络上的反向模式自动微分。」 2

  17. 补充(不在书里,依据我们的 frontier 书架):一个能跑反向传播的最小实现只有 154 行。 依据: shelf=ai-frontier-reference/micrograd@src:micrograd/engine.py:2 事实=engine.py(94 行)里 Value 类的每个数都记录自己的局部梯度并沿计算图反向累积, 配 nn.py(60 行)的神经元库即可训练一个小网络; 拆解见 shelf=ai-frontier-reference/micrograd#01-engine-autograd.md 2

  18. 出处:「第4章 前馈神经网络」第 737 段(text/05-ch04.txt:737,搜「PyTorch」)。 原文:「PyTorch 通常在前向计算时动态记录运算图,并通过 backward() 进行反向自动微分; TensorFlow 2 主要通过 tf.GradientTape 在 eager 模式下记录计算…… JAX 则把 grad、jvp、vjp 等自动微分操作视为程序变换。」

  19. 出处:「第4章 前馈神经网络」第 726 段(text/05-ch04.txt:726,搜「保存前向计算中的中间结果」)。 原文:「反向模式通常需要保存前向计算中的中间结果,因此会带来额外的存储开销。 在训练深层网络和大模型时,常常需要配合重计算等技术来降低显存占用。」 2 3 4

  20. 出处:「第4章 前馈神经网络」第 732 段(text/05-ch04.txt:732,搜「静态计算图」)。 原文:「静态计算图通常在执行前先构建和优化整张计算图,便于进行全局优化和并行调度; 动态计算图则在程序运行时按实际控制流即时构建,灵活性更高,也更便于调试。」 2