共计 1915 个字符,预计需要花费 5 分钟才能阅读完成。
背景介绍
BP 反向传播算法是训练神经网络的核心算法,通过计算损失函数对网络参数的梯度,指导参数更新方向。初学者常面临两个主要困难:一是复杂的数学推导容易让人迷失在链式法则中,二是实现时对矩阵维度处理和计算顺序容易出错。本文将用最直观的方式带你彻底理解这个算法。

数学推导(单隐藏层网络示例)
假设我们有一个三层网络(输入层 - 隐藏层 - 输出层),使用 Sigmoid 激活函数和均方误差损失函数。关键推导步骤如下:
-
前向传播:
$$
z_h = W_{xh}X + b_h \
a_h = \sigma(z_h) \
z_o = W_{ho}a_h + b_o \
\hat{y} = \sigma(z_o)
$$ -
误差反向传播:
- 输出层梯度:
$$
\frac{\partial L}{\partial W_{ho}} = (\hat{y} – y) \cdot \sigma'(z_o) \cdot a_h^T
$$ - 隐藏层梯度:
$$
\frac{\partial L}{\partial W_{xh}} = [(\hat{y} – y) \cdot \sigma'(z_o) \cdot W_{ho}^T] \circ \sigma'(z_h) \cdot X^T
$$
Python 实现(带维度注释)
import numpy as np
def sigmoid(x):
return 1/(1+np.exp(-x))
def bp_network(X, y, hidden_size=4, lr=0.1, epochs=1000):
# 初始化参数 (注意维度注释)
W_xh = np.random.randn(X.shape[1], hidden_size) # [input_dim, hidden_dim]
W_ho = np.random.randn(hidden_size, 1) # [hidden_dim, output_dim]
b_h = np.zeros((1, hidden_size)) # [1, hidden_dim]
b_o = np.zeros((1, 1)) # [1, output_dim]
for epoch in range(epochs):
# 前向传播
z_h = np.dot(X, W_xh) + b_h # [batch, hidden_dim]
a_h = sigmoid(z_h)
z_o = np.dot(a_h, W_ho) + b_o # [batch, 1]
y_pred = sigmoid(z_o)
# 计算损失
loss = 0.5 * np.mean((y_pred - y)**2)
# 反向传播
dL_dzo = (y_pred - y) * y_pred * (1 - y_pred) # [batch, 1]
dW_ho = np.dot(a_h.T, dL_dzo) # [hidden_dim, 1]
db_o = np.sum(dL_dzo, axis=0) # [1, 1]
dL_dzh = np.dot(dL_dzo, W_ho.T) * a_h * (1 - a_h) # [batch, hidden_dim]
dW_xh = np.dot(X.T, dL_dzh) # [input_dim, hidden_dim]
db_h = np.sum(dL_dzh, axis=0) # [1, hidden_dim]
# 参数更新
W_ho -= lr * dW_ho
W_xh -= lr * dW_xh
b_o -= lr * db_o
b_h -= lr * db_h
可视化技巧
-
Graphviz 绘制计算图:
from graphviz import Digraph dot = Digraph() dot.node('X', 'Input') dot.node('Wxh', 'Weight\n(input→hidden)') dot.node('Zh', 'Z_h') dot.edges(['X->Zh', 'Wxh->Zh']) display(dot) -
Matplotlib 动态演示:
- 用箭头表示梯度流动方向
- 用颜色深浅表示梯度大小
- 分帧展示前向 / 反向传播过程
避坑指南
- 梯度爆炸:
- 使用梯度裁剪(
grad = np.clip(grad, -1, 1)) -
改用 ReLU 激活函数时注意 Dead Neuron 问题
-
学习率设置:
- 先用 0.01-0.1 的小学习率测试
-
实现学习率衰减(
lr *= 0.95每 100 轮) -
数值不稳定:
- 对 Sigmoid 函数实现数值稳定版本:
def sigmoid(x): x = np.clip(x, -50, 50) return 1/(1+np.exp(-x))
性能优化
- 矩阵运算优化:
- 使用
np.einsum进行复杂矩阵乘法 -
合并小矩阵操作为单次大矩阵运算
-
GPU 加速:
import cupy as cp # 将 np 替换为 cp 即可 W_xh = cp.random.randn(...)
延伸思考
- 如果隐藏层增加到 3 层,反向传播公式会如何变化?
- 如何修改代码实现 Mini-batch 梯度下降?
- 尝试用 PyTorch 的 autograd 验证你的梯度计算结果
正文完
