共计 1680 个字符,预计需要花费 5 分钟才能阅读完成。
为什么需要反向传播?
前向传播过程中,神经网络通过层层权重计算得到预测输出,但无法直接知道如何调整权重来减小预测误差。例如一个 3 层网络需要调整 $W_1,W_2,W_3$ 三组参数时,前向传播只能获得最终误差 $\mathcal{L}$,却无法区分各层对误差的具体贡献度。

通过计算图的链式法则,误差梯度可以反向传递:
$$
\frac{\partial \mathcal{L}}{\partial W_1} = \frac{\partial \mathcal{L}}{\partial a_3} \cdot \frac{\partial a_3}{\partial z_3} \cdot \frac{\partial z_3}{\partial a_2} \cdots \frac{\partial z_1}{\partial W_1}
$$
梯度下降的几何意义
假设损失函数是 $J(\theta_1,\theta_2)$ 的曲面,参数更新相当于沿着曲面最陡峭的下降方向移动:
- 计算当前位置梯度 $\nabla_\theta J(\theta)$
- 沿负梯度方向更新:$\theta \leftarrow \theta – \eta \nabla_\theta J(\theta)$
- 学习率 $\eta$ 控制步长大小
Python 实现核心算法
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def bp_nn(X, y, hidden_size=64, lr=0.1, epochs=1000):
# 初始化参数
W1 = np.random.randn(X.shape[1], hidden_size)
b1 = np.zeros(hidden_size)
W2 = np.random.randn(hidden_size, 1)
b2 = np.zeros(1)
for epoch in range(epochs):
# 前向传播
z1 = X.dot(W1) + b1
a1 = sigmoid(z1)
z2 = a1.dot(W2) + b2
pred = sigmoid(z2)
# 计算损失 (MSE)
loss = np.mean((pred - y)**2)
# 反向传播
d_pred = 2 * (pred - y) / len(y)
d_z2 = d_pred * pred * (1 - pred) # sigmoid 导数
d_W2 = a1.T.dot(d_z2)
d_b2 = np.sum(d_z2, axis=0)
d_a1 = d_z2.dot(W2.T)
d_z1 = d_a1 * a1 * (1 - a1)
d_W1 = X.T.dot(d_z1)
d_b1 = np.sum(d_z1, axis=0)
# 参数更新
W2 -= lr * d_W2
b2 -= lr * d_b2
W1 -= lr * d_W1
b1 -= lr * d_b1
激活函数对比实验
| 函数类型 | 梯度表达式 | 梯度特性 |
|---|---|---|
| Sigmoid | $f'(x)=f(x)(1-f(x))$ | 容易饱和导致梯度消失 |
| Tanh | $1-f(x)^2$ | 零中心化但仍有饱和区 |
| ReLU | $\text{max}(0,x)$ | 正区间无梯度衰减 |
MNIST 实战注意事项
- 输入数据需要归一化到 [0,1] 范围
- 使用交叉熵损失比 MSE 更适合分类任务
- 每批次训练后记录 loss 值并绘制曲线
# 梯度裁剪示例
grad_norm = np.linalg.norm(grad)
if grad_norm > threshold:
grad = grad * threshold / grad_norm
# BatchNorm 层实现
batch_mean = np.mean(X, axis=0)
batch_var = np.var(X, axis=0)
X_hat = (X - batch_mean) / np.sqrt(batch_var + eps)
out = gamma * X_hat + beta
思考题分析方向
当出现高训练准确率但验证集表现差时,可以从 BP 算法角度检查:
1. 梯度更新是否仅拟合了训练集噪声(观察梯度幅值变化)
2. 验证集梯度方向与训练集是否一致
3. 参数更新量级是否过大导致震荡(需调整学习率)
完整的训练过程可视化代码和更详细的数学推导,可以参考 GitHub 项目示例。理解 BP 算法需要多从计算图的角度思考梯度流动,这是掌握深度学习的基础核心。
正文完
