链式法则与自动微分


文档摘要

链式法则与自动微分 本节摘要:链式法则是每一个会学习的神经网络背后的引擎。简单的函数你能手算导数,但神经网络不是简单函数——它是上百个函数的复合:矩阵乘、加偏置、激活、再乘、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 节(导数与梯度)。

学习目标

阅读完本节,你应当能够:

  1. 构建一个最小自动微分引擎(Value 类),记录运算并通过反向模式自动微分计算梯度。
  2. 拓扑排序实现计算图的前向与反向传播。
  3. 仅用自研自动微分引擎,构建并训练一个 MLP 解决 XOR。
  4. 梯度检查(对照数值有限差分)验证自动微分的正确性。

一、问题与直觉

简单函数你会算导数,神经网络却不是简单函数。它是上百个函数的复合:矩阵乘 → 加偏置 → 激活 → 再矩阵乘 → softmax → 交叉熵损失。输出是「函数的函数的函数」。

要训练网络,你需要损失对每个权重的梯度。手算百万参数不可能,数值差分太慢。链式法则给数学,自动微分给算法——两者合力,在「与一次前向传播同阶」的时间内算出任意复合的精确梯度。这正是 PyTorch/TensorFlow/JAX 的工作方式。本节从零搭一个迷你版。

1.1 链式法则

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)。神经网络的每一层就是这链中的一环。

1.2 计算图

计算图让链式法则可视化:每个运算是一个节点,数据前向流动,梯度反向流动。

前向传播(算数值):

反向传播(算梯度):

反向传播在每个节点套链式法则,从输出向输入传播梯度。

1.3 前向模式 vs 反向模式

套链式法则穿过图有两种方式。

前向模式:从输入推到输出,种子 dx/dx = 1。适合「输入少、输出多」。

反向模式:从输出拉回输入,种子 dy/dy = 1。适合「输入多、输出少」。

模式 种子 方向 最佳场景
前向 dxᵢ/dxᵢ = 1 输入→输出 输入少、输出多
反向 dy/dy = 1 输出→输入 输入多、输出少(神经网络)

神经网络有百万输入(权重)、一个输出(损失)。反向模式一次反向传播算出全部梯度——这就是反向传播用反向模式的原因。

1.4 前向模式的对偶数实现

前向模式可用**对偶数(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')

1.5 自动微分引擎三件套

  1. 值包装:把每个数包进一个对象,存值与梯度。
  2. 图记录:每次运算记录输入与局部梯度函数。
  3. 反向传播:拓扑排序图,逆序遍历,每节点套链式法则。

这正是 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 风格的关键骨架。

2.1 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 存数值、梯度、反向函数、指向子节点的指针。

2.2 带梯度跟踪的算术运算

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)。用 +=(而非 =)是为了处理「一个值被多个运算使用」的情形——梯度需累加

2.3 反向传播:拓扑排序 + 逆序套链式法则

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()

2.4 复合运算复用基础运算

__sub____truediv__ 可由已有的加、乘、幂定义,从而免费得到正确梯度(链式法则自动复合)。此外补齐 explogtanhpow 等:

运算 反向规则 用途
__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²)·上游 经典激活

2.5 用 Value 搭 MLP

class 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()]

Neurontanh(w1·x1 + … + b),Layer 是一组神经元,MLP 堆叠层。每个权重都是 Value,所以 loss.backward() 把梯度传到每个参数。

2.6 在 XOR 上训练

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。

2.7 梯度检查(Gradient Check)

把自动微分梯度与数值有限差分对照,差距应 < 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 次前向,太慢)
自动微分代码单元测试 自动化必做

三、框架对比

与 PyTorch 对照

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/

五、练习

  1. (Easy)Value__pow__,验证 d/dx(x³)x=2 处为 12.0。
  2. (Easy)tanh,验证 tanh'(0)=1tanh'(2)≈0.0707
  3. (Medium) 构建单神经元 y = relu(w1·x1 + w2·x2 + b) 的计算图,算全部五个梯度,与 PyTorch 对照。
  4. (Hard) 用对偶数实现前向模式自动微分(Dual 类),验证它与反向模式给出相同导数。

本节要点回顾

  1. 链式法则:复合函数的导数 = 各局部导数之积;神经网络每一层就是链中的一环。
  2. 计算图把链式法则可视化:节点是运算,前向传值、反向传梯度。
  3. 反向模式(反向传播)适合「输入多、输出少」,一次反向算出全部权重梯度——这就是神经网络的场景。
  4. 自动微分三件套:值包装、图记录、拓扑排序后的反向遍历。
  5. 梯度累加:+= 而非 =,处理一个值被多处使用的情形;每步前须手动清零。
  6. 动态图(PyTorch 风格)支持模型内控制流;每次前向重建图。
  7. 梯度检查用数值有限差分校验自动微分,是写新运算与调试训练循环的必备工具。
  8. micrograd 揭示本质:商用深度学习框架与你这百行代码做的是同一件事,只是规模更大。

下一节,我们离开确定性世界,进入概率与分布——开始用数学描述不确定性,从随机变量到正态分布、从大数定律到中心极限定理。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U