共计 3488 个字符,预计需要花费 9 分钟才能阅读完成。
为什么需要理解 BP 算法?
反向传播(Backpropagation,简称 BP)算法是神经网络训练的基石。尽管现代深度学习框架(如 TensorFlow、PyTorch)已经帮我们封装好了 BP 算法,但如果不理解其原理,遇到模型不收敛、梯度消失等问题时就会束手无策。

- 常见误区 :很多工程师只停留在调 API 层面,当模型效果不好时盲目调整超参数
- 严重后果 :可能导致训练时间大幅增加,甚至永远无法得到理想模型
数学原理:链式法则的力量
BP 算法的核心是链式法则(Chain Rule)。我们通过一个简单例子来理解:
假设网络输出为 $L$,隐藏层输出为 $a$,权重为 $w$,则梯度计算为:
$$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial w}$$
实际网络中,这个链条会更长。以两层网络为例:
-
前向传播:
$$z_1 = W_1x + b_1$$
$$a_1 = \sigma(z_1)$$
$$z_2 = W_2a_1 + b_2$$
$$a_2 = \sigma(z_2)$$
$$L = \frac{1}{2}(a_2 – y)^2$$ -
反向传播时,梯度从后向前计算:
$$\frac{\partial L}{\partial W_2} = (a_2 – y) \cdot \sigma'(z_2) \cdot a_1^T$$
$$\frac{\partial L}{\partial W_1} = (W_2^T \cdot ((a_2 – y) \odot \sigma'(z_2))) \odot \sigma'(z_1) \cdot x^T$$
关键观察 :
– Sigmoid 函数的导数 $\sigma'(z) = \sigma(z)(1-\sigma(z))$,最大值为 0.25,容易导致梯度消失
– ReLU 的导数为 0 或 1,能缓解梯度消失,但也可能造成神经元 ” 死亡 ”
Python 实现:从零搭建 BP 网络
下面我们实现一个简单的全连接网络:
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros((1, output_size))
def sigmoid(self, z):
return 1 / (1 + np.exp(-z))
def sigmoid_derivative(self, z):
s = self.sigmoid(z)
return s * (1 - s)
def forward(self, X):
self.z1 = np.dot(X, self.W1) + self.b1
self.a1 = self.sigmoid(self.z1)
self.z2 = np.dot(self.a1, self.W2) + self.b2
self.a2 = self.sigmoid(self.z2)
return self.a2
def backward(self, X, y, learning_rate):
m = X.shape[0]
# 输出层误差
dL_da2 = self.a2 - y
da2_dz2 = self.sigmoid_derivative(self.z2)
delta2 = dL_da2 * da2_dz2
# 隐藏层误差
dL_da1 = np.dot(delta2, self.W2.T)
da1_dz1 = self.sigmoid_derivative(self.z1)
delta1 = dL_da1 * da1_dz1
# 更新参数
self.W2 -= learning_rate * np.dot(self.a1.T, delta2) / m
self.b2 -= learning_rate * np.sum(delta2, axis=0) / m
self.W1 -= learning_rate * np.dot(X.T, delta1) / m
self.b1 -= learning_rate * np.sum(delta1, axis=0) / m
关键点说明 :
- 权重初始化使用小随机数,避免初始梯度太大
- 每次迭代计算平均梯度(除以 m),保证 batch 大小不影响学习率
- sigmoid 导数计算利用了前向传播时保存的中间结果
工程实践中的技巧
梯度检查 (Gradient Checking)
在实现 BP 算法后,我们可以用数值梯度验证解析梯度的正确性:
def grad_check(X, y, epsilon=1e-7):
# 计算解析梯度
nn.forward(X)
nn.backward(X, y, learning_rate=0, update=False)
# 对每个参数进行数值梯度估计
for param, grad in [('W1', dW1), ('b1', db1), ('W2', dW2), ('b2', db2)]:
param_value = getattr(nn, param)
numerical_grad = np.zeros_like(param_value)
for i in range(param_value.shape[0]):
for j in range(param_value.shape[1]):
# 正向扰动
param_value[i,j] += epsilon
loss_plus = compute_loss(X, y)
# 负向扰动
param_value[i,j] -= 2*epsilon
loss_minus = compute_loss(X, y)
# 恢复原值
param_value[i,j] += epsilon
# 计算数值梯度
numerical_grad[i,j] = (loss_plus - loss_minus) / (2*epsilon)
# 比较差异
diff = np.linalg.norm(grad - numerical_grad) / np.linalg.norm(grad + numerical_grad)
assert diff < 1e-7, f"Gradient check failed for {param}"
优化器比较
-
SGD:简单但容易陷入局部最优
self.W -= learning_rate * dW -
Momentum:加入惯性
self.vW = beta * self.vW + (1-beta) * dW self.W -= learning_rate * self.vW -
Adam:结合动量与自适应学习率
self.mW = beta1*self.mW + (1-beta1)*dW # 一阶矩 self.vW = beta2*self.vW + (1-beta2)*(dW**2) # 二阶矩 self.W -= learning_rate * self.mW / (np.sqrt(self.vW) + eps)
常见问题与解决方案
梯度消失 / 爆炸
现象 :
– 梯度消失:深层网络早期层梯度接近 0
– 梯度爆炸:梯度值指数增长导致 NaN
解决方案 :
1. 使用 ReLU 及其变体(LeakyReLU、PReLU)代替 Sigmoid
2. 合理的权重初始化(如 He 初始化)
3. 梯度裁剪(Gradient Clipping)
grad_norm = np.linalg.norm(grad)
if grad_norm > threshold:
grad = grad * threshold / grad_norm
批量归一化 (BatchNorm)
BatchNorm 通过规范化激活值分布来加速训练:
def batchnorm_forward(x, gamma, beta, eps=1e-5):
mu = np.mean(x, axis=0)
var = np.var(x, axis=0)
x_hat = (x - mu) / np.sqrt(var + eps)
out = gamma * x_hat + beta
cache = (x, x_hat, mu, var, gamma, beta, eps)
return out, cache
延伸思考
- 与自动微分的关系 :现代框架(如 PyTorch)通过计算图自动实现 BP,原理相同但实现更高效
- 扩展到 CNN:卷积层的 BP 只需将全连接层的矩阵乘法替换为卷积运算,核心思想不变
- 二阶优化方法 :如牛顿法、拟牛顿法,虽然收敛快但计算复杂度高
结语
理解 BP 算法不仅能帮助我们更好地调试模型,也是设计新网络架构的基础。建议读者:
- 先手动实现一个简单网络(如本文示例)
- 用梯度检查验证实现正确性
- 逐步尝试更复杂的优化器和正则化方法
- 最终在实际项目中结合框架使用,但保持对底层原理的掌握
