ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

机器视觉期末作业之Python手写数字识别项目完整剖析

机器视觉期末作业之Python手写数字识别项目完整剖析 简介一套完整的机器视觉期末大作业实现方案基于Python完成手写体字符识别适合高校学生在期末大作业、课程设计中作为高分模板或参考项目。资源包共8个文件包含5个Python源代码文件、1个数据集压缩包、1个说明文档和1个Markdown说明整体大小约31.64MB。代码注释非常详细覆盖数据加载、模型构建多层感知机与轻量级VGG、训练与测试全流程即使是初学者也能快速读懂并部署运行。系统功能完善、界面友好同时附带清晰的项目说明便于理解整体架构与复现结果。目前已有344人学习下载对于需要完成机器视觉相关作业或入门手写体识别项目的读者来说是一份实用且可直接运行的参考资料。1. 机器视觉期末作业的“满分”底稿Python 手写体字符识别项目构成手写数字识别大概是机器视觉课程里最容易被“做烂”的题查个教程、调一个内置分类器跑出准确率就收工。这份期末作业源码不太一样——dataset.py 单独负责数据管线MLP.py 和 MiniVGG.py 各自封装成模型类train.py 与 test.py 把训练和推理分开。四个环节四个模块已经不是演示脚本而是能继续改的工程骨架。只图一个能跑的成绩它比“调库封神”多费一点理解成本但拿它当课程设计或期末大作业底稿把每个文件的职责看清楚后面换数据、改模型、调超参都会顺手很多代码注释密度也基本是照着“答辩能直接讲”来写的。下面我从数据管线、模型实现、训练与推理三条线拆这份源码最后补一个答辩演示时很实用的验证技巧。2. 数据管线与 dataset.pyMNIST 数据集加载、归一化和验证集划分的细节2.1 项目目录与文件职责边界压缩包解压之后目录层级是这样的machine_vision_handwriting/ ├── dataset.py # 数据加载、归一化、划分 train/val/test ├── train.py # 模型实例化、训练循环、保存 checkpoint ├── test.py # 加载权重、批量推理/单张推理 ├── requirements.txt # 依赖列表 ├── README.md # 使用说明与复现步骤 ├── models/ │ ├── __init__.py │ ├── MLP.py # 多层感知机模型类 │ └── MiniVGG.py # 小型 VGG 卷积网络模型类 └── data/ └── MNIST_Dataset.zip # 原始训练与测试数据这个目录设计有个容易被忽视的优点数据加载和模型定义彻底解耦。dataset.py 不关心后面接的是 MLP 还是 MiniVGG它只输出(images, labels)这样的标准元组models 里的模型类也不关心数据从哪个磁盘路径读出来forward 只接收张量。这意味着后期想把 MNIST 换成 Fashion-MNIST 甚至自建的手写数据集只需要动 dataset.py模型代码和训练循环一行都不用改这在做大作业迭代时非常省事。2.2 dataset.py 的加载与归一化代码实现MNIST 原始数据是 IDX 二进制格式逐字节解析容易出错常见做法是用 torchvision 封装好的数据集类来读。project 里 dataset.py 的核心逻辑一般是这样的import torch from torch.utils.data import DataLoader, random_split from torchvision import datasets, transforms def load_mnist(batch_size64, val_ratio0.2): transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set datasets.MNIST( root./data, trainTrue, downloadFalse, transformtransform) test_set datasets.MNIST( root./data, trainFalse, downloadFalse, transformtransform) # 从训练集里切出验证集用于每个 epoch 结束后的模型评估 val_len int(len(train_set) * val_ratio) train_len len(train_set) - val_len train_subset, val_subset random_split(train_set, [train_len, val_len]) train_loader DataLoader(train_subset, batch_sizebatch_size, shuffleTrue) val_loader DataLoader(val_subset, batch_sizebatch_size, shuffleFalse) test_loader DataLoader(test_set, batch_sizebatch_size, shuffleFalse) return train_loader, val_loader, test_loader这里有两个容易踩的坑。第一个是Normalize的两个参数0.1307 是 MNIST 训练集的像素均值0.3081 是标准差这两个是官方统计好的常数。ToTensor()先把像素从 0-255 缩到 0-1 区间再做标准化让数据分布逼近标准正态梯度更新会更平稳。如果写成Normalize((0.5,), (0.5,))模型也能收敛但收敛速度和最终精度通常不如官方统计值。第二个是downloadFalse。因为压缩包已经带了 MNIST 原始数据跑不起来时要先检查 data 目录下有没有解压出train-images-idx3-ubyte这类文件而不是直接把参数改成downloadTrue走联网下载——机器视觉课程设计答辩现场往往没有稳定外网提前把数据落地是基本操作。2.3 验证集划分与 DataLoader 参数含义random_split返回的是原数据集的子集视图不拷贝像素数据内存占用小。但要注意它按索引切分切完之后 train_subset 和 val_subset 之间没有重叠可如果你的代码里意外用了原始train_set做评估验证集数据其实也包含在训练数据里准确率会虚高这是期末答辩被追问时最容易答错的一个细节。val_ratio0.2 对应 48000 张训练、12000 张验证这个比例对 MNIST 这种 6 万张规模的数据集是够用的。DataLoader 的参数里shuffleTrue只在训练集打开作用是每个 epoch 重新打乱样本顺序防止模型学到批次排列的规律验证集和测试集保持原序。另外shuffle的随机性没有固定种子多次运行结果会有微小浮动这在作业报告里不影响结论但如果要严格复现需要在 train.py 开头加torch.manual_seed(0)。提示MNIST 的标签是 0-9 的整数索引CrossEntropyLoss 要求的正是这种格式模型输出的 logits 不需要再手动转 one-hot。3. 模型实现对比MLP.py 与 MiniVGG.py 的架构差异和选型依据3.1 MLP.py全连接堆叠的网络结构与参数规模MLP 是最直观的基线模型。输入是 28×28 灰度图展平后变成 784 维向量过两层全连接加 ReLU 激活最后输出 10 类 logits。工程里的标准写法是继承nn.Module并封装成类import torch.nn as nn class MLP(nn.Module): def __init__(self, input_size784, hidden_sizes(256, 128), num_classes10): super().__init__() layers [] prev_size input_size for h in hidden_sizes: layers.append(nn.Linear(prev_size, h)) layers.append(nn.ReLU()) prev_size h layers.append(nn.Linear(prev_size, num_classes)) self.network nn.Sequential(*layers) def forward(self, x): # (batch, 1, 28, 28) - (batch, 784) x x.view(x.size(0), -1) return self.network(x)参数量可以手算第一层 784×256256200,960第二层 256×12812832,896输出层 128×10101,290合计约 23.5 万。这个规模在 CPU 上跑一个 epoch 只要几十秒。但全连接结构完全不感知像素的二维邻接关系——28×28 展平之后原本上下相邻的像素在向量里可能隔了几十个位置模型只能靠训练集里的大量样本去硬记位置分布这也决定了它很难泛化到“位置偏移”的手写输入。用hidden_sizes元组控制层宽的好处是换结构时不需要改 forward加一层就把元组变成三个元素。3.2 MiniVGG.py卷积特征提取与分类头的组合实现MiniVGG 的思路是把 VGG 的“卷积块 池化”堆叠缩到适配 MNIST 的规模输入保持单通道 28×28经过两组卷积块后特征图降到 7×7×64再接全连接分类头import torch.nn as nn class MiniVGG(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): return self.classifier(self.features(x))三个设计点值得细说。第一padding1配合kernel_size3让卷积不改变特征图尺寸图像经过两个MaxPool2d(2, 2)从 28×28 降到 14×14 再到 7×7所以 classifier 输入的维度是64*7*7。第二BatchNorm 加在卷积之后、ReLU 之前它按批次对激活值做归一化能显著缓解训练初期梯度不稳定这也是 MiniVGG 比 MLP 更容易收敛到高精度的一个重要原因。第三Dropout(0.5)只加在全连接之前卷积层不参与随机失活这样既压制了分类头的过拟合又保留了卷积特征的完整性。3.3 两套模型的参数量对照与作业场景选型建议对比项MLP.pyMiniVGG.py总参数量约 23.5 万约 168 万结构类型全连接堆叠卷积 全连接MNIST 典型测试准确率约 0.98约 0.99 以上CPU 单 epoch 耗时短约 3-5 倍过拟合现象不明显训练集趋向 1.0验证集稳定答辩可展开的分析点少卷积核、池化、dropout 都能讲如果期末作业只求跑通交 MLP 就够但想在报告里写清楚“为什么卷积网络适合图像”MiniVGG 几乎是必选的对比组。它的过拟合曲线比 MLP 清晰得多——训练集准确率逼近 1.0验证集停在 0.99 附近这个差距本身就是很好的分析素材。MiniVGG 在 CPU 上跑 20 个 epoch 大约十分钟量级机器视觉课程设计的硬件条件完全能接受。4. train.py 超参数配置与 test.py 推理链路从训练循环到预测结果4.1 超参数设置与优化器选择train.py 开头一般会集中放一组可调超参数这也是期末报告里最容易拿分的表格素材config { model: minivgg, # mlp 或 minivgg epochs: 20, batch_size: 64, lr: 0.001, weight_decay: 1e-4, }这组配置里lr0.001对 Adam 是常用值如果换成 SGD这个学习率会明显偏小通常要调到 0.01 再配合 momentum0.9。batch_size64对应每轮 750 次迭代48000/64显存占用小CPU 也能跑得动调大反而可能让单次梯度估计过于平滑、收敛变慢。weight_decay是 L2 正则控制权重范数防止过拟合1e-4 是稳妥起点调太大容易欠拟合导致验证集准确率不升反降。4.2 训练循环中的模型状态切换与损失计算训练循环的结构值得照着抄因为它对model.train()和model.eval()的切换是很多初写训练脚本的人容易漏掉的点model MiniVGG().to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam( model.parameters(), lrconfig[lr], weight_decayconfig[weight_decay]) for epoch in range(config[epochs]): model.train() total_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() total_loss loss.item() * images.size(0) # 每个 epoch 结束后在验证集上评估 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_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) epoch_loss total_loss / len(train_loader.dataset) val_acc correct / total print(fEpoch {epoch1:02d} | loss {epoch_loss:.4f} | val_acc {val_acc:.4f})关键点在第一处model.train()它让 BatchNorm 使用当前批次的统计量、Dropout 随机失活。验证时如果忘了切回model.eval()Dropout 会让每次前向得到不同的结果你可能会看到验证准确率忽高忽低却找不到原因。第二处是with torch.no_grad()验证阶段不需要反向传播关闭自动求导图能省接近一半显存。outputs.argmax(dim1)是按类别维度取最大 logits 对应的整数索引和标签的 0-9 整数格式直接比较。用逐元素比较再求和得到的是正确预测张数除以总样本数就是验证集准确率。这里建议同时打印 loss 和 val_acc只打印 loss 会掩盖过拟合训练 loss 一直降、val_acc 却不再涨这时候就该降低weight_decay或者提前停止。提示如果训练多个 epoch 后 loss 变成 nan优先检查lr从 0.001 往下调一个数量级往往就能解决。4.3 模型保存格式与 test.py 的加载推理训练结束后保存的是state_dict而不是整个模型对象这是主流做法因为权重文件更小、换架构或改超参数后能灵活复用torch.save(model.state_dict(), fcheckpoints/{config[model]}.pth)test.py 加载时要先实例化相同结构的模型再把权重灌进去from models.MiniVGG import MiniVGG device torch.device(cuda if torch.cuda.is_available() else cpu) model MiniVGG(num_classes10) model.load_state_dict(torch.load(checkpoints/minivgg.pth, map_locationdevice)) model.to(device) model.eval()map_locationdevice解决的是“训练用 GPU、测试用 CPU”的迁移问题少了它torch 会报权重存储位置不匹配。load_state_dict要求字典的 key 与模型层名严格对应如果加载时报 missing keys 或 unexpected keys说明保存权重的模型结构和当前实例化的结构不一致常见原因是改了层名、增删了层或者用 MLP 的权重去加载 MiniVGG 的模型。这类问题在期末演示前最容易出提前跑一遍 test.py 确认输出能过。5. 答辩前验证用自绘手写数字检查模型识别边界MNIST 测试集准确率再高也只能说明模型对“扫描入库的标准手写体”有效。答辩现场评委经常随手画一个数字或者让你拿手机拍一张手写输入试一下。更稳妥的方式是自己构造一张图跑推理提前搞清楚模型的置信边界在哪里。常见做法是用 PIL 在 28×28 画布上画一个粗糙的数字再走一遍训练时相同的预处理链路from PIL import Image, ImageDraw import torch from torchvision import transforms img Image.new(L, (28, 28), color0) draw ImageDraw.Draw(img) # 画一个类似数字 7 的线条示意 draw.line([(5, 5), (14, 25)], fill255, width3) draw.line([(14, 25), (23, 5)], fill255, width3) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) tensor transform(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1)[0] top3 torch.topk(probs, 3) for i in range(3): idx top3.indices[i].item() print(f数字 {idx}: {probs[idx].item():.4f})这个验证脚本在答辩时很有价值原因在于它暴露的是真实分布外的问题。比如把置信度低于 0.7 的样本拿过来逐项排查会发现三类典型原因笔画太细MNIST 的平均笔画宽度在 3-4 像素宽度设为 1 会让模型认为是噪声、数字位置没有居中MNIST 训练样本中心对齐的比例很高偏移超过两个像素就开始掉点、明暗方向反转MNIST 是黑底白字还是白底黑字依赖预处理搞反了预测概率会近乎均匀分布。这些观察可以直接写进报告的“模型局限”小节。如果top3里出现概率比较接近的两个类别比如 4 和 9 或者 7 和 1说明笔画结构在卷积特征层面确实相近这时候可以进一步打印特征图或者把输入图放大看像素值分布找出是哪个局部结构造成的歧义。答辩现场能讲清楚的往往就是这些 MNIST 干净样本之外、自己画一画就露出来的识别边界。把这类样本的 top3 输出和预处理链路原样贴进报告比单独列一个准确率数字更能体现对模型的理解。本文还有配套的精品资源点击获取
返回列表