链式法则与自动微分 本节摘要:链式法则是每一个会学习的神经网络背后的引擎。简单的函数你能手算导数,但神经网络不是简单函数——它是上百个函数的复合:矩阵乘、加偏置、激活、再乘、softmax、交叉熵……输出是「函数的函数的函数」。要训练它,你需要损失对每一个权重的梯度。手算百万参数不可能,数值差分太慢。链式法则给数学,自动微分(autodiff)给算法——两者合力,让你在与一次前向传播同阶的时间内,算出任意复合函数的精确梯度。本节我们亲手搭一个迷你自动微分引擎(micrograd 风格的 类):用对象包住每个数,记录每个运算的局部梯度,再用拓扑排序反向遍历图,逐节点套链式法则。
本节摘要:链式法则是每一个会学习的神经网络背后的引擎。简单的函数你能手算导数,但神经网络不是简单函数——它是上百个函数的复合:矩阵乘、加偏置、激活、再乘、softmax、交叉熵……输出是「函数的函数的函数」。要训练它,你需要损失对每一个权重的梯度。手算百万参数不可能,数值差分太慢。链式法则给数学,自动微分(autodiff)给算法——两者合力,让你在与一次前向传播同阶的时间内,算出任意复合函数的精确梯度。本节我们亲手搭一个迷你自动微分引擎(micrograd 风格的
Value类):用对象包住每个数,记录每个运算的局部梯度,再用拓扑排序反向遍历图,逐节点套链式法则。然后用纯 Python(无 PyTorch、无 NumPy)构建一个多层感知机(MLP),在 XOR 上训练;最后用梯度检查(与数值有限差分对照)验证引擎正确性——这正是 PyTorch/TensorFlow/JAX 的同一套机制,只是规模更大。
对应原课程:Phase 01 · Lesson 05 ·
chain-rule-and-autodiff(原英文phases/01-math-foundations/05-chain-rule-and-autodiff/docs/en.md)。前置:第 4 节(导数与梯度)。
阅读完本节,你应当能够:
Value 类),记录运算并通过反向模式自动微分计算梯度。简单函数你会算导数,神经网络却不是简单函数。它是上百个函数的复合:矩阵乘 → 加偏置 → 激活 → 再矩阵乘 → softmax → 交叉熵损失。输出是「函数的函数的函数」。
要训练网络,你需要损失对每个权重的梯度。手算百万参数不可能,数值差分太慢。链式法则给数学,自动微分给算法——两者合力,在「与一次前向传播同阶」的时间内算出任意复合的精确梯度。这正是 PyTorch/TensorFlow/JAX 的工作方式。本节从零搭一个迷你版。
若 y = f(g(x)),则:
dy/dx = dy/dg · dg/dx = f'(g(x)) · g'(x)
沿链乘导数,每环贡献自己的局部导数。
例:y = sin(x²)
g(x) = x², g'(x) = 2x f(g) = sin(g), f'(g) = cos(g) dy/dx = cos(x²) · 2x
更深的复合:链继续延伸 dy/dx = f'(g(h(x)))·g'(h(x))·h'(x)。神经网络的每一层就是这链中的一环。
计算图让链式法则可视化:每个运算是一个节点,数据前向流动,梯度反向流动。
前向传播(算数值):
反向传播(算梯度):
反向传播在每个节点套链式法则,从输出向输入传播梯度。
套链式法则穿过图有两种方式。
前向模式:从输入推到输出,种子 dx/dx = 1。适合「输入少、输出多」。
反向模式:从输出拉回输入,种子 dy/dy = 1。适合「输入多、输出少」。
| 模式 | 种子 | 方向 | 最佳场景 |
|---|---|---|---|
| 前向 | dxᵢ/dxᵢ = 1 |
输入→输出 | 输入少、输出多 |
| 反向 | dy/dy = 1 |
输出→输入 | 输入多、输出少(神经网络) |
神经网络有百万输入(权重)、一个输出(损失)。反向模式一次反向传播算出全部梯度——这就是反向传播用反向模式的原因。
前向模式可用**对偶数(Dual Number)**优雅实现:形如 a + b·ε,其中 ε² = 0。对偶数 = (值, 导数),(2, 1) 表示值为 2、对 x 的导数为 1。算术规则让导数自动随运算传播:(a,a')·(b,b') = (a·b, a'·b + a·b'),sin(a,a') = (sin a, cos a · a')。
这正是 PyTorch autograd 的做法:torch.Tensor 包值,requires_grad=True 时记录运算,.backward() 触发反向自动微分。图为动态(define-by-run),每次前向都重建——这就是 PyTorch 能在模型内支持 if/else、循环的原因。
完整源码见 phases/01-math-foundations/05-chain-rule-and-autodiff/code/。下面给出 micrograd 风格的关键骨架。
Value 类:值的容器class Value: def __init__(self, data, children=(), op=''): self.data = data # 数值 self.grad = 0.0 # 梯度(初始 0) self._backward = lambda: None # 局部反向函数 self._prev = set(children) # 产生它的子节点 self._op = op
每个 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 # 加法的局部导数是 1 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 # 乘法:对 self 的局部导数是 other.data other.grad += self.data * out.grad out._backward = _backward return out def relu(self): out = Value(max(0, self.data), (self,), 'relu') def _backward(): self.grad += (1.0 if out.data > 0 else 0.0) * out.grad out._backward = _backward return out
每个运算创建一个闭包,知道如何算局部梯度并乘以上游梯度(out.grad)。用 +=(而非 =)是为了处理「一个值被多个运算使用」的情形——梯度需累加。
def backward(self): topo = []; visited = set() def build_topo(v): # 拓扑排序:子节点先于父节点入列 if v not in visited: visited.add(v) for child in v._prev: build_topo(child) topo.append(v) build_topo(self) self.grad = 1.0 # 种子 dy/dy = 1 for v in reversed(topo): # 逆序:确保父节点梯度算完再传给子节点 v._backward()
__sub__、__truediv__ 可由已有的加、乘、幂定义,从而免费得到正确梯度(链式法则自动复合)。此外补齐 exp、log、tanh、pow 等:
| 运算 | 反向规则 | 用途 |
|---|---|---|
__sub__ |
复用 add + neg | 损失计算(pred − target) |
__pow__ |
n·xⁿ⁻¹ |
MSE(error²) |
__truediv__ |
复用 mul + pow(−1) | 归一化 |
exp |
exp(x)·上游 |
softmax、对数似然 |
log |
(1/x)·上游 |
交叉熵、log 概率 |
tanh |
(1−tanh²)·上游 |
经典激活 |
Value 搭 MLPclass Neuron: def __init__(self, n_inputs): self.w = [Value(random.uniform(-1,1)) for _ in range(n_inputs)] self.b = Value(0.0) def __call__(self, x): act = sum((wi*xi for wi,xi in zip(self.w,x)), self.b) return act.tanh() def parameters(self): return self.w + [self.b] class Layer: def __init__(self, n_in, n_out): self.neurons = [Neuron(n_in) for _ in range(n_out)] def __call__(self, x): return [n(x) for n in self.neurons] def parameters(self): return [p for n in self.neurons for p in n.parameters()] class MLP: def __init__(self, sizes): self.layers = [Layer(sizes[i], sizes[i+1]) for i in range(len(sizes)-1)] def __call__(self, x): for layer in self.layers: x = layer(x) return x[0] if len(x)==1 else x def parameters(self): return [p for layer in self.layers for p in layer.parameters()]
Neuron 算 tanh(w1·x1 + … + b),Layer 是一组神经元,MLP 堆叠层。每个权重都是 Value,所以 loss.backward() 把梯度传到每个参数。
model = MLP([2, 4, 1]) xs = [[0,0],[0,1],[1,0],[1,1]] ys = [-1, 1, 1, -1] # XOR(用 −1/1 配 tanh) for step in range(100): preds = [model(x) for x in xs] loss = sum((p - y)**2 for p, y in zip(preds, ys)) for p in model.parameters(): p.grad = 0.0 # 清零! loss.backward() for p in model.parameters(): p.data -= 0.05 * p.grad
设计警示:每步前必须把所有参数梯度清零(
p.grad = 0.0)——因为 PyTorch 风格的梯度是累加的。忘清零是初学者最常见的训练 bug。
把自动微分梯度与数值有限差分对照,差距应 < 1e-5:
def gradient_check(build_expr, x_val, h=1e-7): x = Value(x_val); y = build_expr(x); y.backward() autodiff_grad = x.grad y_plus = build_expr(Value(x_val + h)).data y_minus = build_expr(Value(x_val - h)).data numerical_grad = (y_plus - y_minus) / (2*h) return autodiff_grad, numerical_grad, abs(autodiff_grad - numerical_grad)
| 情形 | 要做梯度检查吗? |
|---|---|
| 给自动微分引擎加新运算 | 必做 |
| 调试不收敛的训练循环 | 先查梯度 |
| 生产训练 | 不必(每个参数要多 2 次前向,太慢) |
| 自动微分代码单元测试 | 自动化必做 |
import torch x1 = torch.tensor(2.0, requires_grad=True) x2 = torch.tensor(3.0, requires_grad=True) y = torch.relu(x1 * x2 + 1.0) y.backward() print(x1.grad.item(), x2.grad.item()) # 3.0, 2.0 —— 与自研引擎完全一致
同样的梯度。你的引擎与 PyTorch 算出相同结果,因为数学相同:链式法则上的反向模式自动微分。
💡 PyTorch 的图是动态(define-by-run):每次前向都重建图,所以能在模型里写 Python 控制流。TensorFlow 1.x 是静态图,TF 2.x 默认也改成了动态(Eager)。
本节产出:
outputs/skill-autodiff.md:一份关于构建与调试自动微分系统的「技能说明」。code/autodiff.py:一个可扩展的最小自动微分引擎,可作为后续第 3 章神经网络训练循环的基石——也是排查梯度 bug 时的「教学参照实现」。源码见 phases/01-math-foundations/05-chain-rule-and-autodiff/code/。
Value 加 __pow__,验证 d/dx(x³) 在 x=2 处为 12.0。tanh,验证 tanh'(0)=1、tanh'(2)≈0.0707。y = relu(w1·x1 + w2·x2 + b) 的计算图,算全部五个梯度,与 PyTorch 对照。Dual 类),验证它与反向模式给出相同导数。+= 而非 =,处理一个值被多处使用的情形;每步前须手动清零。下一节,我们离开确定性世界,进入概率与分布——开始用数学描述不确定性,从随机变量到正态分布、从大数定律到中心极限定理。