BP神经网络实战:从数学推导到Python实现解决分类问题

1次阅读
没有评论

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

image.webp

从 MNIST 分类任务说起

MNIST 手写数字识别是神经网络入门的经典案例。我们需要将 28×28 像素的灰度图像分类为 0 - 9 共 10 个数字类别。这个任务特别适合用 BP 神经网络来解决,因为它具有以下特点:

BP 神经网络实战:从数学推导到 Python 实现解决分类问题

  • 输入数据维度适中(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$ 为例:

  1. 输出层梯度:

    \frac{\partial L}{\partial Z^{[2]}} = A^{[2]} - Y

  2. 隐藏层梯度(含 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}

  3. 参数梯度:

    \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$ 为输出维度。

延伸思考

  1. 如何用 PyTorch 的自动微分重构本案例?
  2. 利用 torch.autograd 自动计算梯度
  3. 使用 nn.Module 构建网络结构

  4. 针对类别不平衡的改进方案:

  5. 在损失函数中添加类别权重
  6. 采用过采样 / 欠采样策略
  7. 使用 Focal Loss

总结

通过这次实践,我们完整实现了 BP 神经网络从理论到代码的转换。关键收获包括:

  • 理解了反向传播的数学本质
  • 掌握了向量化实现的技巧
  • 学会了常见的调优和正则化方法
  • 积累了处理实际问题的经验

建议读者尝试用不同的激活函数(如 ReLU)和优化器(如 Adam)来进一步改进模型性能。

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