ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

手写数字识别实战:从MNIST到CNN模型部署的完整源码指南

手写数字识别实战:从MNIST到CNN模型部署的完整源码指南 简介这份资源是一套基于Python机器学习的手写数字识别系统设计源码面向具备一定Python基础、希望深入理解图像分类与深度学习落地流程的学习者与开发者。项目以MNIST手写数字数据集为训练与测试基础结合VGG19等卷积神经网络结构完整呈现从数据加载、模型训练到界面交互的识别系统实现路径。压缩包共44个文件约8.06MB包含8个Python源码文件、6个编译后文件、5个XML配置、11张PNG图片以及idx格式的MNIST数据文件源码覆盖网络定义、训练脚本、绘图板与主界面等模块配置与说明文档便于快速部署运行。目前已有514人学习下载。读者可借此掌握手写数字识别的完整工程结构理解深度学习模型训练与GUI交互的衔接方式并参考其中的网络定义与训练脚本进行二次开发或课程设计实践。1. 手写数字识别系统从 MNIST 到可部署源码的完整路径很多做 Python 机器学习入门的人第一个真正跑通的项目就是手写数字识别。它不像房价预测那样只有几列特征也不像文本分类那样需要处理分词和词向量而是直接给你一张 28×28 的灰度图让你从像素里把 0 到 9 认出来。这个任务看起来简单但它把机器学习的完整链路都串起来了数据加载、预处理、模型定义、训练、评估、保存、推理。你在这个项目里踩过的坑换到人脸识别、车牌识别、工业质检上几乎一模一样。这套源码要解决的核心问题是给定一张手写数字图片输出它属于哪个数字并且置信度是多少。适合谁适合刚学完 Python 基础语法、想找一个能写进简历的机器学习项目的人也适合已经会调 sklearn 但没亲手搭过神经网络、想搞清楚前向传播和反向传播到底在干什么的人。我见过太多人直接复制一段 MNIST 代码跑出 99% 准确率就结束了但一问“模型文件怎么保存”“怎么用自己的图片测试”“为什么训练集准确率 100% 测试集只有 92%”就答不上来。这篇笔记就是把这些空白补上。2. 环境搭建与 MNIST 数据加载把第一行代码跑通2.1 Python 环境与机器学习常用包的版本选择做这个项目不需要太复杂的配置但版本不匹配会让你在 import 阶段就翻车。我一般用 Python 3.9 到 3.11 之间的版本太新的版本有时候 PyTorch 或 TensorFlow 的轮子还没跟上。核心包就四个numpy 负责数组运算matplotlib 负责可视化scikit-learn 负责数据拆分和评估指标深度学习框架选 PyTorch 或 TensorFlow 都行。如果你只是想做传统机器学习模型比如 SVM 或随机森林那 scikit-learn 就够了但标题里写了机器学习我建议至少跑一遍神经网络不然源码的含金量会打折扣。安装命令如下建议在虚拟环境里操作避免污染全局环境python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install numpy matplotlib scikit-learn torch torchvision这里解释一下参数python -m venv venv创建名为 venv 的虚拟环境source venv/bin/activate在 Linux 或 macOS 下激活Windows 用反斜杠路径。PyTorch 的安装命令在不同 CUDA 版本下不一样如果你没有 NVIDIA 显卡直接用 CPU 版本就行MNIST 这个规模 CPU 训练也就几分钟。装完之后用python -c import torch; print(torch.__version__)验证一下能打印出版本号就说明环境通了。2.2 用 torchvision 加载 MNIST 并做归一化MNIST 数据集在 torchvision 里已经封装好了不需要你去手动下载解压。但有一个细节很多人忽略归一化参数。MNIST 全局像素均值是 0.1307标准差是 0.3081这两个数字是官方统计出来的直接用就行。归一化之后像素值会从 0 到 255 变成接近标准正态分布这样梯度下降收敛更快。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理转张量 归一化 transform transforms.Compose([ transforms.ToTensor(), # 把 PIL 图片或 numpy 数组转成 [0,1] 的 Tensor transforms.Normalize((0.1307,), (0.3081,)) # 减均值除标准差 ]) # 加载训练集和测试集 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 用 DataLoader 做批处理 train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse)逻辑说明transforms.Compose把多个预处理步骤串起来先转 Tensor 再归一化。datasets.MNIST的root参数指定数据存放目录downloadTrue表示如果本地没有就自动下载。DataLoader的batch_size我设了 64这是训练时的常见值太小会导致训练慢太大会吃内存。shuffleTrue只在训练集上用测试集不需要打乱因为评估结果和顺序无关。跑完这段代码你会看到 data 目录下多了 MNIST 的二进制文件训练集 60000 张测试集 10000 张每张都是 28×28 的灰度图。3. 从传统机器学习到 CNN模型选型与训练脚本3.1 为什么先用 SVM 跑一个基线在直接上神经网络之前我强烈建议先用 SVM 跑一个基线。原因很简单如果 SVM 都能做到 97% 以上你后面用 CNN 做到 99% 才有对比才知道深度模型到底带来了多少提升。而且 SVM 训练快代码短适合验证数据加载和预处理有没有问题。把 28×28 的图片展平成 784 维向量直接丢给 SVM 就行。from sklearn import svm from sklearn.metrics import accuracy_score import numpy as np # 把 DataLoader 里的数据转成 numpy 数组 def extract_data(loader): X, y [], [] for images, labels in loader: X.append(images.view(images.size(0), -1).numpy()) # 展平 y.append(labels.numpy()) return np.concatenate(X), np.concatenate(y) X_train, y_train extract_data(train_loader) X_test, y_test extract_data(test_loader) # 训练 SVM用 RBF 核 clf svm.SVC(kernelrbf, C1.0, gammascale) clf.fit(X_train, y_train) # 预测并评估 y_pred clf.predict(X_test) print(fSVM 测试集准确率: {accuracy_score(y_test, y_pred):.4f})参数说明kernelrbf是径向基核函数适合非线性可分的数据C1.0是惩罚系数越大越容易过拟合越小越容易欠拟合gammascale表示核系数自动按特征方差调整。这段代码在普通笔记本上大概跑一两分钟准确率通常在 97% 到 98% 之间。如果低于 96%检查一下归一化是不是漏了或者数据有没有正确展平。3.2 用 PyTorch 搭一个 CNN 并训练CNN 是手写数字识别的标准解法因为它能利用图像的局部相关性。一个经典的 LeNet 变体就够了两个卷积层、两个池化层、三个全连接层。卷积核用 5×5池化用 2×2 最大池化激活函数用 ReLU。训练时用交叉熵损失和 Adam 优化器学习率设 0.001。import torch.nn as nn import torch.nn.functional as F import torch.optim as optim class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 nn.Conv2d(1, 32, 5, 1) # 输入1通道输出32通道5x5卷积核 self.conv2 nn.Conv2d(32, 64, 5, 1) # 输入32输出64 self.fc1 nn.Linear(64 * 4 * 4, 128) # 展平后接全连接 self.fc2 nn.Linear(128, 10) # 输出10个类别 def forward(self, x): x F.relu(self.conv1(x)) # 第一层卷积 ReLU x F.max_pool2d(x, 2) # 2x2 最大池化 x F.relu(self.conv2(x)) # 第二层卷积 ReLU x F.max_pool2d(x, 2) # 再池化 x x.view(-1, 64 * 4 * 4) # 展平 x F.relu(self.fc1(x)) # 全连接 ReLU x self.fc2(x) # 输出层不加 softmax因为 CrossEntropyLoss 自带 return x model Net() optimizer optim.Adam(model.parameters(), lr0.001) criterion nn.CrossEntropyLoss() # 训练循环 for epoch in range(5): model.train() for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() print(fEpoch {epoch1}, Loss: {loss.item():.4f})逻辑说明Conv2d(1, 32, 5, 1)表示输入 1 个通道输出 32 个通道卷积核 5×5步长 1。经过两次卷积和池化后28×28 变成 4×4通道数变成 64所以展平后是 64×4×41024 维。CrossEntropyLoss内部会做 softmax所以网络最后一层不要加 softmax。训练 5 个 epoch 后测试集准确率通常能到 99% 左右。如果你用 GPU记得把模型和数据都.to(device)速度会快很多。3.3 模型保存与加载别让训练成果白费训练完不保存模型下次用还得重新跑这是新手最容易犯的错。PyTorch 保存模型有两种方式保存整个模型结构或者只保存参数字典。我推荐后者因为更灵活加载时可以换代码结构。# 保存参数字典 torch.save(model.state_dict(), mnist_cnn.pth) # 加载时先实例化模型再加载参数 model Net() model.load_state_dict(torch.load(mnist_cnn.pth)) model.eval() # 切换到评估模式关闭 dropout 和 batchnorm 更新参数说明state_dict()返回一个字典键是层名值是参数张量。load_state_dict要求模型结构和保存时一致否则会报 key 不匹配。model.eval()很重要虽然这个简单 CNN 没有 dropout但养成习惯推理前一定调用。保存后的文件大概几 MB方便传到服务器或嵌入到其他 Python 脚本里。4. 推理与可视化用自己的图片测试模型4.1 单张图片的预处理与预测训练时用的是 MNIST 格式的 28×28 灰度图但你自己用手机拍的照片可能是 RGB、尺寸也不对。所以推理前必须做三件事转灰度、缩放到 28×28、归一化。注意 MNIST 是黑底白字如果你拍的是白底黑字还要反色。from PIL import Image import torchvision.transforms as T def predict_image(image_path, model): # 读取图片并转灰度 img Image.open(image_path).convert(L) # 定义和训练时一致的预处理 transform T.Compose([ T.Resize((28, 28)), T.ToTensor(), T.Normalize((0.1307,), (0.3081,)) ]) img_tensor transform(img).unsqueeze(0) # 增加 batch 维度 model.eval() with torch.no_grad(): output model(img_tensor) pred output.argmax(dim1, keepdimTrue) prob torch.softmax(output, dim1).max().item() return pred.item(), prob逻辑说明convert(L)把图片转成 8 位灰度图。Resize((28,28))强制缩放到 MNIST 尺寸。unsqueeze(0)在第 0 维增加一个维度因为模型期望输入是[batch, channel, height, width]。torch.no_grad()关闭梯度计算节省内存。argmax取最大概率的类别softmax把输出转成概率值。如果预测结果总是错先检查图片是不是反色了MNIST 是黑底白字你拍的白底黑字需要先做ImageOps.invert。4.2 用混淆矩阵看模型到底错在哪准确率 99% 听起来很高但 10000 张测试集里还是有 100 张错。搞清楚错在哪些数字上比单纯看准确率有用得多。混淆矩阵能告诉你模型是不是把 4 认成 9或者把 7 认成 1。from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt # 收集所有预测结果 all_preds, all_labels [], [] model.eval() with torch.no_grad(): for data, target in test_loader: output model(data) preds output.argmax(dim1) all_preds.extend(preds.numpy()) all_labels.extend(target.numpy()) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.show()参数说明confusion_matrix的行是真实标签列是预测标签对角线是正确分类的数量。annotTrue在格子里显示数字fmtd表示整数格式。跑完你会看到大部分错误集中在 4 和 9、3 和 5、7 和 1 这几组上因为这些数字在低分辨率下确实容易混淆。如果某一类错误特别多可以考虑对该类做数据增强或者调整网络结构。5. 避坑与排查手写数字识别源码常见的五个翻车点5.1 准确率虚高训练集和测试集混用现象训练时准确率 99.9%测试时只有 90% 出头。原因把测试集也拿去训练了或者用测试集调参调了很多轮。解决训练集只用来更新参数测试集只在最后评估一次。如果要做验证从训练集里再切 10% 出来做验证集不要动测试集。5.2 归一化参数不一致现象训练时用了 Normalize推理时忘了加导致预测全错。原因预处理管道在训练和推理两个地方分别写容易漏。解决把 transform 定义成全局变量或函数训练和推理都调用同一个。归一化的均值和标准差必须完全一致差一点都会影响结果。5.3 图片反色导致预测失败现象用自己拍的图片测试模型总是预测成 0 或 8。原因MNIST 是黑底白字手机拍的是白底黑字像素分布完全反了。解决推理前用PIL.ImageOps.invert反色或者训练时就把数据增强加上随机反色让模型见过两种风格。5.4 DataLoader 的 num_workers 在 Windows 上报错现象在 Windows 上设num_workers4跑训练直接卡死或报BrokenPipeError。原因Windows 的多进程和 Linux 不一样DataLoader 的多 worker 需要放在if __name__ __main__保护块里。解决Windows 下把num_workers设成 0或者把训练代码包进 main 函数。Linux 下可以放心用 4 或 8。5.5 模型保存后加载报 key 不匹配现象load_state_dict时报Missing key(s)或Unexpected key(s)。原因保存时用了DataParallel或改了层名加载时模型结构对不上。解决保存时用model.module.state_dict()如果用了 DataParallel加载时打印model.state_dict().keys()和保存的 keys 对比确保层名一致。最稳妥的办法是保存整个模型torch.save(model, full_model.pth)但这样加载时需要原始类定义。6. 进阶技巧把准确率从 99% 推到 99.5% 以上如果你已经跑通了上面的流程想再往上提一点有几个方向可以试。第一个是数据增强在训练时随机旋转 ±10 度、随机平移 2 个像素、随机缩放 0.9 到 1.1 倍。MNIST 的数字本来就有手写风格的差异增强能让模型更鲁棒。用 torchvision 的transforms.RandomAffine就能做注意只对训练集做测试集不要做。train_transform transforms.Compose([ transforms.RandomAffine(degrees10, translate(0.1, 0.1), scale(0.9, 1.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])第二个是集成学习训练 3 到 5 个结构略有不同的 CNN推理时把它们的 softmax 输出平均取最大概率的类别。这个方法在 MNIST 上通常能提升 0.2 到 0.3 个百分点但代价是推理时间翻倍。第三个是学习率调度用torch.optim.lr_scheduler.StepLR每 3 个 epoch 把学习率乘以 0.1让模型在后期微调得更细。还有一个容易被忽略的点把模型导出成 ONNX 格式这样可以用 ONNX Runtime 推理速度比 PyTorch 原生快不少而且方便部署到 C 或 Java 环境。导出命令很简单dummy_input torch.randn(1, 1, 28, 28) torch.onnx.export(model, dummy_input, mnist_cnn.onnx, input_names[input], output_names[output])参数说明dummy_input是一个示例输入形状必须和真实输入一致。input_names和output_names是给 ONNX 图里的输入输出节点命名方便后续调用。导出后用onnxruntime加载推理一张图大概 1 到 2 毫秒。我自己在这个项目上最大的教训是不要一上来就追求 99.9% 的准确率先把数据管道、训练循环、模型保存、推理接口这四块跑通再回头调参。我见过太多人卡在环境配置上三天结果连 MNIST 都没加载出来。另外源码里的每一行最好都自己敲一遍不要直接复制粘贴因为复制的时候很容易漏掉model.eval()或optimizer.zero_grad()这种关键步骤然后花几个小时排查一个低级错误。希望帮到你。本文还有配套的精品资源点击获取
返回列表