共计 2954 个字符,预计需要花费 8 分钟才能阅读完成。
从 MNIST 分类任务说起
MNIST 手写数字识别是神经网络入门的经典案例。我们需要将 28×28 像素的灰度图像分类为 0 - 9 共 10 个数字类别。这个任务特别适合用 BP 神经网络来解决,因为它具有以下特点:

- 输入数据维度适中(784 维)
- 输出是明确的离散类别
- 数据量足够大(6 万训练样本)
- 可以很好地展示神经网络的非线性分类能力
数学推导: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]} &= \text{softmax}(Z^{[2]})
\end{aligned}
其中 $\sigma$ 表示 Sigmoid 激活函数,softmax 用于多分类输出概率。
反向传播的链式法则
反向传播的核心是计算损失函数对各参数的梯度。以交叉熵损失 $L$ 为例:
-
输出层梯度:
\frac{\partial L}{\partial Z^{[2]}} = A^{[2]} - Y -
隐藏层梯度(含 Sigmoid 导数):
\begin{aligned} \frac{\partial L}{\partial Z^{[1]}} &= (W^{[2]})^T \cdot \frac{\partial L}{\partial Z^{[2]}} \odot \sigma'(Z^{[1]}) \\ \sigma'(z) &= \sigma(z)(1-\sigma(z)) \end{aligned} -
参数梯度:
\begin{aligned} \frac{\partial L}{\partial W^{[2]}} &= \frac{\partial L}{\partial Z^{[2]}} (A^{[1]})^T \\ \frac{\partial L}{\partial b^{[2]}} &= \sum{\frac{\partial L}{\partial Z^{[2]}}} \\ \frac{\partial L}{\partial W^{[1]}} &= \frac{\partial L}{\partial Z^{[1]}} X^T \\ \frac{\partial L}{\partial b^{[1]}} &= \sum{\frac{\partial L}{\partial Z^{[1]}}} \end{aligned}
Python 实现:从零搭建神经网络
网络结构初始化
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
# He 初始化缓解梯度消失
self.W1 = np.random.randn(hidden_size, input_size) * np.sqrt(2./input_size)
self.b1 = np.zeros((hidden_size, 1))
self.W2 = np.random.randn(output_size, hidden_size) * np.sqrt(2./hidden_size)
self.b2 = np.zeros((output_size, 1))
向量化前向传播
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
return self.softmax(self.Z2)
批次训练实现
def train(self, X, Y, learning_rate=0.1, epochs=100, batch_size=64):
for epoch in range(epochs):
# 随机批次划分
permutation = np.random.permutation(X.shape[1])
for i in range(0, X.shape[1], batch_size):
batch_idx = permutation[i:i+batch_size]
X_batch = X[:, batch_idx]
Y_batch = Y[:, batch_idx]
# 前向传播
output = self.forward(X_batch)
# 反向传播
dZ2 = output - Y_batch
dW2 = np.dot(dZ2, self.A1.T) / batch_size
db2 = np.sum(dZ2, axis=1, keepdims=True) / batch_size
dA1 = np.dot(self.W2.T, dZ2)
dZ1 = dA1 * self.sigmoid_derivative(self.A1)
dW1 = np.dot(dZ1, X_batch.T) / batch_size
db1 = np.sum(dZ1, axis=1, keepdims=True) / batch_size
# 梯度裁剪防止爆炸
for grad in [dW1, db1, dW2, db2]:
np.clip(grad, -5, 5, out=grad)
# 参数更新
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
调优实战:提升模型性能
学习率衰减策略对比
- 指数衰减:
lr = initial_lr * (decay_rate)^(epoch/step_size) - 线性衰减:
lr = initial_lr * (1 - epoch/total_epochs)
实际测试中,指数衰减在初期收敛更快,但线性衰减在后期更稳定。
正则化技术实现
L2 正则化:在损失函数中添加权重惩罚项
loss += 0.5 * lambda_ * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
Dropout:训练时随机丢弃部分神经元
mask = (np.random.rand(*A1.shape) < keep_prob) / keep_prob
A1 *= mask # 前向传播时应用
避坑指南
梯度爆炸识别与处理
当出现以下情况时可能发生梯度爆炸:
– 损失值突然变成 NaN
– 权重值变得极大
解决方案:
1. 梯度裁剪(如代码示例)
2. 使用更小的初始化权重
3. 调整网络深度
隐层神经元数量经验公式
常用启发式公式:
N_h = \frac{N_i + N_o}{2} + \sqrt{\text{样本数}}
其中 $N_i$ 为输入维度,$N_o$ 为输出维度。
延伸思考
- 如何用 PyTorch 的自动微分重构本案例?
- 利用
torch.autograd自动计算梯度 -
使用
nn.Module构建网络结构 -
针对类别不平衡的改进方案:
- 在损失函数中添加类别权重
- 采用过采样 / 欠采样策略
- 使用 Focal Loss
总结
通过这次实践,我们完整实现了 BP 神经网络从理论到代码的转换。关键收获包括:
- 理解了反向传播的数学本质
- 掌握了向量化实现的技巧
- 学会了常见的调优和正则化方法
- 积累了处理实际问题的经验
建议读者尝试用不同的激活函数(如 ReLU)和优化器(如 Adam)来进一步改进模型性能。
正文完
