共计 1963 个字符,预计需要花费 5 分钟才能阅读完成。
为什么需要理解反向传播?
刚开始学深度学习时,很多人会直接调用 TensorFlow 或 PyTorch 的model.fit(),输入数据就能得到结果。但这种‘黑箱’操作存在明显问题:
- 网络出 bug 时不会调试(比如梯度消失 / 爆炸)
- 调参时盲目尝试(不知道学习率如何影响权重更新)
- 无法定制特殊网络结构(例如自定义损失函数)
本文将通过 手写 Python 实现 + 数学推导,带你彻底理解误差反向传播(Backpropagation)的核心原理。完整代码已放在Github 仓库,建议配合代码阅读本文。
数学原理:计算图与链式法则
反向传播的本质是 多元微积分的链式法则。我们用一个 3 层网络为例(输入层→隐藏层→输出层),拆解权重更新的完整过程:

(示意图:展示从输出层误差回传到隐藏层权重的路径)
关键公式推导
-
前向传播(隐藏层计算):
$$
z_h = W_{xh}^T X + b_h,\quad a_h = \sigma(z_h)
$$
其中 $\sigma$ 是 sigmoid 函数:$\sigma(z) = \frac{1}{1+e^{-z}}$ -
损失函数(MSE):
$$
L = \frac{1}{2}(y_{pred} – y_{true})^2
$$ -
反向传播(以输出层权重 $W_{hy}$ 为例):
$$
\frac{\partial L}{\partial W_{hy}} = \frac{\partial L}{\partial y_{pred}} \cdot \frac{\partial y_{pred}}{\partial z_y} \cdot \frac{\partial z_y}{\partial W_{hy}}
$$
其中 sigmoid 导数有特殊性质:
$$
\sigma'(z) = \sigma(z)(1-\sigma(z))
$$
Python 实现(numpy 版)
1. 网络初始化
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
# 初始化权重(小随机数避免梯度消失)self.W_xh = np.random.randn(input_size, hidden_size) * 0.01
self.W_hy = np.random.randn(hidden_size, output_size) * 0.01
# 初始化偏置
self.b_h = np.zeros((1, hidden_size))
self.b_y = np.zeros((1, output_size))
2. 前向传播实现
def forward(self, X):
# 隐藏层计算
self.z_h = np.dot(X, self.W_xh) + self.b_h
self.a_h = self._sigmoid(self.z_h) # 激活函数
# 输出层计算
self.z_y = np.dot(self.a_h, self.W_hy) + self.b_y
y_pred = self._sigmoid(self.z_y)
return y_pred
def _sigmoid(self, z):
return 1 / (1 + np.exp(-z))
3. 反向传播核心
def backward(self, X, y_true, y_pred, learning_rate=0.1):
# 输出层梯度
dy_pred = y_pred - y_true
dW_hy = np.dot(self.a_h.T, dy_pred * y_pred * (1 - y_pred))
# 隐藏层梯度(链式法则逐层回传)dh = np.dot(dy_pred * y_pred * (1 - y_pred), self.W_hy.T)
dW_xh = np.dot(X.T, dh * self.a_h * (1 - self.a_h))
# 更新参数
self.W_hy -= learning_rate * dW_hy
self.W_xh -= learning_rate * dW_xh
性能对比:向量化 vs 循环
| 实现方式 | 1000 次迭代耗时 |
|---|---|
| 向量化 | 0.8 秒 |
| 循环 | 12.3 秒 |
避坑指南
- 学习率设置:
- 现象:损失函数震荡不收敛
-
解决:尝试 0.01→0.1→0.5 等不同值,或用学习率衰减策略
-
梯度爆炸:
- 现象:权重值变成 NaN
-
解决:权重初始化改用 Xavier 方法,或添加梯度裁剪
-
激活函数饱和:
- 现象:隐藏层输出全部为 0 或 1
- 解决:改用 ReLU 激活函数,或调整初始化范围
延伸思考
- 计算效率:全连接层的矩阵乘法时间复杂度是 $O(n^2)$,如何通过稀疏连接降低计算量?
- 局部最优:SGD 容易陷入局部极小值,有哪些优化器能改善这个问题?(提示:动量、Adam)
通过这次手写实现,我深刻体会到反向传播中‘误差逐层分配’的精妙设计。建议读者尝试用这个代码训练一个 XOR 分类器(只有 4 个样本的小数据集),观察权重如何逐步调整到最优值。当你看到网络最终能正确预测时,那种成就感是调用现成框架无法比拟的!
