共计 2150 个字符,预计需要花费 6 分钟才能阅读完成。
最近在入门机器学习时,发现很多教程对 BP 神经网络的讲解要么过于理论化,要么直接调库一笔带过。作为被反向传播折磨过的菜鸟,我决定结合数学推导和代码实现,把这块硬骨头啃下来分享给大家。

为什么需要 BP 神经网络?
先看两个典型场景:
- 手写数字识别:输入 28×28 像素的图片,输出 0 - 9 的分类结果。像素之间关系复杂,传统算法难以建模
- 房价预测:根据房屋面积、位置、房龄等特征,预测合理价格。特征与目标存在非线性关系
这些场景的共同特点是:输入和输出的映射关系复杂,且数据本身存在噪声。BP 神经网络通过多层非线性变换,可以自动学习这些复杂模式。
数学推导:误差是如何反向传播的?
前向传播的矩阵表示
假设一个三层网络(输入层→隐藏层→输出层),前向传播过程为:
\begin{aligned}
Z^{[1]} &= W^{[1]}X + b^{[1]} \\
A^{[1]} &= \sigma(Z^{[1]}) \\
Z^{[2]} &= W^{[2]}A^{[1]} + b^{[2]} \\
A^{[2]} &= \sigma(Z^{[2]})
\end{aligned}
其中 $\sigma$ 是 Sigmoid 函数:$\sigma(z) = \frac{1}{1+e^{-z}}$
损失函数与反向传播
采用平方误差损失:
L = \frac{1}{2}(Y - A^{[2]})^2
关键步骤是通过链式法则计算梯度:
-
输出层梯度:
\frac{\partial L}{\partial W^{[2]}} = (A^{[2]} - Y) \cdot \sigma'(Z^{[2]}) \cdot A^{[1]T} -
隐藏层梯度:
\frac{\partial L}{\partial W^{[1]}} = \underbrace{(A^{[2]} - Y) \cdot \sigma'(Z^{[2]})}_{\delta^{[2]}} \cdot W^{[2]T} \cdot \sigma'(Z^{[1]}) \cdot X^T
梯度下降的直观理解
学习率 $\alpha$ 就像下山时的步长:
- 太大:容易错过最低点(振荡)
- 太小:收敛速度过慢
参数更新公式:
W = W - \alpha \cdot \frac{\partial L}{\partial W}
Python 实现:手写三层神经网络
下面是仅用 NumPy 的实现(完整代码见 GitHub):
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
# 参数初始化(重要!)self.W1 = np.random.randn(hidden_size, input_size) * 0.01
self.b1 = np.zeros((hidden_size, 1))
self.W2 = np.random.randn(output_size, hidden_size) * 0.01
self.b2 = np.zeros((output_size, 1))
def sigmoid(self, z):
return 1 / (1 + np.exp(-z))
def forward(self, X):
# 前向传播
self.Z1 = np.dot(self.W1, X) + self.b1
self.A1 = self.sigmoid(self.Z1)
self.Z2 = np.dot(self.W2, self.A1) + self.b2
self.A2 = self.sigmoid(self.Z2)
return self.A2
def backward(self, X, Y, learning_rate):
m = X.shape[1] # 样本数
# 输出层梯度
dZ2 = self.A2 - Y
dW2 = (1/m) * np.dot(dZ2, self.A1.T)
db2 = (1/m) * np.sum(dZ2, axis=1, keepdims=True)
# 隐藏层梯度
dZ1 = np.dot(self.W2.T, dZ2) * (self.A1 * (1 - self.A1))
dW1 = (1/m) * np.dot(dZ1, X.T)
db1 = (1/m) * np.sum(dZ1, axis=1, keepdims=True)
# 参数更新
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
避坑指南:新手常见问题
梯度消失问题
现象:深层网络训练时,前面的层学习速度极慢
解决方法:
– 使用 ReLU 代替 Sigmoid
– 残差连接(ResNet)
– 梯度裁剪
学习率设置
经验法则:
– 初始尝试 0.001-0.1 范围
– 使用学习率衰减:lr = lr0 / (1 + decay_rate * epoch)
参数初始化
千万不要全零初始化!推荐:
– Xavier 初始化:W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)
– He 初始化(ReLU 适用)
思考与延伸
- 如果在隐藏层后加入 BatchNorm 层,会对梯度传播产生什么影响?
- 对比 SGD 和 Adam 优化器在 MNIST 数据集上的收敛曲线差异
实现完这个简单版本后,建议尝试用 PyTorch 框架重写,感受自动求导的便捷性。理解底层原理后,你会更清楚高级 API 背后的魔法。
正文完
