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

1次阅读
没有评论

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

image.webp

引言

在机器学习领域,手写数字识别(MNIST 数据集分类)是一个经典的入门案例。这个任务看似简单,但涉及到了图像处理、特征提取和分类算法等多个环节。今天,我们就从 BP 神经网络的角度,一步步实现这个分类任务,并深入探讨其中的数学原理和工程实践。

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

数学基础

前向传播的矩阵化表示

BP 神经网络的前向传播可以用矩阵运算高效表示。对于一个三层网络(输入层、隐藏层、输出层),其计算过程如下:

  1. 输入层到隐藏层:
    $$h = \sigma(W_1 x + b_1)$$
  2. 隐藏层到输出层:
    $$y = \text{softmax}(W_2 h + b_2)$$

其中,$\sigma$ 是激活函数(通常用 Sigmoid 或 ReLU),softmax 用于多分类输出概率。

反向传播的链式法则推导

反向传播的核心是链式法则。以 Sigmoid 激活函数为例,其导数为:
$$\sigma'(z) = \sigma(z)(1-\sigma(z))$$

对于输出层的梯度计算:
$$\frac{\partial L}{\partial W_2} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial z_2} \cdot \frac{\partial z_2}{\partial W_2}$$

隐藏层的梯度计算:
$$\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial h} \cdot \frac{\partial h}{\partial z_1} \cdot \frac{\partial z_1}{\partial W_1}$$

Python 实现

网络结构定义

import numpy as np

class NeuralNetwork:
    def __init__(self, input_size, hidden_size, output_size):
        # 参数初始化
        self.W1 = np.random.randn(input_size, hidden_size) * 0.01
        self.b1 = np.zeros((1, hidden_size))
        self.W2 = np.random.randn(hidden_size, output_size) * 0.01
        self.b2 = np.zeros((1, output_size))

    def sigmoid(self, z):
        return 1 / (1 + np.exp(-z))

    def sigmoid_derivative(self, z):
        return self.sigmoid(z) * (1 - self.sigmoid(z))

    def softmax(self, z):
        exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))
        return exp_z / np.sum(exp_z, axis=1, keepdims=True)

训练过程

def train(self, X, y, learning_rate=0.1, epochs=1000):
    for epoch in range(epochs):
        # 前向传播
        z1 = np.dot(X, self.W1) + self.b1
        h = self.sigmoid(z1)
        z2 = np.dot(h, self.W2) + self.b2
        y_pred = self.softmax(z2)

        # 计算损失(交叉熵)m = y.shape[0]
        loss = -np.sum(y * np.log(y_pred + 1e-15)) / m

        # 反向传播
        dy = y_pred - y
        dW2 = np.dot(h.T, dy) / m
        db2 = np.sum(dy, axis=0, keepdims=True) / m

        dh = np.dot(dy, self.W2.T)
        dz1 = dh * self.sigmoid_derivative(z1)
        dW1 = np.dot(X.T, dz1) / m
        db1 = np.sum(dz1, axis=0, keepdims=True) / m

        # 梯度裁剪(防止梯度爆炸)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

调优技巧

学习率衰减策略

  1. 指数衰减:
    learning_rate = initial_lr * (0.95 ** epoch)
  2. 线性衰减:
    learning_rate = initial_lr * (1 - epoch/epochs)

正则化实现

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

l2_loss = 0.5 * reg_lambda * (np.sum(self.W1**2) + np.sum(self.W2**2))
loss += l2_loss

Dropout 则在训练时随机丢弃部分神经元:

mask = (np.random.rand(*h.shape) < keep_prob) / keep_prob
h *= mask

避坑指南

梯度爆炸处理

梯度爆炸表现为参数更新时出现 NaN 值。解决方案包括:

  1. 梯度裁剪(如代码中所示)
  2. 使用更小的初始学习率
  3. 尝试不同的权重初始化方法

隐层神经元数量

经验公式:
$$N_h = \sqrt{N_i + N_o} + \alpha$$
其中 $N_i$ 是输入维度,$N_o$ 是输出维度,$\alpha$ 是调整系数(通常 5 -10)

总结与思考

通过这个案例,我们实现了从数学推导到 Python 代码的完整 BP 神经网络。在实际应用中,你可能会遇到更多挑战:

  1. 如何用 PyTorch 的自动微分机制重构本案例?
  2. 当面对类别不平衡数据时,有哪些改进方案?(如类别权重、过采样等)

希望这篇笔记能帮助你理解 BP 神经网络的实现细节,并在实际项目中灵活应用。

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