ARTICLE DETAIL

资讯详情

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

PyTorch搭建CNN识别MNIST:手写数字图像分类完整实战

PyTorch搭建CNN识别MNIST:手写数字图像分类完整实战 简介这是一份基于Python和PyTorch实现卷积神经网络识别MNIST手写数字数据集的课程设计资源包面向深度学习初学者、高校学生及需要完成图像分类入门项目的开发者涵盖从模型搭建、训练到测试评估的完整CNN实现流程。压缩包共11个文件包含Python源码、设计报告Word文档、训练/测试过程图表及项目配置文件等整体大小仅176KB结构精简便于快速下载和对照学习。目前已有2027人浏览学习可作为课程设计或毕业设计的参考资料。资源不仅提供了可直接运行的Python脚本还附带了多张训练过程与样本测试截图以及详细的实验报告能够帮助读者理解卷积层、池化层、全连接层等核心概念快速复现经典的手写数字识别实验节省从零编码与调试的时间。1. 从 MNIST 到 CNN这个经典实验为什么值得自己完整跑一遍基于 Python 的卷积神经网络CNN识别 MNIST 数据集是我当年第一次完整跑通的深度学习实验。说“完整”是因为它不只是调用一个现成模型而是把数据加载、网络设计、训练、测试、日志输出、可视化最后连课程设计报告都一起交付了。MNIST 手写数字识别在业界被比作机器学习界的“果蝇实验”因为数据简单但流程五脏俱全。这个资源适合正在做课程设计、毕业设计或想用 PyTorch 入门图像分类的读者它能帮你少走弯路直接看到一份代码和设计报告该长什么样。通过复现这个项目你得到的不只是 97% 以上的准确率而是深度学习标准工作流的手感。2. 复现前的准备环境搭建、文件清单与 MNIST 数据形态2.1 Python 与 PyTorch 环境搭建这个项目是 PyTorch 生态所以第一步不是直接写代码而是把 Python 环境确定下来。我一般会建议用 conda 新建一个干净环境避免和系统 Python 混在一起。Python 版本选 3.8~3.10 最稳PyTorch 在 3.11 以后虽然也能装但一些课程设计里常见的依赖包可能会有兼容问题。conda create -n mnist python3.9 conda activate mnist pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install matplotlib pillow如果只是复现这个资源CPU 版就够用了MNIST 图片只有 28×28 灰度图模型也很小两轮训练在我的旧笔记本上大概几十秒。如果你机器有 NVIDIA GPU把最后一行--index-url .../cpu去掉默认装 CUDA 版即可。装完以后可以用下面这段验证环境python -c import torch; print(torch.__version__, torch.cuda.is_available())看到版本号和False说明是 CPU 环境不是报错。PyTorch 会自动回落到 CPU 计算。这里要注意很多人以为torch.cuda.is_available()必须为True才能跑其实不是device cuda if torch.cuda.is_available() else cpu这个习惯一定要尽早养成后面代码里也会用到。2.2 资源包结构哪些文件是交付刚需解压基于Python的卷积神经网络(CNN)识别MNIST数据集.zip之后核心文件不算多但每一个都有明确用途。我把它们在交付时的作用列一下文件/目录作用cnn.py主程序包含数据加载、模型定义、训练、测试全流程output.txt训练和测试过程中打印的日志报告里的实验结果直接引它设计报告.docx课程设计报告我后续会讲怎么改重点images/training_2epoch.png训练过程的损失或准确率曲线图images/testing_2epoch.png测试集上的结果可视化比如混淆矩阵或预测样例images/digits.pngMNIST 样本展示图用来说明数据集images/sample_digit.png单张手写数字样例验证模型预测用.gitignore、README.md、LICENSE工程规范文件直接沿用没问题我最看重的其实是cnn.py和设计报告.docx。因为很多初学者能跑通代码但写报告时不知道从哪下手有了报告底子改成自己的项目说明就容易多了。images 目录里的四张图片是报告插图现成素材比自己截图干净。2.3 MNIST 数据加载为什么 normalize 参数这么重要MNIST 数据集由 60000 张训练图和 10000 张测试图组成每张图都是 28×28 的灰度图像素值范围从 0 到 255标签是 0~9 的数字。用 PyTorch 加载最常见的写法是from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(./data, trainFalse, downloadTrue, transformtransform)ToTensor()会把 H×W×C 的 PIL 图像转成 C×H×W 的张量并且把像素值缩放到 0~1 之间。Normalize((0.1307,), (0.3081,))是 MNIST 全量数据的全局均值和标准差减均值除标准差能让输入分布接近标准正态分布这对 CNN 训练的稳定性帮助很大。如果不做 Normalize虽然也能收敛但可能要多花一两轮 epoch损失曲线也更抖。如果你看到训练 loss 乱跳先检查这一步是不是漏了。3. 把 CNN 模型写出来网络结构、训练循环与测试输出3.1 为什么图像分类选卷积而不是全连接面对 28×28 的灰度图全连接网络也能做但需要把图片展平成一个 784 维的向量这样做会丢失像素的空间结构比如数字“1”的竖线位置、数字“0”的圆圈形状。CNN 通过卷积核在图像上滑动每一次只观察局部区域再通过多层堆叠获得更大感受野。权值共享又让参数量大幅下降所以 MNIST 这种小尺寸图片用 CNN 不仅准确率高训练速度也不慢。这也是课程设计中一个合理的选型用 CNN 而不是 MLP理由写起来很充分。3.2 网络结构Conv2d、BatchNorm 与 Dropout 的搭配cnn.py里核心模型的常见写法是这样的我拆解过很多类似项目这个结构算是入门到进阶之间比较舒服的平衡点import torch.nn as nn class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(64) self.pool nn.MaxPool2d(2, 2) self.fc1 nn.Linear(64 * 7 * 7, 128) self.dropout nn.Dropout(0.25) self.fc2 nn.Linear(128, 10) def forward(self, x): x self.pool(torch.relu(self.bn1(self.conv1(x)))) x self.pool(torch.relu(self.bn2(self.conv2(x)))) x x.view(-1, 64 * 7 * 7) x torch.relu(self.fc1(x)) x self.dropout(x) return self.fc2(x)输入是 1 通道的 28×28 灰度图。第一次卷积输出 32 个通道第二次卷积输出 64 个通道。padding1保证 28×28 在经过 3×3 卷积后尺寸不变所以每次下采样都由MaxPool2d(2, 2)完成第一次池化后 14×14第二次后 7×7。全连接层输入维度就是64 * 7 * 7这个数字不能拍脑袋改改输入图片尺寸时这里是第一个要跟着动的地方。Dropout(0.25)在全连接层之前起作用训练时随机丢弃 25% 的神经元测试时自动失效这是防止过拟合的常见做法。BatchNorm2d放在卷积和激活之间能让每层输入分布稳定学习率可以稍微调大一点。如果你拿到的 repo 里没有 BN去掉也能跑但加上会让 2 轮 epoch 的效果更体面。3.3 训练循环数据加载、损失函数与反向传播训练部分的写法大同小异关键点是三个数据加载器要设好 batch size损失函数用交叉熵优化器建议用 Adam 或 SGD。下面这段代码说明了每一轮的节奏import torch import torch.nn.functional as F def train_epoch(model, device, train_loader, optimizer, epoch): model.train() 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.cross_entropy(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fTrain Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} f({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f})cross_entropy在 PyTorch 里已经包含了 Softmax 的计算所以模型最后一层不需要额外加 LogSoftmax。optimizer.zero_grad()是必须的否则梯度会累加如果你的 loss 不下降先看是不是忘了清零。batch size 没有在这个函数里出现但它由DataLoader决定常见值 64 或 128我一般用 128两轮训练能更快看到曲线。数据加载和优化器的设置from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size128, shuffleTrue) test_loader DataLoader(test_dataset, batch_size128, shuffleFalse) optimizer torch.optim.Adam(model.parameters(), lr0.001)Adam 的默认学习率 1e-3 在这个任务上表现稳定基本不需要调。SGD 如果要达到同样效果学习率建议从 0.01 起步并且加 momentum但演示项目用 Adam 更省心。3.4 测试与日志output.txt 里那一行行数据是怎么来的测试阶段的核心是关闭梯度计算因为在验证时不需要反向传播开着no_grad能省显存和计算时间。同时要记住切换model.eval()它会让 BatchNorm 和 Dropout 变成推理模式。def test(model, device, test_loader): model.eval() test_loss 0 correct 0 with torch.no_grad(): for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) test_loss F.cross_entropy(output, target, reductionsum).item() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() test_loss / len(test_loader.dataset) print(fTest set: Average loss: {test_loss:.4f}, fAccuracy: {correct}/{len(test_loader.dataset)} f({100. * correct / len(test_loader.dataset):.2f}%))output的形状是[batch_size, 10]每一行代表当前样本属于 0~9 的概率未归一化。argmax(dim1)取每行最大值的索引作为预测类别。pred.eq(target.view_as(pred))逐元素比较预测和真值然后用sum()统计对的数量。整个函数最后输出平均损失和准确率output.txt就是把这些标准输出重定向保存下来的结果python cnn.py output.txt 21这样你就能在训练结束后用tail output.txt看到最终的测试准确率报告里的实验结果直接引用这个文件里的数字比临时截图更可信。4. 看训练结果日志、可视化图片与 sample_digit 的由来4.1 读懂output.txt里的训练日志资源里output.txt是代码重定向保存的内容大致分两类训练 loss 和测试准确率。两轮 epoch 的训练 loss 通常呈现出快速下降再趋于平稳的趋势。比如第一轮从 0.4 左右一路掉到 0.1 附近第二轮开头又反弹一点随后继续下降这是正常的因为每个 epoch 都会重新打乱数据初始几个 batch 容易偏高。如果看到第二轮 loss 降到 0.05 以下测试准确率接近 98%说明模型训练到位了。如果准确率只到 90%别急着调参先检查测试时有没有忘记model.eval()或者是否在测试代码里也跑了optimizer.step()。这些都是我自己实际翻车过的点。4.2training_2epoch.png和testing_2epoch.png可视化了什么这两张图是报告里最直观的素材。training_2epoch.png一般是训练 loss 随迭代次数变化的曲线有些实现也会同时画准确率。testing_2epoch.png则倾向展示测试集中的预测结果比如画一个 4×4 的网格每张子图里是测试图片、真实标签和预测标签预测错的样本会用红色标出来这是我个人比较推荐的做法因为一眼能看出模型哪些数字容易混淆。如果你从零复现并且想自己生成图常见做法是在训练循环里把每个 batch 的 loss 收集到一个列表训练结束后统一画import matplotlib.pyplot as plt loss_history [] # 在训练循环里loss_history.append(loss.item()) plt.plot(range(len(loss_history)), loss_history) plt.xlabel(Iteration) plt.ylabel(Loss) plt.title(Training Loss over 2 Epochs) plt.savefig(images/training_2epoch.png, dpi150)dpi150是为了保证插入 Word 报告时不发虚。画测试可视化时注意保存图片前调用plt.figure(figsize(8, 8))否则子图会挤成一团。images/目录在 repo 里已经存在直接覆盖写入即可。4.3sample_digit.png是怎么来的如何用模型验证它sample_digit.png是一张裁剪好的手写数字图目的是模拟“用真实手写输入做单张预测”的场景。很多入门项目只会在测试集上评估但课程设计的加分项往往是展示模型对一张新图的预测结果。手动验证的方式很简单准备好一张 28×28 的灰度图走一遍和训练时相同的预处理然后丢给模型推理from PIL import Image import matplotlib.pyplot as plt img Image.open(images/sample_digit.png).convert(L) # 转成灰度 img img.resize((28, 28)) # 统一尺寸 # 预览一下这张图确保没裁坏 plt.imshow(img, cmapgray) plt.axis(off) plt.show()如果你在 jupyter 里看到这条图注意数字笔画是否完整。resize会把原图拉伸到 28×28如果原图不是正方形数字会变形这时最好先ImageOps.contain或者自己裁剪。这一步也是我在实际过程中最经常踩坑的地方数据集里的图片本来就是 28×28但自定义图片往往分辨率更高、白边更多必须经过 resize 和归一化才能喂给模型。5. 复现避坑指南下载 404、维度报错和不收敛的排查5.1 torchvision 下载 MNIST 报 404现象执行downloadTrue时终端抛出HTTP Error 404: Not Found或urllib.error.URLError数据集一直下不下来。原因torchvision 内置的 MNIST 下载地址指向的官方源会有迁移和更新某些网络环境下访问不稳定就会出现资源找不到或超时。这不是代码逻辑错了而是数据源访问问题。解决手动下载数据集放到指定目录。先确认你的root参数是./data然后创建目录mkdir -p data/MNIST/raw接着把mnist.pkl.gz或四个标准文件train-images-idx3-ubyte.gz 等按 torchvision 期望的文件名放进去再在代码里把download改成False同时保留trainTrue/False。这样加载时会直接读取本地文件。如果你用的 PyTorch 版本较新也可以试试把torchvision升级到最新版内置链接可能已经更新。以后遇到类似下载问题我一般先检查缓存目录再考虑换源。5.2 明明装了 PyTorchtorch.cuda.is_available()还是 False现象跑任何代码都不报错但训练特别慢查看 GPU 信息发现 PyTorch 根本没在用它。原因安装的 PyTorch 是 CPU 版本。很多新手用pip install torch默认装的是 CPU 版或者机器上确实没有可用 CUDA 的 GPU。解决先确认硬件是否支持 CUDA支持就去官网按你的 CUDA 版本重新安装 GPU 版。如果只是课程设计演示CPU 版完全够用关键是让训练代码不依赖 CUDAdevice torch.device(cuda if torch.cuda.is_available() else cpu) model CNN().to(device) data, target data.to(device), target.to(device)写完这段代码无论 CPU 还是 GPU项目都能自动选择设备。不要写死device cuda不然 CPU 环境直接崩。5.3 模型前向传播时维度对不上报mat1 and mat2 shapes cannot be multiplied现象训练刚启动第一个 batch 就报RuntimeError提示矩阵形状无法相乘通常涉及fc1层的输入。原因我在 3.2 里强调过fc1的输入维度是64 * 7 * 7这是基于两张输入图都是 28×28 计算出来的。如果你把resize改了、或者网络中多加了卷积层/池化层最后的特征图尺寸就不是 7×7线性层输入维度自然对不上。解决不要手算用代码动态展平。更稳妥的做法是x x.view(x.size(0), -1) x self.fc1(x)这样无论特征图变成[batch, 64, 7, 7]还是别的尺寸都能自动展成二维矩阵同时保留 batch 维度。但要注意fc1初始化时的输入维度还是得手动对齐这里采用动态展平后再定义线性层的话就只能用nn.Linear(-1, 128)这种不行的写法所以更常见的做法是先在网络外跑一次假数据拿到维度。这也是行业内常说的“用假张量过一遍网络”我调试任何 CNN 结构都强制走这一步。5.4 训练 loss 不下降准确率稳定在 10% 左右现象每轮 epoch 打印的 loss 始终在 2.3 附近晃测试准确率约 10%和随机猜差不多。原因10% 这个数字在 MNIST 十分类任务里就是“模型没学到东西”的典型标志。常见原因有三个一个是数据没有归一化像素值范围差异大另一个是学习率设置极端还有一个是标签与输入不对应比如 shuffle 时把 data 和 target 分别打乱了。解决先确认transforms.Compose里同时包含ToTensor()和Normalize别只留归一化忘了转张量。再打印一组output看看数值范围如果第一层输出全是零说明权重初始化或输入数据有问题。最后检查DataLoader是否用了shuffleTrue以及有没有在迭代里错误地对target做索引操作。我把这条排在前面的方法基本两分钟能定位是否数据问题。5.5 VSCode 跑代码时FileNotFoundError或者图片保存后中文乱码现象在 VSCode 里按 F5 运行cnn.py提示找不到images/目录或者生成图片上的中文字体变成方块。原因VSCode 默认把当前打开文件夹或终端所在目录作为工作目录如果直接从 IDE 里运行相对路径./data和images/可能指向错误位置。matplotlib 默认字体不包含中文字符标题里有中文就会乱码。解决在项目根目录打开 VSCode并把cnn.py所在目录设为工作区根目录。如果还是不行代码开头显式切到脚本所在目录import os os.chdir(os.path.dirname(os.path.abspath(__file__)))中文字体问题则在画图前加两行plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus False是修复坐标轴上负号显示成方块的老问题这个坑我几乎每台新机器都会踩一遍建议直接写进模板里。6. 一份能上交的课程设计报告结构、自定义数字识别与交付套路6.1 设计报告从哪里下手资源里附带的设计报告.docx是很好的底稿。我通读这类报告后的感受是课程设计最看重的不是模型多复杂而是逻辑闭环。报告至少要覆盖这几块背景与意义、数据集说明、网络结构设计、训练过程、实验结果分析和总结。尤其是实验结果分析不要只写准确率 98%要把output.txt里几个关键节点写进去比如第一轮 loss 从多少降到多少测试集上哪些数字容易混淆。如果你用的是两轮训练就突出“小训练量下也能达到可接受效果”这个点这是卷积网络权值共享和局部感受野带来的优势。6.2 用自定义数字图片让你的报告更完整很多报告止步于测试集准确率但如果你能展示模型对images/sample_digit.png的预测结果说服力会更强。下面这段代码把前面的推理步骤串起来直接输出预测类别from PIL import Image import torch import torchvision.transforms as T img Image.open(images/sample_digit.png).convert(L) img img.resize((28, 28)) transform T.Compose([ T.ToTensor(), T.Normalize((0.1307,), (0.3081,)) ]) x transform(img).unsqueeze(0) # 增加 batch 维度 model.eval() with torch.no_grad(): pred model(x).argmax(dim1).item() print(Predicted digit:, pred)unsqueeze(0)很关键模型期望的输入形状是[batch, 1, 28, 28]单张图只有三维所以需要补上 batch 维。易混淆的数字常见于 4 和 9、3 和 8 之间如果你发现预测错了不要立刻怀疑模型先看看自定义图片的线宽、居中是否和图谱分布一致。MNIST 训练集的数字笔画很规整手写体太潦草时预测错是正常的报告中也可以把它写成未来优化方向。6.3 交付前强制走一遍的检查清单我把这个项目完整复现过不止一次现在每次做类似课程设计都会强制走一遍下面的检查清单第一确认output.txt是最新一次运行生成的不是改了代码后忘跑就塞进压缩包第二打开images/下四张图片确认没有空白或乱码第三把整个目录在没有 GPU 的干净环境里重新跑一遍看能否通过第四设计报告里的数字和图片必须跟output.txt、images/实际内容对应不能出现报告写 98%日志里却是 93% 的闹剧。这些细节才是资源和普通作业拉开差距的地方。如果你也想快速跑通这个经典实验并拿到一份能直接改写的课程设计报告这个资源里的cnn.py、设计报告.docx和图片素材是很省心的起点。希望帮到你。本文还有配套的精品资源点击获取
返回列表