首页 / 资讯中心 / 文章详情

手写体字符识别实战:从MNIST到CNN的机器视觉期末作业指南

手写体字符识别实战:从MNIST到CNN的机器视觉期末作业指南 ★ FEATURED ARTICLE
简介这份资源面向高校学生与机器视觉初学者提供一套可直接用于期末大作业或课程设计的手写体字符识别完整方案帮助解决从模型搭建到测试评估的全流程问题。压缩包共8个文件约31.64MB以5个Python源码文件为核心涵盖数据处理、MLP与MiniVGG模型定义、训练与测试脚本另附requirements依赖清单、README说明文档及MNIST数据集压缩包结构清晰、便于按模块阅读。代码注释详尽新手也能理解网络结构与训练逻辑。目前已有344人学习下载具备一定参考热度。读者可据此掌握手写数字识别的数据加载、模型构建、训练调参与测试评估思路并直接部署运行省去从零搭建的时间适合作为高分课程设计或入门机器视觉的实践范本。1. 手写体字符识别到底在识别什么从一张 28×28 的灰度图说起很多人第一次做机器视觉期末作业看到「手写体字符识别」这几个字脑子里想的是 OCR 那种从一张随手拍的照片里把一整段文字抠出来的场景。真上手才发现作业要的其实是更基础的一步给一张已经裁好、摆正、灰度化的单字符小图判断它是 0 到 9 里的哪一个或者再加几个字母。这件事听起来简单但它把机器视觉里最核心的几件事全串起来了——图像怎么表示、特征怎么提、模型怎么训、结果怎么评。你把这套流程跑通一遍后面做零件缺陷检测、电子称数值识别、车牌识别底层逻辑是同一套。这篇笔记面向的是要交机器视觉期末作业、或者想拿一个完整小项目入门 Python 机器视觉的人。我会按「数据集怎么准备 → 模型怎么搭 → 怎么训练和调参 → 怎么评估和排错 → 怎么把作业做出区分度」这条线讲代码能直接抄参数会告诉你为什么这么设。手写体字符识别这个题目最大的好处是数据量可控、训练快、结果直观一张普通笔记本 CPU 就能跑完不需要显卡这对期末周来说很关键。2. 数据集怎么选怎么读MNIST 之外的三个现实问题2.1 为什么默认从 MNIST 起步但它不是终点MNIST 几乎是手写体字符识别的默认起点6 万张训练图加 1 万张测试图每张 28×28 灰度标签是 0 到 9。它的好处是干净、格式统一、加载方便很多框架直接内置了下载接口。但你要清楚它的局限这些数字是经过居中、归一化处理的笔画粗细和位置都比较规范。一旦你拿自己手写的数字去测准确率经常掉一截这就是所谓的分布差异。所以我的建议是作业主体用 MNIST 跑通但一定要留一步「用自己的手写图测试」这一步能体现你对泛化问题的理解也是拉开分数的地方。常见做法是用画图工具或者手机拍一张写数字的纸裁成单字符再缩放到 28×28。2.2 用 Python 把数据集读进来的最小代码下面这段代码用 torchvision 加载 MNIST同时做了归一化。归一化这步别省它把像素值从 0 到 255 压到均值 0.1307、标准差 0.3081 附近能让训练稳定很多。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理转张量 标准化 transform transforms.Compose([ transforms.ToTensor(), # 把 PIL 图转成 [0,1] 的张量 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) train_loader DataLoader(train_set, batch_size64, shuffleTrue) test_loader DataLoader(test_set, batch_size1000, shuffleFalse) print(len(train_set), len(test_set))逻辑说明ToTensor会把像素从整数变成 0 到 1 的浮点Normalize再按通道做减均值除标准差。batch_size训练时设 64 是折中太小训练慢太大显存或内存吃紧测试时设 1000 是为了少跑几轮循环。shuffleTrue只在训练集开测试集不要打乱方便你按顺序对照结果。2.3 如果老师要求换数据集怎么接有些作业会要求不能用 MNIST那你可以考虑 EMNIST它把字母也加进来了格式和 MNIST 一样改个名字就能加载。或者用 sklearn 自带的 digits 数据集只有 1797 张 8×8 的图量小但足够演示流程。换数据集时要注意两点一是图像尺寸变了网络输入层要跟着改二是类别数变了输出层维度要改。这两处不改训练直接报维度错误。提示数据集下载慢是常见问题可以提前把文件放到./data目录下代码检测到已存在就不会重复下载。3. 模型怎么搭从全连接到卷积参数到底怎么定3.1 先跑一个全连接基线知道下限在哪别一上来就堆卷积先用两层全连接跑一个基线准确率大概能到 92% 左右。这个数字很重要它是你的下限后面换卷积如果没超过它说明哪里写错了。import torch.nn as nn import torch.nn.functional as F class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(28*28, 256) # 输入展平成 784 维 self.fc2 nn.Linear(256, 10) # 输出 10 类 def forward(self, x): x x.view(-1, 28*28) # 展平-1 表示自动推断 batch x F.relu(self.fc1(x)) x self.fc2(x) return x逻辑说明view(-1, 784)把[batch, 1, 28, 28]变成[batch, 784]。fc1输出 256 是经验值太小欠拟合太大容易过拟合且慢。最后不加 softmax因为交叉熵损失函数内部会做。3.2 换成卷积网络三个关键参数怎么设卷积网络对手写字符更合适因为它能利用局部空间关系。下面这个结构是我常用的两层卷积加两层全连接参数量不大训练几分钟就能收敛。class CNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) # 1 通道进32 通道出 self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) # 32 进64 出 self.pool nn.MaxPool2d(2) # 2x2 池化尺寸减半 self.fc1 nn.Linear(64*7*7, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x self.pool(F.relu(self.conv1(x))) # 28x28 - 14x14 x self.pool(F.relu(self.conv2(x))) # 14x14 - 7x7 x x.view(-1, 64*7*7) x F.relu(self.fc1(x)) x self.fc2(x) return x参数说明kernel_size3配padding1能保持特征图尺寸不变这是最常用的组合。conv1输出 32 通道、conv2输出 64 通道是逐层翻倍的思路让网络先学边缘再学组合特征。池化用 2×2 最大池化把 28 压到 14 再压到 7。fc1输入维度 64×7×7 必须和前面特征图尺寸对上这是新手最容易写错的地方尺寸不对就报矩阵乘法错误。3.3 训练循环里必须盯住的三个量训练代码本身不长但有几个量要盯损失是不是在降、训练准确率和测试准确率的差距、以及学习率有没有设对。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model CNN().to(device) optimizer optim.Adam(model.parameters(), lr1e-3) # Adam 对新手友好 criterion nn.CrossEntropyLoss() for epoch in range(5): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() # 梯度清零别忘 out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() print(fepoch {epoch}, loss {loss.item():.4f})逻辑说明zero_grad必须在反向传播前调用否则梯度会累加。学习率1e-3是 Adam 的常用起点太大震荡太小收敛慢。跑 5 个 epoch 通常测试准确率能到 99% 左右。如果 loss 不降先检查标签有没有对齐、学习率是不是太大。4. 训练完怎么评估别只看一个准确率数字4.1 混淆矩阵能告诉你错在哪准确率是个笼统指标混淆矩阵才能看出模型把哪个数字认成了哪个。比如 4 和 9、3 和 8 经常混这跟笔画结构有关。from sklearn.metrics import confusion_matrix import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) preds model(imgs).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)逻辑说明model.eval()会关掉 dropout 和 batchnorm 的训练行为评估时必须开。torch.no_grad()省内存也加速。argmax(dim1)取每行最大值的下标作为预测类别。拿到混淆矩阵后看对角线外的数字哪两个类之间错得多就针对性地补这类样本。4.2 用自己的手写图测试暴露真实问题这一步是作业加分项。用画图工具写一个数字存成 png然后走一遍预处理再喂给模型。from PIL import Image img Image.open(my_digit.png).convert(L) # 转灰度 img img.resize((28, 28)) # 缩放到训练尺寸 img transforms.ToTensor()(img) img transforms.Normalize((0.1307,), (0.3081,))(img) img img.unsqueeze(0).to(device) # 加 batch 维度 model.eval() with torch.no_grad(): pred model(img).argmax(dim1).item() print(预测结果:, pred)逻辑说明convert(L)保证单通道resize保证尺寸一致unsqueeze(0)补上 batch 维度。这里最常见的翻车是图片背景是白色、数字是黑色而 MNIST 是黑底白字导致预测全错。解决办法是做一次颜色反转或者训练时就把数据增强加上反转。注意自己拍的图往往有噪声和倾斜直接缩放效果一般可以先用阈值二值化再裁剪到字符边界框。5. 避坑与排查五个我踩过的坑坑一训练准确率很高测试准确率很低。现象是训练集 99.9%测试集只有 90%。原因是过拟合模型把训练样本背下来了。解决办法是加 dropout 层、做数据增强随机平移、旋转或者减小模型参数量。坑二loss 一直是 2.3 左右不降。现象是训练几轮损失几乎不动。原因是标签和输出没对齐或者学习率太小。解决办法是检查CrossEntropyLoss的输入是不是原始 logits不要自己先 softmax学习率调到 1e-3 试试。坑三换数据集后报维度错误。现象是mat1 and mat2 shapes cannot be multiplied。原因是全连接层输入维度和特征图尺寸对不上。解决办法是打印卷积输出后的 shape把fc1的输入改成对应值。坑四预测结果全是同一个数字。现象是无论输入什么都输出 0。原因是数据没归一化或者初始化有问题。解决办法是确认Normalize的均值和标准差和训练时一致检查权重初始化。坑五用自己的图测试全错。现象是 MNIST 测试集 99%自己写的数字全认错。原因是背景色和训练数据相反或者字符没居中。解决办法是做颜色反转并把字符裁剪到边界框再缩放。6. 把作业做出区分度数据增强和可视化两个技巧想让期末作业不只是「跑通」可以加两件事。第一件是数据增强用 torchvision 的RandomAffine做小角度旋转和平移模拟手写的自然变化通常能把自测准确率提几个点。train_transform transforms.Compose([ transforms.RandomAffine(degrees10, translate(0.1, 0.1)), # 旋转±10度平移10% transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])逻辑说明degrees10控制旋转范围translate控制平移比例这两个值别设太大否则数字变形过度反而学不好。增强只加在训练集测试集保持原样。第二件是把卷积层的特征图可视化出来看看网络到底学到了什么。取第一层卷积的输出选几个通道画成热力图你会看到有的通道对横线敏感有的对竖线敏感。这个图放进作业报告里比单纯贴准确率有说服力得多。# 取一个 batch 的第一张图看 conv1 输出 model.eval() with torch.no_grad(): sample next(iter(test_loader))[0][:1].to(device) feat F.relu(model.conv1(sample)) # [1, 32, 28, 28] feat feat.squeeze(0).cpu() # [32, 28, 28] import matplotlib.pyplot as plt fig, axes plt.subplots(4, 8, figsize(12, 6)) for i, ax in enumerate(axes.flat): ax.imshow(feat[i], cmapgray) ax.axis(off) plt.show()逻辑说明conv1输出 32 个通道这里画前 32 个。squeeze(0)去掉 batch 维度。可视化能帮你判断网络是不是真的在学笔画特征如果特征图一片模糊可能是学习率或初始化有问题。我自己做这类作业的习惯是先把基线跑通拿到一个保底分数再逐步加增强、加可视化、加自测环节每一步都记录准确率变化。这样报告里既有过程又有对比老师一眼能看出你是真动手了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站