从零实现反向传播 本节摘要:反向传播(Backpropagation)是让学习成为可能的算法。没有它,神经网络只是一台昂贵的随机数发生器。设想你的网络有 768 个输入、3072 个输出,那是 235 万多个权重;它预测错了,你究竟该怪哪个权重?逐个试一遍要做 230 万次前向传播。反向传播在一次反向遍历里就算清全部 230 万个梯度。这不是优化,而是「可训练」与「不可能」之间的分界。它的本质是把微积分里的链式法则系统性地套到计算图上。本节将构建一个基于 的自动求导引擎:每个数字都记住自己是怎么来的、梯度该怎么往回传,用拓扑排序驱动反向遍历,在 XOR 与圆形分类上从零训练出能用的网络,并理解深 sigmoid 网络里梯度消失(Vanishing Gradient)为何必然发生。
本节摘要:反向传播(Backpropagation)是让学习成为可能的算法。没有它,神经网络只是一台昂贵的随机数发生器。设想你的网络有 768 个输入、3072 个输出,那是 235 万多个权重;它预测错了,你究竟该怪哪个权重?逐个试一遍要做 230 万次前向传播。反向传播在一次反向遍历里就算清全部 230 万个梯度。这不是优化,而是「可训练」与「不可能」之间的分界。它的本质是把微积分里的链式法则系统性地套到计算图上。本节将构建一个基于
Value的自动求导引擎:每个数字都记住自己是怎么来的、梯度该怎么往回传,用拓扑排序驱动反向遍历,在 XOR 与圆形分类上从零训练出能用的网络,并理解深 sigmoid 网络里梯度消失(Vanishing Gradient)为何必然发生。
阅读完本节,你应当能够:
Value 的自动求导引擎,构建计算图并用拓扑排序计算梯度。你的网络有单隐藏层,768 个输入、3072 个输出——那是 2,359,296 个权重。它预测错了,哪些权重惹的祸?逐个权重试探意味着 230 万次前向传播。
朴素的办法:挑一个权重,微微拨动一点,再跑一次前向传播,看损失是升是降——这就得到这个权重的梯度。然后对网络里每一个权重重复一遍,再乘上千次训练步骤、百万级数据点。你需要地质年代才能训出任何有用的东西。
反向传播解决了这一切:一次前向、一次反向,所有梯度一次算清。诀窍是微积分的链式法则,系统性地套到计算图上。这是让深度学习变得可行的算法,没有它我们至今还卡在玩具问题上。
你在数学基础阶段见过链式法则:若 y = f(g(x)),则 dy/dx = f'(g(x)) × g'(x)——沿链条把导数乘起来。
在神经网络里,「链条」就是从输入到损失的一系列运算。每一层施加权重、加偏置、过激活;损失函数把最终输出和目标比对。反向传播顺着这条链反向走,计算每个运算对误差的贡献。
每一次前向传播都会构建一张图。每个节点是一个运算(乘、加、sigmoid),每条边向前传一个值、向后传一个梯度。
前向:值从左往右流。x 与 w 得到 z1 = w*x,加 b 得 z2,sigmoid 给出激活 a,把 a 与目标 y 用损失函数比对。
反向:梯度从右往左流。从 dL/da(损失对激活的变化率)开始,乘 da/dz2(sigmoid 的导数)得 dL/dz2。再拆成 dL/db(等于 dL/dz2,因为 z2 = z1 + b)与 dL/dz1。然后 dL/dw = dL/dz1 × x,dL/dx = dL/dz1 × w。
反向传播期间,图里每个节点只做一件事:接收上方传来的梯度,乘上自己的局部导数,往下传。
前向传播要存下每个中间值:z、a、每层输入。反向传播需要这些缓存值来算梯度。这是反向传播核心的「以存换算」权衡:用内存(存激活)换速度(一次遍历代替百万次)。
三层网络里,梯度穿过每一层:
每过一层,梯度都要乘一次 sigmoid 的导数。sigmoid 导数是 a × (1 − a),最大值只有 0.25(在 a = 0.5 时取到)。三层之后,梯度至多被乘了 0.25³ = 0.0156。十层之后:0.25¹⁰ = 0.000001。
这就是梯度消失问题。sigmoid 把输出压在 0 到 1 之间,导数始终小于 0.25。堆够多层,梯度就缩到接近零,早期层几乎学不动,因为它们收到的梯度近乎零。
sigmoid(z): 输出范围 [0, 1] sigmoid'(z): 最大值 0.25(在 z = 0 处) 5 层之后: 梯度 × 0.25^5 = 原来的 0.001 倍 10 层之后: 梯度 × 0.25^10 = 原来的 0.000001 倍
正因如此,深 sigmoid 网络几乎没法训。解法——ReLU 及其变体——是第 04 节的主题。这里只需明白:反向传播本身工作得完美,问题出在它要穿透的东西上。
给出输入 x、sigmoid 隐藏层、sigmoid 输出层、MSE 损失的网络的硬核数学。
前向传播:
z1 = W1 * x + b1 a1 = sigmoid(z1) z2 = W2 * a1 + b2 a2 = sigmoid(z2) L = (a2 - y)^2
反向传播(逐步套链式法则):
dL/da2 = 2(a2 - y) da2/dz2 = a2 * (1 - a2) dL/dz2 = dL/da2 * da2/dz2 = 2(a2 - y) * a2 * (1 - a2) dL/dW2 = dL/dz2 * a1 dL/db2 = dL/dz2 dL/da1 = dL/dz2 * W2 da1/dz1 = a1 * (1 - a1) dL/dz1 = dL/da1 * da1/dz1 dL/dW1 = dL/dz1 * x dL/db1 = dL/dz1
每一个梯度,都是从损失回溯的局部导数乘积。反向传播的全部,仅此而已。
完整代码见原课程 phases/03-deep-learning-core/03-backpropagation/code/ 相应文件。这里讲清每个关键步骤。
计算里的每个数字都变成一个 Value。它存着自己的数据、梯度,以及自己是怎么来的(以便知道反向时该怎么算梯度)。
class Value: def __init__(self, data, children=(), op=''): self.data = data self.grad = 0.0 self._backward = lambda: None self._children = set(children) self._op = op
梯度初始为 0,反向函数初始为空操作,_children 记录哪些 Value 生成了自己,以便后面做拓扑排序。
每个运算生成一个新 Value,并定义梯度如何穿过它往回传。
def __add__(self, other): other = other if isinstance(other, Value) else Value(other) out = Value(self.data + other.data, (self, other), '+') def _backward(): self.grad += out.grad other.grad += out.grad out._backward = _backward return out def __mul__(self, other): other = other if isinstance(other, Value) else Value(other) out = Value(self.data * other.data, (self, other), '*') def _backward(): self.grad += other.data * out.grad other.grad += self.data * out.grad out._backward = _backward return out
加法:d(a+b)/da = 1,d(a+b)/db = 1,所以两个输入都直接拿到输出的梯度。
乘法:d(a×b)/da = b,d(a×b)/db = a,每个输入拿到对方数值乘以输出梯度。
那个 += 至关重要:一个 Value 可能在多个运算里被用到,它的梯度是所有路径梯度之和。
sigmoid 导数:sigmoid(x) × (1 − sigmoid(x))。前向传播时我们已经算出 sigmoid(x) = s,直接复用,无需额外开销。MSE 损失把单个输出的 (predicted − target)² 表达成「predicted 加上一个取负的 Value,再自乘」。
拓扑排序确保按正确顺序处理节点——一个节点的梯度在被传播之前已经完全累积好。从损失出发(梯度 = 1.0,因为 dL/dL = 1),沿排序后的图反向走,每个节点的 _backward 把梯度推向它的子节点。
一个 Neuron 接收输入、算加权求和加偏置、套 sigmoid。权重初始化按 sqrt(2/n_inputs) 缩放,以防深网里 sigmoid 饱和。Layer 是一列 Neuron,Network 是一列 Layer,parameters() 把所有可学习的 Value 收集起来供更新。
random.seed(42) net = Network([2, 4, 1]) xor_data = [ ([0.0, 0.0], 0.0), ([0.0, 1.0], 1.0), ([1.0, 0.0], 1.0), ([1.0, 1.0], 0.0), ] learning_rate = 1.0 for epoch in range(1000): total_loss = Value(0.0) for inputs, target in xor_data: x = [Value(i) for i in inputs] pred = net(x) loss = mse_loss(pred, target) total_loss = total_loss + loss net.zero_grad() total_loss.backward() for p in net.parameters(): p.data -= learning_rate * p.grad
眼看损失下降:从随机预测到正确的 XOR 输出,全靠反向传播算梯度、把权重推向正确方向。
第 02 节你手工调了圆形分类的权重,现在让网络自己学。用在线 SGD——每个样本算完就更新权重,而不是累积整个 batch,这样更快打破对称性、避免 sigmoid 在全 batch 损失面上饱和。每轮打乱数据,防止网络记住顺序。
无需任何手工调参,网络自己发现圆形决策边界。这就是反向传播的力量:你定义架构、损失函数和数据,算法搞定权重。
PyTorch 几行就能做完上面所有事,核心思想完全一致——autograd 在前向传播时构建计算图,反向时沿图回溯算梯度。
import torch import torch.nn as nn model = nn.Sequential( nn.Linear(2, 4), nn.Sigmoid(), nn.Linear(4, 1), nn.Sigmoid(), ) optimizer = torch.optim.SGD(model.parameters(), lr=1.0) criterion = nn.MSELoss() X = torch.tensor([[0,0],[0,1],[1,0],[1,1]], dtype=torch.float32) y = torch.tensor([[0],[1],[1],[0]], dtype=torch.float32) for epoch in range(1000): pred = model(X) loss = criterion(pred, y) optimizer.zero_grad() loss.backward() optimizer.step()
loss.backward() 就是你的 total_loss.backward(),optimizer.step() 就是你手写的 p.data -= lr × p.grad,optimizer.zero_grad() 就是你的 net.zero_grad()。同一个算法,工业级实现。PyTorch 还接管 GPU 加速、混合精度、梯度检查点、上百种层类型,但反向传播就是对同一张计算图套同一套链式法则。
训练时跑前向再跑反向再更新权重;推理时只跑前向——没有梯度,没有更新。这个区分很重要,因为推理是生产里发生的事:你调用 Claude 或 GPT 的 API,跑的就是推理,提示词前向流过网络,token 从另一头出来,权重一个都不变。理解反向传播之所以重要,是因为它塑造了那张网络里的每一个权重。
本节产出(位于原课程 outputs/):
prompt-gradient-debugger.md:一个可复用提示,用于诊断任意神经网络里的梯度问题(消失、爆炸、NaN)。__sub__(a − b = a + (−1 × b))与 __neg__,用简单表达式如 (a − b)² 验证梯度与手算一致。relu 方法(输出 max(0, x),导数 x > 0 时为 1 否则为 0),在 XOR 上把隐藏层的 sigmoid 换成 relu 再训,比较收敛速度——你应当看到更快,这是第 04 节的预告。__pow__(整数幂),用它把 mse_loss 换成真正的 (predicted - target) ** 2,验证梯度与原实现一致。+= 累加梯度:一个 Value 可能在多条路径上被用到,它的梯度是各路径之和。下一节,我们逐一吃透各种激活函数——sigmoid 为何让深网训不动、ReLU 如何让深网变可训、GELU 为何成为 Transformer 的默认选择。