简介这份资源是面向深度学习初学者与课程作业需求者的PyTorch实战资料包围绕MNIST手写数字识别任务展开帮助读者掌握卷积神经网络基本原理与主流框架用法。内容以LeNet为例涵盖卷积层、池化层与全连接层的构建思路并演示如何调用GPU加速训练最终在测试集上达到98%及以上的准确率。压缩包共7个文件约264KB包含可直接运行的Python源码、带注释的主程序、实验报告文档以及训练过程截图与可视化素材便于对照代码理解每一步实现。目前已有2574人学习下载适合需要完成课程作业、巩固CNN基础或快速上手PyTorch项目的中低阶学习者。通过阅读源码与实验报告读者能够理清数据加载、模型定义、训练循环与评估流程并借鉴其中的注释与排错思路独立复现手写数字识别实验。1. 从一份「深度学习作业」说起手写数字识别到底在练什么很多人第一次接触深度学习都是从一份「基于 PyTorch 框架 Python 实现手写数字识别完整源码代码注释实验报告」的作业包开始的。它看起来平平无奇——不就是识别 0 到 9 吗但真正动手跑一遍就会发现这份作业几乎把深度学习入门要踩的坑全踩了一遍环境装不上、CUDA 和 PyTorch 版本对不上、MNIST 下载卡住、模型训练不收敛、准确率卡在 10% 不动。它解决的不是「识别数字」这个玩具问题而是让你完整走通「数据加载 → 模型定义 → 训练循环 → 评估保存 → 实验报告」这条链路。适合谁刚学完 Python、想入门深度学习的学生需要交课程设计作业的人以及想用最小成本验证自己环境是否可用的工程师。下面我按自己复现这类作业的真实顺序把每一步讲透。2. 环境搭建与版本对齐PyTorch 装不对后面全是白费2.1 为什么版本对齐比装包本身更重要手写数字识别这个任务本身对算力要求极低CPU 都能跑但新手翻车最多的地方恰恰是环境。PyTorch、Python、CUDA 三者有严格的对应关系装错一个torch.cuda.is_available()就返回 False或者直接 import 报错。我一般会先确定三件事Python 版本建议 3.93.11、是否用 GPU没有 NVIDIA 显卡就老老实实装 CPU 版、CUDA 版本用nvidia-smi看驱动支持的最高版本。很多人一上来就pip install torch结果装到最新版和教程里的 API 对不上代码跑不通就开始怀疑人生。常见做法是用 conda 建独立环境避免污染系统 Python。下面是我常用的建环境流程# 创建独立环境指定 Python 版本避免和系统环境冲突 conda create -n mnist python3.10 -y conda activate mnist # 查看显卡驱动支持的 CUDA 版本没有 GPU 可跳过 nvidia-smi # 安装 PyTorchCPU 版用下面这条 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 有 GPU 且 CUDA 11.8 的情况用这条 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118逻辑说明conda create建隔离环境是后悔药装崩了直接删掉重建不影响其他项目。--index-url指定官方 wheel 源比默认源快且版本明确。参数上cu118对应 CUDA 11.8如果你的驱动只支持到 11.7就换成cu117不要硬装高版本。装完必须验证import torch print(torch.__version__) # 确认版本号 print(torch.cuda.is_available()) # GPU 用户应为 TrueCPU 用户为 False 正常如果cuda.is_available()是 False 但你有显卡八成是装成了 CPU 版或者驱动太旧。这时候别急着重装系统先pip uninstall torch torchvision再按对应 CUDA 版本重装即可。2.2 MNIST 数据集加载与预处理的两个关键参数MNIST 是 28×28 的灰度图6 万张训练、1 万张测试。用torchvision.datasets.MNIST加载最省事但有两个参数必须注意transform和download。ToTensor()会把像素从 0-255 缩放到 0-1这一步不做模型输入量级太大训练会震荡。Normalize用 MNIST 的全局均值 0.1307 和标准差 0.3081这是官方统计值用了收敛更快。from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理转张量 标准化 transform transforms.Compose([ transforms.ToTensor(), # 像素归一化到 [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) # DataLoaderbatch_size 和 shuffle 是重点 train_loader DataLoader(train_set, batch_size64, shuffleTrue) test_loader DataLoader(test_set, batch_size1000, shuffleFalse)逻辑说明batch_size64是入门常用值太小训练慢太大显存吃紧且泛化略差。训练集shuffleTrue必须开否则模型会学到样本顺序的伪规律测试集shuffleFalse方便复现结果。downloadTrue第一次会联网下载如果卡住可以手动下载四个 idx 文件放到./data/MNIST/raw/目录下这是最常见的离线解法。3. 模型定义与训练循环CNN 比全连接强在哪代码怎么写3.1 为什么手写数字识别首选 CNN 而不是全连接全连接网络把 28×28 拉平成 784 维丢掉了像素的空间关系参数量还大。CNN 用卷积核在局部感受野上提取边缘、笔画特征参数共享对平移有一定鲁棒性。MNIST 上一个两层卷积的简单 CNN 就能轻松到 99% 以上而全连接通常卡在 97%98%。这不是玄学是卷积的归纳偏置更匹配图像任务。下面是我常用的网络结构import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 nn.Conv2d(1, 32, 3, 1) # 输入1通道输出32通道3x3卷积 self.conv2 nn.Conv2d(32, 64, 3, 1) # 第二层卷积 self.dropout1 nn.Dropout(0.25) # 防过拟合 self.dropout2 nn.Dropout(0.5) self.fc1 nn.Linear(9216, 128) # 64*12*129216 self.fc2 nn.Linear(128, 10) # 10 个类别 def forward(self, x): x F.relu(self.conv1(x)) # 28x28 - 26x26 x F.max_pool2d(x, 2) # 26x26 - 13x13 x F.relu(self.conv2(x)) # 13x13 - 11x11 x F.max_pool2d(x, 2) # 11x11 - 5x5实际取整为 5 x self.dropout1(x) x torch.flatten(x, 1) # 展平 x F.relu(self.fc1(x)) x self.dropout2(x) x self.fc2(x) return F.log_softmax(x, dim1) # 配合 NLLLoss逻辑说明Conv2d(1,32,3,1)中 1 是灰度图通道数32 是卷积核个数3 是核大小1 是步长。两次池化后特征图变成 5×5但代码里fc1输入写的是 9216对应 64×12×12这是很多教程的写法差异——实际取决于是否用 padding。我一般会在 forward 里加一句print(x.shape)确认展平维度避免维度不匹配报错。log_softmax配合NLLLoss是 PyTorch 官方 MNIST 示例的经典组合也可以用CrossEntropyLoss并去掉 log_softmax。3.2 训练循环里必须盯住的三个量训练循环看着简单但 loss、准确率、学习率这三个量决定了你能不能跑出结果。优化器选 Adam 还是 SGD入门我推荐 Adam学习率 1e-3收敛快、对超参不敏感想复现论文里的 99.5% 可以换 SGD momentum 0.9。下面是一个完整可跑的训练片段import torch from torch import optim device torch.device(cuda if torch.cuda.is_available() else cpu) model Net().to(device) optimizer optim.Adam(model.parameters(), lr1e-3) criterion torch.nn.NLLLoss() def train(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 criterion(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fEpoch {epoch} [{batch_idx*len(data)}/{len(train_set)}] loss{loss.item():.4f}) for epoch in range(1, 6): train(epoch)逻辑说明optimizer.zero_grad()必须放在反向传播前否则梯度会跨 batch 累加这是新手最常犯的错之一。model.train()和后面的model.eval()要成对出现影响 Dropout 和 BatchNorm 行为。loss.item()取标量别直接打印 tensor。跑 5 个 epoch测试集准确率通常能到 99% 左右。如果 loss 一直不降先检查数据是否标准化、标签是否对得上、学习率是否过大。4. 评估、保存与实验报告让结果可复现、可交付4.1 测试集评估的正确姿势与模型保存评估阶段最容易犯的错是忘了model.eval()和torch.no_grad()导致 Dropout 还在随机丢神经元、显存白白占用。正确写法是def test(): model.eval() correct 0 with torch.no_grad(): # 关闭梯度省显存提速 for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) pred output.argmax(dim1) # 取概率最大的类别 correct pred.eq(target).sum().item() acc 100. * correct / len(test_set) print(fTest Accuracy: {acc:.2f}%) return acc test() torch.save(model.state_dict(), mnist_cnn.pt) # 只存参数体积小逻辑说明argmax(dim1)在类别维度取最大值索引。state_dict()只保存权重加载时先实例化Net()再load_state_dict。别用torch.save(model)存整个模型换环境容易加载失败。准确率打印保留两位小数方便写进实验报告。4.2 实验报告该写什么才算完整一份能交差的实验报告核心是「可复现」。我一般包含环境版本表、数据集说明、模型结构图文字描述即可、超参表、训练 loss 曲线、测试准确率、错误样本分析。下面这张表是我常用的超参记录格式参数取值说明batch_size64训练批大小learning_rate1e-3Adam 初始学习率epochs5训练轮数optimizerAdam优化器dropout0.25/0.5两层丢弃率test_acc~99%测试集准确率错误样本分析是加分项把预测错的图挑出来看往往是书写潦草或标注有争议的样本写进报告能体现你真的分析了模型行为而不是只贴了个准确率数字。5. 避坑与排查那些让准确率卡在 10% 的坑5.1 现象训练 loss 不降准确率约 10%原因标签和输出维度对不上或者忘了zero_grad导致梯度爆炸。10% 正好是 10 分类瞎猜的概率。解决打印一个 batch 的target看范围是否在 0-9检查fc2输出是否为 10确认zero_grad在backward之前。5.2 现象MNIST 下载卡住或报 HTTP 错误原因官方源网络不稳定。解决手动下载train-images-idx3-ubyte.gz等四个文件放到./data/MNIST/raw/再运行代码会自动解压。别反复重试浪费时间。5.3 现象cuda.is_available()为 False原因装了 CPU 版或驱动与 CUDA 版本不匹配。解决pip uninstall torch torchvision后按nvidia-smi显示的 CUDA 版本重装对应 wheel。实在搞不定就用 CPU 跑MNIST 用 CPU 也就几分钟一轮。5.4 现象测试准确率远低于训练准确率原因过拟合或者测试集忘了eval()。解决加 Dropout、减少 epoch确认测试时调用了model.eval()。MNIST 上过拟合不常见多半是评估代码写错。5.5 现象展平维度报错mat1 and mat2 shapes cannot be multiplied原因卷积输出尺寸算错fc1输入维度和实际不符。解决在flatten前打印x.shape按实际值改fc1。这是血泪经验改一次记一辈子。6. 进阶技巧把准确率从 99% 推到 99.5% 的几个手段入门跑通 99% 之后想再往上抠靠的是数据增强和训练策略。我一般会加随机旋转和小幅平移模拟手写差异train_transform transforms.Compose([ transforms.RandomAffine(degrees10, translate(0.1, 0.1)), # 随机旋转±10度、平移10% transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])逻辑说明RandomAffine只在训练集用测试集保持原样。旋转角度别太大MNIST 数字旋转 30 度以上语义就变了反而掉点。配合学习率调度器效果更稳scheduler optim.lr_scheduler.StepLR(optimizer, step_size1, gamma0.7) # 每个 epoch 后调用 scheduler.step()StepLR每轮把学习率乘 0.7后期微调更精细。另一个技巧是集成训练 3 个不同初始化的模型预测时取平均准确率能再涨 0.10.2 个百分点代价是推理变慢。验证方法很简单固定随机种子跑三次取均值别只看一次结果就下结论。我自己踩过的最大坑是盲目堆 epoch跑到 20 轮测试准确率反而降了后来才明白 MNIST 这种简单任务 58 轮足够多了就是过拟合。希望帮到你。本文还有配套的精品资源点击获取