简介:这是一份基于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 python=3.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.png | MNIST 样本展示图,用来说明数据集 |
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', train=True, download=True, transform=transform) test_dataset = datasets.MNIST('./data', train=False, download=True, transform=transform)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_size=3, padding=1) self.bn1 = nn.BatchNorm2d(32) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) 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 个通道。padding=1保证 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(f'Train 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_size=128, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False) optimizer = torch.optim.Adam(model.parameters(), lr=0.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, reduction='sum').item() pred = output.argmax(dim=1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_loader.dataset) print(f'Test set: Average loss: {test_loss:.4f}, ' f'Accuracy: {correct}/{len(test_loader.dataset)} ' f'({100. * correct / len(test_loader.dataset):.2f}%)')output的形状是[batch_size, 10],每一行代表当前样本属于 0~9 的概率(未归一化)。argmax(dim=1)取每行最大值的索引作为预测类别。pred.eq(target.view_as(pred))逐元素比较预测和真值,然后用sum()统计对的数量。整个函数最后输出平均损失和准确率,output.txt就是把这些标准输出重定向保存下来的结果:
python cnn.py > output.txt 2>&1这样你就能在训练结束后用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', dpi=150)dpi=150是为了保证插入 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, cmap='gray') plt.axis('off') plt.show()如果你在 jupyter 里看到这条图,注意数字笔画是否完整。resize会把原图拉伸到 28×28,如果原图不是正方形,数字会变形,这时最好先ImageOps.contain或者自己裁剪。这一步也是我在实际过程中最经常踩坑的地方:数据集里的图片本来就是 28×28,但自定义图片往往分辨率更高、白边更多,必须经过 resize 和归一化才能喂给模型。
5. 复现避坑指南:下载 404、维度报错和不收敛的排查
5.1 torchvision 下载 MNIST 报 404
现象:执行download=True时,终端抛出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,同时保留train=True/False。这样加载时会直接读取本地文件。如果你用的 PyTorch 版本较新,也可以试试把torchvision升级到最新版,内置链接可能已经更新。以后遇到类似下载问题,我一般先检查缓存目录,再考虑换源。
5.2 明明装了 PyTorch,torch.cuda.is_available()还是 False
现象:跑任何代码都不报错,但训练特别慢,查看 GPU 信息发现 PyTorch 根本没在用它。
原因:安装的 PyTorch 是 CPU 版本。很多新手用pip install torch默认装的是 CPU 版,或者机器上确实没有可用 CUDA 的 GPU。
解决:先确认硬件是否支持 CUDA,支持就去官网按你的 CUDA 版本重新安装 GPU 版。如果只是课程设计演示,CPU 版完全够用,关键是让训练代码不依赖 CUDA:
device = 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是否用了shuffle=True,以及有没有在迭代里错误地对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(dim=1).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和图片素材是很省心的起点。希望帮到你。
本文还有配套的精品资源,点击获取