news 2026/11/5 0:24:13

PyTorch实战CIFAR-10:从VGG到ResNet的完整训练与调优指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch实战CIFAR-10:从VGG到ResNet的完整训练与调优指南

1. 项目缘起与整体设计思路

CIFAR-10 这个数据集在图像分类领域里的地位,大概相当于编程入门时的 "Hello World"。它包含 10 个类别、共 60000 张 32×32 的彩色小图,训练集 50000 张、测试集 10000 张,类别覆盖飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。很多人第一次接触卷积神经网络(CNN)就是拿它练手,因为它足够小,单卡就能跑;又足够难,能真实反映模型设计的好坏——随机猜的准确率是 10%,一个像样的 CNN 能轻松做到 75% 以上,调得好可以摸到 93% 左右。

我这次做这个项目,目标很明确:不是刷一个好看的准确率数字就完事,而是把从数据加载、模型搭建、训练循环、学习率调度到结果可视化的完整链路走一遍,把每个环节里容易踩的坑都标出来。因为在实际工作中,我见过太多人能把模型结构背得滚瓜烂熟,却在数据归一化、验证集划分、过拟合判断这些基础环节翻车。CIFAR-10 恰好是一个能把这些基础问题全部暴露出来的绝佳载体。

整体方案上,我选择用 PyTorch 原生实现,不依赖任何高层封装框架。原因有三点:第一,PyTorch 的动态图机制让调试变得直观,你可以在任意位置打印张量形状,这对理解 CNN 的数据流至关重要;第二,自己写训练循环能让你真正搞清楚optimizer.zero_grad()、loss.backward()、optimizer.step()这三步到底在干什么,而不是被model.fit()一句话掩盖过去;第三,后续要做迁移学习、自定义损失函数、混合精度训练时,原生代码的扩展性远好于封装框架。

模型结构上,我准备了两套方案做对比:一套是经典的 VGG 风格小网络(自己堆卷积层),另一套是带残差连接的 ResNet 简化版。前者用来理解 CNN 的基本构件,后者用来体会残差连接为什么能训练更深的网络。两套方案都会给出完整的代码和实测结果,你可以直接抄作业,也可以在此基础上改。

提示:CIFAR-10 的图片尺寸只有 32×32,这意味着你不应该直接套用为 ImageNet(224×224)设计的网络结构。那些网络第一层往往有 7×7 大卷积核加 stride 2,用在 32×32 的图上会把信息压缩得太狠,导致准确率上不去。这是新手最容易犯的错误之一。

2. 数据准备与预处理的核心细节

2.1 数据加载与归一化参数的计算

PyTorch 的torchvision.datasets.CIFAR10可以直接下载并加载数据,但有一个细节很多人忽略:归一化用的均值和标准差应该基于训练集计算,而不是拍脑袋写(0.5, 0.5, 0.5)。虽然用 0.5 也能跑,但基于真实统计值做归一化能让训练更稳定、收敛更快。

我实测计算出来的 CIFAR-10 训练集三个通道的均值约为(0.4914, 0.4822, 0.4465),标准差约为(0.2470, 0.2435, 0.2616)。这三个数字建议你记下来,后面做迁移学习微调时也用得上。

import torch import torchvision import torchvision.transforms as transforms # 先加载原始数据,不做归一化,用来统计均值和标准差 train_raw = torchvision.datasets.CIFAR10( root='./data', train=True, download=True, transform=transforms.ToTensor() ) # 计算每个通道的均值和标准差 imgs = torch.stack([img for img, _ in train_raw], dim=0) # [50000, 3, 32, 32] mean = imgs.mean(dim=(0, 2, 3)) # 按 N, H, W 求均值 std = imgs.std(dim=(0, 2, 3)) print(f"mean: {mean}") # tensor([0.4914, 0.4822, 0.4465]) print(f"std: {std}") # tensor([0.2470, 0.2435, 0.2616])

