☰
PyTorch CIFAR-10图像识别实战:从环境搭建到模型训练全解析
2026/9/26 10:00:17 网站建设 项目流程

简介:基于PyTorch框架的CIFAR-10图像识别方案,面向机器学习初学者与计算机视觉入门者,解决如何用卷积神经网络完成图像分类任务的问题。压缩包共5个文件,包含2个Python脚本、1个已训练模型权重、1个数据元信息文件和1份说明文档,整体大小7.15MB,结构精简便于快速上手。已有52人学习下载。两个Python脚本分别覆盖CNN网络结构定义、损失函数与优化器配置、完整训练流程,以及CIFAR-10数据集的加载、标准化与增强预处理;已训练权重文件可跳过训练直接用于预测或微调;说明文档则给出项目结构、使用方法和常见问题解答。通过实际操作,读者能掌握从数据处理到模型训练与部署的全流程,为后续深度学习研究打下坚实基础。

1. 基于 PyTorch 的 CIFAR-10 图像识别:你拿到的 zip 里到底装着什么

第一次跑深度学习图像识别的人,多数不是从论文开始,而是从别人打包好的项目开始。比如这个“基于 PyTorch 的 CIFAR-10 图像识别”项目包:数据集是 10 类、60000 张 32×32 的彩色小图,模型用 PyTorch 搭建,训练完能区分飞机、汽车、鸟、猫等常见物体。它解决的是入门阶段最典型的四件事——数据怎么读、网络怎么搭、损失怎么算、权重怎么存。适合两类人:一是刚装完 PyTorch,想跑通第一个图像识别任务的新手;二是要做课程实验或算法对比,需要一个干净基准的从业者。下文不逐行解读某个现成 zip,而是把这一类项目从零写到跑通,并标出哪些位置最容易翻车。

2. 把 CIFAR-10 请进 PyTorch:环境搭建与数据加载

2.1 安装 PyTorch:先定版本,再跑命令

很多入门项目跑不起来,第一步就死在环境组合上。CIFAR-10 识别本身不挑显卡,但 PyTorch 基础框架与 torchvision 的版本必须配套,否则最常见的报错就是torchvision里找不到datasets,或者torch与torchvision的编译版本冲突。

如果你用 Anaconda 配置 PyTorch 环境,我建议先建一个独立环境,不要直接装进 base:

# conda 创建独立环境,Python 3.10 起步比较稳 conda create -n cifar10 python=3.10 -y conda activate cifar10 # 不折腾 GPU 时,先装 CPU 版把逻辑跑通 pip install torch torchvision

这段命令有两个值得注意的参数。python=3.10是整个环境的基础解释器版本,PyTorch 对 3.10 的支持已经非常成熟,而 3.12、3.13 在某些旧版本 torchvision 上会出现二进制包缺失的问题。-y表示跳过 conda 的二次确认,交互式安装容易中断自动化脚本,加上它更省事。

如果你是 NVIDIA GPU 用户,装完nvidia-smi能看到驱动支持的 CUDA 版本,再到 PyTorch 官网 Install 页复制对应 cu 版本生成的命令。常见做法是用 pip 加--index-url参数指定 cu118 或 cu121 的 wheel 目录,命令形式大致是pip install torch torchvision --index-url ...whl/cu121。这里不要直接装默认的torch,因为没指定源的官方 pip 包在多数平台上是 CPU 版,装上之后torch.cuda.is_available()永远是 False。

装完先花十秒验证环境,这个操作值得养成习惯:

import torch print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) if torch.cuda.is_available(): print("gpu:", torch.cuda.get_device_name(0)) else: print("当前设备只能跑 CPU,训练会慢一些")

这段代码第一行打印 PyTorch 版本,能顺便确认你装的是 CPU 版还是 GPU 版。第二行cuda available是判断环境是否可用的金标准,它返回 True 才说明 CUDA 工具链和驱动都对上了。最后一行把设备名也打出来,方便确认是不是你要用的那张卡。如果输出显示cuda available: False,不要急着重装系统,先用 CPU 把项目跑通,再回头查 PyTorch 版本与 CUDA 驱动是否匹配。

