一
回到上一章结尾的困境。单层感知机画不出能分开 XOR 的直线,但只要在输入和输出之间加一层“隐藏单元”,让第一层先把四个点变换到一个新的坐标系里、在那里它们变得线性可分,第二层就能轻松分开。多层网络的表达能力远超单层——1989 年 Cybenko 对 sigmoid 激活、以及 Hornik 等人随后的工作,更是证明了只要一个隐藏层、足够多的单元,就能以任意精度逼近任意连续函数(通用逼近定理)1。结构上的潜力是确定的。
问题出在训练。感知机的学习规则只对单层有效:它直接比较“输出层的预测”和“标签”,用两者的差去修正权重。可一旦有了隐藏层,麻烦就来了——隐藏单元没有“标签”。我们知道最终输出错了多少,却不知道隐藏层里那个具体的权重“该为这个错误负多少责任”。这个“功劳/过错分配问题”(credit assignment problem)才是训练多层网络的真正拦路虎。
反向传播就是这个问题的解。它的答案,藏在一条所有理科生都学过的法则里:链式法则。
二
先把一个多层网络的前向计算写清楚。以最简单的两层网络(一个隐藏层)为例,输入 ,第一层权重 、偏置 ,激活函数 :
再定义一个损失函数 衡量预测 与真实标签 的差距,比如均方误差 。训练的目标是调整所有的 使 最小。
最小化一个可微函数的标准武器是梯度下降:沿着损失对参数的负梯度方向走一小步,
所以全部问题归结为一件事:怎么算出 对每一个权重的偏导数 。网络可能有几百万个权重,深处的权重和最终损失之间隔着层层非线性变换。这正是链式法则登场的地方。
三
链式法则说:复合函数的导数等于各层导数的乘积。如果 依赖 , 依赖 , 依赖 , 依赖 ,那么
反向传播的全部精髓,是观察到这条乘积链可以从右到左(从损失端到输入端)逐项复用,而不必为每个权重从头乘一遍。定义一个关键的中间量——第 层的“误差信号” ,它表示“该层的加权输入对最终损失的敏感度”。
对输出层,误差信号直接来自损失:
( 是逐元素乘。对均方误差,。)
关键的递推是:上一层(更靠近输入)的误差信号,可以由下一层(更靠近输出)的误差信号“反传”得到:
读这个公式: 把输出层的误差按权重“摊回”到隐藏层各单元——一个隐藏单元如果通过大权重强烈影响了输出,它就该为输出的误差承担更多责任;再乘上本层激活函数的导数 ,因为误差要穿过这层的非线性。
有了每层的 ,对应的权重梯度就是一个外积:
整个算法就是一次前向传播(算出各层 并缓存),再一次反向传播(从输出层开始,逐层用上面的递推算出 和梯度)。
四
为什么这件事是个突破,而不是“显然的链式法则练习”?因为效率。
朴素地算梯度,可以用数值微分:把每个权重轻轻扰动一点 ,看损失变化多少,。但这要求每个权重都跑一次完整的前向传播。一个有 个参数的网络,算一次完整梯度就要跑 次前向,对百万参数的网络是灾难。
反向传播的魔法在于:算完整梯度的代价,和算一次前向传播的代价是同一个量级。一次前向、一次反向,就拿到了所有 个参数的偏导数。这正是“反向模式自动微分”(reverse-mode automatic differentiation)的核心性质——前向与反向的计算成本相当,而一次反向就能得到一个标量输出对所有输入的全部梯度2。现代深度学习框架(PyTorch 的 autograd、TensorFlow 的计算图)本质上就是把这套反向模式自动微分通用化、自动化2。把训练上亿参数的模型变得可行,靠的就是这个 倍而非 倍的效率。
五
“谁发明了反向传播”是技术史里最著名的优先权公案之一,值得分层并陈,而不是简单归给某一个名字。
1970 年,Seppo Linnainmaa。芬兰人 Linnainmaa 在硕士论文里第一次描述了“在任意、离散、可能稀疏连接的类神经网络中,显式而高效的误差反向传播”——也就是反向模式自动微分。他的论文甚至附带了实现该方法的 FORTRAN 代码,后来于 1976 年发表在 BIT 期刊上2。Schmidhuber 据此主张,今天 TensorFlow 这类软件包所基于的,正是 Linnainmaa 1970 年的方法2。
1974 / 1982 年,Paul Werbos。美国人 Werbos 在 1974 年的博士论文(5.5.1 节)里初步讨论了反向传播,但当时并未专门把它应用到神经网络上;按 Schmidhuber 的考据,首次把高效反向传播明确用于神经网络的,是 Werbos 1982 年一篇关于非线性敏感度分析的会议论文2。Werbos 的工作在当时几乎无人理会——那正是第一次 AI 寒冬,神经网络研究本就处于低潮,他这些后来被证明革命性的想法,在整个 1970 年代被学界基本忽略3。
1986 年,Rumelhart、Hinton、Williams。这三人 1985 年对“已知的方法”做了实验分析,并于 1986 年在《自然》上发表《通过反向传播误差学习表示》(Learning representations by back-propagating errors)4。这篇论文的历史地位,不在于发明了算法(它甚至没有引用该方法的来源2),而在于令人信服地展示了反向传播能让多层网络的隐藏层自动学到有用的内部表示——它把一个被忽视的数学技巧,变成了一个能解决实际问题、能产生可解释中间特征的工具。这篇论文与 1980 年代神经网络研究的复苏同步,被引爆性地传播,把整个领域从寒冬里拉了出来35。
Schmidhuber 对这段历史给出了一个值得记住的区分原则:发明一个重要方法的人,应当因发明而获得荣誉;她未必是让它流行起来的人,那么让它流行的人应当因普及而获得荣誉——但不是因发明2。普及不等于发明。把反向传播简单说成“Hinton 发明的”,是把普及之功误记成了发明之功;而把它说成“Linnainmaa 一个人的”,又抹掉了 Werbos 首次用于神经网络、以及 RHW 让它真正可用并改变历史的贡献。三方各有其不可替代的位置。
六
把理论落到能跑的代码上。下面这段纯 NumPy 程序,训练一个 2-2-1 的小网络去学 XOR——正是上一章里单层感知机永远学不会的那个函数(完整文件 code/02_backprop_xor.py,可直接运行):
展开代码 · 02_backprop_xor.py
"""
第 02 章配套代码:反向传播从零实现,训练一个 2-2-1 MLP 学会 XOR
Runnable with: numpy only. python3 02_backprop_xor.py
这正是单层感知机做不到、而多层网络 + 反向传播能做到的事(呼应第 01 章)。
完整演示前向传播、链式法则反向求梯度、梯度下降更新。
"""
import numpy as np
rng = np.random.default_rng(1)
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
def sigmoid_grad(a):
# 注意:输入是 sigmoid 的输出 a,导数 a*(1-a)
return a * (1.0 - a)
# XOR 数据
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
Y = np.array([[0], [1], [1], [0]], dtype=float)
# 2-2-1 网络参数
W1 = rng.normal(0, 1, (2, 2)); b1 = np.zeros((1, 2))
W2 = rng.normal(0, 1, (2, 1)); b2 = np.zeros((1, 1))
lr = 0.5
for epoch in range(20000):
# ---- 前向 ----
z1 = X @ W1 + b1 # (4,2)
a1 = sigmoid(z1) # 隐层激活
z2 = a1 @ W2 + b2 # (4,1)
a2 = sigmoid(z2) # 输出
# ---- 损失:均方误差 ----
loss = np.mean((a2 - Y) ** 2)
# ---- 反向(链式法则)----
# dL/da2 = 2(a2-Y)/N ; da2/dz2 = sigmoid'(a2)
dz2 = (a2 - Y) * sigmoid_grad(a2) # (4,1) delta_output
dW2 = a1.T @ dz2 # (2,1)
db2 = dz2.sum(0, keepdims=True)
# 误差反传到隐层:dz1 = (dz2 W2^T) * sigmoid'(a1)
dz1 = (dz2 @ W2.T) * sigmoid_grad(a1) # (4,2) delta_hidden
dW1 = X.T @ dz1 # (2,2)
db1 = dz1.sum(0, keepdims=True)
# ---- 梯度下降 ----
W2 -= lr * dW2; b2 -= lr * db2
W1 -= lr * dW1; b1 -= lr * db1
if epoch % 4000 == 0:
print(f"epoch {epoch:5d} loss={loss:.5f}")
# 最终预测
a1 = sigmoid(X @ W1 + b1)
pred = sigmoid(a1 @ W2 + b2)
print("\n最终预测 (应接近 0,1,1,0):")
for xi, pi in zip(X, pred.ravel()):
print(f" XOR{tuple(xi.astype(int))} -> {pi:.3f}")
def sigmoid(z): return 1/(1+np.exp(-z))
def sigmoid_grad(a): return a*(1-a) # 输入是激活值 a
for epoch in range(20000):
# 前向
a1 = sigmoid(X @ W1 + b1) # 隐藏层
a2 = sigmoid(a1 @ W2 + b2) # 输出层
# 反向(链式法则)
dz2 = (a2 - Y) * sigmoid_grad(a2) # 输出层误差信号 delta^(2)
dW2 = a1.T @ dz2
dz1 = (dz2 @ W2.T) * sigmoid_grad(a1) # 反传到隐藏层 delta^(1)
dW1 = X.T @ dz1
# 梯度下降
W2 -= lr*dW2; W1 -= lr*dW1; b2 -= lr*dz2.sum(0); b1 -= lr*dz1.sum(0)
代码里 dz1 = (dz2 @ W2.T) * sigmoid_grad(a1) 这一行,就是第三节那条反传递推 的逐字翻译。运行结果是:
epoch 0 loss=0.27990
epoch 16000 loss=0.00017
最终预测 (应接近 0,1,1,0):
XOR(0,0) -> 0.013 XOR(0,1) -> 0.989
XOR(1,0) -> 0.989 XOR(1,1) -> 0.011
损失从 0.28 一路降到接近 0,四个 XOR 样本的预测分别收敛到 0、1、1、0。十七年前 Minsky 和 Papert 划下的那条“单层不可逾越”的线,被“多层 + 反向传播”干净地跨了过去。把第 01 章那段永不收敛的感知机日志,和这一段一路下降的 loss 并排放,就是神经网络从寒冬走向复兴的最小完整叙事。
七
理解反向传播,最好的图像是想象误差像水一样“倒流”。前向传播时,信号从输入流向输出,每一层做一次“加权求和 + 非线性”;反向传播时,误差从输出流回输入,每经过一层就被该层的权重转置 重新分配、再被该层激活函数的导数 调制一次。
前向: x ──W1──► z1 ──σ──► a1 ──W2──► z2 ──σ──► ŷ ──► L(ŷ,y)
│
反向: δ1 ◄─W2ᵀ── δ2 ◄────────────────────── ∂L/∂ŷ ◄──────┘
│ │
(×σ'(z1)) (×σ'(z2))
│ │
∂L/∂W1 ∂L/∂W2 ← 每层用 δ 和前一层激活的外积得到权重梯度
配套的 manim 动画 assets/manim/ch02_backprop.py(Backprop Scene)把这件事演成几何:先看前向的值沿计算图从左流到右算出预测,再看误差从输出端逆着同一张图的边反向流回,每条边携带一个“局部梯度”,链式法则就是这些局部梯度沿路径逐边相乘、累积到每个参数上。一次前向加一次反向,就拿到了所有参数的梯度——这正是“误差倒流”的全部内容。
这个“误差倒流”的机制,从此成了几乎所有神经网络训练的引擎。但它也带着一个隐患——误差每反传一层,都要乘一次 和一次 。如果这些乘子持续小于 1,误差信号在深层网络或长序列里会指数衰减到几乎为零,深处的层根本收不到有效的学习信号。这个被称为“梯度消失”的问题,将在 1991 年被一位叫 Hochreiter 的学生第一次形式化地剖开,并在很大程度上塑造了此后二十年神经网络架构的演化方向。那是关于循环网络与 LSTM 的故事。
本质
反向传播不是一个新算法,而是对一个老定理(链式法则)的一次极其高效的组织方式:它发现,只要按计算顺序的逆序走一遍,就能用一次遍历的代价算出损失对每一个参数的偏导,而不必对每个参数单独求一次导。它把“训练一个任意深的网络”从指数级的不可能,压缩成了与前向计算同量级的可行操作。多层之所以有用,也在这里第一次变得可操作——每一层都在把数据重新表示一次,把上一层里分不开的东西,变换成下一层里分得开的东西,而反向传播让这一连串变换能被同一个误差信号一致地调整。它的强大与它的脆弱同源:既然信号要沿着一长串乘法传播,那么这串乘子是大于一还是小于一,就决定了深层网络是能学还是学不动。
参考文献
-
Cybenko, G. (1989) 对 sigmoid 激活的通用逼近证明;Hornik, K., Stinchcombe, M., & White, H. (1989/1991) 多层前馈网络是通用逼近器。综述:https://en.wikipedia.org/wiki/Universal_approximation_theorem ;Hornik et al. 原文 PDF(CMU 镜像):https://www.cs.cmu.edu/~epxing/Class/10715/reading/Kornick_et_al.pdf
-
Schmidhuber, J. “Who Invented Backpropagation?”(含 Linnainmaa 1970 reverse-mode AD、FORTRAN 代码、1976 BIT 发表、Werbos 1982 首次 NN 应用、reverse-mode 效率性质、“发明 vs 普及”原则)。IDSIA:https://people.idsia.ch/~juergen/who-invented-backpropagation.html
-
Backpropagation 历史综述(Werbos 1974 博论被寒冬忽视、1986 复兴)。Wikipedia:https://en.wikipedia.org/wiki/Backpropagation ;“The Backstory of Backpropagation”(二手史料梳理):https://yuxi-liu-wired.github.io/essays/posts/backstory-of-backpropagation/
-
Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). Learning representations by back-propagating errors. Nature, 323, 533–536. https://www.nature.com/articles/323533a0
-
反向传播终结第一次寒冬的影响综述(RHW 1986 与 1980s 神经网络复兴)。Backpropagation Wikipedia(历史小节):https://en.wikipedia.org/wiki/Backpropagation