ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Python的手写数字识别系统:从环境搭建到CNN模型部署的完整指南

基于Python的手写数字识别系统:从环境搭建到CNN模型部署的完整指南 简介这份资源面向计算机相关专业的毕业设计学生及Python深度学习入门者提供一套基于Numpy从零实现的手写数字识别系统完整源码与使用教程帮助读者理解神经网络底层原理并完成可运行的毕设项目。压缩包共28个文件约14.18MB以9个py源码文件为核心涵盖激活函数、全连接层、卷积层、池化层、网络结构接口定义及参数保存加载等模块另含10个npz训练参数文件、4张png结果图、2组idx格式MNIST数据集及说明文档目录按data、parameters、figure分层组织。项目同时提供BP神经网络与卷积神经网络两套测试入口训练日志与各轮次正确率参数均有留存最高正确率约96.98%便于对照实验效果。目前已有1152人学习下载适合需要完整代码框架、可复现训练流程与排错参考的读者。1. 从一份毕业设计压缩包说起手写数字识别到底难在哪很多人拿到「基于Python实现的手写数字识别系统源码使用教程毕业设计.zip」这类资源第一反应是解压、装依赖、跑python main.py然后看到准确率 98% 就以为万事大吉。但真正动手做过 MNIST 手写数字识别的人都知道这个任务的门槛不在模型有多深而在工程细节有多碎。MNIST 本身只有 7 万张 28×28 的灰度图用 CNN 跑几轮就能到 99% 以上可一旦你要把它做成一个能演示、能答辩、能换自己手写图片测试的完整系统问题就来了环境怎么配、数据怎么读、模型怎么存、界面怎么接、自己写的数字为什么识别不出来。这篇笔记面向三类人正在做计算机毕业设计、需要一套能跑通且能讲清楚的手写数字识别系统的同学刚学完 CNN 基础、想找一个完整项目练手的 Python 入门者以及需要快速搭一个图像分类 demo 的工程师。我会按「环境搭建 → 数据与模型 → 训练与评估 → 系统集成 → 避坑 → 进阶」的顺序把一份典型的手写数字识别源码拆开讲透让你不只是跑通而是知道每一步为什么这么做、参数怎么调、翻车了去哪找原因。2. 环境搭建与依赖安装把 numpy、CNN 框架和编辑器一次配好2.1 为什么推荐 Anaconda PyTorch 而不是裸 pip手写数字识别系统的依赖链其实不短numpy 做数组运算、matplotlib 做可视化、PyTorch 或 TensorFlow 做 CNN 训练、Pillow 做图片读取、有时还要 Flask 或 Tkinter 做界面。裸 pip 装最大的问题是 numpy 版本不匹配——很多毕业设计源码写的时候用的是 numpy 1.x你本地默认装了 2.x一跑就报np.float已移除或者numpy三维数组相乘维度对不上。Anaconda 的好处是能把整个环境隔离在一个 conda env 里numpy、scipy、matplotlib 的版本由 conda 统一解析冲突概率低很多。我一般会这么做先建一个专用环境Python 版本选 3.9 或 3.10这两个版本对 PyTorch 和 TensorFlow 的兼容性最稳。不要用 3.12部分老源码里的np.int之类写法会直接报错。# 创建名为 mnist 的虚拟环境指定 Python 3.10 conda create -n mnist python3.10 -y # 激活环境 conda activate mnist # 安装核心依赖numpy 锁在 1.26 以下避免 API 移除问题 pip install numpy2.0 matplotlib pillow # 安装 PyTorchCPU 版即可手写数字识别不需要 GPU pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu这段命令的逻辑是conda 负责环境隔离和 Python 版本pip 负责装深度学习框架。numpy2.0这个约束很关键因为 numpy 2.0 移除了np.float、np.int等别名而大量毕业设计源码还在用这些写法。PyTorch 用 CPU 版就够MNIST 训练一轮在普通笔记本上也就十几秒没必要折腾 CUDA。参数说明-n mnist是环境名可以改成你喜欢的python3.10是版本约束--index-url指定 PyTorch 官方 CPU 轮子源避免默认源下载慢或版本不对。如果你用的是 TensorFlow把最后一行换成pip install tensorflow-cpu即可但要注意 TensorFlow 2.15 之后对 numpy 版本也有要求建议同样锁 numpy。提示装完以后跑一句python -c import numpy, torch; print(numpy.__version__, torch.__version__)确认两个库都能正常导入再往下走。这一步能挡掉后面 80% 的「ImportError」。2.2 VSCode Python 环境配置与解释器选择编辑器我推荐 VSCode轻量且对 Python 支持好。装好 VSCode 后需要做三件事装 Python 扩展、选对解释器、配好调试配置。很多人代码跑不起来不是代码问题而是 VSCode 默认用了系统 Python 而不是 conda 环境里的 Python。操作步骤按CtrlShiftP打开命令面板输入Python: Select Interpreter选择路径里带envs/mnist的那个解释器。然后在项目根目录建一个.vscode/launch.json内容如下{ version: 0.2.0, configurations: [ { name: Python: 当前文件, type: python, request: launch, program: ${file}, console: integratedTerminal, justMyCode: true } ] }这个配置的作用是让 F5 调试时用集成终端运行当前文件justMyCode设为 true 可以避免调试时跳进库源码。如果你要调试训练脚本把program改成${workspaceFolder}/train.py这种固定入口更稳。常见的一个坑是VSCode 终端里conda activate不生效。这是因为默认终端是 PowerShell 且没初始化 conda。解决办法是在设置里把默认终端改成 Command Prompt或者执行conda init powershell后重启 VSCode。环境配好之后后面所有代码都在这个环境里跑不要再混用系统 Python。3. 数据加载与 CNN 模型从 MNIST 到可保存的权重文件3.1 MNIST 数据集的读取、归一化与 DataLoader 参数MNIST 手写数字识别的数据加载看起来简单但有几个参数直接决定训练能不能收敛。原始图片是 28×28 的灰度图像素值 0 到 255。直接喂给网络不是不行但收敛慢且容易梯度爆炸。标准做法是做归一化把像素值映射到 0 到 1 或者标准化到均值 0.1307、标准差 0.3081——这两个数是 MNIST 训练集的全局统计量用它们做标准化能让输入分布更接近标准正态训练更稳。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理转张量 标准化 transform transforms.Compose([ transforms.ToTensor(), # 把 PIL 图片转成 [0,1] 的张量形状 (1,28,28) transforms.Normalize((0.1307,), (0.3081,)) # MNIST 全局均值和标准差 ]) # 下载并加载训练集和测试集 train_set datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_set datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # DataLoaderbatch_size 和 shuffle 是关键参数 train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers0) test_loader DataLoader(test_set, batch_size1000, shuffleFalse, num_workers0)逻辑说明ToTensor()会自动把像素除以 255所以 Normalize 里的均值 0.1307 是针对已经除以 255 后的数据。batch_size64是手写数字识别的常用值太小训练慢太大显存吃紧且泛化略差。shuffleTrue只在训练集开测试集必须关掉否则评估结果不可复现。num_workers在 Windows 上建议设 0设大了容易报多进程相关的错。参数怎么改如果你数据量小、想快速看效果batch_size 可以调到 128如果发现 loss 震荡厉害降到 32。标准化那一步如果你用的是自己手写的数字图片做测试也要用同样的均值和标准差处理否则训练和推理的输入分布不一致识别率会断崖式下跌。3.2 一个够用的 CNN 结构两层卷积加全连接手写数字识别不需要 ResNet 那种深度两层卷积加两层全连接就能到 99%。结构设计的关键是卷积核数量、池化方式和 Dropout 的位置。下面是一个我常用的结构参数量小、训练快、容易解释答辩时也好讲。import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 第一层卷积1 通道输入32 个 3x3 卷积核 self.conv1 nn.Conv2d(1, 32, 3, padding1) # 第二层卷积32 通道输入64 个 3x3 卷积核 self.conv2 nn.Conv2d(32, 64, 3, padding1) # 池化层2x2 最大池化 self.pool nn.MaxPool2d(2, 2) # Dropout随机丢弃 25% 神经元防过拟合 self.dropout1 nn.Dropout2d(0.25) self.dropout2 nn.Dropout(0.5) # 全连接经过两次池化后是 7x764 通道 self.fc1 nn.Linear(64 * 7 * 7, 128) self.fc2 nn.Linear(128, 10) # 10 个数字类别 def forward(self, x): x self.pool(F.relu(self.conv1(x))) # - (32,14,14) x self.pool(F.relu(self.conv2(x))) # - (64,7,7) x self.dropout1(x) x torch.flatten(x, 1) # 展平成 (batch, 64*7*7) x F.relu(self.fc1(x)) x self.dropout2(x) x self.fc2(x) return F.log_softmax(x, dim1) # 配合 NLLLoss 使用逻辑说明padding1保证卷积后尺寸不变这样两次池化后正好从 28 降到 7。Dropout2d用在卷积层后按通道丢弃普通Dropout用在全连接层后。最后用log_softmax是因为训练损失用NLLLoss如果你习惯用CrossEntropyLoss就把最后一行去掉因为 CrossEntropyLoss 内部已经包含 softmax。参数说明卷积核数量 32 和 64 是经验值翻倍到 64 和 128 准确率提升很小但训练时间翻倍。fc1的 128 是隐藏层维度调到 256 也可以但要注意过拟合。Dropout 的 0.25 和 0.5 是经典配置如果发现训练集准确率远高于测试集可以把 0.5 提到 0.6。3.3 训练循环、模型保存与加载的完整写法训练循环里最容易被忽略的是model.train()和model.eval()的切换以及保存模型时到底存什么。只存state_dict是最推荐的做法体积小、加载灵活。import torch.optim as optim device torch.device(cpu) # 有 GPU 可改 cuda model Net().to(device) optimizer optim.Adam(model.parameters(), lr0.001) def train(epoch): model.train() # 开启训练模式Dropout 生效 for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() # 清空上一轮梯度 output model(data) loss F.nll_loss(output, target) loss.backward() # 反向传播 optimizer.step() # 更新参数 if batch_idx % 100 0: print(f训练轮次 {epoch} 批次 {batch_idx} 损失 {loss.item():.4f}) def test(): model.eval() # 关闭 Dropout固定 BatchNorm 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 correct / len(test_loader.dataset) print(f测试集准确率 {acc:.4f}) return acc for epoch in range(1, 6): train(epoch) test() # 只保存参数字典不保存整个模型对象 torch.save(model.state_dict(), mnist_cnn.pt)逻辑说明optimizer.zero_grad()必须放在反向传播前否则梯度会累加。torch.no_grad()在测试时能显著降低内存占用。保存state_dict而不是整个模型是因为整个模型对象依赖类定义换环境加载容易出问题。加载时这样写model Net() model.load_state_dict(torch.load(mnist_cnn.pt, map_locationcpu)) model.eval()参数说明学习率 0.001 配 Adam 是稳妥选择如果 loss 不下降可以试 0.0005。训练 5 轮通常能到 99% 左右轮次太多会过拟合。map_locationcpu保证在没 GPU 的机器上也能加载。4. 系统集成与推理把模型接上界面和真实手写图片4.1 用 Flask 搭一个最小可用的识别接口毕业设计通常要求有界面演示最省事的是 Flask 起一个网页上传图片返回识别结果。核心是把上传的图片转成模型能吃的张量注意预处理必须和训练时一致。from flask import Flask, request, jsonify from PIL import Image import torch import io app Flask(__name__) model Net() model.load_state_dict(torch.load(mnist_cnn.pt, map_locationcpu)) model.eval() app.route(/predict, methods[POST]) def predict(): file request.files[image] img Image.open(io.BytesIO(file.read())).convert(L) # 转灰度 img img.resize((28, 28)) # 缩放到 28x28 tensor transforms.ToTensor()(img) # 转张量 tensor transforms.Normalize((0.1307,), (0.3081,))(tensor) tensor tensor.unsqueeze(0) # 加 batch 维度 with torch.no_grad(): output model(tensor) pred output.argmax(dim1).item() return jsonify({digit: pred}) if __name__ __main__: app.run(host0.0.0.0, port5000)逻辑说明convert(L)把彩色图转灰度resize统一尺寸unsqueeze(0)补上 batch 维度因为模型期望输入是(N,1,28,28)。预处理顺序必须和训练时完全一致否则识别率会崩。参数说明host0.0.0.0让局域网内其他设备也能访问答辩时可以用手机拍照上传演示。端口 5000 如果被占用改成 5001。4.2 自己手写的数字为什么识别不出来这是最高频的翻车点。MNIST 的图片是白字黑底、居中、笔画粗细固定而你自己用画图工具写的是黑字白底、可能偏左偏上、笔画粗细随意。直接丢进去识别率极低。解决办法是做反色和居中裁剪。import numpy as np from PIL import Image, ImageOps def preprocess_handwritten(path): img Image.open(path).convert(L) img ImageOps.invert(img) # 黑字白底 - 白字黑底 img img.resize((28, 28)) arr np.array(img) # 按像素重心居中先找非零区域 ys, xs np.nonzero(arr) if len(xs) 0: cx, cy int(xs.mean()), int(ys.mean()) arr np.roll(arr, 14 - cx, axis1) arr np.roll(arr, 14 - cy, axis0) img Image.fromarray(arr) tensor transforms.ToTensor()(img) tensor transforms.Normalize((0.1307,), (0.3081,))(tensor) return tensor.unsqueeze(0)逻辑说明ImageOps.invert做反色np.nonzero找笔画像素np.roll把重心平移到图像中心。这一步做完自己手写的数字识别率能从 30% 提到 80% 以上。参数说明14 - cx里的 14 是 28 的一半目标是让重心落在中心。如果你的图片笔画特别粗可以在 resize 前做一次二值化把灰度值大于 128 的置 255其余置 0。5. 避坑与排查手写数字识别系统最常见的 5 个翻车现场5.1 现象训练 loss 一直是 2.3 不下降原因log_softmax和CrossEntropyLoss混用了等于做了两次 softmax梯度被压平。或者学习率设得太大直接跳过最优解。解决检查损失函数和模型最后一层是否匹配。用NLLLoss就保留log_softmax用CrossEntropyLoss就去掉。学习率从 0.001 开始试不降就减半。5.2 现象测试准确率 99% 但自己上传图片全错原因训练数据是白字黑底且居中你的输入是黑字白底且偏移分布不一致。解决按 4.2 节做反色和重心居中。另外确认上传图片确实是灰度单通道彩色图要先转L模式。5.3 现象加载模型时报Missing key(s) in state_dict原因保存时用了torch.save(model)存整个对象加载时类定义变了或者保存的是 DataParallel 包装后的模型key 前面多了module.。解决统一用state_dict保存和加载。如果 key 有module.前缀加载时用model.load_state_dict({k.replace(module.,):v for k,v in sd.items()})去掉。5.4 现象numpy 报module numpy has no attribute float原因numpy 2.0 移除了np.float、np.int等别名老源码里还在用。解决降级到pip install numpy2.0或者全局搜索把np.float改成float、np.int改成int。这是毕业设计源码在新环境跑不起来的第一大原因。5.5 现象DataLoader 在 Windows 上报多进程错误原因num_workers大于 0 时 Windows 的 spawn 机制和某些库不兼容。解决把num_workers设为 0或者把训练代码放在if __name__ __main__:保护块里。前者最省事MNIST 数据量小单进程加载完全够快。6. 进阶技巧用混淆矩阵和置信度把系统讲出深度答辩时如果只报一个准确率老师很容易问「哪些数字容易混」。这时候混淆矩阵就是你的后悔药。MNIST 里最容易混的是 4 和 9、3 和 5、7 和 1把这些可视化出来再配合置信度输出整个系统的完成度会高一个档次。from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_targets [], [] with torch.no_grad(): for data, target in test_loader: output model(data) pred output.argmax(dim1) all_preds.extend(pred.numpy()) all_targets.extend(target.numpy()) cm confusion_matrix(all_targets, all_preds) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(预测标签) plt.ylabel(真实标签) plt.title(MNIST 混淆矩阵) plt.savefig(confusion_matrix.png, dpi150)逻辑说明confusion_matrix的行是真实标签、列是预测标签对角线是正确分类。annotTrue把数字标在格子里fmtd保证显示整数。跑完这张图你能直接指出模型在哪些类别上弱。置信度输出也很简单把log_softmax的输出取指数就是概率with torch.no_grad(): output model(tensor) prob torch.exp(output) # log_softmax - 概率 conf, pred prob.max(dim1) print(f预测 {pred.item()}置信度 {conf.item():.2%})参数说明置信度低于 60% 的样本可以单独挑出来人工检查这在实际系统里就是「不确定就转人工」的雏形。如果某个数字的置信度普遍偏低说明训练集里这类样本太少可以针对性做数据增强比如轻微旋转和缩放。我自己做这类系统最大的教训是不要一上来就堆模型深度先把数据预处理和评估做扎实。一个两层 CNN 加正确的归一化比一个五层网络加错误的输入处理要强得多。每次改完预处理先跑一遍测试集看准确率有没有掉再去看单张图片的预测这个习惯帮我省了无数返工时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表