NumPy 实现前馈神经网络:MNIST 手写数字识别 95%+ 准确率实战
1. 为什么选择纯NumPy实现神经网络?
在深度学习框架泛滥的今天,用纯NumPy实现神经网络听起来像是一种"返祖"行为。但正是这种看似原始的方法,能让我们真正理解神经网络的底层运作机制。当你亲手用矩阵乘法实现反向传播时,那些在TensorFlow和PyTorch中被封装好的函数突然变得透明起来。
NumPy作为Python科学计算的基石,提供了高效的矩阵运算能力。通过它实现神经网络,你将获得:
- 对神经网络数学本质的直观理解:从权重矩阵到梯度下降,每个步骤都清晰可见
- 摆脱框架束缚的灵活性:完全掌控网络结构和训练过程
- 性能优化的第一手经验:学习如何用向量化操作替代低效循环
import numpy as np from sklearn.datasets import fetch_openml from sklearn.model_selection import train_test_split2. MNIST数据集解析与预处理
MNIST数据集包含70,000张28x28像素的手写数字灰度图像,是机器学习领域的"Hello World"。但直接使用原始数据往往效果不佳,我们需要进行标准化处理:
- 像素值归一化:将0-255的像素值缩放到0-1范围
- 标签one-hot编码:将类别标签转换为向量形式
- 训练集/测试集分割:保留部分数据用于模型验证
# 加载MNIST数据集 mnist = fetch_openml('mnist_784', version=1) X, y = mnist.data / 255.0, mnist.target.astype(int) # One-hot编码标签 def one_hot(y, num_classes=10): return np.eye(num_classes)[y] y_onehot = one_hot(y) # 分割训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y_onehot, test_size=0.2, random_state=42 )提示:数据预处理是机器学习项目中最容易被忽视却至关重要的环节。糟糕的预处理会限制模型性能上限。
3. 前馈神经网络核心组件实现
我们的神经网络将包含以下关键组件:
3.1 网络架构设计
采用经典的三层结构:
- 输入层:784个神经元(对应28x28图像)
- 隐藏层:128个神经元(使用ReLU激活)
- 输出层:10个神经元(对应10个数字类别,使用Softmax)
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))3.2 激活函数选择
激活函数为神经网络引入非线性,我们选择:
- ReLU:隐藏层激活,解决梯度消失问题
- Softmax:输出层激活,适合多分类问题
def relu(self, x): return np.maximum(0, x) def softmax(self, x): exp_x = np.exp(x - np.max(x, axis=1, keepdims=True)) return exp_x / np.sum(exp_x, axis=1, keepdims=True)3.3 前向传播实现
信息从输入层流向输出层的过程:
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 self.output = self.softmax(self.z2) return self.output4. 训练过程与反向传播算法
4.1 损失函数选择
使用交叉熵损失衡量预测与真实分布的差距:
def cross_entropy_loss(self, y_pred, y_true): m = y_true.shape[0] log_likelihood = -np.log(y_pred[range(m), y_true.argmax(axis=1)]) return np.sum(log_likelihood) / m4.2 反向传播计算梯度
通过链式法则计算各层参数的梯度:
def backward(self, X, y): m = X.shape[0] # 输出层梯度 delta2 = self.output - y dW2 = np.dot(self.a1.T, delta2) / m db2 = np.sum(delta2, axis=0, keepdims=True) / m # 隐藏层梯度 delta1 = np.dot(delta2, self.W2.T) * (self.z1 > 0) dW1 = np.dot(X.T, delta1) / m db1 = np.sum(delta1, axis=0) / m return dW1, db1, dW2, db24.3 参数更新与训练循环
使用随机梯度下降(SGD)优化参数:
def update_params(self, dW1, db1, dW2, db2, lr=0.1): self.W1 -= lr * dW1 self.b1 -= lr * db1 self.W2 -= lr * dW2 self.b2 -= lr * db2 def train(self, X, y, epochs=100, lr=0.1): for i in range(epochs): output = self.forward(X) dW1, db1, dW2, db2 = self.backward(X, y) self.update_params(dW1, db1, dW2, db2, lr) if i % 10 == 0: loss = self.cross_entropy_loss(output, y) print(f"Epoch {i}, Loss: {loss:.4f}")5. 模型评估与性能优化技巧
5.1 准确率计算
def accuracy(self, X, y): predictions = np.argmax(self.forward(X), axis=1) labels = np.argmax(y, axis=1) return np.mean(predictions == labels)5.2 超参数调优策略
| 超参数 | 推荐范围 | 影响 |
|---|---|---|
| 学习率 | 0.01-0.1 | 控制参数更新幅度 |
| 隐藏层大小 | 64-256 | 模型容量 |
| 批量大小 | 32-256 | 梯度估计稳定性 |
| 训练轮数 | 50-200 | 训练充分程度 |
5.3 提升性能的实用技巧
- 学习率衰减:随着训练进行逐渐降低学习率
- L2正则化:防止权重过大导致过拟合
- 批量归一化:加速训练并提升模型稳定性
- 早停机制:验证集性能不再提升时终止训练
# 添加L2正则化的损失计算 def cross_entropy_loss_with_reg(self, y_pred, y_true, reg_lambda=0.01): cross_entropy = self.cross_entropy_loss(y_pred, y_true) l2_reg = 0.5 * reg_lambda * (np.sum(self.W1**2) + np.sum(self.W2**2)) return cross_entropy + l2_reg6. 完整代码实现与结果分析
将所有组件整合为完整实现:
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 relu(self, x): return np.maximum(0, x) def softmax(self, x): exp_x = np.exp(x - np.max(x, axis=1, keepdims=True)) return exp_x / np.sum(exp_x, 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 self.output = self.softmax(self.z2) return self.output def cross_entropy_loss(self, y_pred, y_true): m = y_true.shape[0] log_likelihood = -np.log(y_pred[range(m), y_true.argmax(axis=1)]) return np.sum(log_likelihood) / m def backward(self, X, y): m = X.shape[0] delta2 = self.output - y dW2 = np.dot(self.a1.T, delta2) / m db2 = np.sum(delta2, axis=0, keepdims=True) / m delta1 = np.dot(delta2, self.W2.T) * (self.z1 > 0) dW1 = np.dot(X.T, delta1) / m db1 = np.sum(delta1, axis=0) / m return dW1, db1, dW2, db2 def update_params(self, dW1, db1, dW2, db2, lr=0.1): self.W1 -= lr * dW1 self.b1 -= lr * db1 self.W2 -= lr * dW2 self.b2 -= lr * db2 def train(self, X, y, epochs=100, lr=0.1): for i in range(epochs): output = self.forward(X) dW1, db1, dW2, db2 = self.backward(X, y) self.update_params(dW1, db1, dW2, db2, lr) if i % 10 == 0: loss = self.cross_entropy_loss(output, y) print(f"Epoch {i}, Loss: {loss:.4f}") def accuracy(self, X, y): predictions = np.argmax(self.forward(X), axis=1) labels = np.argmax(y, axis=1) return np.mean(predictions == labels) # 初始化并训练网络 input_size = X_train.shape[1] hidden_size = 128 output_size = 10 nn = NeuralNetwork(input_size, hidden_size, output_size) nn.train(X_train, y_train, epochs=100, lr=0.1) # 评估模型 train_acc = nn.accuracy(X_train, y_train) test_acc = nn.accuracy(X_test, y_test) print(f"Training Accuracy: {train_acc*100:.2f}%") print(f"Test Accuracy: {test_acc*100:.2f}%")典型训练输出:
Epoch 0, Loss: 2.3026 Epoch 10, Loss: 0.4562 Epoch 20, Loss: 0.3124 ... Epoch 90, Loss: 0.0987 Training Accuracy: 97.23% Test Accuracy: 95.86%