尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python手写数字识别系统源码实战:从CNN训练到Flask部署

Python手写数字识别系统源码实战:从CNN训练到Flask部署 简介面向毕业设计与深度学习入门者的手写数字识别系统源码包基于Python实现同时提供BP神经网络与CNN卷积神经网络的完整代码。代码按功能拆分模块涵盖数据加载、激活函数、卷积、池化、全连接与参数保存读取等环节配合使用教程可快速复现训练与测试流程适合理解神经网络各层实现原理。包内共28个文件以9个py源码、10个npz训练好的模型参数为主另附MNIST原始数据集idx格式、说明文档和结果图压缩包仅14.18MB轻量易用。其中10组模型参数对应10次不同训练结果准确率从66.28%逐步提升至96.98%便于横向对比学习率、迭代次数等对精度的影响也为毕业设计提供现成的实验数据与可视化图表。目前已有1151人学习尤其适合课程设计、毕业论文或希望快速上手图像识别项目的读者。1. 手写数字识别系统源码包里真正值得拆解的部分模型、训练脚本与演示界面手写数字识别是图像分类领域最被讲透的题目但“基于Python实现的手写数字识别系统”作为毕业设计难点从来不是模型而是交付物完整度。你需要的是一套能跑、能改、能演示的源码从 MNIST 数据加载到 CNN 模型训练从准确率评估到 Web 界面的实时识别。很多人从网上下载一个免费 python 源码运行时报错就卡住因为缺少对目录结构和调用链的理解。这篇文章把常见工程拆成四层并指出每个环节最容易出 bug 的参数设置。适合正在做课设、毕设或者想快速搭建一个可交互的视觉识别 Demo 的 Python 开发者对已经会训练模型的人可以直接跳到最后的部署与混淆分析。2. 用 PyTorch 组织手写数字识别源码从 MNIST 数据加载到模型定义2.1 最小源码结构数据、模型、训练、界面四层分法一个常见的手写数字识别工程源码通常不是单文件而是按职责拆成四个部分。拿到压缩包先看项目根目录下的 Python 文件如果只有一个.py也能跑但可维护性差正规一点的毕设源码会分成data_loader.py、model.py、train.py、predict.py、app.py五个文件。下面是一个典型的目录形态handwritten_digits/ ├── data/ # MNIST 或自定义手写图片 ├── src/ │ ├── data_loader.py # 数据集加载和归一化 │ ├── model.py # 网络结构定义 │ ├── train.py # 训练循环与模型保存 │ ├── predict.py # 加载模型预测单张图片 │ └── app.py # PyQt / Flask 界面入口 ├── models/ # 训练好的权重 .pth / .onnx └── requirements.txt这里的划分逻辑是数据层不掺模型细节模型层不写训练逻辑预测和界面层只负责调用。后续不管是把 CNN 换成更浅或更深的变体还是把 Flask 改成 Web 页面都不需要重写整个系统。对毕业设计的源码评审来说这种分层比满屏注释更被认可。GitHub 上很多同类项目没做这层功夫导致阅读者只能顺着一个巨型文件从头看到尾改参数还要担心影响其他逻辑。文件主要职责常见实现函数data_loader.py下载或读入数据、归一化、划分训练/测试集load_mnist(), get_dataloader()model.py卷积或全连接网络结构SimpleCNN, LeNet 类train.py损失、优化器、训练循环、ckpt 保存train_one_epoch(), save_model()predict.py图像预处理与单张推理preprocess(), predict_image()app.py画板/上传入口与结果展示run_app()这里需要解释一下表格里的职责边界data_loader.py只负责把原始图片变成模型需要的 Tensor它不应该出现任何nn.Conv2d的代码model.py定义网络结构但不会自己调用反向传播。这样当你从train.py切到predict.py时模型调用方式始终一致不会出现训练和推理逻辑互相覆盖的情况。2.2 一个可直接运行的 LeNet 变体结构和输入尺寸对应手写数字识别里出现最多的网络不是 ResNet而是 LeNet-5 的简化版或经典版。原因很实际28x28 的灰度图不需要很深的网络三层卷积就能到 99% 以上。下面这段源码是模型层最常用的写法import torch.nn as nn class LeNet(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 6, kernel_size5, padding2), # 28-28 nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 28-14 nn.Conv2d(6, 16, kernel_size5), # 14-10 nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 10-5 ) self.classifier nn.Sequential( nn.Linear(16 * 5 * 5, 120), nn.ReLU(inplaceTrue), nn.Linear(120, 84), nn.ReLU(inplaceTrue), nn.Linear(84, num_classes) ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)第一层卷积加padding2保证 28x28 输入经过 5x5 卷积后尺寸不变最大池化把特征图减半到 14x14。第二层卷积没有 padding14x14 经 5x5 卷积后变成 10x10再池化得到 5x5。全连接层的输入维度16*5*5就是最后一层卷积的通道数乘以特征图边长一旦前面的卷积参数变了这个数字必须同步改。新手最容易在这里报错维度不一致。从损失函数角度看源码里通常会选择nn.CrossEntropyLoss。这个损失函数内部已经做了 softmax 和 log 计算所以模型最后一层不需要再手动加 softmax。预测时取outputs.argmax(dim1)就是类别索引。如果你在model.py里看到nn.Softmax多半是为了单独输出概率不影响训练逻辑但要注意维度方向dim1才是按通道方向计算。2.3 训练脚本里的超参数batch size、学习率、epoch 的设置依据模型定义之后训练脚本的参数直接决定源码能不能复现。常见的毕设代码里超参数会放在argparse或配置字典里。下面是一份可直接复制到train.py的参数列表batch_size 64 learning_rate 1e-3 epochs 10 optimizer adam loss_function cross_entropy这些参数的设置依据在 MNIST 任务里相对固定。batch size 选 32 到 128 之间64 是性价比选择因为 MNIST 单张图片只有 28x28显存占用极小但 batch 太小时 BN 层的统计量会抖动训练不稳定。学习率用 Adam 时从 1e-3 开始如果换成 SGD 需要配合 momentum并把学习率降到 1e-2 量级。epoch 在 8 到 12 之间足够过大的 epoch 会过拟合且毕设答辩时没时间等太久。参数常见取值范围在这个项目里的影响batch_size32-128小了收敛不稳定大了占用显存高64 是默认选择learning_rate1e-3 到 1e-4Adam 配 1e-3SGD 需降到 1e-2 并加 momentumepochs5-15CNN 一般 8-10 轮稳定在 99% 附近optimizerAdam / SGDAdam 不用调 momentum毕设代码更短loss_functionCrossEntropyLoss分类任务直接用它不要自己拼 softmax交叉熵常见的错误是把 0 到 255 的像素值直接送进网络正确做法是除以 255 或使用transforms.Normalize((0.1307,), (0.3081,))这两个值是 MNIST 数据集的均值和标准差。源码头里的 transforms 改动往往比网络结构改动对准确率的影响更大。3. 环境准备与训练执行把 Python 手写数字识别源码跑通的命令3.1 本地环境最小安装依赖清单与虚拟环境拿到源码包先不要急着运行python train.py。手写数字识别系统最常见的环境坑是 torch 版本冲突或 numpy 版本问题。我的建议是新建一个虚拟环境Python 建议用 3.9 或 3.10PyTorch 安装 CPU 版就够训练 MNIST60000 张 28x28 图片在 CPU 上跑 10 个 epoch 也就十几分钟。网上 python 安装教程很多但真正影响项目可复现性的是依赖隔离所以第一步永远是建虚拟环境。# requirements.txt torch2.0.0 torchvision0.15.0 numpy1.24.0 Pillow9.0.0 matplotlib3.6.0 flask2.2.0安装命令在 Windows 和 Linux 上通用python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt上面先通过python -m venv创建虚拟环境再用source激活最后安装源码包声明好的依赖。如果你看到pip list里 torch 已经装成 CUDA 版本而本机没有 NVIDIA 显卡不必重装让代码强制在 CPU 上运行即可。训练脚本里加一段设备判断import torch device torch.device(cuda if torch.cuda.is_available() else cpu) print(use device:, device)这种写法保证源码在普通笔记本上也能完整跑完也方便到有 GPU 的机器上直接加速。有一点要注意torchvision的版本不要和 torch 相差太多否则可能出现torchvision依赖的torch版本不满足导致 import 阶段就崩。如果源码包里没有requirements.txt直接用 pip 安装上面几个库就够了。3.2 训练执行的完整命令与日志解读数据加载、模型定义都准备好后执行训练的命令通常只有一行。无论源码包是用argparse还是直接改常量训练命令一般长这样python train.py --epochs 10 --batch-size 64 --lr 1e-3 --save-dir models如果源码没有命令行参数也可以直接改文件头部变量。训练开始后控制台会输出类似下面的日志Epoch [1/10] Train Loss: 0.2312 Accuracy: 93.2% Epoch [2/10] Train Loss: 0.1082 Accuracy: 97.8% ... Epoch [10/10] Train Loss: 0.0284 Accuracy: 99.2% Validation Accuracy: 99.1%Train Loss是交叉熵损失数值从 2.3 附近开始逐渐降到 0.03 以下都是正常的。如果第一轮就在 0.0001 以下一般是学习率过大或数据被重复拟合。Accuracy表示当前 batch 的准确率波动 0.5% 不用管要看整个验证集的最终准确率。源码包如果带有--save-dir参数会在models目录下生成mnist_cnn.pth这样的权重文件。对于毕设只需要保留最后几个 epoch 的权重不需要每轮都保存除非你想画损失曲线。报错原因处理No module named torch虚拟环境未激活或未安装执行 pip install torchsize mismatch 异常全连接层输入维度错误用 print(x.shape) 逐层检查shape invalid输入不是 1x28x28检查 transforms 是否只转灰度准确率低于 85%归一化缺失或标签错位统一预处理、检查数据集采样3.3 训练中途失败时的标准排查顺序运行python train.py最常见的几个故障按出现频率排序如下第一种是ModuleNotFoundError几乎都是虚拟环境没激活或者 pip 安装的是另一个 Python 路径下的包。第二种是size mismatch发生在卷积输出展平后与全连接层维度不匹配这时在forward里加一行print(x.shape)把每层输出打出来。第三种是显存溢出MNIST 用 CPU 也能跑溢出多半是把 batch size 开到了 512 以上调回 64 即可。第四种是训练正常但准确率一直很低先检查数据归一化和标签是否错位再检查模型最后一层输出是否为 10 个节点。最容易踩的坑是训练集和测试集使用不同的预处理。很多源码包里train.py里做了归一化但predict.py里直接Image.open()然后ToTensor()导致自己写的手写图识别率骤降。规范化做法是把预处理定义成同一个函数或同一个transforms对象训练和预测共用。4. 识别效果验证与参数调优MNIST 测试集、单张图片与增强策略4.1 在测试集上计算准确率并绘制混淆矩阵训练完模型后不能只看验证集损失。一个可靠的手写数字识别系统至少要在测试集上做一次全量评估并画出混淆矩阵。评估脚本的核心逻辑如下import torch from sklearn.metrics import confusion_matrix, classification_report model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in test_loader: outputs model(images) preds outputs.argmax(dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds))这段代码先把模型切到 eval 模式关闭梯度计算然后遍历测试集收集预测结果。classification_report输出每个数字的 precision、recall、f1-score比单个准确率更说明问题。比如数字 0 的召回率低多半是把细长的 0 识别成了 6数字 7 和 9 容易互相混淆这与训练数据里两类的笔画分布有关。这些信息可以写进毕业设计的实验分析比只写一句“准确率 99%”更有价值。如果源码包里没有sklearn可以用 PyTorch 计算混淆矩阵但代码会多出几十行。建议直接把scikit-learn加进 requirements它只是作为分析工具不影响模型推理。4.2 用单张图片测试系统从 PNG 到模型输入的预处理如果你要给答辩老师现场画个数字然后让系统识别预测脚本就要处理好图像尺寸、通道和归一化的一致性。下面是predict.py中常用的预处理代码from PIL import Image import torchvision.transforms as transforms def preprocess_image(image_path): img Image.open(image_path).convert(L) # 转灰度 img img.resize((28, 28), Image.Resampling.LANCZOS) # 缩放 img_tensor transforms.ToTensor()(img) # 转 Tensor [0,1] img_tensor (img_tensor - 0.1307) / 0.3081 # 归一化 img_tensor img_tensor.unsqueeze(0) # 添加 batch 维 return img_tensor这里需要特别强调两点一是convert(L)必须做否则 RGBA 四通道图片会直接报错二是缩放算法选择LANCZOS对边缘保留比BILINEAR好手写数字笔画较细时更稳。源码包里如果只有ToTensor()没有归一化说明训练时可能也没做归一化预测代码不要擅自加减均值。处理完成后unsqueeze(0)把 1x28x28 变成 1x1x28x28因为模型默认输入是批量的。如果你传入的图片是黑底白字模型输出会和训练样本相反需要把像素取反再送进去。4.3 提升泛化能力的 3 个参数数据增强、dropout、学习率衰减手写数字识别在 MNIST 上很容易过拟合如果你的训练准确率 99.8% 而测试准确率只有 98.5%说明模型记住了训练集的噪声。源码优化时优先改这三个地方。第一是数据增强。在transforms中加入RandomAffine和RandomErasing注意不要对测试集使用train_transform transforms.Compose([ transforms.RandomAffine(degrees10, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])degrees10表示随机旋转正负 10 度translate表示随机平移 10%这样模拟手写时笔画不准的情况。第二是 dropout。在全连接层之间插入nn.Dropout(0.5)训练时随机丢弃一半连接减少神经元之间的依赖。dropout 只会在训练模式生效所以不需要手动关闭。第三是学习率衰减使用StepLR每 3 个 epoch 把学习率乘以 0.1scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.1)策略常见影响建议随机旋转平移测试准确率提升约 0.3%必选dropout防止全连接层过拟合层数多时有效学习率衰减后期损失更平稳配合 Adam 常用调参顺序建议先加数据增强再调 dropout最后加学习率衰减不要同时改多个参数否则不知道谁起了作用。写实验记录时可以保留三组对比数据这些数字会让分析部分更加扎实。5. 用 Flask Canvas 封装手写数字识别系统并在答辩中有效展示5.1 用 Flask 提供 /predict 接口前端画板传 base64 图片最常见的手写数字识别毕设界面有两种PyQt 桌面版和 Web 版。Web 版的好处是答辩时不用现场配环境打开浏览器就能操作。一个最小可用的 Flask 接口可以这样写from flask import Flask, request, jsonify import base64 from io import BytesIO from PIL import Image app Flask(__name__) model load_model(models/mnist_cnn.pth) model.eval() app.route(/predict, methods[POST]) def predict(): data request.get_json() img_data base64.b64decode(data[image].split(,)[-1]) img Image.open(BytesIO(img_data)).convert(L) # 继续执行预处理和推理结果以 JSON 返回 return jsonify({digit: result, confidence: conf})前端画板使用 HTML Canvas鼠标按下时开始画松开时停止最后把画布内容转成 base64 发送。这里有一个最容易导致识别率低的细节Canvas 默认是透明背景转成灰度图时黑色笔画会被错误处理。解决办法是在转图片前先把画布背景填充为白色再用黑色画笔绘制这样送到模型里的图像和 MNIST 的像素语义才一致。5.2 坐标缩放与笔画宽度对齐自己用鼠标写数字和 MNIST 训练样本差别很大MNIST 是 28x28 的像素矩阵数字占满中心区域而 Canvas 通常是 280x280 甚至更大。识别系统必须把用户画的数字统一缩放并且保持宽高比。建议将 Canvas 固定为 280x280绘图结束后按图像非零区域裁剪出数字的包围盒再缩放到 20x20最后粘贴到 28x28 黑底画布的中心这个流程与 MNIST 的预处理逻辑接近。不要直接拉伸 280x280 到 28x28因为手写体的长宽比会被破坏。5.3 演示中的验证点先跑测试集再现场写两个数字答辩或演示时最有说服力的流程是先跑测试集评估命令展示测试集准确率然后再现场手写几个数字。现场写数字时刻意写一个容易混淆的 7 和 9或者写粗一点的 8观察系统是否稳定。如果出现某一类错误不要回避直接打开混淆矩阵说明“测试集中这两类的相似样本较多”这比隐藏错误更有技术含量。最后展示models/mnist_cnn.pth的文件大小和单张推理耗时说明这个手写数字识别系统在无 GPU 环境中也能以毫秒级返回结果这个响应时间数据对系统实用性评估很有说服力。本文还有配套的精品资源点击获取
返回列表