☰
CIFAR10图像分类实战:用PyTorch构建CNN模型完整指南
2026/10/4 8:30:47 网站建设 项目流程

简介:CIFAR-10数据集详解与卷积神经网络图像分类训练资料,面向深度学习初学者和需要快速构建图像识别任务的开发者,可从中掌握标准数据集加载、CNN模型搭建及训练评估流程。压缩包内含10个文件,约162.4MB,包含Python训练脚本、H5格式的模型权重、batches.meta元数据,以及data_batch_1至data_batch_5和test_batch六个数据批次;其中数据按5个训练批次和1个测试批次划分,每批10000张图像,结构清晰、兼容常见加载方式,便于直接复用。目前已有1448人学习下载。资料附赠完整可运行的CNN训练代码(含模型定义、训练循环与评估逻辑)和已训练好的模型文件,免去重复训练步骤,可直接调用进行分类测试;同时记录训练与测试过程中损失函数和准确率的变化,最后一轮测试准确率约0.6839,可作为调参、数据增强或更换网络结构时的性能对照基准。整体内容精炼,适合作为图像分类入门实践与实验参考。

1. CIFAR10 数据集与卷积神经网络:为什么这个小数据集是图像分类的试金石

CIFAR10 数据集几乎是每个入门卷积神经网络的人绕不开的第一个坎:32×32 的彩色小图、10 个类别、6 万张样本,规模小到普通显卡几分钟就能训练一轮,又真实到分类难度足以把各种“玄学调参”打回原形。这篇文章给你一套可以直接照抄的完整代码——用 PyTorch 写一个能在 CIFAR10 上达到 80% 左右准确率的图像分类模型,并附上训练好的模型文件加载方式,下载权重后即可直接对单张图片做推理。适合刚学完深度学习基础、想用一个小项目把 CNN 从数据加载到训练再到部署全流程走通的从业者。

2. CIFAR10 数据集拆解与 PyTorch 加载:先搞清楚你在训练什么

2.1 CIFAR10 的构成:10类、6万张图、32×32 像素意味着什么

CIFAR10 由 Alex Krizhevsky 等人从 8000 万张小图数据集里挑选整理而来,全称是 Canadian Institute for Advanced Research 的 10 类子集。它包含 60000 张 32×32 的 RGB 彩色图像,分成 50000 张训练图和 10000 张测试图,类别覆盖飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。每个类别恰好 6000 张,训练集和测试集里各占 5000 和 1000,类别分布均匀,这让准确率指标天生就有明确的可比性——任何超过 10% 的结果都说明模型学到了一点东西。

32×32 意味着什么?一张图只有 1024 个像素,每个通道 32×32。相比 ImageNet 的 224×224,这个分辨率低得惊人,但你别小看它——猫和狗的区别、汽车和卡车的区别,在 32×32 下往往只有几个像素的纹理差异,肉眼都容易看错。这也是为什么 CIFAR10 至今仍是测试 CNN 结构设计能力的标准数据集:模型必须有足够的特征提取能力,又不能在 6 万张图上轻易过拟合。很多在 CIFAR10 上有效的训练技巧,比如数据增强、批归一化、学习率衰减,迁移到大模型场景时依然是基本功。

2.2 PyTorch 自带数据集加载:三行代码拿到训练集和测试集

PyTorch 的 torchvision.datasets 模块内置了 CIFAR10 的下载和解析,不需要自己写数据加载器。下面是加载训练集和测试集的最简代码:

import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4, pin_memory=True) test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False, num_workers=4, pin_memory=True)

逻辑说明:CIFAR10 的 download=True 会在 root 目录下自动创建 data 文件夹,首次运行时会从服务器下载压缩包,解压后生成 cifar-10-batches-py 目录。transform 里的 ToTensor 会把 PIL 图像从 0~255 的 uint8 转成 0~1 的 float32 张量,Normalize 再用 CIFAR10 数据集的均值 (0.4914, 0.4822, 0.4465) 和标准差 (0.2470, 0.2435, 0.2616) 做标准化,这一步是所有训练能快速收敛的前提。很多人训练时把 Normalize 漏掉,像素值范围变成 0~1 和 ±2 之间的随机分布,梯度数值不稳定,第一轮 loss 就飙到几十。

