尧图精选

手写数字识别系统实战:从KNN基线到CNN模型部署

🕒 发布时间:2026/10/1 19:47:18 📁 来源:尧图网络
简介基于Python与神经网络的手写数字识别系统是面向计算机、电子信息等专业毕业设计或课程设计场景的高分项目资源。项目利用MNIST数据集训练深度神经网络模型实现对0-9手写数字的自动识别与预测完整覆盖数据预处理、模型搭建、训练评估与推理演示等关键环节适合需要从零掌握神经网络图像分类原理、快速搭建可演示识别系统的开发者参考。压缩包共包含18个文件整体大小约11.11MB。除model.py、demo.py、登录注册代码实现.py等Python源码外还提供RAR格式压缩的MNIST训练与测试数据集、需求分析规格说明书与登录模块测试用例docx文档、SQL登录注册界面脚本以及requirements.txt、README等环境说明文件涵盖项目开发所需的代码、数据、文档与配置方便直接复现和二次开发。目前已有96人学习下载。依托这套资料读者可以对照源码理解神经网络在数字识别中的实际应用也能借助需求分析和测试文档完善自己的毕设材料还可通过自带登录模块扩展为完整系统用于毕设答辩或课程项目展示。1. 数字识别系统毕设选题里性价比最高的那个方向答辩前一周导师问了一句“你的系统能不能识别我随手写出来的数字”这个问题其实是整个基于Python的数字识别系统最核心的验收标准——模型在公开数据集上的准确率再漂亮也得接得住真实输入还得能做成一个能演示、能交互的成品。数字识别本质上是图像分类任务输入一张 28×28 的灰度图输出 09 的类别标签常见的技术路线是 PyTorch 或 scikit-learn 训练 MNIST 数据集再配上 GUI 画板或 OpenCV 预处理。它适合做毕设的理由很直接数据公开、模型规模适中、效果能量化而且天然留好了“交互界面”这个加分位。新手能顺着 MNIST 跑通全流程熟手可以在模型结构和部署形态上继续做深后文按我自己的实操顺序展开。2. 环境与数据集准备把 Python、PyTorch 和 MNIST 一次装到位这一章解决的是“代码没写一行先被环境卡死”的常见开局。很多同学第一次跑数字识别项目不是死在模型上而是死在 Python 解释器选错、torch 装不上、数据集下载失败这些环节。把这些前置工作理顺后面所有代码才能跑得动。2.1 Python 环境conda 虚拟环境与 VSCode 解释器选择如果是从零开始先装 Python 本身。Windows 下从 python 官网下载安装包时务必勾选“Add Python to PATH”否则后续命令行里敲python会提示找不到命令。装完在终端里输入python --version验证版本3.9 或 3.10 都可以不建议直接用 3.13 这类过新版本部分库的预编译包可能还没跟上。我一般会用 conda 建一个独立环境避免把系统 Python 搞乱conda create -n digit_rec python3.10 -y conda activate digit_rec pip install torch torchvision scikit-learn matplotlib opencv-python参数说明-n digit_rec指定环境名python3.10指定环境内的解释器版本-y跳过确认提示。pip install后面的包按用途分torch和torchvision负责模型训练与数据集加载scikit-learn提供 KNN 这类传统分类器matplotlib用来画训练曲线和数据预览opencv-python给后续真实手写数字预处理用。CPU 版 PyTorch 对 MNIST 这种 28×28 小图完全够用没必要为一万张测试图去折腾 GPU 环境。提示如果你用的是 VSCode装完后按CtrlShiftP输入 “Python: Select Interpreter”选digit_rec环境。这一步漏了后面import torch会报 ModuleNotFoundError这是环境问题里最常见的翻车点不是代码问题。2.2 获取 MNIST 数据torchvision 一行代码加载数字识别最常用的公开数据集是 MNIST包含 60000 张训练图和 10000 张测试图。torchvision 把它封装成了一行调用不需要自己写下载脚本from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_set datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform)这里Compose把两个预处理操作串起来ToTensor()把 PIL 图像转成张量并把像素值从 0255 缩放到 01Normalize用 MNIST 全量数据的均值 0.1307 和标准差 0.3081 做标准化。标准化不是可选项后面 CNN 训练时梯度更新会更平稳。root./data表示数据集存到当前目录下的 data 文件夹trainTrue/False区分训练集和测试集第一次运行downloadTrue会自动下载下载完再跑可以把download改为False避免每次启动都检查网络。下载完成后./data/MNIST/raw下会有四个.gz文件./data/MNIST/processed下是 torchvision 预处理好的缓存。验证一下形状print(train_set.data.shape) # torch.Size([60000, 28, 28]) print(test_set.data.shape) # torch.Size([10000, 28, 28]) print(train_set.targets.shape) # torch.Size([60000])2.3 数据预览训练前先看一遍图很多同学拿到数据集直接开训最后自己手写数字识别不准回头才发现训练集里根本没有“黑字白底”或者“倾斜笔画”的样本。训练前花一分钟把数据画出来能省掉后面大量无效调试import matplotlib.pyplot as plt fig, axes plt.subplots(1, 10, figsize(12, 3)) for i in range(10): idx (train_set.targets i).nonzero()[0][0] axes[i].imshow(train_set.data[idx], cmapgray) axes[i].set_title(flabel:{i}) axes[i].axis(off) plt.show()这段代码对每个数字类别取第一张样本画出来确认图是白字黑底、数字基本居中。MNIST 本身做过尺寸归一化但真实场景里自己手写的数字大小、位置、笔画粗细都不同这就是为什么后面必须加预处理步骤。数据这一关过了环境问题基本就清零了。3. 先用 KNN 跑通基线拿到第一个可答辩的准确率模型选型上我不建议一上来就写 CNN。先跑一个传统机器学习基线是数字识别项目里最容易被低估但收益极高的一步它给你一个参照系也能在答辩时讲清楚“为什么需要深度学习”。3.1 为什么要先做 KNN传统分类器的价值K 近邻KNN的思路很直白把 28×28 的图像拉平成 784 维向量测试样本和训练样本算欧氏距离取最近的 K 个邻居投票决定类别。它没有训练过程不需要调学习率也不会遇到梯度消失。MNIST 上 KNN 在完整训练集上能达到 96%97% 的准确率这个数字意味着一个刚接触机器学习的人用不到 50 行代码就能拿到一个能用的识别系统。这个基线的意义在于对照如果后面 CNN 准确率只有 95%那说明模型有问题而不是数据有问题如果 CNN 到了 99%你可以用 KNN 的 96.5% 做衬托解释卷积为什么更强。答辩时这类对比非常加分它证明你不是只会调包。3.2 用 scikit-learn 跑通 KNN 最小实现import numpy as np from sklearn.neighbors import KNeighborsClassifier train_images train_set.data.numpy().reshape(-1, 28 * 28) / 255.0 test_images test_set.data.numpy().reshape(-1, 28 * 28) / 255.0 train_labels train_set.targets.numpy() knn KNeighborsClassifier(n_neighbors3, weightsdistance, n_jobs-1) knn.fit(train_images[:20000], train_labels[:20000]) pred knn.predict(test_images[:2000]) acc (pred test_set.targets.numpy()[:2000]).mean() print(fKNN accuracy on 2000 test samples: {acc:.4f})reshape(-1, 28 * 28)把每张二维图展平成 784 维向量-1表示自动推断样本数除以 255 把像素值压到 01避免距离计算时像素值差异被放大。n_neighbors3是投票邻居数weightsdistance让距离近的样本投票权重更高n_jobs-1表示用满 CPU 多核加速。我故意只取 20000 张训练图和 2000 张测试图是因为 KNN 的预测阶段要实时计算距离样本量太大时内存和时间开销都容易失控这点在第 5 章会展开讲。先在小规模上验证代码通顺再根据机器配置逐步加样本。3.3 调参方向K 值与距离权重的选择K 值直接决定模型的偏置和方差。K1 时决策边界最细碎容易过拟合K 太大时会把相距很远的样本也拉进来投票边界过于平滑欠拟合。weights选uniform还是distance也会影响结果——用distance时离测试样本更近的训练样本话语权更大在有噪声的数据集上通常更稳。可以用网格搜索快速找一组好参数from sklearn.model_selection import GridSearchCV param_grid { n_neighbors: [1, 3, 5, 7], weights: [uniform, distance] } grid GridSearchCV(KNeighborsClassifier(), param_grid, cv3) grid.fit(train_images[:10000], train_labels[:10000]) print(grid.best_params_)在 MNIST 这类数据上n_neighbors3搭配weightsdistance通常是个稳妥的起点。跑完这步你已经有一个能工作的数字识别模型了。但别急着优化 KNN它的天花板摆在那里接下来的重头戏是 CNN这才是数字识别系统真正拉开分数差距的地方。4. 用 PyTorch 写 CNN 模型从 LeNet-5 出发训练与验证全流程KNN 只能停在 97% 附近因为全连接的距离度量很难捕捉笔画局部的结构特征。CNN 通过卷积核在图像上滑动把边缘、端点、弧线这些局部模式逐层组合成高阶特征这也是图像分类任务的主流做法。4.1 网络结构选型为什么 LeNet-5 的后续版本依然能打数字识别任务输入只有 28×28 单通道灰度图类别只有 10 个不需要 ResNet 这种百层模型。经典的 LeNet-5 结构经过小幅改动后在这个任务上就能跑到 99% 左右训练时间在 CPU 上也就几分钟。我常用的结构如下本质是“卷积 池化 × 2再接全连接”层输出尺寸参数数量作用Conv2d(1, 32, 3, padding1)28×28×32320提取低层边缘特征ReLU MaxPool2d(2)14×14×320下采样保留主要响应Conv2d(32, 64, 3, padding1)14×14×6418496组合形成局部纹理特征ReLU MaxPool2d(2)7×7×640进一步压缩空间维度Linear(64×7×7, 128)128401024全连接做高阶组合Linear(128, 10)101290输出 10 个类别的得分第一层卷积用 3×3 卷积核加 padding1保证输出尺寸仍为 28×28这样空间信息不会被卷积核迅速削掉。池化用 2×2 最大池化每经过一次尺寸减半28→14→7最后全连接层的输入维度是 64×7×7。4.2 定义模型PyTorch 代码与维度推导import torch import torch.nn as nn class DigitCNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Linear(128, 10) ) def forward(self, x): return self.classifier(self.features(x))features里两个卷积层分别输出 32 通道和 64 通道的特征图池化后空间尺寸从 28 变成 7。Flatten()把三维特征图拉平成 64×7×73136 维向量Linear(3136, 128)负责把局部特征组合成全局表示最后一层Linear(128, 10)输出每个类别的原始得分。整个过程不做 Softmax因为后面CrossEntropyLoss内部已经包含了 Softmax 计算提前做反而会带来数值不稳定性。4.3 数据加载与训练循环核心超参数怎么设数据加载用 DataLoader 分批次喂给模型from torch.utils.data import DataLoader train_loader DataLoader(train_set, batch_size64, shuffleTrue) test_loader DataLoader(test_set, batch_size256, shuffleFalse)batch_size64表示每轮迭代处理 64 张图这是 MNIST 上稳定且训练速度合理的常见取值shuffleTrue打乱训练顺序避免模型学到样本顺序的假规律测试集不需要打乱shuffleFalse即可。训练阶段的核心代码device torch.device(cuda if torch.cuda.is_available() else cpu) model DigitCNN().to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) for epoch in range(10): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fEpoch {epoch 1}, loss: {running_loss / len(train_loader):.4f})关键在三个调用optimizer.zero_grad()清空上一轮的梯度loss.backward()反向传播计算梯度optimizer.step()用梯度更新权重。这三个顺序反了或漏了训练要么不收敛要么参数原地不动。优化器选 Adam 而不是 SGD因为 Adam 自带学习率自适应lr0.001是它的默认建议值基本不需要额外调整。epoch 设为 10 轮每轮训练完后 loss 应该持续下降如果 loss 在第 5 轮后不降反升通常不是模型问题而是学习率过大把lr降到 0.0005 再试。4.4 测试集评估准确率到底有多少训练完必须在没参与训练的测试集上验证才有说服力model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fTest accuracy: {correct / total:.4f})model.eval()切换模型到评估模式它会关闭 Dropout 等训练专用行为——这个调用漏掉是测试准确率玄学波动的最常见原因之一。torch.no_grad()告诉 PyTorch 关闭梯度记录推理阶段不需要梯度能省内存、提速。argmax(dim1)取模型输出得分最高的类别作为预测结果(preds labels).sum().item()统计预测正确的数量。按这组超参数训练测试准确率通常在 98.5%99.2% 区间。如果没到这个范围优先检查数据预处理是否漏了标准化以及model.eval()是否忘写。这个版本的 CNN 已经是能拿去答辩的水平但距离“高分毕设”还差最后一步——让它能识别真实手写数字而不只是 MNIST 压平的数组。5. 常见问题与避坑数字识别开发里最容易翻车的 5 个环节从环境到模型再到真实场景这一路踩过的坑比任何教程都有参考价值。我按现象、原因、解决的顺序梳理了下面几个高频问题基本覆盖绝大多数数字识别项目的报错现场。5.1 MNIST 下载失败或卡在 Downloading 不动现象第一次运行datasets.MNIST(downloadTrue)进度条长时间不走或直接报连接超时。原因MNIST 原始数据托管在国外服务器国内网络环境下直连经常不稳定或超时。解决最快的路是手动下载四个.gz原始文件——train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz、t10k-images-idx3-ubyte.gz、t10k-labels-idx1-ubyte.gz放到./data/MNIST/raw/目录下然后把download参数改成False。torchvision 检测到 raw 目录下文件齐全就不会再触发下载。这个坑几乎每个跑 MNIST 的人都遇到过提前把文件备好后面就一劳永逸。5.2 像素没有正确归一化loss 不降或降得极慢现象模型训练几十轮准确率还在 90% 附近晃loss 曲线下不去。原因图像像素值如果没有被ToTensor()缩放到 01或者只做了缩放但没做标准化数值范围过大会让梯度更新不稳定。 解决检查 transform 里Normalize((0.1307,), (0.3081,))是否生效。如果只是import torchvision.datasets后没传 transform拿到的是 0255 的原始像素务必加上前面的Compose配置。训练前用一行确认数据范围print(train_set.data[0].float().min().item(), train_set.data[0].float().max().item())最小值应为 0、最大值应为 1否则就是预处理没接上。5.3 测试准确率忽高忽低Dropout 一直在干扰现象同一个模型连续跑两次测试准确率相差 23 个百分点很不稳定。原因模型定义里如果用了 Dropout训练结束后未切换到model.eval()Dropout 在推理阶段仍然随机屏蔽神经元导致每次输出不同。 解决推理前显式调用model.eval()并记住训练时用model.train()切回来。这个问题的隐蔽之处在于 pytorch 官方示例里通常会写但自己写代码时一紧张就漏了。配套检查手段是在测试阶段打印model.training输出 True 说明还在训练模式。5.4 自己手写的数字识别不准预处理是最大变量现象MNIST 测试准确率 99%但打开画板用鼠标写一个“7”模型识别成“2”。原因MNIST 的样本是白字黑底、数字居中、大小占满画面而且笔画粗细相对统一鼠标写的数字可能是黑字白底、位置偏移、笔画极细或断断续续。这是典型的数据分布不一致问题不是模型本身坏了。 解决写一个统一的预处理函数把任意输入转成 MNIST 风格import cv2 import numpy as np def preprocess_digit(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, th cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV) contours, _ cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) x, y, w, h cv2.boundingRect(contours[0]) digit th[y:y h, x:x w] digit cv2.resize(digit, (20, 20), interpolationcv2.INTER_AREA) canvas np.zeros((28, 28), dtypenp.uint8) canvas[4:24, 4:24] digit return canvasTHRESH_BINARY_INV做二值化并把前景变白适配 MNIST 白字黑底findContours找到数字所在的最小外接矩形resize缩放到 20×20 再贴到 28×28 中心位置留出 4 像素边距。这样处理后的输入和 MNIST 训练样本的分布基本一致识别率会立刻恢复正常。很多人不写这步直接用原图喂模型效果差是必然的。5.5 KNN 预测时内存飙升或卡死现象用完整 60000 张训练集拟合 KNN然后直接predict全部 10000 张测试图进程卡死或内存被吃满。原因KNN 的预测阶段要计算测试样本与所有训练样本的距离6 万×1 万的浮点距离矩阵会占用大量内存和 CPU 时间。 解决减少训练样本量或者分批预测pred_chunks [] for i in range(0, len(test_images), 2000): pred_chunks.extend(knn.predict(test_images[i:i 2000]))每批预测 2000 张逐一拼结果内存占用会明显下降。这个坑也提醒一点KNN 在 MNIST 上只能当基线真要上线识别系统还得靠 CNN。6. 模型保存、GUI 落地与答辩演示把准确率变成能看见的东西模型在测试集上跑到 99%实时画板却识别不出来等于白做。最后一步是把训练好的 CNN 变成一个能打开就用的系统同时准备一套能打动评委的演示流程。先保存模型权重别每次都重新训练torch.save(model.state_dict(), digit_cnn.pt)推理时加载权重注意map_locationcpu是为了避免 GPU 上训练、CPU 上加载时出现设备不匹配model DigitCNN() model.load_state_dict(torch.load(digit_cnn.pt, map_locationcpu)) model.eval()GUI 层面Tkinter 是 Python 自带库不需要额外安装用 Canvas 组件做一个可鼠标绘制的画板是最快的方案。关键交互是鼠标按下时画线抬起后把 Canvas 内容转成图片调用preprocess_digit处理再喂给模型。Canvas 的坐标范围通常是 200×200转成 28×28 前需要先缩放注意保持数字长宽比否则会变形。对鼠标写的数字预处理脚本比模型本身更值得反复调这是真实的血泪经验。答辩演示我建议准备三个场景按顺序展示用 GUI 画板写一个数字立刻识别用手机或白板笔写一个数字拍照喂进去对比同一张图在 KNN 和 CNN 下的识别结果与置信度。这个顺序能自然带出项目的完整链路——数据、预处理、传统基线、深度模型、交互系统。一个容易被忽略的加分项是置信度展示model(images)的输出经过softmax后取最大值能说明模型是“确定地识别”还是“蒙对的”这个细节在答辩里很显专业。我现在接到识别类任务仍然习惯先跑一版 KNN 摸清数据底细再写 CNN 出正式结果最后才做交互层。这个顺序帮我省掉过无数次返工也让我在答辩时能把每个模块为什么存在讲得很笃定。按这条路径把最后一步做完祝你能把 99% 的准确率真正握在手里希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →