☰
PyTorch实战:手写数字识别从环境搭建到99.5%准确率完整指南
2026/10/2 7:49:51 网站建设 项目流程

简介:这份资源是面向深度学习初学者与课程作业需求者的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.9~3.11)、是否用 GPU(没有 NVIDIA 显卡就老老实实装 CPU 版)、CUDA 版本(用nvidia-smi看驱动支持的最高版本)。很多人一上来就pip install torch,结果装到最新版,和教程里的 API 对不上,代码跑不通就开始怀疑人生。

常见做法是用 conda 建独立环境,避免污染系统 Python。下面是我常用的建环境流程:

# 创建独立环境,指定 Python 版本,避免和系统环境冲突 conda create -n mnist python=3.10 -y conda activate mnist # 查看显卡驱动支持的 CUDA 版本(没有 GPU 可跳过) nvidia-smi # 安装 PyTorch:CPU 版用下面这条 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 用户应为 True,CPU 用户为 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', train=True, download=True, transform=transform) test_set = datasets.MNIST(root='./data', train=False, download=True, transform=transform) # DataLoader:batch_size 和 shuffle 是重点 train_loader = DataLoader(train_set, batch_size=64, shuffle=True) test_loader = DataLoader(test_set, batch_size=1000, shuffle=False)

逻辑说明:batch_size=64是入门常用值,太小训练慢,太大显存吃紧且泛化略差。训练集shuffle=True必须开,否则模型会学到样本顺序的伪规律;测试集shuffle=False方便复现结果。download=True第一次会联网下载,如果卡住,可以手动下载四个 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*12=9216 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, dim=1) # 配合 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(), lr=1e-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(f'Epoch {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(dim=1) # 取概率最大的类别 correct += pred.eq(target).sum().item() acc = 100. * correct / len(test_set) print(f'Test Accuracy: {acc:.2f}%') return acc test() torch.save(model.state_dict(), 'mnist_cnn.pt') # 只存参数,体积小

逻辑说明:argmax(dim=1)在类别维度取最大值索引。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(degrees=10, 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_size=1, gamma=0.7) # 每个 epoch 后调用 scheduler.step()

StepLR每轮把学习率乘 0.7,后期微调更精细。另一个技巧是集成:训练 3 个不同初始化的模型,预测时取平均,准确率能再涨 0.1~0.2 个百分点,代价是推理变慢。验证方法很简单,固定随机种子跑三次取均值,别只看一次结果就下结论。我自己踩过的最大坑是盲目堆 epoch,跑到 20 轮测试准确率反而降了,后来才明白 MNIST 这种简单任务 5~8 轮足够,多了就是过拟合。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询