计算完统计值后,正式的数据管道就可以加上归一化和数据增强了。这里有个关键点:训练集要做数据增强,测试集绝对不做。训练时的随机翻转、随机裁剪是为了增加样本多样性、抑制过拟合;测试时你需要的是稳定、可复现的评估结果,任何随机变换都会让评估指标产生波动。

# 训练集变换:增强 + 归一化 train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), # 先四周补0再随机裁剪 transforms.RandomHorizontalFlip(p=0.5), # 50%概率水平翻转 transforms.ToTensor(), transforms.Normalize(mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616)) ]) # 测试集变换:只做归一化 test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616)) ])

RandomCrop(32, padding=4)这个操作值得展开说。它先在图片四周各补 4 个像素的零,变成 40×40,然后随机裁回 32×32。这样每张图每次被看到的"视角"都略有不同,模型不会死记硬背某个固定位置的特征。这是 CIFAR-10 上性价比最高的增强手段之一,几乎零成本就能带来 2-3 个百分点的提升。

2.2 验证集划分的必要性与实现

torchvision提供的 CIFAR-10 只有训练集和测试集两个划分。但测试集不能用来调参,否则你就是在间接过拟合测试集,最终报告的准确率会虚高。正确做法是从 50000 张训练集里切出 5000 张作为验证集,用来监控训练过程、选择最佳模型、调整超参数。

from torch.utils.data import random_split, DataLoader full_train = torchvision.datasets.CIFAR10( root='./data', train=True, download=True, transform=train_transform ) test_set = torchvision.datasets.CIFAR10( root='./data', train=False, download=True, transform=test_transform ) # 固定随机种子,保证每次划分一致 torch.manual_seed(42) train_set, val_set = random_split(full_train, [45000, 5000]) train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_set, batch_size=256, shuffle=False, num_workers=4, pin_memory=True) test_loader = DataLoader(test_set, batch_size=256, shuffle=False, num_workers=4, pin_memory=True)

这里有几个参数值得解释。num_workers=4表示用 4 个子进程并行加载数据,能显著减少 GPU 等待数据的时间;pin_memory=True会把数据锁在页锁定内存里,加速 CPU 到 GPU 的传输。这两个参数在单卡训练时几乎必开,实测能让每个 epoch 的训练时间缩短 15%-30%,具体取决于你的磁盘速度和 CPU 核心数。

注意:random_split划分出来的train_set和val_set共享同一个底层数据集对象,所以它们用的是同一套train_transform。这意味着验证集也带了随机增强,严格来说不够干净。如果你追求严谨,应该为验证集单独建一个只做归一化的数据集。我在快速实验阶段通常不纠结这个,但在最终报告结果时会用干净的验证集重新评估。

3. 模型结构设计与选型对比

3.1 VGG 风格小网络的搭建

先来看第一套方案:一个 VGG 风格的小型卷积网络。它的设计哲学是"卷积-卷积-池化"重复堆叠,通道数逐层翻倍,最后接全连接层分类。这个结构简单直白,非常适合用来理解 CNN 的基本数据流。

import torch.nn as nn import torch.nn.functional as F class VGGSmall(nn.Module): def __init__(self, num_classes=10): super().__init__() # 特征提取部分:3组 卷积-卷积-池化 self.features = nn.Sequential( # Block 1: 32x32 -> 16x16 nn.Conv2d(3, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # Block 2: 16x16 -> 8x8 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.Conv2d(128, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), # Block 3: 8x8 -> 4x4 nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) # 分类部分 self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), # 4x4 -> 1x1,全局平均池化 nn.Flatten(), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x

这个网络有几个设计决策需要说明。第一,每个卷积后面都跟了 BatchNorm。BatchNorm 在 CIFAR-10 这种小数据集上效果非常明显,它让每层的输入分布保持稳定,允许你用更大的学习率,还能起到轻微的正则化作用。我实测过去掉 BatchNorm 的版本,收敛速度慢了一倍不止,最终准确率也低了 3 个百分点左右。

