共计 2871 个字符,预计需要花费 8 分钟才能阅读完成。
背景:为什么需要 BP 算法?
神经网络训练的本质是通过调整权重参数,使得模型输出尽可能接近真实值。这个过程需要两个关键步骤:

- 前向传播 :输入数据经过层层计算得到预测值
- 反向传播 :根据预测误差,从输出层反向计算各层的参数更新量
BP 算法正是解决第二个问题的核心方法,它通过链式法则高效计算所有参数的梯度。
数学原理:链式法则的直观理解
想象一个三明治的制作流程:
- 买面包 → 涂抹黄油 → 加配料 → 成品
- 如果成品太咸,我们需要知道:
- 咸度多少来自配料?
- 多少来自黄油?
- 面包的选择有没有影响?
链式法则就是这种 ” 责任追溯 ” 的数学表达。在神经网络中,我们用计算图表示这种依赖关系:
输入 X → 隐藏层 H → 输出 Y → 损失 L
计算梯度时的核心公式:
$$\frac{\partial L}{\partial W} = \frac{\partial L}{\partial Y} \cdot \frac{\partial Y}{\partial H} \cdot \frac{\partial H}{\partial W}$$
关键痛点:梯度消失问题
当使用 Sigmoid 激活函数时,由于其导数最大值为 0.25,多层连乘会导致:
$$\frac{\partial L}{\partial W_{early}} ≈ 0.25^n \cdot \frac{\partial L}{\partial W_{late}}$$
这意味着:
- 浅层网络的权重几乎得不到有效更新
- 网络越深,问题越严重
- 表现为训练早期 loss 下降缓慢甚至停滞
代码实现:NumPy 版 BP 算法
import numpy as np
from matplotlib import pyplot as plt
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size):
# Xavier 初始化
self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1/input_size)
self.b1 = np.zeros(hidden_size)
self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1/hidden_size)
self.b2 = np.zeros(output_size)
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, X):
self.hidden = self.sigmoid(X @ self.W1 + self.b1) # (N, hidden_size)
self.output = self.sigmoid(self.hidden @ self.W2 + self.b2) # (N, output_size)
return self.output
def backward(self, X, y, lr=0.1):
# 输出层梯度
d_output = (self.output - y) * self.output * (1 - self.output) # (N, output_size)
# 隐藏层梯度(注意梯度消失!)d_hidden = (d_output @ self.W2.T) * self.hidden * (1 - self.hidden) # (N, hidden_size)
# 参数更新
self.W2 -= lr * self.hidden.T @ d_output # (hidden_size, output_size)
self.b2 -= lr * d_output.mean(axis=0)
self.W1 -= lr * X.T @ d_hidden # (input_size, hidden_size)
self.b1 -= lr * d_hidden.mean(axis=0)
# 示例训练流程
net = TwoLayerNet(2, 4, 1)
loss_history = []
for epoch in range(1000):
# 假设 X,y 是训练数据
output = net.forward(X)
loss = ((output - y)**2).mean()
net.backward(X, y, lr=0.1/(1+epoch*0.01)) # 学习率衰减
loss_history.append(loss)
plt.plot(loss_history)
plt.xlabel('Epoch')
plt.ylabel('Loss')
优化技巧实践
Xavier 初始化
通过调整初始化范围,使各层输出的方差保持一致:
# 对于第 l 层,初始化标准差为 1/sqrt(n_{l-1})
self.W = np.random.randn(input_dim, output_dim) * np.sqrt(1/input_dim)
学习率衰减
随着训练进行逐渐减小学习步伐:
initial_lr = 0.1
def get_lr(epoch):
return initial_lr / (1 + epoch*0.01)
避坑指南:梯度校验
手动实现的梯度计算容易出错,可以通过数值方法验证:
def grad_check(net, X, y, param='W1', epsilon=1e-7):
original = getattr(net, param).copy()
grad = np.zeros_like(original)
# 数值计算梯度
for i in range(original.shape[0]):
for j in range(original.shape[1]):
# 正向扰动
net.W1[i,j] += epsilon
loss_plus = ((net.forward(X) - y)**2).mean()
# 负向扰动
net.W1[i,j] -= 2*epsilon
loss_minus = ((net.forward(X) - y)**2).mean()
# 中心差分
grad[i,j] = (loss_plus - loss_minus) / (2*epsilon)
net.W1[i,j] = original[i,j] # 恢复原值
# 与 BP 结果对比
net.forward(X)
net.backward(X, y, lr=0)
bp_grad = getattr(net, param)
return np.allclose(grad, bp_grad, atol=1e-5)
扩展思考
自动微分 vs 手动实现
- 手动实现 :
- 优点:深入理解算法细节
-
缺点:易出错,修改网络结构需要重写
-
自动微分(如 PyTorch):
- 优点:自动计算梯度,支持动态计算图
- 缺点:隐藏实现细节,可能成为 ” 黑箱 ”
用 ReLU 解决梯度消失
尝试将代码中的 Sigmoid 替换为:
def relu(x):
return np.maximum(0, x)
观察训练速度和最终效果的变化。ReLU 的梯度在正区间恒为 1,避免了连乘导致的梯度衰减。
结语
实现 BP 算法就像学习骑自行车——刚开始需要关注每一个踏板动作,但掌握原理后,你可以更专注于路线规划(网络结构设计)。建议读者:
- 先用这个小例子理解每个变量的维度变化
- 尝试增加网络深度观察梯度变化
- 用不同激活函数比较训练效果
当你能直观地 ” 看到 ” 梯度在网络中的流动时,就真正掌握了深度学习的核心引擎。
