资讯详情

资讯详情

纯Python实现BP神经网络:MNIST手写数字识别完整教程

简介基于BP神经网络的MNIST手写数字识别Python实现源码是一套面向计算机相关专业期末大作业与课程设计的完整项目适合需要实战练习的学生直接参考或二次开发。项目曾获得98分覆盖BP网络构建、训练、测试与调参流程代码结构清晰并附带数据集、训练好的模型权重及可视化测试图片便于快速复现和理解手写数字识别的核心原理。资源包共61个文件约15.3MB以Python脚本、npz数据文件、jpg图片、pkl模型文件为主同时包含说明文档、运行环境依赖和效果展示视频能够帮助读者从环境配置到结果展示走通全流程。目前已有534人学习下载对于正在完成课程设计或期末大作业的同学是一份具备较高参考价值和可操作性的实践资料。1. 为什么MNIST手写数字识别现在还用BP神经网络MNIST是深度学习的Hello World但很多人一提手写数字识别就默认要上CNN。实际上BP神经网络在MNIST测试集上就能跑到97%以上的正确率而且代码量小、原理透明、答辩好讲。这个项目就是一套完整的Python实现源码包含网络定义、数据加载、预处理缓存、超参数调整脚本和测试图片是一份拿过98分的期末大作业。它不需要GPU一台普通笔记本就能跑完训练和评估非常适合计算机专业学生做课程设计也适合想快速跑通神经网络全流程的开发者。如果你正在纠结怎么把作业做得既完整又容易展示这套实现可以省掉不少踩坑时间。2. BP神经网络结构设计与MNIST数据集的Python建模2.1 三层全连接网络为什么够用MNIST每张图片固定为28×28像素展平后是784维特征向量输出是09共10个类别的概率。网络结构按“输入-隐层-输出”展开即可。项目里的network.py默认创建一个三层的Network输入层784个神经元隐藏层100个输出层10个。隐藏层使用sigmoid激活函数输出层使用sigmoid后接交叉熵损失。虽然现在深度学习框架里更流行ReLU和Softmax但在这个纯numpy实现里sigmoid和交叉熵能让反向传播的推导更直白出问题也更好用打印数值来定位。网络层神经元数激活函数输出形状输入层784无(784, 1)隐藏层100可调sigmoid(100, 1)输出层10sigmoid(10, 1)为什么隐藏层选100而不是更大这个项目里我一般先试100因为大作业场景下要控制训练时间又希望模型学得动。隐藏层太多比如300、500MNIST上准确率并不会成比例提高反而训练变慢期末考试时现场演示容易等得尴尬。2.2 network.py里的前向与反向传播实现network.py是整个项目的核心。它与PyTorch或TensorFlow的模型定义不同完全用numpy手写前向和反向。下面是简化后的关键代码import numpy as np class Network: def __init__(self, sizes): self.num_layers len(sizes) self.sizes sizes self.weights [np.random.randn(y, x) / np.sqrt(x) for x, y in zip(sizes[:-1], sizes[1:])] self.biases [np.zeros((y, 1)) for y in sizes[1:]] def sigmoid(self, z): return 1.0 / (1.0 np.exp(-z)) def softmax(self, z): exp_z np.exp(z - np.max(z, axis0, keepdimsTrue)) return exp_z / np.sum(exp_z, axis0, keepdimsTrue) def feedforward(self, a): for w, b in zip(self.weights, self.biases): a self.sigmoid(np.dot(w, a) b) return a def backprop(self, x, y): # 前向缓存每层的z和a zs, acts [], [x] a x for w, b in zip(self.weights, self.biases): z np.dot(w, a) b zs.append(z) a self.sigmoid(z) acts.append(a) # 反向逐层计算梯度 delta (acts[-1] - y) # 交叉熵 sigmoid 输出层的误差简化形式 grads_w [] grads_b [] for i in reversed(range(len(self.weights))): grads_w.append(np.dot(delta, acts[i].T)) grads_b.append(delta) if i 0: delta np.dot(self.weights[i].T, delta) * (acts[i] * (1 - acts[i])) return list(reversed(grads_w)), list(reversed(grads_b))这里__init__里用np.random.randn(y, x) / np.sqrt(x)做权重初始化。除以sqrt(x)是Xavier/Glorot初始化的简化版目的是让前向传播的方差保持稳定避免在浅层网络里一上来就出现梯度消失。zeros初始化偏置保证第一轮输出稳定。前向传播中输入a依次经过线性变换w·a b和sigmoid激活。backprop里关键是输出层误差delta acts[-1] - y这是交叉熵损失对输出层加权输入的梯度简化结果。如果不是交叉熵而是二次代价这里就要多乘一个sigmoid(z)学习速率会明显变慢。中间层则通过delta W^T * delta * sigmoid(z)完成误差回传。代码里的feedforward没有做Softmax因为训练时交叉熵配合sigmoid输出已经能得到足够好的梯度。推理时如果想输出概率可以在main.py里把最后一层换成softmax或者直接用np.argmax取最大值的索引。2.3 学习率、batch和随机梯度下降的关系训练用的是随机梯度下降。学习率是这个里面最敏感的参数代码里hyperparameters_adjust.py就是为了一次跑多组参数组合。一般来说学习率取1.0、3.0、5.0这一档隐藏层100batch大小10到20epoch 20到40MNIST测试集上正确率能到97%附近。如果学习率太高比如10.0损失会出现反复震荡太低比如0.1训练速度很慢30个epoch可能还到不了90%。3. MNIST数据集的加载、预处理与缓存机制3.1 从mnist.npz加载数据项目里直接放了dataset/mnist.npz不用再去torchvision下载。这里要特别提一句如果你用torchvision.datasets.MNIST自动下载现在很多环境会遇到404或网络超时的问题而把数据预下载成npz以后训练脚本完全不依赖外网。dataset_load.py完成的就是这件事import numpy as np def load_mnist_npz(pathdataset/mnist.npz): data np.load(path) train_images data[x_train] train_labels data[y_train] test_images data[x_test] test_labels data[y_test] return train_images, train_labels, test_images, test_labelsnpz是numpy的压缩存档格式里面按key存取。MNIST原始发布格式是IDX文件很多人初次接触会被字节序搞懵项目直接给npz相当于省掉了解析IDX的步骤。3.2 归一化和one-hot标签mnist.npz里的像素值是0到255的uint8直接喂网络会导致梯度计算不稳定。一般做法是把数值缩放到0到1之间除以255.0同时把标签转成one-hot向量比如数字3变成[0,0,0,1,0,0,0,0,0,0]。下面的代码是dataset_load.py中的典型处理def normalize_and_one_hot(train_images, train_labels, test_images, test_labels): train_images train_images.reshape(-1, 784).astype(np.float64) / 255.0 test_images test_images.reshape(-1, 784).astype(np.float64) / 255.0 def to_one_hot(y, num_classes10): one_hot np.zeros((y.shape[0], num_classes)) one_hot[np.arange(y.shape[0]), y] 1.0 return one_hot train_labels to_one_hot(train_labels) test_labels to_one_hot(test_labels) return train_images, train_labels, test_images, test_labels这里reshape(-1, 784)把每张28×28的图拉直成784维向量astype(np.float64) / 255.0完成归一化。注意mnist.npz原始数据在预处理前像素顺序是按行展开的所以不需要额外转置。one_hot[np.arange(y.shape[0]), y] 1.0这一行利用了numpy的高级索引比循环快得多。3.3 缓存预处理结果避免每次训练重复计算项目里有个img_preprocess_and_cache.py它把归一化后的训练集和测试集打包成pickle文件下次训练直接加载pkl。这在大作业里可能看起来不是必需品但如果同学间反复调试参数每次启动都重新归一化几万张图其实很浪费时间。常见做法是import pickle def cache_mnist(images, labels, cache_pathdataset/cache.pkl): with open(cache_path, wb) as f: pickle.dump({images: images, labels: labels}, f) def load_cached_mnist(cache_pathdataset/cache.pkl): with open(cache_path, rb) as f: return pickle.load(f)缓存文件名、路径都在requirements.txt和readme里能对上。需要留意的是pickle是Python对象序列化格式只适合本机或同一版本Python环境复用如果跨版本匹配不上时重新生成缓存即可。3.4 带噪点的jpg图片如何走预处理除了mnist数据项目里还有test_imgs目录放了一批真实拍摄或手机截图的数字图片。它们不是标准MNIST格式需要自己写预处理管线。img_preprocess_and_cache.py里就做了这件事读图、转灰度、缩放、拉伸到28×28再按MNIST的像素分布处理。通常我用这样一段代码import cv2 import numpy as np def preprocess_image(img_path): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (28, 28), interpolationcv2.INTER_AREA) img 255.0 - img # 反色白底黑字转成黑底白字 img img / 255.0 return img.reshape(-1, 784).astype(np.float64)这里cv2.imread以灰度模式读入resize到28×28255.0 - img把白底黑字变成MNIST中的黑底白字。如果你的测试图是黑底白字就不需要反色否则识别率会明显下降。前提是环境里装好了opencv-python这通常写在requirements.txt里。4. 训练脚本与超参数调优让准确率跑到97%以上4.1 训练主流程train_example.pytrain_example.py把网络搭建、数据加载、训练和评估串了起来。它的核心是mini-batch随机梯度下降一段简化版训练循环如下def sgd(net, train_data, epochs, batch_size, eta, test_dataNone): n len(train_data) for epoch in range(epochs): np.random.shuffle(train_data) batches [train_data[k:kbatch_size] for k in range(0, n, batch_size)] for batch in batches: grad_w, grad_b accumulate_gradients(net, batch) # 更新参数 net.weights [w - eta * gw for w, gw in zip(net.weights, grad_w)] net.biases [b - eta * gb for b, gb in zip(net.biases, grad_b)] if test_data: acc evaluate(net, test_data) print(fepoch {epoch1}: accuracy {acc:.2%})参数含义epochs是遍历整个训练集的次数batch_size是每次梯度估算用的样本数eta是学习率。shuffle操作保证每个batch的分布有随机性避免模型学到样本顺序中的伪规律。accumulate_gradients会把每个样本的梯度求平均再统一更新参数。实际项目里train_example.py默认是epoch30、batch_size10、eta3.0隐藏层100。这个配置在不做任何数据增强的情况下测试集能稳定到达97%以上。4.2 hyperparameters_adjust.py里的网格搜索这个脚本的存在是整套源码里最有“大作业”价值的地方。它把训练封装成可循环调用的函数对隐藏层节点数、学习率、batch_size做遍历记录每组参数的正确率和损失最后输出一个对比表。核心思路是这样的results [] for hidden in [50, 100, 200]: for eta in [0.5, 1.0, 3.0]: acc train_and_evaluate(hiddenhidden, etaeta, batch_size10, epochs20) results.append((hidden, eta, acc)) print(fhidden{hidden}, eta{eta}, acc{acc:.2%})运行这个脚本需要的时间取决于机器。在纯CPU环境hidden100、epoch20、batch_size10大约一到两分钟一轮hidden200会明显变慢。如果只是验证代码能跑可以先epoch设成3跑一遍。下面是基于典型环境跑出来的一组参考数据隐藏层节点数学习率batch_size训练epoch测试集正确率501.0102095.8%1000.5102095.5%1003.0102097.1%2003.0102097.3%2005.0203096.9%注意这里并不是在暗示参数越大越好。隐藏层200比100只高了0.2个百分点训练时间却可能多出近一倍学习率3.0明显比0.5更容易跑到97%。大作业展示时用hidden100、eta3.0这一组是最稳妥的速度快效果也不差。4.3 如何判断过拟合并做早停MNIST数据集比较容易过拟合尤其当隐藏层节点数很多且训练超过30个epoch时。观察方法是在每个epoch末尾用验证集或测试集计算正确率。如果训练集损失持续下降但测试集正确率开始下降或徘徊就是过拟合信号。项目里没有专门给出早停脚本我一般会自己在训练循环里加一个if acc best_acc: patience 1的计数器。更简单的方式是固定epoch为30比较训练集和测试集正确率如果差距超过1.5%说明模型开始背样本了。5. 模型推理与自绘制图片识别验证技巧5.1 main.py中的推理流程main.py负责加载训练好的模型然后对test_imgs里的图片做预测。模型文件是models/3_layers.pkl里面保存了训练完成后的weights和biases。推理代码逻辑如下import pickle import numpy as np from network import Network from img_preprocess_and_cache import preprocess_image with open(models/3_layers.pkl, rb) as f: model_state pickle.load(f) net Network([784, 100, 10]) net.weights, net.biases model_state[weights], model_state[biases] img preprocess_image(test_imgs/5.jpg) output net.feedforward(img.reshape(-1, 1)) pred np.argmax(output) print(f预测结果: {pred}, 置信度: {float(output[pred]):.4f})这里feedforward接收的是形状(784,1)的列向量因为训练时代码就是这么设计的。np.argmax返回输出层中值最大的索引对应数字类别。5.2 让自绘制图片识别率更高的几个处理细节如果你在Word或画图软件里写数字导出png后直接交给这个流程很可能识别失败。原因有三类图片不是白底黑字、字符没有居中、长宽比被拉伸。常见处理是读图后先找字的边界做一次重心居中再resize到20×20的方形区域最后放进28×28画布中央。在img_preprocess_and_cache.py里项目自己实现过一个简化版但针对更复杂的噪声图我一般会补上中值滤波img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.medianBlur(img, 3) # 去掉椒盐噪声 _, img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU)medianBlur用3×3邻域中值替代原像素对扫描字迹的小黑点很有效OTSU阈值把图片自动变成纯黑白的二值图同时完成反色。随后再按前面提到的resize和归一化流程走。你在答辩前可以用test_imgs里的全部图片先跑一遍确认每张预测置信度都高于0.9再现场演示就不会翻车。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →