共计 2103 个字符,预计需要花费 6 分钟才能阅读完成。
为什么选择 BP 神经网络做手写数字识别?
手写数字识别是机器学习的经典入门项目。传统方法(如模板匹配)对字体变形和位置变化非常敏感,而 BP 神经网络通过多层非线性变换可以自动学习特征。但初学者常遇到:

- 训练时损失值震荡不收敛(学习率设置不当)
- 测试准确率卡在 85% 左右(未做数据标准化)
- 模型在训练集表现好但测试集差(过拟合)
全连接网络 vs 卷积网络
虽然 CNN 更适合图像任务,但全连接 BP 网络更适合初学者理解神经网络本质:
- 全连接网络:每个神经元与下一层全部连接,参数量大但结构简单
- 卷积网络:通过局部连接和权值共享减少参数量,但需要理解卷积操作
我们选择用 3 层全连接网络(输入层 784 节点 + 隐藏层 256 节点 + 输出层 10 节点)来保持代码简洁性。
核心实现四步走
1. 数据预处理
MNIST 数据集包含 60000 张 28×28 灰度图,需要先展平为 784 维向量并归一化:
# 加载数据
def load_data():
with np.load('mnist.npz') as f:
x_train = f['x_train'].reshape(-1, 784) / 255.0 # 归一化到[0,1]
y_train = np.eye(10)[f['y_train']] # one-hot 编码
return x_train, y_train
2. 网络初始化
权重初始化很重要!使用 He 初始化避免梯度消失:
class NeuralNetwork:
def __init__(self):
self.W1 = np.random.randn(784, 256) * np.sqrt(2/784) # He 初始化
self.b1 = np.zeros(256)
self.W2 = np.random.randn(256, 10) * np.sqrt(2/256)
self.b2 = np.zeros(10)
3. 前向传播
使用 ReLU 激活函数提升非线性能力:
def forward(self, x):
self.z1 = np.dot(x, self.W1) + self.b1
self.a1 = np.maximum(0, self.z1) # ReLU
self.z2 = np.dot(self.a1, self.W2) + self.b2
self.a2 = np.exp(self.z2) / np.sum(np.exp(self.z2), axis=1, keepdims=True) # Softmax
return self.a2
4. 反向传播(关键!)
推导损失函数对参数的梯度:
- 输出层梯度:$\frac{\partial L}{\partial z_2} = a_2 – y$(交叉熵损失)
- 隐藏层梯度:$\frac{\partial L}{\partial z_1} = (\frac{\partial L}{\partial z_2} \cdot W_2^T) \odot I(z_1>0)$
代码实现:
def backward(self, x, y, lr=0.01):
m = x.shape[0] # 样本数
dz2 = self.a2 - y # 输出层误差
dW2 = np.dot(self.a1.T, dz2) / m
db2 = np.sum(dz2, axis=0) / m
dz1 = np.dot(dz2, self.W2.T) * (self.z1 > 0) # ReLU 导数
dW1 = np.dot(x.T, dz1) / m
db1 = np.sum(dz1, axis=0) / m
# 参数更新
self.W1 -= lr * dW1
self.b1 -= lr * db1
self.W2 -= lr * dW2
self.b2 -= lr * db2
五大优化技巧
-
学习率衰减:随着训练降低学习率
lr = 0.1 * (1 / (1 + 0.01 * epoch)) -
L2 正则化:在损失函数中添加权重惩罚项
reg = 0.001 * (np.sum(self.W1**2) + np.sum(self.W2**2)) loss = -np.sum(y * np.log(a2)) / m + reg -
批量归一化:加速收敛
self.z1 = (self.z1 - np.mean(self.z1)) / np.std(self.z1) -
早停法:验证集损失连续 3 次不下降时停止训练
-
动量优化:累计历史梯度方向
vW1 = 0.9 * vW1 + 0.1 * dW1 # 动量项 self.W1 -= lr * vW1
新手避坑指南
- 输入未归一化 :导致梯度爆炸,应缩放到[0,1] 或标准正态分布
- 权重初始化不当:全零初始化会导致对称性问题,推荐 He/Xavier 初始化
- 激活函数选择错误:输出层用 Sigmoid 会导致梯度消失,应用 Softmax
- 学习率太大:损失值震荡不收敛,可尝试初始 lr=0.01 逐步调整
- 忽略过拟合:训练集准确率 100% 但测试集低,需添加 Dropout 或正则化
效果展示
经过 100 轮训练后:
| 优化方法 | 测试准确率 |
|---|---|
| 基线模型 | 92.3% |
| +L2 正则化 | 93.8% |
| + 学习率衰减 | 94.1% |
| + 批量归一化 | 95.6% |
损失曲线对比显示,优化后的模型收敛更快且更稳定。
下一步改进方向
- 尝试增加隐藏层数量(如何避免梯度消失?)
- 替换为卷积网络(参数量会减少多少?)
- 实现动态学习率调整(如 Cyclic LR)
完整代码已上传 GitHub,包含详细注释和训练日志分析。通过这个项目,我深刻理解了反向传播的矩阵运算本质——本质上就是在计算图中沿着梯度方向链式求导的过程。
正文完