2.2 torchvision 加载 CIFAR-10:transform 与下载

CIFAR-10 数据本身不大,压缩包大约一百多兆,torchvision 的datasets.CIFAR10接口会自动下载并解析成PIL.Image。新手最容易漏掉的是 transform:直接把 PIL 图像喂给网络会报类型错误,必须先用ToTensor()把图像转成torch.FloatTensor,再做标准化。

from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集和测试集用不同的 transform train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) train_set = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform) test_set = datasets.CIFAR10(root='./data', train=False, download=True, transform=test_transform) print(len(train_set), len(test_set))

这里先拆开说数据集的三个参数。root='./data'指定数据存放目录,第一次运行会新建data/文件夹并下载压缩包,之后再次运行直接读本地文件,不会再走网络。train=True表示加载 50000 张训练图,train=False表示加载 10000 张测试图,这个标志位决定了数据集内部的文件名映射,传错会导致类别标签对不上。download=True是在本地没有数据时自动下载,如果你在一个无法访问官方源的网络环境里,可以先手动下载 CIFAR-10 的压缩包放进root对应目录,再把download设为 False。

transform 里的RandomCrop(32, padding=4)和RandomHorizontalFlip()是数据增强,只在训练集上用。CIFAR-10 的图像本来就是 32×32,RandomCrop 先向外填充 4 像素再随机裁剪回 32×32,相当于让模型每次看到同一张图的不同位置。Normalize的四个数字是 CIFAR-10 数据集的全局均值和标准差,PyTorch 官方和各种开源项目都沿用这一组数值,不建议自己重算。

2.3 DataLoader 参数怎么调:batch_size、num_workers 与 pin_memory

数据集对象只是把图片和标签按索引管理起来,真正喂给模型的是 DataLoader。它内部会做采样、打包、多进程预读,参数设置直接影响训练速度和显存占用。

train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2, pin_memory=True, drop_last=True) test_loader = DataLoader(test_set, batch_size=128, shuffle=False, num_workers=2, pin_memory=True, drop_last=False)

batch_size=128是个比较平衡的起点:显存占用适中,训练速度也够快。显存小于 4G 时降到 64,大于 8G 时可以上调到 256,但要注意后续学习率也需要跟着调。shuffle=True只在训练集打开,目的是让每个 epoch 的样本顺序不同,避免模型学到批次内的顺序假象。num_workers是数据预读的进程数,Linux 下可以开到 4 或 8,Windows 下建议保守一点设成 2,具体原因在第 5 章会展开讲。pin_memory在 GPU 训练时建议开启,它让数据在内存中的存放方式更适合拷贝到显存,能省一小段拷贝时间;纯 CPU 训练开不开无所谓。drop_last=True在训练时丢掉最后不足一个 batch 的样本,防止 batch 大小不一致导致 BatchNorm 层统计量抖动。

2.4 训练前先看数据形状

下载完不要急着开训,先取一个 batch 打印形状。这一步能提前暴露八成以上的低级错误,比如图像通道顺序、标签类型、数据归一化范围。

images, labels = next(iter(train_loader)) print(images.shape) # 期望输出 torch.Size([128, 3, 32, 32]) print(labels.shape) # 期望输出 torch.Size([128]) print(labels.dtype) # 期望输出 torch.int64 print(images.min().item(), images.max().item()) # 期望约 -2 到 2

images.shape的四维顺序是[batch, channel, height, width],在 PyTorch 里必须是这个顺序,如果你把形状看成[128, 32, 32, 3]就得检查前面 transform 的处理流程。labels.dtype必须是torch.int64,因为后面要直接传给交叉熵损失函数,如果它是torch.int32或torch.long之外的类型,需要先做.long()转换。images.min()和max()验证归一化是否正确,正常输出在 -2 到 2 之间;如果还是 0 到 1,说明 Normalize 没有生效,典型原因是 transform 列表里把ToTensor()写在了 Normalize 后面。

