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

数学基础
前向传播的矩阵化表示
BP 神经网络的前向传播可以用矩阵运算高效表示。对于一个三层网络(输入层、隐藏层、输出层),其计算过程如下:
- 输入层到隐藏层:
$$h = \sigma(W_1 x + b_1)$$ - 隐藏层到输出层:
$$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
调优技巧
学习率衰减策略
- 指数衰减:
learning_rate = initial_lr * (0.95 ** epoch) - 线性衰减:
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 值。解决方案包括:
- 梯度裁剪(如代码中所示)
- 使用更小的初始学习率
- 尝试不同的权重初始化方法
隐层神经元数量
经验公式:
$$N_h = \sqrt{N_i + N_o} + \alpha$$
其中 $N_i$ 是输入维度,$N_o$ 是输出维度,$\alpha$ 是调整系数(通常 5 -10)
总结与思考
通过这个案例,我们实现了从数学推导到 Python 代码的完整 BP 神经网络。在实际应用中,你可能会遇到更多挑战:
- 如何用 PyTorch 的自动微分机制重构本案例?
- 当面对类别不平衡数据时,有哪些改进方案?(如类别权重、过采样等)
希望这篇笔记能帮助你理解 BP 神经网络的实现细节,并在实际项目中灵活应用。
