1. 引言
反向传播(Backpropagation)是训练神经网络的核心算法,它解决了如何高效计算深层网络中每个参数的梯度这一难题。本教程将从数学原理到代码实现,完整揭示反向传播的奥秘。
2. 问题的提出
2.1 神经网络的前向传播
考虑一个简单的三层网络:
输入层 → 隐藏层 → 输出层
x → h → y
数学表示:
z1 = W1·x + b1
h = σ(z1) # 激活函数
z2 = W2·h + b2
ŷ = σ(z2)
L = loss(ŷ, y) # 损失函数
2.2 核心挑战
我们需要计算:
∂L/∂W1, ∂L/∂b1, ∂L/∂W2, ∂L/∂b2
朴素方法: 对每个参数数值微分
∂L/∂w ≈ (L(w+ε) - L(w)) / ε
问题:
- 需要前向传播 2×参数数量 次
- 数值不稳定
- 计算复杂度 O(n²)
反向传播的贡献: 只需一次前向+一次反向,复杂度 O(n)
3. 数学基础:链式法则
3.1 一维链式法则
若 y = f(u) 且 u = g(x),则:
dy/dx = (dy/du) · (du/dx)
3.2 多维链式法则
若 z 依赖多个中间变量:
z = f(u, v)
u = g(x)
v = h(x)
则:
∂z/∂x = (∂z/∂u)·(∂u/∂x) + (∂z/∂v)·(∂v/∂x)
3.3 向量对向量求导
雅可比矩阵(Jacobian Matrix):
若 y = f(x),其中 x ∈ ℝⁿ, y ∈ ℝᵐ,则:
J = [∂y_i/∂x_j] (m×n 矩阵)
链式法则的矩阵形式:
∂z/∂x = (∂z/∂y) · (∂y/∂x)
4. 计算图与反向传播
4.1 计算图表示
将神经网络表示为有向无环图(DAG):
x
↓
[W1·x + b1] = z1
↓
[σ] = h
↓
[W2·h + b2] = z2
↓
[σ] = ŷ
↓
[loss] = L
4.2 前向传播
从输入到输出,逐层计算:
# 前向传播伪代码
def forward(x, W1, b1, W2, b2):
z1 = np.dot(W1, x) + b1
h = sigmoid(z1)
z2 = np.dot(W2, h) + b2
y_pred = sigmoid(z2)
return y_pred, (z1, h, z2) # 缓存中间结果
4.3 反向传播核心思想
从输出到输入,逐层计算梯度:
∂L/∂ŷ → ∂L/∂z2 → ∂L/∂W2, ∂L/∂b2, ∂L/∂h
→ ∂L/∂z1 → ∂L/∂W1, ∂L/∂b1
关键: 利用已计算的梯度,避免重复计算。
5. 逐层反向传播推导
场景:二分类任务
网络结构:
x (输入) → W1, b1 → σ → h → W2, b2 → σ → ŷ → BCE Loss
损失函数(Binary Cross-Entropy):
L = -[y·log(ŷ) + (1-y)·log(1-ŷ)]
第一步:输出层梯度
(1) ∂L/∂ŷ
∂L/∂ŷ = -y/ŷ + (1-y)/(1-ŷ)
(2) ∂L/∂z2 (链式法则)
∂L/∂z2 = (∂L/∂ŷ) · (∂ŷ/∂z2)
其中 ŷ = σ(z2), σ'(z) = σ(z)·(1-σ(z))
∂ŷ/∂z2 = ŷ·(1-ŷ)
代入:
∂L/∂z2 = [-y/ŷ + (1-y)/(1-ŷ)] · ŷ·(1-ŷ)
= -y(1-ŷ) + (1-y)ŷ
= ŷ - y
惊人的简洁性! 这是 Sigmoid + BCE 组合的优美之处。
第二步:输出层参数梯度
(3) ∂L/∂W2
z2 = W2·h + b2
∂L/∂W2 = (∂L/∂z2) · (∂z2/∂W2)
注意 z2 对 W2 的每个元素 W2_ij 的偏导:
∂z2_i/∂W2_ij = h_j
因此:
∂L/∂W2 = (∂L/∂z2) ⊗ h^T (外积)
矩阵形式:
∂L/∂W2 = (∂L/∂z2) @ h.T
(4) ∂L/∂b2
∂L/∂b2 = ∂L/∂z2
第三步:隐藏层梯度
(5) ∂L/∂h
h 影响 z2,z2 影响 L:
∂L/∂h = (∂L/∂z2) · (∂z2/∂h) = W2^T · (∂L/∂z2)
(6) ∂L/∂z1
∂L/∂z1 = (∂L/∂h) ⊙ σ'(z1)
= (∂L/∂h) ⊙ h ⊙ (1-h)
符号说明:
- ⊙: 逐元素乘法(Hadamard product)
- @: 矩阵乘法
第四步:输入层参数梯度
(7) ∂L/∂W1
∂L/∂W1 = (∂L/∂z1) @ x.T
(8) ∂L/∂b1
∂L/∂b1 = ∂L/∂z1
6. 完整代码实现
6.1 从零构建神经网络
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
# He 初始化
self.W1 = np.random.randn(hidden_size, input_size) * np.sqrt(2/input_size)
self.b1 = np.zeros((hidden_size, 1))
self.W2 = np.random.randn(output_size, hidden_size) * np.sqrt(2/hidden_size)
self.b2 = np.zeros((output_size, 1))
def sigmoid(self, z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500))) # 防止溢出
def sigmoid_derivative(self, z):
s = self.sigmoid(z)
return s * (1 - s)
def forward(self, x):
"""前向传播"""
self.z1 = self.W1 @ x + self.b1
self.h = self.sigmoid(self.z1)
self.z2 = self.W2 @ self.h + self.b2
self.y_pred = self.sigmoid(self.z2)
return self.y_pred
def backward(self, x, y):
"""反向传播"""
m = x.shape[1] # batch size
# 输出层梯度
dz2 = self.y_pred - y # (output_size, m)
dW2 = (1/m) * (dz2 @ self.h.T) # (output_size, hidden_size)
db2 = (1/m) * np.sum(dz2, axis=1, keepdims=True)
# 隐藏层梯度
dh = self.W2.T @ dz2 # (hidden_size, m)
dz1 = dh * self.sigmoid_derivative(self.z1)
dW1 = (1/m) * (dz1 @ x.T) # (hidden_size, input_size)
db1 = (1/m) * np.sum(dz1, axis=1, keepdims=True)
return dW1, db1, dW2, db2
def compute_loss(self, y_pred, y):
"""二元交叉熵损失"""
m = y.shape[1]
epsilon = 1e-8 # 防止 log(0)
loss = -(1/m) * np.sum(
y * np.log(y_pred + epsilon) + (1-y) * np.log(1-y_pred + epsilon)
)
return loss
def train(self, X, y, epochs, learning_rate):
"""训练"""
losses = []
for epoch in range(epochs):
# 前向传播
y_pred = self.forward(X)
# 计算损失
loss = self.compute_loss(y_pred, y)
losses.append(loss)
# 反向传播
dW1, db1, dW2, db2 = self.backward(X, y)
# 更新参数
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
return losses
6.2 测试:XOR 问题
# XOR 数据集
X = np.array([[0, 0, 1, 1],
[0, 1, 0, 1]])
y = np.array([[0, 1, 1, 0]])
# 创建网络
np.random.seed(42)
nn = NeuralNetwork(input_size=2, hidden_size=4, output_size=1)
# 训练
losses = nn.train(X, y, epochs=10000, learning_rate=0.5)
# 测试
print("\n预测结果:")
predictions = nn.forward(X)
for i in range(4):
print(f"Input: {X[:, i]}, Predicted: {predictions[0, i]:.4f}, Actual: {y[0, i]}")
输出示例:
Epoch 0, Loss: 0.7135
Epoch 100, Loss: 0.6931
...
Epoch 9900, Loss: 0.0023
预测结果:
Input: [0 0], Predicted: 0.0156, Actual: 0
Input: [0 1], Predicted: 0.9841, Actual: 1
Input: [1 0], Predicted: 0.9839, Actual: 1
Input: [1 1], Predicted: 0.0162, Actual: 0
7. 深度网络的反向传播
7.1 通用公式
对于 L 层网络,第 l 层:
(1) 当前层的激活梯度
δ^(l) = (W^(l+1))^T · δ^(l+1) ⊙ σ'(z^(l))
(2) 参数梯度
∂L/∂W^(l) = δ^(l) @ (a^(l-1))^T
∂L/∂b^(l) = δ^(l)
初始化:
δ^(L) = ∂L/∂a^(L) ⊙ σ'(z^(L))
7.2 梯度消失与爆炸
问题根源:
梯度传播:
∂L/∂W^(1) = ∂L/∂z^(L) · ∂z^(L)/∂z^(L-1) · ... · ∂z^(2)/∂z^(1) · ∂z^(1)/∂W^(1)
每一项包含 W^(l) 和 σ'(z^(l)):
∂z^(l)/∂z^(l-1) = W^(l) ⊙ σ'(z^(l-1))
Sigmoid 的问题:
σ'(z) = σ(z)(1-σ(z)) ∈ (0, 0.25]
多层相乘:
(0.25)^n → 0 (梯度消失)
解决方案:
- ReLU 激活函数: σ'(z) = 1 (z>0)
- 残差连接(ResNet): 梯度直接跳过层
- BatchNorm: 归一化激活值
- 梯度裁剪: 限制梯度范数
8. 常见激活函数的导数
8.1 Sigmoid
σ(z) = 1/(1+e^(-z))
σ'(z) = σ(z)·(1-σ(z))
8.2 Tanh
tanh(z) = (e^z - e^(-z))/(e^z + e^(-z))
tanh'(z) = 1 - tanh²(z)
8.3 ReLU
ReLU(z) = max(0, z)
ReLU'(z) = {1, z>0; 0, z≤0}
优势: 计算快,缓解梯度消失
问题: Dying ReLU (负区域梯度为0)
8.4 Leaky ReLU
LeakyReLU(z) = max(αz, z) (α=0.01)
LeakyReLU'(z) = {1, z>0; α, z≤0}
8.5 GELU (现代 Transformer 常用)
GELU(z) = z·Φ(z) (Φ: 标准正态CDF)
GELU'(z) ≈ Φ(z) + z·φ(z) (φ: 正态PDF)
9. 矩阵微积分技巧
9.1 常用恒等式
(1) 标量对向量求导
∂(a^T x)/∂x = a
∂(x^T A x)/∂x = (A + A^T)x
(2) 矩阵乘法的导数
∂(AB)/∂A = B^T ⊗ I
9.2 维度检查技巧
规则: 梯度的形状 = 参数的形状
例如:
W: (m, n) → ∂L/∂W: (m, n)
b: (m, 1) → ∂L/∂b: (m, 1)
调试技巧: 在代码中加入 assert
assert dW1.shape == self.W1.shape
10. 数值梯度检验
10.1 为什么需要?
反向传播易出错:
- 转置错误
- 广播问题
- 索引错误
10.2 梯度检验方法
def gradient_check(nn, x, y, epsilon=1e-7):
"""数值梯度 vs 反向传播梯度"""
# 反向传播梯度
y_pred = nn.forward(x)
dW1_bp, db1_bp, dW2_bp, db2_bp = nn.backward(x, y)
# 数值梯度
def compute_numerical_gradient(param, name):
grad = np.zeros_like(param)
it = np.nditer(param, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
old_value = param[idx]
param[idx] = old_value + epsilon
loss_plus = nn.compute_loss(nn.forward(x), y)
param[idx] = old_value - epsilon
loss_minus = nn.compute_loss(nn.forward(x), y)
grad[idx] = (loss_plus - loss_minus) / (2 * epsilon)
param[idx] = old_value
it.iternext()
return grad
dW1_num = compute_numerical_gradient(nn.W1, 'W1')
# 计算相对误差
diff = np.linalg.norm(dW1_bp - dW1_num) / (
np.linalg.norm(dW1_bp) + np.linalg.norm(dW1_num)
)
print(f"W1 梯度相对误差: {diff}")
if diff < 1e-7:
print("✓ 梯度检验通过!")
else:
print("✗ 梯度计算可能有误")
11. 实战技巧
11.1 批量处理
向量化计算:
# 差: 循环处理每个样本
for i in range(m):
y_pred = forward(X[:, i])
backward(X[:, i], y[i])
# 好: 批量处理
Y_pred = forward(X) # X: (n, m)
backward(X, Y)
11.2 缓存中间结果
前向传播时保存 z, h 等:
cache = {'z1': z1, 'h': h, 'z2': z2}
反向传播时复用,避免重新计算。
11.3 梯度累积
处理大 batch:
for mini_batch in split(data, batch_size):
gradients += compute_gradients(mini_batch)
gradients /= num_mini_batches
update_parameters(gradients)
12. 自动微分框架
12.1 PyTorch 的自动求导
import torch
x = torch.tensor([[0., 0., 1., 1.],
[0., 1., 0., 1.]], requires_grad=True)
y = torch.tensor([[0., 1., 1., 0.]])
W1 = torch.randn(4, 2, requires_grad=True)
b1 = torch.zeros(4, 1, requires_grad=True)
W2 = torch.randn(1, 4, requires_grad=True)
b2 = torch.zeros(1, 1, requires_grad=True)
# 前向传播
z1 = W1 @ x + b1
h = torch.sigmoid(z1)
z2 = W2 @ h + b2
y_pred = torch.sigmoid(z2)
# 损失
loss = torch.nn.functional.binary_cross_entropy(y_pred, y)
# 自动反向传播
loss.backward()
print("W1 梯度:", W1.grad)
12.2 计算图的构建
PyTorch 动态构建计算图:
Tensor → Function → Tensor → ...
↓ ↓ ↓
grad grad_fn grad
调用 .backward() 时,沿图反向传播。
13. 总结
核心要点
- 反向传播 = 链式法则的高效实现
- 关键技巧:
- 缓存前向传播结果
- 从后向前逐层计算梯度
- 利用矩阵运算向量化
- 常见陷阱:
- 梯度消失/爆炸
- 维度不匹配
- 数值稳定性
进阶方向
- 自动微分理论 (Forward/Reverse Mode)
- 高阶导数 (Hessian, Hessian-Vector Product)
- 分布式训练中的梯度同步
实践建议
- 先在小网络上验证(梯度检验)
- 使用成熟框架(PyTorch/TensorFlow)
- 理解原理后再调优
记住: 反向传播不是黑魔法,而是优雅的数学工程!