3. 模型怎么写:从手写 CNN 到 ResNet 的尺寸推导

3.1 全连接网络为什么不适合 CIFAR-10

很多人拿到 CIFAR-10 的第一反应是:图像是 32×32×3,展平后 3072 维,接几个全连接层不就行了?从数学上讲没问题,但实际效果会差一大截。原因不在数据量,而在全连接层的结构假设——它把每个像素都当成独立特征,忽略了“相邻像素通常属于同一物体”这个空间规律。

我用一个具体例子说明。假设第一层Linear(3072, 512),权重大小是 3072×512,约 157 万个参数。其中大部分参数在拟合像素间的随机关系,而卷积层通过共享权重把参数规模压到很小,同时强制模型学习局部模式。这也就是深度学习图像识别里默认使用 CNN 的核心原因。

当然,全连接网络也不是完全不能用。如果你把学习率调得很低、训练足够久、加上正则化,仍然能跑到 50% 左右的准确率,但相同时间下 CNN 能到 80% 以上。这就是结构带来的收益,不靠堆参数量能弥补。

3.2 一个能直接跑的 SimpleCNN

最精简的 CNN 结构是“卷积 + 激活 + 池化”重复两次,再接全连接分类器。下面这个网络是我在 CIFAR-10 项目里常用的最小版本:

import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 128), nn.ReLU(inplace=True), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))

nn.Conv2d(3, 32, kernel_size=3, padding=1)的含义是:输入通道 3,输出通道 32,卷积核 3×3,padding 为 1 时输入输出尺寸不变。第一层卷积后特征图是 32×32×32,经过MaxPool2d(2)变成 16×16;第二层卷积后是 16×16×64,再池化变成 8×8×64。所以分类器里Linear的输入维度是64 * 8 * 8,这个数字是前面所有层的空间尺寸和通道数共同决定的,改网络时最容易在这里算错。

nn.Flatten()把[batch, 64, 8, 8]展平成[batch, 4096],然后经过两个全连接层输出 10 个 logits。注意 forward 里不要写torch.softmax,因为交叉熵损失函数内部已经包含 Softmax 计算,网络只管输出未归一化的 logits,否则训练时会出现梯度不稳定。

3.3 用 BatchNorm 和 AdaptiveAvgPool 提升稳定性

SimpleCNN 能跑,但训练时对学习率很敏感。我一般会在每个卷积后面加一个BatchNorm2d,并顺手把分类器改成全局平均池化。这两处改动几乎不增加多少训练时间,却能让收敛稳定很多。

class BetterCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, num_classes), ) def forward(self, x): return self.classifier(self.features(x))

BatchNorm2d的作用是在每个 batch 内对通道维做标准化,让激活值维持在均值为 0、方差为 1 的区间附近。它的直接好处是你可以把学习率调大而不太担心梯度爆炸,在 CIFAR-10 这种小数据集上效果尤其明显。需要注意的是,model.train()和model.eval()切换会让 BatchNorm 走不同的计算路径,推理时少写model.eval()是后面要避的高频坑。

AdaptiveAvgPool2d(1)把任意尺寸的特征图压成 1×1,替代手工计算 64×8×8。这样网络不再对输入尺寸做硬编码,以后换大分辨率输入或改池化层位置时,不需要再回头改Linear的输入维度。代价是全局平均池化会丢掉一部分空间细节,但 CIFAR-10 图像本来就小,这个取舍完全合理。

3.4 换成 torchvision 自带的 ResNet18 要改哪两处

当你已经跑通 SimpleCNN 或 BetterCNN,想让验证准确率再上一个台阶,最省力的做法是直接换用 torchvision 里的 resnet18。它带预训练权重,结构上比手写 CNN 深得多,但代码改动只需要小调两处:

from torchvision.models import resnet18 model = resnet18(num_classes=10) # 第一处:把 7x7 的大卷积核换成 3x3,因为 CIFAR-10 图像只有 32x32 model.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) # 第二处:去掉 maxpool,防止特征图被压到 8x8 以下 model.maxpool = nn.Identity()

resnet18 原始设计面向 ImageNet 的 224×224 输入,第一层使用 7×7 且 stride 为 2 的卷积。CIFAR-10 只有 32×32,7×7 卷积核会直接损失大量边缘信息,因此必须换成 3×3、stride 为 1。nn.Identity()是占位层,它不执行任何操作,作用只是把模型原来的 maxpool 替换掉。如果这两处不改,训练过程会明显更慢,最终准确率也上不去。

使用预训练权重时还有一个细节:resnet18(pretrained=True)只加载在 ImageNet 上预训练的参数,而你换掉的conv1是随机初始化的。常见做法是先冻结除conv1和fc之外的所有层,微调几个 epoch 后再全量训练。这个策略在 CIFAR-10 上效果不错,我在第 6 章会再给一段可直接用的微调思路。

4. 训练脚本与调参:把验证集准确率从 60% 拉到 90%

4.1 损失函数与优化器:先选 SGD 还是 Adam

模型搭好之后,训练脚本的核心是三个选择:损失函数、优化器、学习率调度器。CIFAR-10 是单标签多分类任务,损失函数直接选nn.CrossEntropyLoss(),它内部把LogSoftmax和NLLLoss合在一起,所以网络输出 logits 即可。

优化器才是分歧点。PyTorch 入门资料里 Adam 出场率最高,因为它在很多任务上“省心”:学习率稍微设大点也能收敛,适合快速验证模型有没有写对。但 Adam 在 CIFAR-10 上训练较久后容易停在次优解,验证集准确率的峰值通常比 SGD 加动量之后低一两个点。

我自己的经验是分两步走。先拿少量数据用 Adam 跑 10 个 epoch,确认 loss 能稳定下降,这一步排查代码错误最快;确认没问题后,正式训练改用 SGD:

import torch.nn as nn from torch.optim import SGD from torch.optim.lr_scheduler import CosineAnnealingLR criterion = nn.CrossEntropyLoss() optimizer = SGD(model.parameters(), lr=0.02, momentum=0.9, weight_decay=5e-4) scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-4)

SGD的lr=0.02配合momentum=0.9是我在 CIFAR-10 上试过的比较稳的起点。如果你的 batch_size 设为 256,学习率可以提高到 0.04;batch_size 降到 64 则建议回到 0.01,这个缩放规则叫线性缩放法则,核心思想是 batch 越大梯度越稳定,可以承担更大的学习率。weight_decay=5e-4是 L2 正则,抑制过拟合,在 CIFAR-10 这种小数据集上必备。

CosineAnnealingLR让学习率从初始值沿余弦曲线下降到eta_min,相比每 30 个 epoch 减半的 StepLR,它没有“突变”节点,训练后期更容易在局部极小值附近精细收敛。T_max=100表示一个完整余弦周期是 100 个 epoch,如果你的训练轮数不是 100,记得同步修改。

4.2 训练循环与验证循环的完整写法

PyTorch 没有封装好训练循环,需要自己写。下面这份代码我做了注释,可以直接替换进项目模板:

import torch from tqdm import tqdm # 可选,进度条 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = BetterCNN().to(device) best_acc = 0.0 num_epochs = 100 for epoch in range(num_epochs): # 训练阶段 model.train() running_loss = 0.0 for images, labels in tqdm(train_loader, desc=f"Epoch {epoch+1}"): images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) epoch_loss = running_loss / len(train_loader.dataset) # 验证阶段 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total print(f"epoch {epoch+1:3d} | loss {epoch_loss:.4f} | acc {acc:.4f}") if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "cifar10_best.pt")