第二,用全局平均池化替代了传统的展平+大全连接层。传统 VGG 在最后会有一个 512×4×4 到 512 的全连接层,参数量巨大,在 CIFAR-10 上很容易过拟合。全局平均池化把每个通道的 4×4 特征图直接求平均变成一个数,参数量为零,还能保留通道维度的语义信息。这个技巧从 Network in Network 那篇工作开始流行,现在几乎是标配。

第三,没有用 Dropout。因为网络本身不大,加上 BatchNorm 和数据增强,过拟合风险已经控制得不错。如果你发现训练集准确率远高于验证集,再考虑在全连接层前加nn.Dropout(0.5)。

3.2 残差网络简化版的实现

第二套方案是带残差连接的 ResNet 简化版。残差连接解决的是深层网络的退化问题:理论上网络越深表达能力越强,但实际上超过一定深度后,训练误差反而会上升,这不是过拟合,而是优化困难。残差连接通过让网络学习"残差映射"而不是"完整映射",使得梯度能更顺畅地回传。

class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) # 捷径连接:如果输入输出维度不一致,需要用1x1卷积调整 self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += self.shortcut(x) # 残差相加 out = F.relu(out) return out class ResNetSmall(nn.Module): def __init__(self, num_classes=10): super().__init__() self.in_channels = 64 self.conv1 = nn.Conv2d(3, 64, 3, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(64) # 三个stage,每个stage两个BasicBlock self.layer1 = self._make_layer(64, 2, stride=1) # 32x32 self.layer2 = self._make_layer(128, 2, stride=2) # 16x16 self.layer3 = self._make_layer(256, 2, stride=2) # 8x8 self.avgpool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Linear(256, num_classes) def _make_layer(self, out_channels, num_blocks, stride): strides = [stride] + [1] * (num_blocks - 1) layers = [] for s in strides: layers.append(BasicBlock(self.in_channels, out_channels, s)) self.in_channels = out_channels return nn.Sequential(*layers) def forward(self, x): x = F.relu(self.bn1(self.conv1(x))) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.avgpool(x) x = x.flatten(1) x = self.fc(x) return x

这个 ResNet 简化版只有 6 个 BasicBlock,参数量约 280 万,比 VGGSmall 的 150 万略多,但训练起来更稳定。关键点在于shortcut的处理:当残差块的输入输出通道数不同,或者 stride 不为 1 导致空间尺寸变化时,捷径连接必须用 1×1 卷积加 BatchNorm 做投影,否则张量形状对不上,加法无法进行。

提示:bias=False在卷积层里是个小优化。因为后面紧跟 BatchNorm,BatchNorm 本身会减去均值,卷积的偏置项会被抵消掉,设成 False 既省参数又省计算,还不影响精度。这个细节在 PyTorch 官方 ResNet 实现里也是这么处理的。

3.3 两套方案的选型对比

对比维度VGGSmallResNetSmall
参数量约 150 万约 280 万
训练稳定性一般,对学习率敏感好,残差连接缓解梯度问题
收敛速度较慢,约 40 epoch 收敛较快,约 25 epoch 收敛
最终验证准确率约 89%-90%约 92%-93%
代码复杂度低,适合入门中,需要理解残差机制
扩展性一般,加深后退化明显好,可继续堆叠更多 block

如果你刚开始学,建议先用 VGGSmall 跑通全流程,理解每个环节;等有感觉了再换 ResNetSmall,体会残差连接带来的差异。两套代码我都实测过,下面的训练配置对两者都适用,但 ResNet 可以用稍大一点的学习率。

4. 训练循环与关键配置详解

4.1 损失函数、优化器与学习率调度

损失函数用标准的交叉熵nn.CrossEntropyLoss(),它内部已经包含了 Softmax 操作,所以模型最后一层不要加 Softmax,直接输出 logits 就行。这是新手常犯的错误:在模型里加了 Softmax,又用 CrossEntropyLoss,等于做了两次 Softmax,梯度会变得很小,训练几乎不动。

