简介:这份资源是一套基于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', train=True, download=True, transform=transform) test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform) # 用 DataLoader 做批处理 train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)逻辑说明:transforms.Compose把多个预处理步骤串起来,先转 Tensor 再归一化。datasets.MNIST的root参数指定数据存放目录,download=True表示如果本地没有就自动下载。DataLoader的batch_size我设了 64,这是训练时的常见值,太小会导致训练慢,太大会吃内存。shuffle=True只在训练集上用,测试集不需要打乱,因为评估结果和顺序无关。跑完这段代码,你会看到 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(kernel='rbf', C=1.0, gamma='scale') clf.fit(X_train, y_train) # 预测并评估 y_pred = clf.predict(X_test) print(f"SVM 测试集准确率: {accuracy_score(y_test, y_pred):.4f}")参数说明:kernel='rbf'是径向基核函数,适合非线性可分的数据;C=1.0是惩罚系数,越大越容易过拟合,越小越容易欠拟合;gamma='scale'表示核系数自动按特征方差调整。这段代码在普通笔记本上大概跑一两分钟,准确率通常在 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(), lr=0.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(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")逻辑说明:Conv2d(1, 32, 5, 1)表示输入 1 个通道,输出 32 个通道,卷积核 5×5,步长 1。经过两次卷积和池化后,28×28 变成 4×4,通道数变成 64,所以展平后是 64×4×4=1024 维。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(dim=1, keepdim=True) prob = torch.softmax(output, dim=1).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(dim=1) all_preds.extend(preds.numpy()) all_labels.extend(target.numpy()) cm = confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues') plt.xlabel('Predicted') plt.ylabel('True') plt.show()参数说明:confusion_matrix的行是真实标签,列是预测标签,对角线是正确分类的数量。annot=True在格子里显示数字,fmt='d'表示整数格式。跑完你会看到大部分错误集中在 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_workers=4跑训练,直接卡死或报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(degrees=10, 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()这种关键步骤,然后花几个小时排查一个低级错误。希望帮到你。
本文还有配套的精品资源,点击获取