BP神经网络Python实战:从数学原理到手写数字识别

1次阅读
没有评论

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

image.webp

前言

最近在学习机器学习中的神经网络,特别是 BP 神经网络。作为一个初学者,我发现很多教程要么过于理论化,要么代码实现不够清晰。所以决定自己动手实现一个 BP 神经网络,并在 MNIST 手写数字数据集上进行测试。这篇文章将记录我的学习过程,希望能帮助到同样正在入门的小伙伴们。

1. BP 神经网络基础

BP(Back Propagation)神经网络是最基础的前馈神经网络之一。它主要由三部分组成:输入层、隐藏层和输出层。数据从输入层进入,经过隐藏层处理后,最终在输出层得到结果。

1.1 前向传播

前向传播是指数据从输入层流向输出层的过程。对于每一层,我们都会进行两个操作:

  1. 线性变换:$z = Wx + b$
  2. 非线性激活:$a = \sigma(z)$

其中 $W$ 是权重矩阵,$b$ 是偏置向量,$\sigma$ 是激活函数。

1.2 反向传播

反向传播是根据输出误差来调整网络参数的过程。这是 BP 神经网络最核心的部分,也是它名称的由来。

主要步骤包括:

  1. 计算输出层误差:$\delta^L = \nabla_a C \odot \sigma'(z^L)$
  2. 反向传播误差:$\delta^l = ((W^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)$
  3. 计算梯度:$\frac{\partial C}{\partial W^l} = \delta^l (a^{l-1})^T$
  4. 更新参数:$W^l = W^l – \eta \frac{\partial C}{\partial W^l}$

2. 代码实现

下面我们用 Python 和 numpy 来实现一个完整的 BP 神经网络。

2.1 网络初始化

import numpy as np

class NeuralNetwork:
    def __init__(self, layers, learning_rate=0.1):
        """
        初始化神经网络
        :param layers: 网络结构,如 [784, 30, 10] 表示输入层 784 个神经元,隐层 30 个,输出层 10 个
        :param learning_rate: 学习率
        """
        self.layers = layers
        self.learning_rate = learning_rate

        # 初始化权重和偏置
        self.weights = []
        self.biases = []

        # 使用 Xavier 初始化方法
        for i in range(len(layers)-1):
            # 权重初始化为均值为 0,方差为 sqrt(2/(n_in+n_out))的随机数
            w = np.random.randn(layers[i+1], layers[i]) * np.sqrt(2.0/(layers[i]+layers[i+1]))
            b = np.zeros((layers[i+1], 1))
            self.weights.append(w)
            self.biases.append(b)

2.2 激活函数选择

不同的激活函数会对网络性能产生显著影响。我们比较三种常用激活函数:

  1. Sigmoid: $\sigma(z) = \frac{1}{1+e^{-z}}$
  2. Tanh: $tanh(z) = \frac{e^z – e^{-z}}{e^z + e^{-z}}$
  3. ReLU: $ReLU(z) = max(0, z)$
def sigmoid(self, z):
    return 1.0 / (1.0 + np.exp(-z))

def sigmoid_prime(self, z):
    """Sigmoid 函数的导数"""
    s = self.sigmoid(z)
    return s * (1 - s)

def relu(self, z):
    return np.maximum(0, z)

def relu_prime(self, z):
    """ReLU 函数的导数"""
    return (z > 0).astype(float)

2.3 前向传播实现

def forward(self, x):
    """前向传播"""
    activations = [x]  # 存储各层激活值
    zs = []  # 存储各层线性变换结果

    a = x
    for w, b in zip(self.weights, self.biases):
        z = np.dot(w, a) + b
        zs.append(z)
        a = self.relu(z)  # 隐层使用 ReLU
        activations.append(a)

    # 输出层使用 softmax
    z = zs[-1]
    exp_z = np.exp(z - np.max(z))  # 防止数值溢出
    a = exp_z / np.sum(exp_z)
    activations[-1] = a

    return activations, zs

2.4 反向传播实现

def backward(self, x, y, activations, zs):
    """反向传播"""
    nabla_w = [np.zeros(w.shape) for w in self.weights]
    nabla_b = [np.zeros(b.shape) for b in self.biases]

    # 输出层误差
    delta = activations[-1] - y
    nabla_b[-1] = delta
    nabla_w[-1] = np.dot(delta, activations[-2].T)

    # 反向传播误差
    for l in range(2, len(self.layers)):
        z = zs[-l]
        sp = self.relu_prime(z)
        delta = np.dot(self.weights[-l+1].T, delta) * sp
        nabla_b[-l] = delta
        nabla_w[-l] = np.dot(delta, activations[-l-1].T)

    return nabla_w, nabla_b

2.5 参数更新

def update_params(self, nabla_w, nabla_b):
    """更新参数"""
    for i in range(len(self.weights)):
        self.weights[i] -= self.learning_rate * nabla_w[i]
        self.biases[i] -= self.learning_rate * nabla_b[i]

3. 在 MNIST 上的实战

现在我们用这个网络来识别手写数字。MNIST 数据集包含 60000 张训练图片和 10000 张测试图片。

3.1 数据处理

from tensorflow.keras.datasets import mnist

# 加载数据
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

# 归一化
train_images = train_images.reshape((60000, 28*28)) / 255.0
test_images = test_images.reshape((10000, 28*28)) / 255.0

# 标签转为 one-hot
num_classes = 10
train_labels = np.eye(num_classes)[train_labels]
test_labels = np.eye(num_classes)[test_labels]

3.2 训练网络

# 初始化网络
nn = NeuralNetwork([784, 128, 64, 10], learning_rate=0.01)

# 训练参数
epochs = 10
batch_size = 64

# 训练循环
for epoch in range(epochs):
    for i in range(0, len(train_images), batch_size):
        # 获取 batch
        batch_images = train_images[i:i+batch_size].T
        batch_labels = train_labels[i:i+batch_size].T

        # 前向传播
        activations, zs = nn.forward(batch_images)

        # 反向传播
        nabla_w, nabla_b = nn.backward(batch_images, batch_labels, activations, zs)

        # 更新参数
        nn.update_params(nabla_w, nabla_b)

    # 每个 epoch 评估一次
    test_accuracy = evaluate(nn, test_images, test_labels)
    print(f"Epoch {epoch+1}, Test Accuracy: {test_accuracy:.2%}")

4. 调优技巧

4.1 学习率调整

学习率太大可能导致震荡,太小则收敛缓慢。可以尝试:

  1. 学习率衰减:随着训练进行逐渐减小学习率
  2. 自适应学习率方法:如 Adam 优化器

4.2 批量归一化(BatchNorm)

批量归一化可以加速训练,允许使用更高的学习率。通常在激活函数前加入:

def batch_norm(self, z):
    mean = np.mean(z, axis=1, keepdims=True)
    var = np.var(z, axis=1, keepdims=True)
    z_norm = (z - mean) / np.sqrt(var + 1e-8)
    return self.gamma * z_norm + self.beta

4.3 梯度裁剪

当梯度爆炸时,可以限制梯度的大小:

def clip_gradients(self, grads, max_norm):
    total_norm = np.sum([np.sum(np.square(g)) for g in grads])
    scale = max_norm / (np.sqrt(total_norm) + 1e-6)
    if scale < 1:
        for g in grads:
            g *= scale
    return grads

5. 结果分析

经过 10 个 epoch 的训练,我们的网络在测试集上达到了约 96% 的准确率。Loss 曲线如下:

BP 神经网络 Python 实战:从数学原理到手写数字识别

6. 总结

通过这个项目,我深刻理解了 BP 神经网络的工作原理。关键点包括:

  1. 合理的网络初始化非常重要,Xavier 初始化是个不错的选择
  2. 隐层激活函数的选择会影响训练效果,ReLU 通常表现不错
  3. 学习率需要仔细调整,可以考虑自适应方法
  4. 批量归一化可以显著提升训练速度和稳定性

完整的代码可以在我的 GitHub 上找到。希望这篇文章对你有帮助,欢迎留言讨论!

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