Python实战:BP神经网络从零实现与手写数字识别案例详解

1次阅读
没有评论

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

image.webp

核心概念:BP 神经网络如何工作

BP 神经网络的核心是前向传播和反向传播两个过程。前向传播计算预测值,反向传播根据误差调整参数。以下是关键公式:

Python 实战:BP 神经网络从零实现与手写数字识别案例详解

前向传播 (以单隐层为例):

$$
\begin{aligned}
z^{[1]} &= W^{[1]}X + b^{[1]} \
a^{[1]} &= g(z^{[1]}) \
z^{[2]} &= W^{[2]}a^{[1]} + b^{[2]} \
a^{[2]} &= \sigma(z^{[2]})
\end{aligned}
$$

反向传播 (链式求导):

$$
\begin{aligned}
dz^{[2]} &= a^{[2]} – y \
dW^{[2]} &= \frac{1}{m} dz^{[2]} a^{[1]T} \
db^{[2]} &= \frac{1}{m} \sum dz^{[2]} \
dz^{[1]} &= W^{[2]T} dz^{[2]} \odot g'(z^{[1]}) \
dW^{[1]} &= \frac{1}{m} dz^{[1]} X^T \
db^{[1]} &= \frac{1}{m} \sum dz^{[1]}
\end{aligned}
$$

激活函数的选择直接影响梯度传播:

  • Sigmoid 容易导致梯度消失(导数最大仅 0.25)
  • ReLU($\max(0,x)$)能缓解梯度消失,但可能出现神经元死亡

新手常见痛点分析

以下是初学者常踩的坑:

  1. 参数初始化 :全零初始化会导致对称性问题,推荐使用 Xavier 初始化:

    W = np.random.randn(fan_in, fan_out) * np.sqrt(1/fan_in)

  2. 学习率设置 :

  3. 过大(>0.1):损失震荡不收敛
  4. 过小(<1e-5):训练速度极慢

  5. 梯度消失 :使用 Sigmoid 的深层网络,前几层参数几乎不更新

代码实现:从零搭建 BP 网络

# Python 3.8+, 需要 numpy>=1.20 和 matplotlib
import numpy as np

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

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

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

    def forward(self, X):
        self.z1 = np.dot(X, self.W1) + self.b1
        self.a1 = self.relu(self.z1)
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        return self.softmax(self.z2)

    def backward(self, X, y, output, l2_lambda=0.01):
        m = X.shape[0]

        # 输出层误差
        dz2 = output - y
        dW2 = (1/m) * np.dot(self.a1.T, dz2) + (l2_lambda/m)*self.W2
        db2 = (1/m) * np.sum(dz2, axis=0)

        # 隐层误差
        dz1 = np.dot(dz2, self.W2.T) * (self.z1 > 0)  # ReLU 导数
        dW1 = (1/m) * np.dot(X.T, dz1) + (l2_lambda/m)*self.W1
        db1 = (1/m) * np.sum(dz1, axis=0)

        return dW1, db1, dW2, db2

MNIST 实战:手写数字识别

数据预处理

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)

# 归一化到 [0,1]
X = mnist.data / 255.0  
y = mnist.target.astype(np.int8)

# One-hot 编码
y_onehot = np.eye(10)[y]

# 划分训练测试集
X_train, X_test = X[:60000], X[60000:]
y_train, y_test = y_onehot[:60000], y_onehot[60000:]

训练过程可视化

import matplotlib.pyplot as plt

# 训练循环
losses = []
accuracies = []

for epoch in range(100):
    # 前向传播
    output = model.forward(X_train)

    # 计算损失(带 L2 正则化)data_loss = -np.mean(y_train * np.log(output + 1e-10))
    reg_loss = 0.5 * l2_lambda * (np.sum(model.W1**2) + np.sum(model.W2**2)) / len(y_train)
    loss = data_loss + reg_loss
    losses.append(loss)

    # 计算准确率
    preds = np.argmax(output, axis=1)
    truth = np.argmax(y_train, axis=1)
    acc = np.mean(preds == truth)
    accuracies.append(acc)

    # 反向传播
    dW1, db1, dW2, db2 = model.backward(X_train, y_train, output)

    # 参数更新(带动量)beta = 0.9
    model.vW1 = beta * model.vW1 + (1-beta) * dW1
    model.W1 -= learning_rate * model.vW1

    # 其他参数更新类似...

# 绘制曲线
plt.figure(figsize=(12,4))
plt.subplot(121)
plt.plot(losses)
plt.title('Training Loss')

plt.subplot(122)
plt.plot(accuracies)
plt.title('Training Accuracy')

生产环境调优建议

  1. 超参数搜索 :
  2. 使用网格搜索或随机搜索确定最佳学习率(建议范围 1e- 4 到 1e-2)
  3. 隐藏层节点数通常在 50-500 之间

  4. Early Stopping:

    if len(val_loss) > 10 and np.mean(val_loss[-10:]) >= np.mean(val_loss[-20:-10]):
        print('Early stopping triggered')
        break

  5. GPU 加速 :

  6. 将 NumPy 数组转换为 CuPy 数组
  7. 批处理大小设为 2 的幂次(如 64,128,256)

延伸思考

读者可以尝试以下改进:

  • 添加 Dropout 层防止过拟合
  • 使用交叉熵损失替代 MSE
  • 实现 Adam 优化器替代 SGD
  • 添加 Batch Normalization 层

完整代码已上传 GitHub(虚构链接):github.com/username/bpnn-mnist

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