参数说明:batch_size=128 表示每轮迭代喂入 128 张图。CIFAR10 单张图只有 3×32×32=3072 个元素,显存占用很小,普通 6GB 显存显卡跑 128 的 batch 毫无压力。num_workers=4 用 4 个子进程并行做数据读取与预处理,Windows 上如果报 RuntimeError 找不到主进程,改成 num_workers=0 即可,这是 PyTorch 在 Windows 下的知名坑。pin_memory=True 开启锁页内存,配合 GPU 训练能减少 CPU 到 GPU 的拷贝耗时。

2.3 数据增强:为什么随机翻转和裁剪能让准确率涨 3 个点

上面那段代码里的 transform 只做了归一化,实际训练时我会在 Compose 里加上两个增强操作:

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)) ])

逻辑说明:RandomCrop(32, padding=4) 先把原图四周各填 4 个像素的 0(变成 40×40),再随机裁回 32×32,等价于做了随机平移。RandomHorizontalFlip 以 50% 概率左右翻转。这两个操作不改变语义——CIFAR10 里没有上下颠倒的船,但有左右行驶的汽车——却能强迫卷积核学到平移和翻转不变的特征,等于把 5 万张训练图“免费”扩成了近似无限的数据流。

参数说明:padding=4 是经过反复验证的经典值。大于 4 时物体边缘会被裁掉太多,比如鸟的翅膀尖被裁掉后,模型会把“鸟”和“飞机”混在一起;小于 4 则增强效果有限。实测在 3 组 Conv-BN-ReLU 的网络上,加上这两个操作后准确率从 77% 左右提升到 80% 以上,那 3 个点不是来自调参,而是来自网络见过更多“被裁过、被翻过”的样本。

2.4 先把训练好的模型文件跑起来:加载权重直接推理的完整代码

如果你不想从零开始训练,可以直接用附带的训练好的模型文件 best_model.pth 做推理。这里的模型文件和训练代码里的模型类是严格对应的,以下代码在下载后可以直接跑通:

import torch from PIL import Image from torchvision import transforms class ConvNet(torch.nn.Module): def __init__(self): super().__init__() self.features = torch.nn.Sequential( torch.nn.Conv2d(3, 32, 3, padding=1), torch.nn.BatchNorm2d(32), torch.nn.ReLU(inplace=True), torch.nn.Conv2d(32, 32, 3, padding=1), torch.nn.BatchNorm2d(32), torch.nn.ReLU(inplace=True), torch.nn.MaxPool2d(2), torch.nn.Conv2d(32, 64, 3, padding=1), torch.nn.BatchNorm2d(64), torch.nn.ReLU(inplace=True), torch.nn.Conv2d(64, 64, 3, padding=1), torch.nn.BatchNorm2d(64), torch.nn.ReLU(inplace=True), torch.nn.MaxPool2d(2), torch.nn.Conv2d(64, 128, 3, padding=1), torch.nn.BatchNorm2d(128), torch.nn.ReLU(inplace=True), torch.nn.Conv2d(128, 128, 3, padding=1), torch.nn.BatchNorm2d(128), torch.nn.ReLU(inplace=True), torch.nn.MaxPool2d(2), ) self.classifier = torch.nn.Sequential( torch.nn.Flatten(), torch.nn.Linear(128 * 4 * 4, 256), torch.nn.ReLU(inplace=True), torch.nn.Dropout(0.5), torch.nn.Linear(256, 10) ) def forward(self, x): return self.classifier(self.features(x)) model = ConvNet() state_dict = torch.load('best_model.pth', map_location='cpu') model.load_state_dict(state_dict) model.eval() img = Image.open('cat_test.jpg').convert('RGB') transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) input_tensor = transform(img).unsqueeze(0) with torch.no_grad(): logits = model(input_tensor) pred = logits.argmax(dim=1).item() classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] print(f'预测类别: {classes[pred]}')

逻辑说明:torch.load 的 map_location='cpu' 保证模型文件即使在 GPU 上训练保存的,也能在没有 CUDA 的机器上加载。load_state_dict 要求键名严格匹配,如果你的模型定义和训练时有任何一层差异,这里会直接报错,所以不要手动改模型类里的层顺序——这是“训练好的模型文件直接用”最常见的失误点。

推理时有个细节容易被忽略:训练时用了 RandomCrop 和 RandomFlip,推理时必须用 Resize((32,32)) 保证输入尺寸一致,但绝不能加随机翻转或裁剪。一张好好的猫图如果被随机翻转,预测结果可能从“猫”跳到“狗”,这不是模型没学好,是你违背了训练时的数据分布。unsqueeze(0) 是为了给 3×32×32 的张量补一维变成 1×3×32×32,模拟一个只有一张图的 batch。

