BP神经网络手写数字识别实战:从零搭建到模型优化

1次阅读
没有评论

共计 2103 个字符,预计需要花费 6 分钟才能阅读完成。

image.webp

为什么选择 BP 神经网络做手写数字识别?

手写数字识别是机器学习的经典入门项目。传统方法(如模板匹配)对字体变形和位置变化非常敏感,而 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. 反向传播(关键!)

推导损失函数对参数的梯度:

  1. 输出层梯度:$\frac{\partial L}{\partial z_2} = a_2 – y$(交叉熵损失)
  2. 隐藏层梯度:$\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

五大优化技巧

  1. 学习率衰减:随着训练降低学习率

    lr = 0.1 * (1 / (1 + 0.01 * epoch))

  2. L2 正则化:在损失函数中添加权重惩罚项

    reg = 0.001 * (np.sum(self.W1**2) + np.sum(self.W2**2))
    loss = -np.sum(y * np.log(a2)) / m + reg

  3. 批量归一化:加速收敛

    self.z1 = (self.z1 - np.mean(self.z1)) / np.std(self.z1)

  4. 早停法:验证集损失连续 3 次不下降时停止训练

  5. 动量优化:累计历史梯度方向

    vW1 = 0.9 * vW1 + 0.1 * dW1  # 动量项
    self.W1 -= lr * vW1

新手避坑指南

  1. 输入未归一化 :导致梯度爆炸,应缩放到[0,1] 或标准正态分布
  2. 权重初始化不当:全零初始化会导致对称性问题,推荐 He/Xavier 初始化
  3. 激活函数选择错误:输出层用 Sigmoid 会导致梯度消失,应用 Softmax
  4. 学习率太大:损失值震荡不收敛,可尝试初始 lr=0.01 逐步调整
  5. 忽略过拟合:训练集准确率 100% 但测试集低,需添加 Dropout 或正则化

效果展示

经过 100 轮训练后:

优化方法 测试准确率
基线模型 92.3%
+L2 正则化 93.8%
+ 学习率衰减 94.1%
+ 批量归一化 95.6%

损失曲线对比显示,优化后的模型收敛更快且更稳定。

下一步改进方向

  1. 尝试增加隐藏层数量(如何避免梯度消失?)
  2. 替换为卷积网络(参数量会减少多少?)
  3. 实现动态学习率调整(如 Cyclic LR)

完整代码已上传 GitHub,包含详细注释和训练日志分析。通过这个项目,我深刻理解了反向传播的矩阵运算本质——本质上就是在计算图中沿着梯度方向链式求导的过程。

正文完
 0
评论(没有评论)