神经网络的训练本质上是一件事:求出损失函数对每个参数的偏导,然后沿着负梯度方向挪一小步。 框架帮你自动算了这些偏导,但”为什么这么算”才是真正值钱的部分。这篇文章就用手写的方式把它讲透。
反向传播不是某种玄学算法,它就是链式法则在计算图上的高效应用——前向算值,反向算梯度。
1. 计算图与链式法则
把每个运算看成图上的一个节点,数据沿边单向前进,这就是计算图。前向传播时我们只需要数值; 反向传播时,我们从损失出发,沿边逆向把”损失对该节点的敏感度”一路传回去。
设 z = W·x + b,a = σ(z),L = loss(a, y)。 由链式法则,损失对 W 的梯度可以拆成三段相乘:
其中 ∂L/∂a · ∂a/∂z 这一项只和”当前层之后”有关,可以整块作为上游传来的梯度; 而 ∂z/∂W = x 只和本层输入有关。这种局部梯度 × 上游梯度的结构, 正是反向传播能逐层复用的原因。
2. 前向传播
我们搭一个两层网络:输入 2 维 → 隐藏层 8 维(ReLU)→ 输出 1 维(Sigmoid,二分类)。 特意把每层的中间结果缓存下来,反向时要用。
import numpy as np
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
def forward(X, params):
W1, b1, W2, b2 = params
z1 = X @ W1 + b1 # (N,8)
a1 = np.maximum(0, z1) # ReLU
z2 = a1 @ W2 + b2 # (N,1)
a2 = sigmoid(z2) # 输出概率
cache = (X, z1, a1, z2, a2)
return a2, cache
交叉熵损失配合 Sigmoid 有个著名的化简结果:输出层梯度就是 a2 - y, 干净得不像话。这既不是巧合也不是调参技巧,而是对数损失与 Sigmoid 导数互相抵消的结果。
3. 反向传播:梯度怎么流回来
反向的过程就是从输出往输入倒着走一遍,每步都用”上游梯度 × 局部梯度”。 ReLU 的局部梯度是 0 或 1(靠 z1 > 0 判断),矩阵乘法的局部梯度则是另一个乘子。
def backward(cache, y, params):
W1, b1, W2, b2 = params
X, z1, a1, z2, a2 = cache
N = X.shape[0]
# 输出层:交叉熵 + Sigmoid 化简为 (a2 - y)
dz2 = (a2 - y) / N
# 第二层参数梯度
dW2 = a1.T @ dz2
db2 = dz2.sum(axis=0)
# 梯度穿过第二层回到隐藏层
da1 = dz2 @ W2.T
dz1 = da1 * (z1 > 0) # ReLU 导数:正区间为 1
# 第一层参数梯度
dW1 = X.T @ dz1
db1 = dz1.sum(axis=0)
return dW1, db1, dW2, db2
注意所有梯度都除以了 N:因为我们用的是整批样本的平均损失, 梯度自然也要取平均,否则学习率会随 batch 大小变化而”失效”。
4. 训练循环与参数更新
有了前向和反向,训练就是一个反复执行的循环:前向算损失 → 反向算梯度 → 沿负梯度更新参数。
rng = np.random.default_rng(0)
W1 = rng.normal(0, 0.5, (2, 8))
b1 = np.zeros(8)
W2 = rng.normal(0, 0.5, (8, 1))
b2 = np.zeros(1)
params = [W1, b1, W2, b2]
lr = 0.5
for epoch in range(500):
a2, cache = forward(X, params)
# 交叉熵损失
loss = -np.mean(y*np.log(a2+1e-9) + (1-y)*np.log(1-a2+1e-9))
grads = backward(cache, y, params)
# 梯度下降:沿负梯度方向更新
for p, g in zip(params, grads):
p -= lr * g
if epoch % 100 == 0:
print(epoch, f"{loss:.4f}")
训练中加 1e-9 是为了防止 log(0) 产生 -inf,这是数值实现里最朴素也最必要的保护。
5. 完整实现要点
- 缓存中间量:反向要用到
X、z1、a1,不缓存就得重算一遍前向,代价翻倍 - 梯度取平均:损失用 mean,梯度也要 mean,二者必须一致
- 初始化不能全零:否则同一层所有神经元梯度相同,永远学不出差异,要用随机小值打破对称
- 学习率是关键超参:太大损失震荡发散,太小收敛缓慢,可以从 0.1 开始试
- 用数值梯度做校验:对某个参数加减小量
ε,比较解析梯度与差分梯度,能快速定位公式写错的地方
6. 总结
反向传播的全部内容就是一条链式法则加上一张计算图。前向缓存中间结果, 反向用”局部梯度 × 上游梯度”逐层回传,最后用结果更新参数。
手写一遍之后再看框架,loss.backward() 和 optimizer.step() 就不再是黑盒了。 这个理解在排查梯度消失、梯度爆炸、训练不收敛等问题时,价值远超调参技巧本身。
如果这篇文章对你有帮助,请我喝杯茶吧