3. 用卷积神经网络训练 CIFAR10 分类模型:网络设计与完整训练代码

3.1 CNN 结构选型:为什么 Conv-BN-ReLU 三件套是基线标配

CIFAR10 图像是 32×32 的小图,经典 CNN 结构堆叠层数时不能用太大的卷积核。5×5 的卷积核在 32×32 输入上卷积一次,特征图就缩小到 28×28,叠加几次后空间信息丢失太快。常见做法是全用 3×3 卷积核,配合 padding=1 保持特征图尺寸不缩,然后用 MaxPool2d 主动降采样。3×3 卷积核叠加两层等价于一个 5×5 感受野,但参数量只有后者的 18/25,性价比高得多。

Conv-BN-ReLU 这个顺序,最常用的组合是 Conv 之后立刻接 BN,再接 ReLU。为什么 BN 放在 ReLU 前面?批归一化把每层输入的分布拉回到均值 0 方差 1,ReLU 才会真正起到非线性激活的作用;如果 ReLU 在前面,负值被裁掉后再做 BN,均值会偏移,分布在整个训练过程中反复漂移,收敛会明显变慢。这一点在 CIFAR10 这种小图任务上特别明显——图像分辨率低,每个特征图的统计量波动大,BN 的位置直接影响 loss 下降速度。

卷积层数上,如果你的训练时间预算在 15 分钟以内(单张消费级显卡),三层卷积池化足够。每一层做两次 3×3 卷积再接一次池化,这种 block 结构是 VGG 风格在 CIFAR10 上的缩影。残差连接在这个深度下不是必须的,但如果你把网络加深到 6 组 block,内部特征图的通道数超过 256 时,就必须在 block 之间加残差短路,否则梯度消失会让 loss 卡在上百的数值下不来。CIFAR10 的输入分辨率低,网络深度到了一定程度后,加层带来的收益会被过拟合和优化困难抵消,所以基线选 3 组 block 是最稳妥的。

3.2 完整训练代码:数据加载、模型定义、训练循环一步到位

下面的脚本 train.py 是完整的训练入口,包含增强、模型定义、训练与验证、保存最佳权重。复制到本地,装了 PyTorch 2.0 以上版本即可直接运行:

import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader 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_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transform) test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=test_transform) train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4, pin_memory=True) test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False, num_workers=4, pin_memory=True) class CIFAR10CNN(nn.Module): def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.Conv2d(32, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.Conv2d(128, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, 10) ) def forward(self, x): return self.classifier(self.features(x)) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = CIFAR10CNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) EPOCHS = 50 best_acc = 0.0 for epoch in range(EPOCHS): model.train() running_loss = 0.0 for images, labels in train_loader: 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) 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 = 100.0 * correct / total print(f'Epoch {epoch+1}/{EPOCHS} | loss: {running_loss/len(train_dataset):.4f} | val acc: {acc:.2f}%') if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'best_model.pth') print(f'训练完成,最佳验证准确率: {best_acc:.2f}%')

逻辑说明:epoch 内先 model.train() 打开 Dropout 和 BN 的统计更新,验证前必须 model.eval() 关闭它们,否则验证结果会被随机失活和 BN 的 batch 统计量污染。每个 epoch 结束后跑一次完整测试集,这是 CIFAR10 这类小数据集的常见做法,能实时监控过拟合程度。torch.max(outputs, 1) 返回每行最大值的数值和索引,索引就是预测类别。

训练中几个关键参数互相联动:SGD 的 weight_decay=5e-4 是 L2 正则化的实现方式。CIFAR10 只有 5 万张训练图,模型参数量超过 100 万,不加 weight decay 的模型通常在 30 轮以后验证准确率停步甚至倒退,典型的过拟合征兆。学习率用 0.1 而不是默认的 0.001,是因为 BN 层让每层输入的分布相对稳定,SGD 在 BN 网络上能吃下更大的学习率。CosineAnnealingLR 的 T_max=50 表示在 50 个 epoch 里把学习率从 0.1 余弦衰减到接近 0,衰减曲线平滑,不像 StepLR 那样突然降一半导致 loss 在拐点处抖动。

3.3 超参数怎么定:初始学习率、batch size、epoch 与学习率衰减策略

超参数是 CIFAR10 训练里最值得花时间的部分,下面这张表是我多次实验后总结的经验值,你可以在这些值附近做 ±20% 的扰动测试:

超参数推荐值取值范围说明
初始学习率0.10.05~0.2只在 SGD 配合 BN 时适用,Adam 请用 0.001
batch size12864~256小于 64 时 BN 统计量不稳定,大于 256 对显存不友好
epoch 数5030~10050 轮性价比最高,100 轮只多 1 个点
weight_decay5e-41e-4~1e-3太大收敛变慢,太小过拟合
momentum0.90.8~0.95配合 SGD 加速收敛,经典值

初始学习率 0.1 只适用于 batch size 128。如果你把 batch size 调到 256,学习率也应按比例调到 0.2,这是 Linear Scaling Rule 的经验做法。batch size 256 时梯度估计更平滑,0.1 会显得步子太小,训练 50 轮后准确率比 128 低 1~2 个点。epoch 数 50 是性价比最高的区间,CIFAR10 上 10 个 epoch 就能看到 loss 明显下降,但准确率只停在 60% 上下;50 轮后基本收敛;100 轮能再涨 1 个点,但后面 50 轮几乎全靠数据增强和余弦退火在硬撑。

学习率衰减策略的选择:StepLR 每 15 轮乘 0.1,三档衰减也能接近余弦退火的效果,但需要手动调档位节点。CosineAnnealing 不需要调任何档位,实测在同样 50 轮下比 StepLR 高 0.8~1.5 个点,原因是学习率从峰值连续下降,能反复探索损失曲面的不同区域然后收敛到更平坦的最优点。如果你的训练时间紧张,直接用 CosineAnnealingLR 是最省心的选择。

3.4 训练过程监控:loss 曲线与准确率曲线怎么看

训练日志打印的是每个 epoch 的平均 loss 和验证准确率。注意观察两个信号:第一,loss 在 epoch 2 之内从 1.8 降到 1.2 左右是正常;如果 10 轮后 loss 还在 1.5 以上,多半是学习率太大或数据归一化没做对。第二,验证准确率每轮只涨零点几个点属于正常,如果某个 epoch 突然涨 5 个点以上,大概率是学习率衰减到了好区域,也可能是这一轮验证集恰好吃到了容易样本——不用高兴太早,继续看后面两轮是否守住,守不住就是过拟合的早期信号。

我自己会加几行代码把每个 epoch 的 loss 和准确率追加到 CSV 里,训练完用 matplotlib 画两条曲线。画出来的曲线如果 loss 不断下降但验证准确率在某个点掉头向下,这就是过拟合的经典形态,对应前面说的 weight_decay 不够或数据增强过弱。另一个值得关注的是 loss 曲线在 epoch 20 附近有没有一个明显的“台阶”——如果有,说明 CosineAnnealing 的学习率降到了能跳出当前局部最优的区间,这个台阶之后通常紧跟着一波准确率抬升。

4. CIFAR10 训练避坑指南:5 个让模型翻车的常见问题与排查

4.1 现象:训练 loss 不降或震荡

loss 在 2.3 附近横盘,甚至越训越高,验证准确率徘徊在 0.5~0.6 上下。

原因分析:2.3 这个数值很有特征——CIFAR10 是 10 分类,logits 全 0 时交叉熵就是 ln(10)≈2.3026。loss 卡在 2.3 说明网络输出概率接近均匀分布,梯度几乎为零。常见原因有三个:学习率过大导致参数在最优解附近震荡跳不出去;数据没归一化,像素值 0~255 直接喂进网络,梯度数值不稳定;BN 层初始化不当。解决:先从设置里检查 transform 有没有 Normalize,这是八成新手翻车点。然后确认学习率:SGD 用 0.1,Adam 用 0.001,别把两者混用。如果都正常,把 weight_decay 临时设为 0 跑一轮——有时正则化过强会把梯度和参数同时压住。

4.2 现象:验证集准确率停滞在 0.1

模型“认真”训练了 50 轮,训练 loss 确实降到了 0.5 以下,但测试准确率只有 10%,和瞎猜没什么区别。

原因分析:准确率 0.1 是“标签洗牌”级别的破坏信号。最常见原因是 labels 和 images 错位——自己写自定义 Dataset 重建标签时,用了最终被重排的 enumerate 结果,导致图像和类别对不上。另一个常见原因是测试集用了和训练集一样的数据增强——测试时把图片随机翻转、随机裁剪,一张猫图有一半概率被处理成“不猫”的样子。