关键点有三个。第一,model.train()与model.eval()一个都不能少,前者开启 BatchNorm 和 Dropout 的训练行为,后者关闭,混用会让验证结果忽高忽低。第二,梯度清零optimizer.zero_grad()要放在backward之前,很多人把zero_grad放在step之后,导致梯度跨 batch 累加,loss 曲线会周期性抖动。第三,验证阶段用torch.no_grad()包裹,不构建计算图,显存占用和推理速度都会明显改善。

torch.max(outputs, 1)返回两个值,第一个是最大值,第二个是最大值所在的索引,也就是预测类别。labels.size(0)是当前 batch 的样本数,累加到total再算正确率。

4.3 准确率爬升路线:增强、学习率与训练轮数

很多新手跑 CIFAR-10 第一轮只拿到 60% 到 70% 的准确率,就说“模型不行”。实际上 60% 多半是没做数据增强或训练轮数不够。我把常用的提升路线按成本从低到高列一遍:

第一步,确认数据增强已经加到训练集。上一节给出的RandomCrop和RandomHorizontalFlip是最低配置,能直接把准确率拉高 5 个点左右。更进一步可以用transforms.ColorJitter(brightness=0.2, contrast=0.2)做颜色扰动,或者用RandomErasing模拟遮挡。

第二步,把训练轮数从 30 加到 100,配合余弦退火。CIFAR-10 的个体差异很小,模型需要多轮训练才能把决策边界磨得足够细。我在 BetterCNN 上从 30 轮加到 100 轮,验证集准确率能再涨 3 到 4 个点。

第三步,如果还是卡在 80%,换模型。BetterCNN 的容量在 80% 以上的区域会明显吃力,这是结构天花板,不是调参能解决的。换成第 3.4 节的 ResNet18,配合同样的增强和训练策略,很容易到 90% 以上。

下面这张表是我在同类配置下实测的参考区间,不同机器、不同随机种子会有波动,但量级可以参考:

配置验证集典型准确率说明
SimpleCNN,无增强,30 epoch60% - 65%只验证流程用
BetterCNN,加增强,100 epoch78% - 83%手写 CNN 的合理终点
ResNet18,加增强,100 epoch88% - 92%想上 90% 的稳妥路径

最后一行的 ResNet18 可以加载 ImageNet 预训练权重,也可以从零训练。从零训练在 CIFAR-10 上也能到 90% 上下,但需要更久的长训练和更精细的调参;加载预训练权重则聊胜于无。对我而言,做课程实验或复现论文基线,都是从零训练居多,因为预训练权重会引入 ImageNet 的分布偏置,影响对小模型的公平评判。

5. 复现 CIFAR-10 项目常见的 5 个坑:现象、原因、处理

5.1 Loss 停在 2.3 附近,验证准确率约 10%

现象:训练了好几个 epoch,loss一直停留在 2.3 左右,验证集准确率始终在 10% 附近打转,相当于随机猜。

原因:CIFAR-10 是 10 分类,随机猜测的交叉熵正好是ln(10) ≈ 2.3026。loss 卡在这个值说明网络完全没有学到有效特征。最常见的原因是学习率设置过大,导致梯度在参数空间里剧烈震荡却始终到不了局部极小点;其次是数据没有 shuffle,每个 batch 内都是同一类图像,梯度方向被扭曲。

解决:先用一个 batch 做“过拟合测试”,这是我在所有项目里的第一个诊断动作。从训练集里取一个 batch,让模型反复跑同一批数据,如果 loss 能降到很小,说明代码链路没问题,问题出在训练策略:

overfit_loader = DataLoader(train_set, batch_size=64, shuffle=True) model = BetterCNN().to(device) opt = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9) model.train() for _ in range(20): for x, y in overfit_loader: x, y = x.to(device), y.to(device) opt.zero_grad() loss = criterion(model(x), y) loss.backward() opt.step() if _ % 5 == 0: print(f"overfit step {_}, loss {loss.item():.4f}")