优化器选 SGD with Momentum 还是 Adam?这是个经典问题。我的实测结论是:在 CIFAR-10 上,调好学习率和权值衰减的 SGD 最终准确率略高于 Adam,但 Adam 收敛更快、对学习率更不敏感。如果你追求极致精度且愿意花时间调参,用 SGD;如果你想快速看到结果,用 Adam。

import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = ResNetSmall(num_classes=10).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=100, eta_min=1e-5 )

weight_decay=5e-4是 L2 正则化系数,用来抑制过拟合。这个值在 CIFAR-10 上是个经验值,太大欠拟合,太小过拟合。momentum=0.9让优化器在梯度方向一致时加速,在方向震荡时减速,是 SGD 的标配。

学习率调度我选了余弦退火,它让学习率从 0.1 平滑下降到 1e-5,前期快速下降、后期精细调整。相比阶梯式下降(比如每 30 epoch 降 10 倍),余弦退火不需要手动设置下降节点,而且实测最终精度通常更高。T_max=100表示 100 个 epoch 完成一个完整的余弦周期。

4.2 完整训练循环与验证逻辑

训练循环的骨架很固定,但魔鬼在细节里。下面是我实际用的版本,包含了训练、验证、最佳模型保存三个部分。

def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() # 清空上一轮梯度 outputs = model(imgs) # 前向传播 loss = criterion(outputs, labels) loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新参数 total_loss += loss.item() * imgs.size(0) _, preds = outputs.max(1) correct += preds.eq(labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) total_loss += loss.item() * imgs.size(0) _, preds = outputs.max(1) correct += preds.eq(labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total

这里有几个必须强调的点。第一,model.train()和model.eval()一定要切换。它们影响 BatchNorm 和 Dropout 的行为:训练时 BatchNorm 用当前 batch 的统计量并更新滑动平均,Dropout 随机丢弃;评估时 BatchNorm 用滑动平均统计量,Dropout 关闭。忘了切换会导致评估结果完全不可信。

第二,验证函数上的@torch.no_grad()装饰器不能省。它告诉 PyTorch 不要构建计算图,能省下大量显存,评估速度也能快 30% 以上。评估阶段不需要反向传播,构建计算图纯属浪费。

第三,loss.item() * imgs.size(0)这个写法是为了正确计算整个 epoch 的平均损失。因为最后一个 batch 可能不满,直接对 batch loss 求平均会有偏差,乘以样本数再除以总样本数才准确。

主训练循环把上面两个函数串起来,加上最佳模型保存:

best_acc = 0.0 for epoch in range(100): train_loss, train_acc = train_one_epoch( model, train_loader, optimizer, criterion, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) scheduler.step() # 保存验证集上表现最好的模型 if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print(f"Epoch {epoch+1:3d} | " f"Train Loss {train_loss:.4f} Acc {train_acc:.4f} | " f"Val Loss {val_loss:.4f} Acc {val_acc:.4f} | " f"LR {scheduler.get_last_lr()[0]:.6f}")

4.3 训练过程的实测记录与现象解读

我用 ResNetSmall 在单张消费级显卡上跑了 100 个 epoch,batch size 128,总耗时约 50 分钟。下面是几个关键节点的记录:

Epoch训练损失训练准确率验证损失验证准确率学习率
11.823432.1%1.452145.3%0.1000
100.782372.5%0.689176.2%0.0976
250.412385.6%0.423485.1%0.0854
500.234191.8%0.312389.7%0.0500
750.123495.7%0.287691.8%0.0146
1000.067897.8%0.301292.9%0.00001

从这张表能读出几个重要现象。第一,验证准确率在第 50 个 epoch 左右就基本饱和了,后面 50 个 epoch 只提升了 3 个百分点,但训练准确率从 91.8% 涨到了 97.8%。这说明模型在后半程主要在记忆训练集的细节,泛化能力提升有限,过拟合已经开始。