解决:先跑一遍测试集,按总图数统计每类的准确率。如果各类准确率都接近 0.1 而不是个别类别高,基本可以断定标签错位。检查 test_loader 的 shuffle 是否为 False,并确认 labels 在 DataLoader 返回时没有被反转或裁剪。若怀疑增强污染,把测试集的 transform 改成只有 ToTensor + Normalize。

4.3 现象:训练 loss 降到很低,验证准确率却停滞

训练 loss 能到 0.1 以下,验证准确率始终只在 70% 左右徘徊,上不去。

原因分析:这是“背答案”式过拟合——模型把训练集的噪声细节也当成特征。CIFAR10 每类只有 5000 张图,模型参数超过 100 万意味着平均每个参数由 0.05 张图约束,过拟合是结构性的,不是简单调参能解决的。很多人在这个阶段会去加卷积层数,结果适得其反——模型容量越大,泛化缺口越大。

解决:加 Dropout 是最直接的手段。上面的模型里 classifier 的 Dropout(0.5) 已经保护了全连接层,如果还欠,在最后一个卷积 block 的输出后再加一个 Dropout2d(0.2)。确认训练集增强里 RandomCrop 和 RandomFlip 都在,这两者通常能把准确率从 72% 提到 82%。最后一步是把 weight_decay 从 5e-4 调到 1e-3,这个值会让特征图稀疏很多,代价是收敛变慢,但泛化能力提升明显。

4.4 现象:显存不足或训练速度极慢

batch_size=128 在 8GB 显存上能跑,换成 256 后报 CUDA out of memory;或者 GPU 利用率只有 40%,训练速度像蜗牛。

原因分析:batch_size 翻倍后,反向传播中需要保存的中间激活值也翻倍——PyTorch 的 autograd 机制为了算梯度会保留前向的中间结果。显存不足是因为特征图张量在训练期间全程驻留显存,而 CIFAR10 的模型虽小,但中间层特征图数量很多,深层的 128 通道特征图一存就是几万份。速度慢则多半是 CPU 预处理成为瓶颈,GPU 在干等数据。

解决:显存不足优先开启 torch.backends.cudnn.benchmark = True,让 cuDNN 自动选最优卷积算法。还不足时用梯度累积模拟大 batch:batch_size 保持 128,每 2 个 batch 才执行一次 optimizer.step(),等效 batch 256,显存占用只有 128 的量。训练慢的话先检查 GPU 利用率,低于 80% 就把 num_workers 提到 8,或确认 pin_memory=True。Windows 系统 num_workers 过高反而会因进程调度问题变慢,4 是折中值。

4.5 现象:加载训练好的模型文件报错

torch.load 后 load_state_dict 报 size mismatch 或 missing key,模型文件完全用不了。

原因分析:这是训练好的模型文件最常见的使用坑。模型文件里存的是 state_dict——每一层权重的键名和形状。如果你在推理脚本里重新定义模型时漏了一层、改了一个卷积核大小,或者干脆用了另一个模型类,state_dict 里的键名和你当前模型的键对不上,PyTorch 直接抛错而不是静默给你一个坏模型。

解决:模型定义必须和训练脚本逐字一致,然后用 model.load_state_dict(torch.load('best_model.pth'))。如果真的改了模型结构,可以用下面这段代码对比键名差异:

checkpoint = torch.load('best_model.pth', map_location='cpu') print(list(checkpoint.keys())[:10])

观察输出的键名是否包含 features.0.weight、classifier.1.weight 这类前缀。如果只是最后一层分类数不同(比如想用 CIFAR10 模型迁移到只有 5 类的任务),可以手动替换最后一层再加载其余权重,但这是进阶操作,新手不建议一上来就这么干——老老实实把模型类原样复制过去最省心。

5. 从 80% 到 90%:验证模型泛化能力与进阶优化方向

5.1 用测试集评估:准确率、混淆矩阵与单类别的召回率

训练完成后,不建议只用 val acc 一个数字汇报结果。CIFAR10 里鸟、鹿、狗是长期垫底的三类——猫和狗在 32×32 下轮廓高度相似,鹿和马的腿部特征也容易被混淆。用 sklearn 的 confusion_matrix 能清楚看到模型把鹿误判成马、把鸟误判成飞机这些错误的分布。我通常把测试集完整跑一遍,算出每个类别的 precision、recall、F1,再画混淆矩阵。比如“汽车 vs 卡车”这对易混淆类,如果召回率在 95% 以上,说明模型学会了分辨轮子数量和车厢比例;如果只有 80%,说明训练数据里这类样本的增强仍然不够,需要针对性补充。

