news 2026/9/16 13:23:36

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
机器视觉期末作业之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.py:MNIST 数据集加载、归一化和验证集划分的细节

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_size=64, val_ratio=0.2): transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set = datasets.MNIST( root='./data', train=True, download=False, transform=transform) test_set = datasets.MNIST( root='./data', train=False, download=False, transform=transform) # 从训练集里切出验证集,用于每个 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_size=batch_size, shuffle=True) val_loader = DataLoader(val_subset, batch_size=batch_size, shuffle=False) test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False) return train_loader, val_loader, test_loader

这里有两个容易踩的坑。第一个是Normalize的两个参数:0.1307 是 MNIST 训练集的像素均值,0.3081 是标准差,这两个是官方统计好的常数。ToTensor()先把像素从 0-255 缩到 0-1 区间,再做标准化让数据分布逼近标准正态,梯度更新会更平稳。如果写成Normalize((0.5,), (0.5,)),模型也能收敛,但收敛速度和最终精度通常不如官方统计值。

第二个是download=False。因为压缩包已经带了 MNIST 原始数据,跑不起来时要先检查 data 目录下有没有解压出train-images-idx3-ubyte这类文件,而不是直接把参数改成download=True走联网下载——机器视觉课程设计答辩现场往往没有稳定外网,提前把数据落地是基本操作。

2.3 验证集划分与 DataLoader 参数含义

random_split返回的是原数据集的子集视图,不拷贝像素数据,内存占用小。但要注意它按索引切分,切完之后 train_subset 和 val_subset 之间没有重叠,可如果你的代码里意外用了原始train_set做评估,验证集数据其实也包含在训练数据里,准确率会虚高,这是期末答辩被追问时最容易答错的一个细节。val_ratio=0.2 对应 48000 张训练、12000 张验证,这个比例对 MNIST 这种 6 万张规模的数据集是够用的。

DataLoader 的参数里,shuffle=True只在训练集打开,作用是每个 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_size=784, hidden_sizes=(256, 128), num_classes=10): 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×256+256=200,960,第二层 256×128+128=32,896,输出层 128×10+10=1,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_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.Conv2d(32, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 512), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): return self.classifier(self.features(x))

三个设计点值得细说。第一,padding=1配合kernel_size=3让卷积不改变特征图尺寸,图像经过两个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, }

这组配置里,lr=0.001对 Adam 是常用值,如果换成 SGD,这个学习率会明显偏小,通常要调到 0.01 再配合 momentum=0.9。batch_size=64对应每轮 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(), lr=config['lr'], weight_decay=config['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(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) epoch_loss = total_loss / len(train_loader.dataset) val_acc = correct / total print(f'Epoch {epoch+1:02d} | loss {epoch_loss:.4f} | val_acc {val_acc:.4f}')

关键点在第一处model.train():它让 BatchNorm 使用当前批次的统计量、Dropout 随机失活。验证时如果忘了切回model.eval(),Dropout 会让每次前向得到不同的结果,你可能会看到验证准确率忽高忽低却找不到原因。第二处是with torch.no_grad():验证阶段不需要反向传播,关闭自动求导图能省接近一半显存。

outputs.argmax(dim=1)是按类别维度取最大 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(), f'checkpoints/{config["model"]}.pth')

test.py 加载时要先实例化相同结构的模型,再把权重灌进去:

from models.MiniVGG import MiniVGG device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = MiniVGG(num_classes=10) model.load_state_dict(torch.load('checkpoints/minivgg.pth', map_location=device)) model.to(device) model.eval()

map_location=device解决的是“训练用 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), color=0) draw = ImageDraw.Draw(img) # 画一个类似数字 7 的线条示意 draw.line([(5, 5), (14, 25)], fill=255, width=3) draw.line([(14, 25), (23, 5)], fill=255, width=3) 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, dim=1)[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 输出和预处理链路原样贴进报告,比单独列一个准确率数字更能体现对模型的理解。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 13:22:39

ScanSAR成像模式流程解析:burst时序、扇贝效应与方位模糊

简介:面向星载扫描SAR算法研究的资料包,系统梳理了ScanSAR成像模式的核心流程,适合SAR成像算法初学者、遥感信号处理研究人员以及相关专业学生使用。内容聚焦ScanSAR多波束扫描机制下的数据采集、信号处理、几何校正、干涉处理、图像拼接与复…

作者头像 李华
网站建设 2026/9/16 13:19:33

EIP-8246 深度解析:从 EVM 中彻底移除 SELFDESTRUCT 的 ETH 销毁语义

EIP-8246 深度解析:从 EVM 中彻底移除 SELFDESTRUCT 的 ETH 销毁语义 【免费下载链接】EIPs The Ethereum Improvement Proposal repository 项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs 导读 本文以以太坊改进提案 EIP-8246(Remove…

作者头像 李华
网站建设 2026/9/16 13:15:44

一键唤醒鼠标侧键:Mac Mouse Fix 快速上手

一键唤醒鼠标侧键:Mac Mouse Fix 快速上手 【免费下载链接】mac-mouse-fix Mac Mouse Fix - Make Your $10 Mouse Better Than an Apple Trackpad! 项目地址: https://gitcode.com/GitHub_Trending/ma/mac-mouse-fix 你那只几十块的鼠标,侧键在 M…

作者头像 李华