共计 2235 个字符,预计需要花费 6 分钟才能阅读完成。
为什么需要反向传播?
刚入门神经网络时,最让我头疼的就是:那么多参数(Weights 和 Biases)该怎么调整?比如一个简单的 3 层网络,输入层 10 个节点,隐藏层 20 个节点,输出层 2 个节点,光是权重矩阵就有 (10×20)+(20×2)=240 个参数!如果靠瞎猜来调整这些参数,估计训练到宇宙末日也完不成。

链式法则:反向传播的数学核心
反向传播的精髓在于链式法则(Chain Rule)。举个具体例子,假设我们有如下单隐藏层网络:
输入层 → 隐藏层(Sigmoid 激活)→ 输出层(线性输出)
用数学表示前向传播过程:
- 隐藏层输入:$h_{in} = XW_1 + b_1$
- 隐藏层输出:$h_{out} = \sigma(h_{in})$
- 最终输出:$y_{pred} = h_{out}W_2 + b_2$
当使用均方误差损失函数时,我们需要计算 $\frac{\partial Loss}{\partial W_1}$。根据链式法则,这个梯度可以拆解为:
$$
\frac{\partial Loss}{\partial W_1} = \frac{\partial Loss}{\partial y_{pred}} \cdot \frac{\partial y_{pred}}{\partial h_{out}} \cdot \frac{\partial h_{out}}{\partial h_{in}} \cdot \frac{\partial h_{in}}{\partial W_1}
$$
具体到每一步的计算(假设 batch_size=1):
- $\frac{\partial Loss}{\partial y_{pred}} = 2(y_{pred} – y_{true})$(形状:1×2)
- $\frac{\partial y_{pred}}{\partial h_{out}} = W_2^T$(形状:2×20)
- $\frac{\partial h_{out}}{\partial h_{in}} = \sigma'(h_{in})$(形状:1×20)
- $\frac{\partial h_{in}}{\partial W_1} = X^T$(形状:10×1)
最终通过矩阵相乘得到 $\frac{\partial Loss}{\partial W_1}$ 的形状是 10×20,与 $W_1$ 本身的形状一致。
Python 实现详解
下面是用 NumPy 实现的完整代码,关键部分都加了形状变化的注释:
import numpy as np
# 网络参数初始化
input_size = 10
hidden_size = 20
output_size = 2
W1 = np.random.randn(input_size, hidden_size) * 0.01 # 形状:10×20
b1 = np.zeros((1, hidden_size)) # 形状:1×20
W2 = np.random.randn(hidden_size, output_size) * 0.01 # 形状:20×2
b2 = np.zeros((1, output_size)) # 形状:1×2
# 前向传播
def forward(X):
h_in = np.dot(X, W1) + b1 # 形状:1×20
h_out = 1 / (1 + np.exp(-h_in)) # Sigmoid 激活
y_pred = np.dot(h_out, W2) + b2 # 形状:1×2
return h_in, h_out, y_pred
# 反向传播
def backward(X, y_true, h_in, h_out, y_pred):
batch_size = X.shape[0]
# 输出层梯度
dLoss = 2 * (y_pred - y_true) # 形状:1×2
dW2 = np.dot(h_out.T, dLoss) / batch_size # 形状:20×2
db2 = np.sum(dLoss, axis=0) / batch_size # 形状:1×2
# 隐藏层梯度
dh_out = np.dot(dLoss, W2.T) # 形状:1×20
dh_in = dh_out * h_out * (1 - h_out) # Sigmoid 导数
dW1 = np.dot(X.T, dh_in) / batch_size # 形状:10×20
db1 = np.sum(dh_in, axis=0) / batch_size # 形状:1×20
return dW1, db1, dW2, db2
# 参数更新(学习率 lr=0.1)def update_params(dW1, db1, dW2, db2, lr=0.1):
global W1, b1, W2, b2
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
避坑指南
在实际实现时,新手常会遇到这些问题:
- 梯度爆炸 / 消失 :当网络较深时,梯度可能指数级增大或减小。解决方法:
- 使用 ReLU 等改进的激活函数替代 Sigmoid
- 采用梯度裁剪(Gradient Clipping)
-
合理的权重初始化(如 He 初始化)
-
学习率设置不当 :
- 可以从 0.01~0.1 开始尝试
-
更高级的优化器(Adam、RMSProp)能自动调整
-
数值不稳定 :
- 避免除零问题(如在交叉熵损失中加 epsilon)
- 使用对数空间计算(如 log_softmax)
延伸思考
尝试改进上面的基础实现:
- 如何修改代码支持 mini-batch 训练?提示:注意 batch 维度的矩阵运算
- 如果隐藏层改用 ReLU 激活,反向传播的计算需要做哪些修改?
通过这个练习,你会更深刻地理解:反向传播本质上是一套巧妙的梯度分发系统,它让神经网络能够高效地自我修正。建议亲手敲一遍代码,观察中间变量的形状变化,这比看十遍理论都管用!
