直觉:一个会自我修正的可微分函数

神经网络去掉神秘外壳后,就是一个带很多旋钮(参数)的函数。前向传播 = 把输入塞进去算出预测;反向传播 = 根据「错了多少」反推「每个旋钮该往哪拧、拧多少」。整个训练就是这两步的循环,靠链式法则把误差信号从输出一路传回输入端。

下面用一个最小的单隐层网络把数据流、公式和代码全部走通。

机制:前向传播的数据流

设网络结构为 输入 xRdx \in \mathbb{R}^{d} → 隐层(权重 W1W_1、偏置 b1b_1、激活 σ\sigma)→ 输出层(W2,b2W_2, b_2)。前向传播逐层计算:

\begin{aligned} z_1 &= W_1 x + b_1 \\ a_1 &= \sigma(z_1) \\ z_2 &= W_2 a_1 + b_2 \\ \hat{y} &= z_2 \quad (\text{回归}) \quad \text{或} \quad \text{softmax}(z_2)\ (\text{分类}) \end{aligned}

每一层都是「线性变换 + 非线性」。非线性 σ\sigma(如 ReLU:σ(z)=max(0,z)\sigma(z)=\max(0,z))是灵魂——没有它,多层会塌缩成单层线性映射。

然后用损失函数衡量预测与真值的差距。回归常用均方误差:

L=12y^y2\mathcal{L} = \tfrac{1}{2}\|\hat{y} - y\|^2

公式:反向传播就是链式法则的工程化

我们要的是 L/Wl\partial \mathcal{L} / \partial W_lL/bl\partial \mathcal{L} / \partial b_l,好用梯度下降更新。直接对每个参数求偏导会重复大量计算;反向传播的精髓是从后往前逐层复用中间梯度,把复杂度从指数级降到与前向传播同量级。

定义每层的「误差信号」 δl=L/zl\delta_l = \partial \mathcal{L} / \partial z_l。对上面的网络:

δ2=y^yLW2=δ2a1,Lb2=δ2δ1=(W2δ2)σ(z1)LW1=δ1x,Lb1=δ1\begin{aligned} \delta_2 &= \hat{y} - y \\ \frac{\partial \mathcal{L}}{\partial W_2} &= \delta_2\, a_1^\top, \qquad \frac{\partial \mathcal{L}}{\partial b_2} = \delta_2 \\ \delta_1 &= (W_2^\top \delta_2) \odot \sigma'(z_1) \\ \frac{\partial \mathcal{L}}{\partial W_1} &= \delta_1\, x^\top, \qquad \frac{\partial \mathcal{L}}{\partial b_1} = \delta_1 \end{aligned}

其中 \odot 是逐元素相乘,σ\sigma' 是激活的导数。注意这个递推结构:深层的 δ\delta 通过 WW^\top 反向「传播」到浅层,再乘上本层激活的导数。这就是「反向传播」名字的由来。

更新参数(学习率 η\eta):

θθηLθ\theta \leftarrow \theta - \eta \, \frac{\partial \mathcal{L}}{\partial \theta}

代码:30 行手写一个训练步

不用任何框架,把上面的公式落成 NumPy,最能看清没有魔法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import numpy as np

def relu(z): return np.maximum(0, z)
def relu_grad(z): return (z > 0).astype(z.dtype)

# 一个训练步(单样本,便于看清)
def train_step(x, y, W1, b1, W2, b2, lr=0.01):
# ---- 前向 ----
z1 = W1 @ x + b1
a1 = relu(z1)
z2 = W2 @ a1 + b2
y_hat = z2 # 回归输出
loss = 0.5 * np.sum((y_hat - y) ** 2)

# ---- 反向(链式法则)----
d2 = (y_hat - y) # δ2 = ∂L/∂z2
dW2 = np.outer(d2, a1)
db2 = d2
d1 = (W2.T @ d2) * relu_grad(z1) # δ1
dW1 = np.outer(d1, x)
db1 = d1

# ---- 更新 ----
W1 -= lr * dW1; b1 -= lr * db1
W2 -= lr * dW2; b2 -= lr * db2
return loss, (W1, b1, W2, b2)

现代框架(PyTorch/JAX)用自动微分自动完成反向部分:前向时构建计算图,loss.backward() 沿图反向遍历套用链式法则。你写的还是前向逻辑,梯度是「免费」得到的——但底层做的正是上面这套递推。

工程权衡与踩坑

  • 梯度消失/爆炸δ\delta 每反向穿过一层就乘一次 WW^\topσ\sigma'。层很深时,这些因子连乘会指数级缩小(消失)或放大(爆炸)。Sigmoid/tanh 的导数最大值小于 1,深层尤其容易消失——这正是 ReLU、残差连接、归一化层流行的原因。
  • 学习率是头号超参:太大震荡甚至发散,太小训练奇慢。实践中配合 Adam 这类自适应优化器和学习率调度。
  • batch 的作用:单样本梯度噪声大。用 mini-batch 求平均梯度,既稳定又能用矩阵乘法吃满 GPU 并行。
  • 显存账:训练显存 ≈ 参数 + 梯度 + 优化器状态 + 激活值。反向传播需要前向的中间激活 ala_l,所以激活值往往是显存大头;这也是「梯度检查点(gradient checkpointing)」用计算换显存的前提——丢弃部分激活,反向时重算。
  • 初始化不能全零:若 WW 全相同,所有神经元对称地接收相同梯度,永远学不出不同特征。需用随机初始化(如 He/Xavier)打破对称。

小结

神经网络 = 前向传播算预测 + 反向传播算梯度 + 梯度下降更新参数,三步循环。前向是逐层「线性 + 非线性」的复合;反向是链式法则的工程化——用 δ\delta 递推从输出把误差信号传回每一层,从而高效复用中间结果。框架的自动微分替你写了反向代码,但理解 δl=(Wl+1δl+1)σ(zl)\delta_l = (W_{l+1}^\top \delta_{l+1}) \odot \sigma'(z_l) 这条递推,才能解释梯度消失、显存占用、初始化这些真实工程问题的根源。