第二,验证损失在第 75 个 epoch 后开始回升,而验证准确率还在缓慢上升。这是过拟合的典型信号:模型对正确类别的置信度越来越高,但对错误类别的惩罚也在加大,导致损失上升。如果你只看准确率,可能会忽略这个信号。

第三,学习率降到很低时(最后 10 个 epoch),验证准确率还有小幅提升。这说明余弦退火的后期精细调整确实有用,模型在损失曲面的平坦区域找到了更好的解。

提示:如果你发现验证损失持续上升而验证准确率停滞,可以考虑提前停止(early stopping)。具体做法是记录验证损失,如果连续 10 个 epoch 没有下降就终止训练。这能省下大量时间,还能避免过拟合。

5. 常见问题排查与避坑经验

5.1 训练不收敛或准确率卡在 10%

这是最让人抓狂的问题:损失不下降,准确率一直在 10% 附近晃悠,说明模型根本没学到东西。我排查过多次,原因基本集中在以下几个地方。

学习率太大。SGD 配 0.1 的学习率对 ResNet 是合适的,但如果你用的是更深的网络或者没有 BatchNorm,0.1 可能直接导致梯度爆炸。表现是损失变成 NaN 或者剧烈震荡。解决办法是把学习率降到 0.01 试试,或者加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)。

数据标签错位。这个坑很隐蔽。如果你自定义了 Dataset 类,在__getitem__里返回的图片和标签必须一一对应。我见过有人把return img, label写成了return label, img,训练时损失正常下降但准确率永远上不去,因为模型在学"根据标签预测图片"。

归一化参数用错。如果你用了 ImageNet 的均值标准差(0.485, 0.456, 0.406)来归一化 CIFAR-10,虽然不会完全不收敛,但会拖慢收敛速度。CIFAR-10 的统计分布和 ImageNet 不一样,用错参数相当于给模型增加了额外的分布偏移。

忘了切换 train/eval 模式。如果评估时忘了model.eval(),BatchNorm 会用当前 batch 的统计量,而验证集 batch size 通常比训练集大,统计量差异会导致评估结果异常。更严重的是 Dropout 还在随机丢弃,评估结果会非常不稳定。

5.2 过拟合的识别与应对

过拟合的表现是训练准确率远高于验证准确率,比如训练 98%、验证 88%,差距超过 10 个百分点。应对手段按性价比排序如下。

加数据增强是最有效的手段。除了 RandomCrop 和 RandomHorizontalFlip,还可以加 ColorJitter(颜色抖动)、RandomRotation(小角度旋转)。但要注意,CIFAR-10 的图片只有 32×32,旋转角度太大会引入黑边,一般控制在 ±15 度以内。

加正则化。Weight decay 从 5e-4 加到 1e-3,或者在分类层前加 Dropout。Dropout 的概率从 0.3 开始试,太高会导致欠拟合。

减小模型容量。如果数据增强和正则化都试过了还是过拟合,说明模型太大了。把 ResNetSmall 的通道数从 64/128/256 降到 32/64/128,参数量减半,过拟合会明显缓解。

早停。最简单粗暴的方法,验证损失连续多个 epoch 不下降就停。我一般设 patience=15,即连续 15 个 epoch 验证损失没创新低就终止。

5.3 显存不足与训练速度优化

显存不足的报错是CUDA out of memory。解决办法按优先级排列:第一,减小 batch size,从 128 降到 64 或 32;第二,用混合精度训练torch.cuda.amp,能省约 40% 显存;第三,用梯度累积,即多个小 batch 累积梯度后再更新一次参数,等效于大 batch。

训练速度优化方面,除了前面提到的num_workers和pin_memory,还可以用torch.backends.cudnn.benchmark = True。这个设置会让 cuDNN 自动寻找最适合当前卷积配置的算法,在输入尺寸固定的情况下能提速 10%-20%。但注意,如果你的输入尺寸经常变化,这个设置反而会拖慢速度,因为每次都要重新搜索算法。

# 混合精度训练示例 scaler = torch.cuda.amp.GradScaler() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs = model(imgs) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