如果这个loss在 20 步内没有明显下降,优先检查数据流水线:图像通道顺序、标签是否错位、Normalize 参数是否生效。如果 loss 能下降到 0.5 以下,再把学习率从 0.02 调低到 0.001 重新训练。经验上,Adam 的初始学习率从 0.001 起,SGD 从 0.01 到 0.02 起,都不要第一轮就开 0.1。

5.2 训练集接近 100%,验证集 50% 上下

现象:训练 loss 一路降到 0.1 以下,训练集准确率接近 100%,但验证集准确率只有 50% 左右。

原因:这是非常典型的过拟合。CIFAR-10 训练集只有 50000 张图,模型容量一大就很容易把训练样本的噪声也记下来,验证集没见过这些噪声,准确率自然低。

解决:优先检查数据增强是否只加在了训练集而不是测试集;再检查weight_decay是不是忘了加;最后考虑缩小模型容量。按我的经验,三者的优先级是“数据增强 > weight_decay > 减模型”。如果你已经在用 BetterCNN,且weight_decay=5e-4,但验证集仍然上不去,可以把模型中后两层卷积的通道数从 64 降到 48,代价是训练时间略增但泛化会好。另一种更省事的方案是换用resnet18(pretrained=True)做迁移学习,因为在 ImageNet 上预训练的模型自带良好的底层特征,微调时不容易在 CIFAR-10 上过拟合。

5.3 CUDA out of memory

现象:训练刚开始就报RuntimeError: CUDA out of memory,显存直接被占满。

原因:最常见的是 batch_size 太大,其次是输入图像尺寸意外变大,比如你原本以为 32×32,但 transform 里某个操作把它变成了 224×224,显存需求就不是差一点了。

解决:第一步把batch_size从 128 降到 64 或 32,显存占用几乎线性下降。第二步检查 transform 输出尺寸:

sample = transform(train_set[0][0]) print(sample.shape) # 必须是 torch.Size([3, 32, 32])

如果尺寸不是 3×32×32,去检查 transform 列表里有没有混入Resize((224, 224))。第三步是清理不再使用的变量,尤其是在循环里保存了整轮的输出特征,比如为了可视化特征图把outputs一直留在内存里。显存只会在torch.cuda.empty_cache()被调用或进程结束时释放,但手动调用empty_cache()只清空缓存块,无法回收仍被变量引用的显存,所以根治手段是del不再需要的大张量。

5.4 Windows 下 DataLoader 多进程报错

现象:在 Windows 上运行带num_workers > 0的脚本,直接抛RuntimeError: DataLoader worker (pid...) exited unexpectedly。

原因:Windows 没有 Linux 的 fork 机制,DataLoader 的多进程需要重新导入主模块。如果脚本没有放到if __name__ == '__main__'保护内部,每个 worker 进程会递归重新执行整个文件,包括创建 DataLoader 自身,导致死循环或崩溃。

解决:把训练入口包在保护内。我给出的标准写法是:

if __name__ == '__main__': # 所有 DataLoader 和多进程相关代码都放这里 train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2) test_loader = DataLoader(test_set, batch_size=128, shuffle=False, num_workers=2) # 然后调用训练函数

另外,Windows 下num_workers=0是零出错选项,代价是数据加载变慢,训练时 GPU 会有一段时间处于等待状态。如果你只是跑几百个 epoch 的小实验,把num_workers设为 2 并加上入口保护基本够用。还有一个容易忽略的细节:有些 IDE 的交互式控制台里根本不存在__main__判断,所以第一种排查方式是在命令行下执行脚本文件而不是在 Spyder/Jupyter 里直接运行。

5.5 没写 model.eval(),BatchNorm 层拖垮推理结果

现象:训练时验证集准确率 90%,但单独跑测试集或对单张图推理时,准确率明显下降,甚至同类图多次推理结果不一样。