from sklearn.metrics import confusion_matrix import numpy as np all_preds = [] all_labels = [] model.eval() with torch.no_grad(): for images, labels in test_loader: images = images.to(device) outputs = model(images) preds = outputs.argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) print(cm)

注意 confusion_matrix 参数顺序是 (真实标签, 预测标签),写反了会得到转置矩阵,热力图依然“好看”,但结论全错。查看某类准确率时用 cm[i, i] / cm[i, :].sum(),这是召回率;cm[i, i] / cm[:, i].sum() 是精确率,两个指标语义完全不同,混用会导致你误判模型短板。

5.2 三招快速提升:权重初始化、标签平滑、余弦退火的进阶用法

  • 权重初始化:nn.Conv2d 默认的 Kaiming 初始化对 ReLU 是合理的,但如果你手动改成 xavier_uniform_,在 BatchNorm 后面的线性层会明显拖慢收敛。手写模型时不要覆盖默认初始化,这是最省心的做法。有些教程会推荐“自定义初始化”作为进阶调参项,但 CIFAR10 这种任务上,默认初始化已经够用,折腾初始化是在浪费时间。
  • 标签平滑:把 one-hot 标签从 1.0 换成 0.9,非目标类别保留 0.1/9 的软概率,能显著抑制过拟合。在 PyTorch 里 CrossEntropyLoss(label_smoothing=0.1) 一行搞定,损失函数的特性不需要动,训练代码完全不用改。实测能让测试准确率提高 0.5~1 个点。
  • 余弦退火的进阶版是带 warm restart 的 CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2),周期性把学习率抬升再下降,能跳出局部极小。但 CIFAR10 上对最终准确率的提升有限,它主要让训练前中期 loss 下降更快。另外有人在序列模型里对比过一维卷积神经网络和 LSTM,虽然任务不同,但结论相通:结构简单时学习率策略比网络结构更影响最终指标。

5.3 迁移学习兜底:用预训练 ResNet18 把训练时间砍半

如果你不想从零训练 CNN,用 torchvision 里的 ResNet18 做迁移学习是成熟的选择。torchvision 自带在 ImageNet 上预训练的 ResNet18,前几层学到的边缘、纹理特征是通用的。但注意,CIFAR10 只有 32×32,而 ResNet18 默认输入是 224×224,直接替换最后一层分类器训练,准确率大约只有 83%——网络最开始的 stride=2 卷积和 maxpool 会把小图压缩得太狠,空间信息丢失严重。正确做法是把第一个卷积核 stride 从 2 改成 1,并去掉 maxpool 层:

from torchvision import models resnet18 = models.resnet18(pretrained=True) resnet18.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) resnet18.maxpool = nn.Identity() resnet18.fc = nn.Linear(resnet18.fc.in_features, 10)

逻辑说明:conv1 的 stride 改成 1 后,32×32 的图像在第一个卷积层仍保持 32×32 而不是缩小到 16×16。去掉 maxpool 是为了不丢掉空间信息,让整个网络的感受野和特征图尺寸都适配小图。代价是计算量略微上升,但换来的是在 CIFAR10 上能到 90% 左右的准确率。

训练时建议分两阶段:先让 conv1~layer2 的 requires_grad=False,只训练 layer3、layer4 和新 fc 层,跑 10 轮;然后解冻全部参数,用 lr=0.01 的 SGD 再精调 20 轮。这是标准的 fine-tune 两阶段法,能在 30 轮内超过从零训练 50 轮的准确率。torchvision 的 ResNet 各层参数是内部命名的,训练时务必在 train 和 eval 之间正确切换,否则 BN 层的 running_mean 会被验证阶段的 batch 统计量带偏。

说实话,我入行第一年因为图省事,直接把 224×224 的 ResNet 原封不动拿来训 CIFAR10,结果准确率只有 78%,比我自己手写的 CNN 还低 3 个点。后来才明白,模型的输入分辨率是结构设计的一部分,不是简单的 resize 就能适配。现在每次换数据集,我都先写一个小脚本可视化几张增强后的训练图,确认预处理后的图像还“认得出来”,再谈调参。这个习惯救过我好几次——数据管道的细节,往往比模型结构更能决定最终指标。希望这篇 CIFAR10 从数据集介绍、完整代码到训练好的模型文件使用的全过程,能帮你少走这些弯路,把宝贵时间留给真正有价值的结构探索和业务落地。

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

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

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

立即咨询