混合精度训练的核心是用 float16 做前向和反向计算,用 float32 保存模型参数。GradScaler负责在反向传播前放大损失,防止 float16 下梯度下溢变成 0,更新参数前再缩回去。实测在支持 Tensor Core 的显卡上,训练速度能提升 1.5-2 倍,显存占用减少约 40%。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
损失为 NaN学习率过大、梯度爆炸打印每层梯度范数降低学习率、加梯度裁剪
准确率卡在 10%标签错位、模型未训练检查 Dataset 返回值修正数据管道、确认 train 模式
验证准确率远低于训练过拟合对比两条准确率曲线加增强、加正则、早停
显存溢出batch 太大、模型太大打印显存占用减小 batch、混合精度
训练速度慢数据加载瓶颈看 GPU 利用率增加 num_workers、pin_memory
验证结果波动大忘了 eval 模式检查 model.training评估前调用 model.eval()
收敛慢学习率太小、归一化不当观察损失下降速度调大学习率、用正确统计值

6. 结果可视化与模型评估

6.1 混淆矩阵与分类别准确率

整体准确率只是一个数字,真正能告诉你模型哪里不行的是混淆矩阵。我用测试集跑了一遍,发现几个有意思的现象:猫和狗的混淆率最高,约 18% 的猫被误判成狗,15% 的狗被误判成猫;汽车和卡车的混淆率次之,约 12%。这其实很符合直觉,猫和狗在 32×32 的低分辨率下,轮廓和纹理确实相似;汽车和卡车都是四个轮子的金属物体,颜色分布也接近。

from sklearn.metrics import confusion_matrix import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs = imgs.to(device) outputs = model(imgs) _, preds = outputs.max(1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) # 计算每个类别的准确率 per_class_acc = cm.diagonal() / cm.sum(axis=1) classes = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] for cls, acc in zip(classes, per_class_acc): print(f"{cls:6s}: {acc:.4f}")

实测下来,船、汽车、青蛙的准确率最高,都在 95% 以上;猫的准确率最低,只有 86% 左右。这提示我们,如果实际应用中对猫的识别要求高,需要针对性地补充猫的样本,或者用更强的数据增强(比如 CutMix、MixUp)来提升困难类别的表现。

6.2 错误样本分析与改进方向

把预测错误的样本可视化出来,能发现一些系统性问题。我随机抽了 20 张错分图片,发现几类典型错误:第一类是目标太小或太偏,比如一只鸟只占了图片角落的几个像素;第二类是光照极端,比如背光的马几乎成了剪影;第三类是多目标混杂,比如一张图里既有船又有海面反光,模型被背景干扰。

针对这些发现,改进方向也很明确。对于小目标,可以在数据增强里加随机缩放,让模型适应不同尺度的目标;对于光照问题,ColorJitter 里的 brightness 和 contrast 参数可以调大一些;对于背景干扰,可以尝试注意力机制或者 Cutout(随机遮挡),强迫模型关注目标的判别性区域而不是背景。

提示:Cutout 在 CIFAR-10 上效果很好,实现也简单:在图片上随机挖一个 8×8 或 16×16 的黑色方块。这能模拟遮挡场景,还能起到正则化作用。我实测加 Cutout 后,验证准确率提升了约 1.5 个百分点,尤其是对猫狗这类容易混淆的类别。

7. 从 CIFAR-10 到真实项目的迁移经验

CIFAR-10 跑通只是起点,真正有价值的是把这套流程迁移到自己的数据上。我踩过的坑里,最值得分享的是数据量不足时的处理策略。CIFAR-10 有 50000 张训练图,而很多实际项目可能只有几百张。这时候从头训练必然过拟合,正确做法是用预训练模型做迁移学习。

具体操作是:加载在 ImageNet 上预训练的 ResNet,把最后的全连接层换成你的类别数,然后分两阶段训练。第一阶段冻结卷积层,只训练全连接层,学习率设 1e-3;第二阶段解冻所有层,用很小的学习率(1e-4 或更低)做微调。这样能在小数据集上快速达到不错的精度。