原因:模型里带 BatchNorm 时,model.eval()和model.train()模式下,BatchNorm 的计算路径完全不同。训练模式下它使用当前 batch 的均值和方差,验证和推理时必须切换到使用训练阶段累积的全局统计量。如果推理前忘了调用model.eval(),BatchNorm 仍然在用单张图的临时均值和方差,数值自然不对。

解决:固定推理入口,在加载权重之后立刻加一行:

model = BetterCNN() model.load_state_dict(torch.load("cifar10_best.pt", map_location="cpu")) model.eval()

注意map_location="cpu"是另一个常踩的坑。如果你的权重是在 GPU 上保存的,换到没有 GPU 的机器或换了 CUDA 版本,加载时会报设备不匹配,加map_location可以强制映射到 CPU 再重新迁移。model.eval()之后,后面所有层的 Dropout 也会被关闭,这两层逻辑都归这个接口管。

6. 从实验到可交付:checkpoint 保存、推理脚本与一键验证

6.1 保存与加载完整 checkpoint

训练脚本里我只保存了state_dict,它只含权重,不含优化器状态。如果你要中途恢复训练,最好把优化器和学习率调度器的状态一起存下来,避免重新来过:

checkpoint = { 'model_state': model.state_dict(), 'optimizer_state': optimizer.state_dict(), 'scheduler_state': scheduler.state_dict(), 'epoch': epoch, 'best_acc': best_acc, } torch.save(checkpoint, "cifar10_checkpoint.pt")

恢复时按相反顺序取:

ckpt = torch.load("cifar10_checkpoint.pt", map_location="cpu") model.load_state_dict(ckpt['model_state']) optimizer.load_state_dict(ckpt['optimizer_state']) scheduler.load_state_dict(ckpt['scheduler_state']) start_epoch = ckpt['epoch'] + 1

这是做项目包交付时的习惯:让使用者能从任意 checkpoint 继续训练,而不是每次都从第 0 个 epoch 开始。

6.2 推理脚本与一键验证准确率

纯推理脚本的核心只有四行:加载模型、加载权重、model.eval()、torch.no_grad()。下面这段把推理和准确率统计合在一起:

# inference.py import torch from torchvision import datasets, transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) test_set = datasets.CIFAR10(root='./data', train=False, download=False, transform=transform) test_loader = torch.utils.data.DataLoader(test_set, batch_size=128, shuffle=False, num_workers=0) model = BetterCNN().eval() model.load_state_dict(torch.load("cifar10_best.pt", map_location="cpu")) correct = 0 with torch.no_grad(): for images, labels in test_loader: outputs = model(images) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() print(f"test acc: {correct / len(test_set):.4f}")

outputs.argmax(dim=1)与训练里的torch.max(outputs, 1)等价,都是取每个样本预测概率最高的类别。这个脚本解决最后一公里的交付问题:项目包使用者不必重新跑训练,也能验证权重文件是否与模型结构匹配。

6.3 换用预训练 ResNet 做迁移学习的一条捷径

最后补一个提高上限的实操技巧。如果你的项目目标不止 90%,而是想看看 CIFAR-10 能做到多高,路径是“预训练 ResNet + 双线性插值放大”。图像从 32×32 放大到 224×224,输入进标准的 ImageNet 版 resnet18,微调后准确率通常能到 95% 上下:

from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 10) # 冻结前面所有层,只训练最后的全连接层 for name, param in model.named_parameters(): if 'fc' not in name: param.requires_grad = False

这里把fc换成 10 类输出,冻结前面所有层,先用 5 个 epoch 让分类头收敛,再解开冻结全量微调 20 个 epoch。这个策略在我的多次实验里准确率比从零训练一个深网络更稳,因为我现在的习惯是任何 PyTorch 项目到手,先跑通一次单 batch 过拟合,再把训练脚本挂到后台。CIFAR-10 这个量级,一天之内从拿到 zip 到跑出可交付的测试结果完全可行,关键是别在环境上恋战,也别跳过基线验证。希望这篇基于 PyTorch 的 CIFAR-10 图像识别实战拆解能帮到你。

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

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

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

立即咨询