共计 2553 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
很多初学者在学习 BP 神经网络时容易陷入几个误区:

- 混淆权重更新与偏导计算,误以为反向传播直接更新权重
- 对链式求导的过程理解模糊,无法准确写出梯度表达式
- 忽视激活函数在反向传播中的作用
- 不理解矩阵运算的维度匹配规则
这些认知盲区会导致代码实现时出现维度错误、梯度计算不正确等问题。本文将用数学推导 + 代码实现的方式逐个击破这些难点。
数学推导
前向传播计算
考虑单隐藏层网络结构(输入层→隐藏层→输出层):
[
\begin{aligned}
z^{[1]} &= W^{[1]}X + b^{[1]} \
a^{[1]} &= \sigma(z^{[1]}) \
z^{[2]} &= W^{[2]}a^{[1]} + b^{[2]} \
\hat{y} &= a^{[2]} = \sigma(z^{[2]})
\end{aligned}
]
其中:
– $X$ 是输入矩阵(每列一个样本)
– $\sigma$ 是 Sigmoid 激活函数
– 上标 [1]、[2] 表示网络层数
损失函数计算
采用均方误差(MSE)作为损失函数:
[
J = \frac{1}{m}\sum_{i=1}^m (\hat{y}^{(i)} – y^{(i)})^2
]
反向传播推导
核心是计算损失对各参数的偏导(以 $W^{[2]}$ 为例):
-
计算输出层梯度:
[
\frac{\partial J}{\partial a^{[2]}} = \frac{2}{m}(a^{[2]} – y)
] -
经过 Sigmoid 激活:
[
\frac{\partial a^{[2]}}{\partial z^{[2]}} = a^{[2]} \odot (1 – a^{[2]})
] -
对 $W^{[2]}$ 求导:
[
\frac{\partial z^{[2]}}{\partial W^{[2]}} = a^{[1]T}
] -
最终组合(链式法则):
[
\frac{\partial J}{\partial W^{[2]}} = \frac{\partial J}{\partial a^{[2]}} \frac{\partial a^{[2]}}{\partial z^{[2]}} \frac{\partial z^{[2]}}{\partial W^{[2]}}
]
其他参数的梯度计算遵循相同模式,注意矩阵求导时的维度匹配规则。
代码实现
import numpy as np
class TwoLayerNet:
def __init__(self, input_size, hidden_size):
self.W1 = np.random.randn(hidden_size, input_size) * 0.01
self.b1 = np.zeros((hidden_size, 1))
self.W2 = np.random.randn(1, hidden_size) * 0.01
self.b2 = np.zeros((1, 1))
def sigmoid(self, z):
return 1 / (1 + np.exp(-z))
def forward(self, X):
self.Z1 = self.W1.dot(X) + self.b1
self.A1 = self.sigmoid(self.Z1)
self.Z2 = self.W2.dot(self.A1) + self.b2
self.A2 = self.sigmoid(self.Z2)
return self.A2
def backward(self, X, y, lr=0.1):
m = X.shape[1]
# 输出层梯度
dA2 = 2/m * (self.A2 - y)
dZ2 = dA2 * self.A2 * (1 - self.A2)
# 隐藏层梯度
dW2 = dZ2.dot(self.A1.T)
db2 = np.sum(dZ2, axis=1, keepdims=True)
dA1 = self.W2.T.dot(dZ2)
dZ1 = dA1 * self.A1 * (1 - self.A1)
# 输入层梯度
dW1 = dZ1.dot(X.T)
db1 = np.sum(dZ1, axis=1, keepdims=True)
# 参数更新
self.W2 -= lr * dW2
self.b2 -= lr * db2
self.W1 -= lr * dW1
self.b1 -= lr * db1
关键点说明:
dW2计算时的转置操作是为了保持矩阵维度匹配- 梯度计算时保留了 batch 维度(对多个样本求平均)
- 激活函数梯度采用逐元素乘法(
*)实现
避坑指南
学习率设置
- 过大:导致损失震荡甚至发散
- 过小:收敛速度慢,易陷入局部最优
- 建议:先用 0.01 尝试,观察损失曲线调整
梯度消失问题
- 现象:深层网络训练时,前面层梯度接近 0
- 成因:Sigmoid 导数最大仅 0.25,连乘后指数衰减
- 缓解方案:
- 改用 ReLU 激活函数
- 使用 Batch Normalization
- 残差连接
数据标准化
- 未标准化的数据会导致:
- 不同特征尺度差异大,需要不同学习率
- 梯度方向震荡,收敛速度慢
- 推荐做法:
X = (X - np.mean(X, axis=1, keepdims=True)) / np.std(X, axis=1, keepdims=True)
验证实验
用 make_moons 数据集测试:
from sklearn.datasets import make_moons
import matplotlib.pyplot as plt
X, y = make_moons(n_samples=1000, noise=0.1)
X = X.T # 转置为(特征数, 样本数)
y = y.reshape(1, -1) # 调整为(1, 样本数)
# 训练
net = TwoLayerNet(2, 4)
losses = []
for epoch in range(1000):
pred = net.forward(X)
loss = np.mean((pred - y)**2)
net.backward(X, y, lr=0.1)
losses.append(loss)
# 可视化
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.show()
预期结果:
– 损失曲线应单调下降(可能有小幅波动)
– 最终准确率应达到 85% 以上
– 若损失出现 NaN,尝试减小学习率
总结
通过本文的推导与实现,我们完整走通了 BP 网络的:
- 前向传播的矩阵计算流程
- 损失函数对参数的梯度推导
- 反向传播的链式求导过程
- 实际训练中的调参技巧
建议下一步:
– 尝试增加网络层数
– 实现不同的激活函数(如 ReLU)
– 添加 L2 正则化项
– 用更复杂的数据集测试