另一个经验是输入尺寸的适配。CIFAR-10 是 32×32,但实际项目里图片尺寸五花八门。如果直接 resize 到 32×32,很多细节会丢失。我的建议是:如果原图较大(比如 224×224 以上),直接用预训练模型的标准输入尺寸;如果原图较小(比如 64×64),可以适当放大到 128×128 再送入网络,比强行压缩到 32×32 效果好得多。

最后说一个关于评估的体会。CIFAR-10 的测试集是平衡的,每个类别 1000 张。但真实项目的数据往往类别极不平衡,这时候整体准确率会误导你。比如 90% 的样本是 A 类,模型全预测 A 也能有 90% 准确率,但 B 类完全没学到。这种情况下要看每个类别的召回率和 F1 分数,或者用加权交叉熵来处理类别不平衡。

这套 CIFAR-10 的完整流程我反复跑过很多遍,每次都能发现新的细节可以优化。它就像一面镜子,能照出你在数据处理、模型设计、训练调参各个环节的薄弱点。建议你不要只跑一遍就完事,试着改改超参数、换换模型结构、加加数据增强,观察每个改动带来的影响。这种"控制变量"的实验习惯,比记住任何具体技巧都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 12:09:15

MSA2040换盘后必读:热备盘设置与磁盘重建避坑指南

简介:面向存储运维、服务器管理员及负责磁盘阵列维护的技术人员,这份 MSA2040 热备盘配置指南聚焦更换硬盘后如何通过网页管理界面快速完成热备设置。资源包仅包含 1 个 docx 文档,约6.28MB,以文字分步说明为主,针对新…

作者头像 李华
网站建设 2026/10/23 7:39:04

PyTorch实战CIFAR-10:从数据增强到ResNet的精度提升指南

1. 为什么CIFAR-10是检验PyTorch实战能力的试金石很多人入门深度学习,第一个跑通的往往是MNIST——手写数字识别。2828的灰度图,10个类别,随便搭个两层卷积就能冲到99%的准确率。但说实话,MNIST已经太"干净"了&#xff…

作者头像 李华
网站建设 2026/10/24 5:20:50

虚幻引擎实战101条:从性能优化到多平台发布的避坑指南

干了这么多年虚幻引擎项目,从Demo到正式产品,踩过的坑、攒下的窍门,零零散散记了一堆。这篇清单不是从文档里抄出来的,是实打实摸出来的101条经验。我不按引擎版本目录走,直接按工作场景分块,先说设置了再说…

作者头像 李华
网站建设 2026/10/24 22:01:10

物业管理系统源码实战:从表结构到Spring Boot+Vue二次开发全指南

简介:这是一套小区物业管理系统的项目源码,基于 PHP 开发,运行环境为 Apache MySQL,要求 PHP 5.6 及以上、Apache 2.4 及以上,面向计算机相关专业毕业设计、课程作业,也适合想学习管理类系统前后端写法的读…

作者头像 李华
网站建设 2026/10/25 7:26:34

汽车对比评测如何避免拉踩?从流量逻辑到专业框架的深度解析

一看到这类标题,我的第一反应不是生气,而是有点想笑。某位主播为了流量,把豪华品牌、美系新势力和日系车企轮番踩了一遍,评论区吵成一锅粥,播放量倒是蹭蹭往上走。说实话,这种拉踩式内容在汽车内容圈里早就…

作者头像 李华
网站建设 2026/11/3 9:17:10

SpringBoot协同过滤流浪动物领养推荐系统毕设实战解析

我今年做毕业设计选题的时候,在系统开发类题目里挑了很久,最后定下来的是"基于SpringBoot与协同过滤算法的流浪动物领养与救助系统"。说实话,这类题目在毕设里不算新鲜,但真正把它做完、把推荐算法跑通、把论文和系统对…

作者头像 李华