简介:面向高校期末大作业和课程设计场景,这份基于PyTorch的MNIST手写数字图像分类项目源码,覆盖了从数据解压、预处理、模型搭建、训练调参到测试评估的完整流程。压缩包共包含14个文件,核心为三个Python脚本,分别承担数据加载、模型定义和训练测试任务;同时附带MNIST原始训练与测试数据集、依赖环境说明和README文档,解压后按说明配置即可复现实验,整体大小约22MB。项目目录组织规范,关键模块配有注释,能帮助初学者快速理解卷积神经网络处理图像分类任务的数据流与关键参数的作用。目前已有443人在线学习,说明其作为期末大作业或课程设计模板具有较强参考价值。通过动手实践,读者不仅可以获得一个可直接提交的高分项目,还能掌握从图像数据读取、模型训练到准确率评估的完整工程方法,对于写实验报告和答辩演示也很有帮助。
1. 基于pytorch的mnist图像数据集分类实战:这门大作业到底在考什么
同样是 MNIST 手写数字识别,课程作业和线上教程有一个明显分水岭:教程教你把模型跑出 99% 准确率就收工,高分大作业要求的是“把分类项目做成一套能交给别人运行的源码”。这意味着数据读取、模型定义、训练、评估、预测、可视化、异常处理必须各就各位,而多数新手恰恰栽在数据加载和评估环节。基于 PyTorch 做 MNIST 图像数据集分类,技术栈很轻,真正决定分数的部分在工程组织:能不能换一台机器直接复现、能不能讲清楚每个参数为什么这么设、能不能用分类评估指标证明模型不是靠偷数据刷分。适合正在做课设、准备复试或补 CV 基础的人,把 PyTorch 基础框架的整体流转吃透。
2. pytorch安装与mnist数据集加载:torchvision下载404的处理方法
2.1 先配好能跑CNN的pytorch环境,再谈数据集
MNIST 分类项目最怕第一步就倒在环境上。常见的 pytorch 环境搭建方式有 Anaconda 创建虚拟环境、pip 直装、以及 IDE 内嵌解释器。我一般会用 Anaconda 新建一个独立环境,避免把系统 Python 搞乱。CPU 机器直接安装 CPU 版即可,有 NVIDIA GPU 的同学再装 CUDA 版,注意torch、torchvision、python三者版本需要配对。
conda create -n mnist_env python=3.9 -y conda activate mnist_env # CPU 版 pip install torch torchvision torchaudio # GPU 版则用官网生成的命令,例如 cuda 12.1 组合包 # pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121这段命令创建名为mnist_env的环境并安装依赖。参数说明:python=3.9是因为 3.10/3.11 也可以,但 3.9 对国内镜像源兼容性更好;torchvision是处理图像数据集和预训练变换的库,MNIST 数据集的下载接口就在它里面;加注释的 GPU 命令需要根据机器驱动选择合适的 CUDA 版本。很多同学装完跑torch.cuda.is_available()显示False,多半是安装了 CPU 版或者驱动太老,别急着怪代码。
2.2 torchvision下载mnist会404:根本原因与解决方案
torchvision.datasets.MNIST默认从https://yann.lecun.com/exdb/mnist/下载,但这个老站点经常跳转异常,导致torchvision下载时出现 404。另一个高发点是校园网或代理环境下 SSL 证书校验失败。这个问题和模型代码无关,是数据源访问问题,但会让整个大作业卡在第一步。
常见做法是改用国内镜像或离线包。我建议先手动下载四个.gz文件,再让代码读取本地文件。MNIST 的官网镜像很多,但不要在代码里写死不可靠的 URL。更稳妥的方案是修改MNIST类的urls属性:
from torchvision.datasets import MNIST from torchvision.datasets.utils import extract_archive, check_integrity class LocalMNIST(MNIST): urls = [ "https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz", "https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz", "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz", "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz", ] train_set = LocalMNIST(root="./data", train=True, download=True, transform=transform)这段代码的关键是重写urls,把数据源换到较稳定的对象存储服务。root="./data"表示数据存放在当前目录的data文件夹下;train=True加载训练集;download=True表示若本地没有文件就执行下载。参数说明:transform不能省,MNIST 原始数据是 PIL 图像,需要转成张量并归一化,否则模型输入格式会报错。
如果仍然 404,最保险的办法是把下载好的.gz文件手动放进./data/MNIST/raw/目录,然后设置download=False。文件命名必须与库预期一致,否则check_integrity会判定文件缺失。
2.3 数据预处理与DataLoader参数怎么调
MNIST 的图片是 28×28 灰度图,模型输入一般设计成[batch_size, 1, 28, 28]。transforms.Compose里最常用的三段式是ToTensor、归一化和可选的增强。
from torchvision import transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set = LocalMNIST(root="./data", train=True, download=False, transform=transform) valid_set = LocalMNIST(root="./data", train=False, download=False, transform=transform) train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=2, pin_memory=True) valid_loader = DataLoader(valid_set, batch_size=256, shuffle=False)Normalize((0.1307,), (0.3081,))里的两个值是 MNIST 数据集的全局均值和标准差,是公开的统计量,直接复用即可。要注意ToTensor会把像素值从 0~255 缩放到 0~1,Normalize再把它变成均值为 0、方差接近 1 的数据。shuffle=True只在训练集使用,验证集必须保持False,否则评估指标会因数据顺序波动。num_workers=2在 Windows 上如果报多进程错误就改成 0,这是 Windows 和 Linux 在多进程数据加载上的常见差异。
| 参数 | 建议值 | 说明 |
|---|---|---|
batch_size | 训练 64,验证 256 | 小 batch 收敛稳,大 batch 验证更快 |
shuffle | 训练 True,验证 False | 打乱顺序能减少批次间相关性 |
num_workers | 2~4,Windows 可用 0 | 并行加载不一定会更快,可能引入报错 |
pin_memory | True | 使用 GPU 时可减少数据传输时间 |
drop_last | 依赖样本数 | MNIST 训练集是 60000,能被 64 整除,不必设置 |
3. 用pytorch基础框架构建mnist图像数据集分类模型
3.1 为什么选择CNN而不是多层感知机
MNIST 图片尺寸小,但像素之间的邻域关系才是关键。多层感知机把 28×28 拉平成 784 维向量,会丢失空间结构,想达到高准确率需要很宽的隐藏层,参数量大且泛化差。CNN 通过卷积核提取局部特征,用下采样降低分辨率,再通过全连接层输出 10 类概率。对于这个数据集,两到三层卷积就足够,过深的网络反而容易过拟合。
下面是一个典型的高分作业模型结构,把卷积、池化、Dropout、全连接完整呈现:
import torch import torch.nn as nn class MnistCNN(nn.Module): def __init__(self, num_classes=10, dropout=0.25): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Dropout2d(dropout), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Dropout2d(dropout), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 7 * 7, 256), nn.ReLU(inplace=True), nn.Dropout(dropout), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))逻辑说明:第一层卷积输入通道为 1,对应灰度图;输出 32 个特征图。经过 3×3 卷积加padding=1,特征图尺寸保持不变,所以两次卷积后仍是 28×28。MaxPool2d把尺寸减半,第一次池化后 14×14,第二次池化后 7×7。全连接层输入维度是128 * 7 * 7,这个数字是特征图通道数和尺寸的乘积,改卷积层结构时这里必须同步修改,否则Linear会报维度错误。
Dropout 是防止过拟合的关键。Dropout2d按通道随机置零,适合卷积层;Dropout按神经元置零,适合全连接层。大作业里如果训练准确率很高但验证准确率上不去,多半是 Dropout 强度不够或数据增强缺失。
3.2 模型参数表与可调整空间
下表列出了适合 MNIST 的默认参数和调整方向,写报告时可以直接对照解释:
| 模块 | 参数 | 默认值 | 调整方向 |
|---|---|---|---|
| Conv2d | 输出通道 | 32 / 64 / 128 | 加大通道数提升表达力,但会变慢 |
| Conv2d | kernel_size | 3×3 | 5×5 感受野更大,但参数量增加 |
| MaxPool2d | kernel_size / stride | 2 / 2 | 常用配置,改 3/2 会重叠池化 |
| Dropout | p | 0.25 | 过拟合时增到 0.5 |
| BatchNorm | 无 | 不启用 | 加入后可提高收敛稳定性 |
| Optimizer | Adam / SGD | 由训练脚本决定 | Adam 省心,SGD 配合动量泛化好 |
3.3 初始化与随机种子:复现高分结果的前提
大作业源码里最容易忽略的是随机种子。PyTorch 的卷积层和 Dropout 都依赖随机数,如果源码里不固定种子,每次运行结果会不同。教师或审查方复现时发现分数波动,极可能质疑代码稳定性。建议在所有训练入口加这一段:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = Falsecudnn.deterministic = True会让 GPU 上的卷积算法变成确定性实现,代价是速度稍慢,但能保证多次运行结果一致。benchmark = False禁止 cuDNN 自动选择最优算法,这也是复现性必需的。很多在 CPU 上跑得好好的代码,放到 GPU 上结果有细微差异,根源就在这里。
4. 训练、分类评估与模型保存:高分项目的得分点
4.1 训练循环的完整写法
模型定义完成后,训练过程要清晰记录每个 epoch 的损失和准确率。不要只打印 loss,要显式计算准确率,并区分训练集和验证集。下面的代码段可以直接嵌入训练脚本:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = MnistCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0, 0, 0 for images, labels in 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() * labels.size(0) correct += (outputs.argmax(dim=1) == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0, 0, 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) total_loss += loss.item() * labels.size(0) correct += (outputs.argmax(dim=1) == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total这段代码有两个细节:model.train()和model.eval()切换工作模式,影响 Dropout 和 BatchNorm 的表现;loss.item()取出的是当前 batch 的标量值,乘以labels.size(0)是把 loss 还原到样本维度,最后除以总样本数得到的是平均样本损失。outputs.argmax(dim=1)沿类别维度取最大值索引,和真实标签做比较得到预测正确的样本数。
CrossEntropyLoss会自动把模型输出的 logits 转为概率并计算损失,不需要在模型最后一层加 Softmax。这是一个高频误解:训练时用 CrossEntropyLoss,模型输出过 Softmax 反而会降低数值稳定性。
4.2 分类评估指标:不止准确率
MNIST 测试集准确率很容易做到 99% 以上,但如果只报告准确率,项目报告会显得单薄。高分大作业通常要求做分类评估,至少包含 precision、recall、F1-score,以及最直观的混淆矩阵。类别数量是 10,正好能看出哪些数字容易互相混淆,比如 4 和 9、3 和 8。
from sklearn.metrics import classification_report, confusion_matrix all_preds, all_labels = [], [] model.eval() with torch.no_grad(): for images, labels in valid_loader: images = images.to(device) outputs = model(images) all_preds.extend(outputs.argmax(dim=1).cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, digits=4)) conf = confusion_matrix(all_labels, all_preds) print(conf)classification_report会列出每个数字类别的 precision、recall、F1 和样本数。confusion_matrix返回一个 10×10 矩阵,第 i 行第 j 列表示真实类别 i 被预测成 j 的数量。矩阵对角线越深越好,非对角线亮点就是错误聚集处。报告里贴出这张矩阵,比单句“测试准确率 99.3%”更有说服力。
每个指标的含义需要注意:
| 指标 | 计算方式 | MNIST 中关注点 |
|---|---|---|
| accuracy | 正确预测数 / 总数 | 整体水平,类别均衡时可作主指标 |
| precision | TP / (TP + FP) | “预测为 7”中有多少真是 7 |
| recall | TP / (TP + FN) | “真实为 7”中有多少被找出来 |
| F1-score | 2 * precision * recall / (precision + recall) | 综合指标,适合关注少数类错误 |
4.3 模型保存与断点续训
训练结束后,源码里必须有保存模型权重的代码,同时还要保存一个结构文件或说明文件,方便加载。PyTorch 官方推荐只保存state_dict,因为它不绑定模型类定义路径,迁移性更好。
torch.save(model.state_dict(), "mnist_cnn.pt") # 加载时先实例化模型再填权重 loaded_model = MnistCNN() loaded_model.load_state_dict(torch.load("mnist_cnn.pt")) loaded_model.eval()保存state_dict时需要注意:load_state_dict要求model的结构和保存时的结构完全一致,别在训练后临时改了num_classes再加载。另外,如果训练过程很长,建议每个 epoch 都保存一个快照,并在训练代码里实现“验证准确率提高就覆盖保存”的机制,避免后期过拟合覆盖最好权重。
5. 高分大作业源码的组织技巧与3个容易忽略的边界问题
5.1 源码目录结构:让老师一眼看懂模块划分
一份能拿高分的 pytorch mnist 分类项目源码不会只放一个.ipynb文件,而是按职责拆成脚本。常见布局如下:
mnist_project/ ├── README.md ├── requirements.txt ├── config.py ├── data/ │ └── MNIST/ ├── models/ │ ├── __init__.py │ └── cnn.py ├── utils/ │ ├── __init__.py │ ├── dataset.py │ └── metrics.py ├── train.py ├── evaluate.py └── predict.pytrain.py负责训练并保存模型,evaluate.py负责在测试集上输出分类评估报告,predict.py接收单个图片路径并输出预测值。requirements.txt里固定核心依赖即可,不要把整个 conda 环境列表贴进去。大作业报告中解释每个文件的职责,比贴全部代码更能体现工程能力。
5.2 三个容易忽略的边界问题
第一个是验证集和测试集的混用。很多学生顺手把train=False的数据集既当验证集又当测试集,调参后又再测一次。合理做法是从原始训练集切出 10% 作为验证集,train=False作为测试集只在最终评估时使用。可以用torch.utils.data.random_split完成切分,但注意要让MNIST的train=True数据先经过Subset再传入 DataLoader。
第二个是模型处于训练模式就做预测。若在model.train()状态下直接调用分类,Dropout 会随机丢弃部分神经元,导致同一张图片每次预测结果不同。所有预测和评估前必须执行model.eval()并用torch.no_grad()包裹,前者固定 Dropout 行为,后者关闭梯度计算,降低显存占用。
第三个是设备适配问题。源码必须同时兼容 CPU 和 GPU,代码里不要出现images.cuda()这种硬编码,而是用images.to(device)。在predict.py中,加载训练好的模型后还要检查输入图片通道数和尺寸,否则灰度图或 RGBA 图会直接报错。可以在predict函数入口加一段灰度转换:
from PIL import Image import torchvision.transforms.functional as F def preprocess_image(path): img = Image.open(path).convert("L") img = img.resize((28, 28)) tensor = F.to_tensor(img) tensor = (tensor - 0.1307) / 0.3081 return tensor.unsqueeze(0)convert("L")将图片转成单通道灰度图,resize((28, 28))强行对齐输入尺寸,unsqueeze(0)在批次维增加一维,使张量形状成为[1, 1, 28, 28]。注意推理时要做和训练时完全一样的归一化操作,均值和标准差不能改动,否则模型的输入分布被破坏,准确率会明显下降。
本文还有配套的精品资源,点击获取