恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python手写数字识别实战:从MNIST到99%准确率的毕业设计源码
首页
资讯中心
/
Python手写数字识别实战:从MNIST到99%准确率的毕业设计源码
Python手写数字识别实战:从MNIST到99%准确率的毕业设计源码
发布时间:2026/9/23 13:26:28
简介这份资源是面向计算机相关专业学生与机器学习入门者的手写数字识别系统源码包可作为毕业设计、课程设计或期末大作业的参考项目。项目基于Python实现涵盖数据预处理、CNN与BP模型构建、训练测试及可视化界面等完整流程帮助读者理解从图像处理到神经网络分类的实践路径。压缩包共28个文件约14.18MB包含9个py源码文件、10个npz训练参数、4张png结果图、2组idx格式数据集及1份md说明文档源码与教程配套下载后可直接运行。目前已有45人学习。读者可借此掌握卷积神经网络在手写数字识别中的具体实现观察不同训练轮次的准确率变化并参考README完成环境配置与模型调参适合需要项目实战练习或希望加深深度学习理解的学习者。1. 从一份能跑起来的 Python 手写数字识别源码说起如果你正在做计算机方向的毕业设计或者刚学完 Python 基础语法想找个能写进简历的小项目手写数字识别几乎是绕不开的第一个实战选题。它背后是经典的 MNIST 数据集输入是一张 28×28 的灰度图输出是 0 到 9 这十个类别中的一个。听起来简单但真正动手时你会发现环境装不上、数据下载卡住、模型训练完准确率上不去、GUI 界面和推理逻辑对不上——这些问题在教程里往往一笔带过却能让一个毕业设计卡上整整一周。这篇笔记不讲空泛的概念而是围绕一份可复现的 Python 手写数字识别系统源码把环境配置、数据加载、模型搭建、训练调参、界面集成和踩坑排查完整走一遍。适合两类人一是第一次做毕业设计、需要一份能讲清楚原理又能演示的系统二是已经写过 demo但想搞清楚参数怎么调、坑在哪、怎么把准确率从 90% 推到 99% 的从业者。下面所有代码都可以直接抄作业参数我会逐个说明为什么这么设。2. 环境准备与 MNIST 数据加载把第一步走稳2.1 Python 环境与依赖选型做这个项目Python 版本建议 3.8 到 3.10太新的版本在某些深度学习库上反而容易遇到兼容问题。安装 Python 时记得勾选“Add Python to PATH”否则后面在命令行里敲python会提示找不到命令这是新手翻车最多的地方。装完 Python 后用 pip 安装核心依赖pip install torch torchvision numpy matplotlib pillow如果你用的是 CPU 版本上面这条命令就够了如果有 NVIDIA 显卡并且想用 GPU 加速需要去 PyTorch 官网根据 CUDA 版本选择对应的安装命令。这里不展开 GPU 配置因为对 MNIST 这个规模的数据集来说CPU 训练一轮也就十几秒毕业设计演示完全够用。依赖说明torch和torchvision负责模型和数据加载numpy做数值运算matplotlib用来可视化训练过程和预测结果pillow用于处理用户手写的图片输入。这几个库的版本不需要刻意锁定pip 会自动解决依赖关系。提示如果你在安装 torch 时遇到网络超时可以换用国内镜像源例如pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple。这是常见做法能省掉很多等待时间。2.2 用 DataLoader 加载 MNIST 并做归一化MNIST 数据集包含 60000 张训练图和 10000 张测试图每张是 28×28 的灰度图。torchvision 内置了下载和加载接口但直接拿到的像素值是 0 到 255 的整数需要转成张量并归一化到 0 到 1 之间这样模型训练更稳定。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理转张量 归一化 transform transforms.Compose([ transforms.ToTensor(), # 像素值从 0-255 缩放到 0-1 transforms.Normalize((0.1307,), (0.3081,)) # MNIST 全局均值和标准差 ]) # 下载并加载训练集 train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) # 下载并加载测试集 test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse)逻辑说明transforms.ToTensor()把 PIL 图片转成 PyTorch 张量同时把像素值除以 255。Normalize里的两个数字是 MNIST 训练集的全局均值和标准差减均值除标准差后数据分布更接近标准正态梯度下降收敛更快。batch_size64是训练时的批大小显存或内存不够就调小到 32测试时用 1000 是因为不需要反向传播可以一次多放一些。shuffleTrue只在训练集开启保证每个 epoch 的数据顺序不同避免模型学到顺序相关的虚假特征。注意第一次运行会自动下载 MNIST 到./data目录大约 10MB 左右。如果下载卡住可以手动下载四个压缩包放到./data/MNIST/raw/下文件名保持train-images-idx3-ubyte等原始名称即可。3. 卷积网络搭建与训练把准确率推到 99%3.1 为什么选 CNN 而不是全连接网络手写数字识别的本质是图像分类。全连接网络把 28×28 的图展平成 784 维向量会丢失像素之间的空间关系比如一个“1”的竖线在不同位置出现时全连接网络需要分别学习而卷积网络通过卷积核在整张图上滑动能捕捉到这种平移不变性。所以同样的参数量下CNN 的准确率明显更高通常能到 99% 以上而全连接网络大概在 97% 到 98% 徘徊。我一般用两层卷积加两层全连接的结构参数量不到 50 万训练快效果也稳。具体结构第一层卷积 32 个 3×3 核第二层卷积 64 个 3×3 核每层后面接 ReLU 和 2×2 最大池化然后展平接一个 128 维的全连接层最后输出 10 类。import torch.nn as nn import torch.nn.functional as F class DigitCNN(nn.Module): def __init__(self): super(DigitCNN, self).__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool nn.MaxPool2d(2, 2) self.fc1 nn.Linear(64 * 7 * 7, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x self.pool(F.relu(self.conv1(x))) # 28x28 - 14x14 x self.pool(F.relu(self.conv2(x))) # 14x14 - 7x7 x x.view(-1, 64 * 7 * 7) # 展平 x F.relu(self.fc1(x)) x self.fc2(x) return x参数说明padding1保证卷积后特征图尺寸不变这样两次池化后刚好从 28 降到 7。64 * 7 * 7是第二层卷积输出展平后的维度写错这个数字是新手最常见的报错来源。fc2输出 10 是因为有 10 个数字类别不需要再接 softmax因为训练时用的交叉熵损失函数内部已经包含了 softmax。3.2 训练循环与关键超参数训练过程就是反复喂数据、算损失、反向传播、更新参数。下面是一个完整的训练函数包含训练和测试两个阶段。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model DigitCNN().to(device) optimizer optim.Adam(model.parameters(), lr0.001) criterion nn.CrossEntropyLoss() def train(epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fEpoch {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.4f}) def test(): model.eval() correct 0 with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) pred output.argmax(dim1) correct pred.eq(target).sum().item() acc 100. * correct / len(test_loader.dataset) print(fTest Accuracy: {acc:.2f}%) return acc for epoch in range(1, 6): train(epoch) test()逻辑说明optimizer.zero_grad()清空上一轮的梯度否则梯度会累加。loss.backward()计算梯度optimizer.step()更新参数。测试时用model.eval()切换到评估模式关闭 dropout 和 batch norm 的训练行为同时用torch.no_grad()关闭梯度计算节省内存。学习率lr0.001是 Adam 的常用起点如果损失下降太慢可以调到 0.002如果震荡就降到 0.0005。训练 5 个 epoch 通常就能到 99% 左右再多容易过拟合。提示如果测试准确率卡在 98% 上不去先检查归一化参数是否写对再检查fc1的输入维度是否和卷积输出匹配。这两个地方出错不会报错但会让模型学不到东西。4. 推理接口与 GUI 集成让系统能演示4.1 单张图片的预处理与预测毕业设计答辩时老师通常不会只看测试集准确率而是想看到你手写一个数字系统能实时识别出来。所以需要写一个推理函数接收一张任意尺寸的图片预处理成模型需要的 28×28 灰度图再输出预测结果。from PIL import Image, ImageOps import numpy as np def predict_image(image_path, model, device): # 打开图片转灰度 img Image.open(image_path).convert(L) # 反色MNIST 是黑底白字手写通常是白底黑字 img ImageOps.invert(img) # 缩放到 28x28 img img.resize((28, 28), Image.Resampling.LANCZOS) # 转张量并归一化 img_tensor transforms.ToTensor()(img) img_tensor transforms.Normalize((0.1307,), (0.3081,))(img_tensor) img_tensor img_tensor.unsqueeze(0).to(device) # 增加 batch 维度 model.eval() with torch.no_grad(): output model(img_tensor) pred output.argmax(dim1).item() prob torch.softmax(output, dim1).max().item() return pred, prob参数说明convert(L)转灰度ImageOps.invert做反色是因为 MNIST 训练图是黑底白字而手机拍照或画图板画出来通常是白底黑字不反色的话模型会把背景当成笔画。resize用 LANCZOS 插值比默认的最近邻更平滑识别率更高。unsqueeze(0)是给单张图增加一个 batch 维度因为模型 forward 要求输入是 4 维张量。4.2 用 Tkinter 搭一个最小可用的画板界面Python 自带的 Tkinter 就够做一个画板加识别按钮的界面不需要额外装 Qt 或 Web 框架。核心思路是用户在画布上用鼠标画程序记录轨迹并生成一张图片点识别按钮时调用上面的predict_image。import tkinter as tk from PIL import Image, ImageDraw class DrawApp: def __init__(self, root, model, device): self.root root self.model model self.device device self.canvas tk.Canvas(root, width280, height280, bgwhite) self.canvas.pack() self.canvas.bind(B1-Motion, self.draw) self.image Image.new(L, (280, 280), 255) self.draw_obj ImageDraw.Draw(self.image) tk.Button(root, text识别, commandself.recognize).pack() def draw(self, event): x, y event.x, event.y r 8 self.canvas.create_oval(x-r, y-r, xr, yr, fillblack) self.draw_obj.ellipse([x-r, y-r, xr, yr], fill0) def recognize(self): self.image.save(tmp_digit.png) pred, prob predict_image(tmp_digit.png, self.model, self.device) print(f预测结果: {pred}, 置信度: {prob:.4f})逻辑说明画布大小设成 280×280是 28 的 10 倍方便用户画识别时再缩放到 28×28。draw方法里同时画在 Canvas 和 PIL Image 上Canvas 负责显示PIL Image 负责保存成文件给模型用。笔刷半径 8 像素缩放后大约相当于 0.8 个像素线条粗细接近 MNIST 的笔画。识别按钮触发保存和预测结果打印到控制台也可以改成在界面上弹窗显示。注意Tkinter 的坐标原点和 PIL 一致都是左上角不需要翻转。但如果你用其他 GUI 框架比如 PyQt坐标系可能不同需要确认后再对接。5. 避坑与排查那些教程不会告诉你的翻车现场5.1 准确率虚高但实际识别全错现象测试集准确率显示 99%但用画板写数字识别出来全是错的。原因训练时用了Normalize推理时忘了做同样的归一化或者反色没做。解决把推理预处理和训练预处理写成同一个函数确保ToTensor、Normalize、反色三步完全一致。我一般会在推理函数里打印一下输入张量的均值和方差和训练集对比差太多就说明预处理有问题。5.2 模型保存后加载报维度不匹配现象训练完用torch.save(model.state_dict(), model.pth)保存换台机器加载时提示size mismatch。原因加载时用的模型结构定义和保存时不一致比如改了卷积核数量或全连接层维度。解决把模型类定义单独放在一个model.py文件里训练和推理都从这个文件导入不要在两处各写一遍。保存时只存state_dict加载时先实例化同一个类再load_state_dict。5.3 DataLoader 的 num_workers 在 Windows 上卡死现象在 Windows 上设置num_workers4后程序卡住不动也不报错。原因Windows 下多进程的实现方式和 Linux 不同DataLoader 的子进程会重新导入主模块如果主模块里有创建 GUI 或加载模型的代码就会递归卡死。解决把训练代码放在if __name__ __main__:下面或者直接把num_workers设为 0。MNIST 数据量小num_workers0完全够用。5.4 学习率设太大导致损失变成 nan现象训练几个 batch 后损失突然变成nan准确率掉到 10%。原因学习率太大梯度更新步子迈太大参数飞了。解决把学习率从 0.01 降到 0.001 或 0.0005或者在优化器里加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5)。另外检查输入数据有没有异常值归一化后应该在 -1 到 1 附近如果出现几百的数值说明归一化没生效。5.5 画板线条太细导致识别率低现象用户在画板上轻轻画一笔识别不出来但用力画粗一点就能识别。原因画板上的线条缩放到 28×28 后只剩不到 1 个像素宽信息丢失严重。解决把笔刷半径从 8 调到 12 到 15或者在缩放前先做一次高斯模糊再二值化让线条更接近 MNIST 的笔画粗细。MNIST 里的数字笔画大概占 2 到 3 个像素宽缩放后保持这个比例最稳。6. 把模型推到 99.5% 的三个进阶技巧第一个技巧是数据增强。MNIST 训练集只有 60000 张而且数字基本都在图像中央。可以在训练时随机做小角度旋转±10 度、平移±2 像素和缩放0.9 到 1.1 倍让模型见到更多样的样本。用 torchvision 的transforms.RandomAffine就能实现加在ToTensor之前。注意增强只加在训练集测试集保持原样否则评估结果不可比。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,)) ])第二个技巧是学习率调度。固定学习率在后期容易在最优解附近震荡用StepLR每 3 个 epoch 把学习率乘以 0.5能让模型在后期更精细地收敛。实测从 99.1% 能提到 99.4% 左右。scheduler optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.5) # 在每个 epoch 的 train 之后调用 scheduler.step()第三个技巧是模型集成。训练 3 个结构相同但初始化不同的模型推理时把三个模型的输出概率平均取最大概率对应的类别。这个方法在 MNIST 上能把准确率推到 99.6% 以上代价是推理时间变成三倍。毕业设计答辩时如果老师问“还能不能更高”这就是一个很好的回答方向。验证方法很简单每次改动后跑一遍测试集记录准确率同时用画板手写 20 个数字做实际测试。测试集准确率高但画板识别差说明预处理或界面有问题两者都高才说明系统真正可用。我自己的习惯是每改一次预处理或模型结构就重新跑一遍完整的训练和测试不跳过任何一步。这个项目看起来简单但把每个环节都做扎实答辩时被问到任何一处都能讲清楚为什么这么设比堆一堆花哨功能更有说服力。希望帮到你。本文还有配套的精品资源点击获取