CIFAR100图像分类实战:从数据增强到WideResNet调优全解析
1. 为什么选择CIFAR100作为图像分类的试炼场CIFAR100这个数据集在图像分类圈子里算是老面孔了但它的地位一直很稳。很多人刚入门的时候都是从CIFAR10开始的10个类别每个类别6000张图随便搭个几层卷积就能跑到90%以上的准确率成就感来得很快。但当你真正想验证一个网络结构、一个训练策略、或者一个正则化方法到底有没有效果的时候CIFAR10往往就不够用了——它太简单了简单到很多设计上的缺陷会被高准确率掩盖掉。CIFAR100就不一样了。它同样是60000张32x32的彩色图像但类别数直接翻了十倍变成100个类每个类只有600张训练图。这意味着什么意味着每个类别的样本量骤降到原来的十分之一模型必须在极有限的样本下学会区分100个细粒度类别。这个难度提升不是线性的而是指数级的。你会在训练过程中明显感受到模型“学不动”或者“学偏了”的状态而这恰恰是检验你工程能力和调参直觉的最佳场景。我个人的习惯是任何新的网络结构或者训练技巧先在CIFAR100上跑一遍。如果它能在这个数据集上稳定提升那才有资格上更大的数据集去验证。原因很简单CIFAR100的计算成本可控单卡就能跑一轮实验下来几个小时就能看到结果迭代速度快。而且它的100个类里面有很多语义上非常接近的类别比如“苹果”和“橘子”、“狮子”和“老虎”模型必须学到真正有判别力的特征才能分开这对特征提取能力是极大的考验。这篇文章我会从数据预处理、网络结构设计、训练策略、学习率调度、正则化手段、以及实际跑实验中遇到的各种坑完整地拆一遍。适合已经跑通过CIFAR10、想进一步挑战自己的读者也适合那些想找一个中等规模数据集来验证自己想法的人。我不会只贴代码更重要的是把每个决策背后的逻辑讲清楚让你知道为什么这么做以及不这么做会怎样。2. 数据预处理与增强策略的深度拆解2.1 为什么CIFAR100的预处理不能照搬CIFAR10很多人从CIFAR10迁移到CIFAR100的时候预处理代码基本原封不动地搬过来然后发现模型收敛得特别慢或者验证集准确率卡在40%左右上不去。问题往往就出在预处理上。CIFAR10的10个类别差异很大飞机和猫、汽车和狗类间距离远模型很容易找到区分性的特征。但CIFAR100里面有大量的细粒度类别比如“向日葵”和“玫瑰”都是花“摩托车”和“自行车”都是两轮交通工具。这些类别在32x32的分辨率下像素层面的差异非常小。如果你只是简单地做归一化模型很难捕捉到那些微小的判别性线索。我的做法是在标准归一化之前先做一步逐通道的均值方差分析。具体来说计算训练集每个通道的均值和标准差然后用这些统计量做标准化。CIFAR100的RGB三通道均值和CIFAR10略有不同大概在(0.507, 0.487, 0.441)附近标准差在(0.267, 0.256, 0.276)左右。这个数值看起来和CIFAR10差不多但如果你直接用CIFAR10的统计量会引入一个微小的偏差在细粒度分类任务上这个偏差会被放大。注意归一化用的均值和标准差必须从训练集计算不能用测试集也不能用两者的混合。这是铁律但在实际项目中我见过不止一次有人图省事直接写死一个经验值结果验证集准确率莫名其妙低了两三个点。2.2 数据增强的组合拳怎么打CIFAR100的训练集只有50000张图100个类平均下来每类500张。这个数据量对于深度网络来说是不够的必须靠数据增强来扩充。但增强也不是随便加加错了反而会伤害性能。我常用的增强组合是随机裁剪带padding、随机水平翻转、颜色抖动再加上一个近年来被验证非常有效的Cutout或者RandAugment。随机裁剪我一般设置padding为4裁剪到32x32这样模型对平移的鲁棒性会明显提升。随机水平翻转的概率设0.5这个不用多说绝大多数自然图像水平翻转后语义不变。颜色抖动这块要小心。CIFAR100里面有些类别是靠颜色区分的比如“橙色”和“苹果”如果你把亮度、对比度、饱和度调得太狠模型可能会学到错误的颜色关联。我的经验是亮度变化幅度控制在0.2以内对比度和饱和度控制在0.2以内色调变化控制在0.1以内。这个幅度既能增加多样性又不会破坏类别的核心视觉特征。Cutout是我强烈推荐的一个增强手段。做法很简单在图像上随机选一个16x16的区域把这个区域的像素全部置零。这个操作强迫模型不能只依赖图像的某一块局部区域做判断必须利用全局信息。在CIFAR100上Cutout通常能带来1到2个百分点的准确率提升。RandAugment则更激进一些它从一组增强操作中随机选两个每个操作有随机的强度效果也很好但训练时间会稍微长一点。增强方法参数设置预期收益注意事项随机裁剪padding4, 输出32x322~3%padding太小会导致边缘信息丢失水平翻转p0.51~2%垂直翻转不适用语义会变颜色抖动亮度/对比度/饱和度0.2色调0.11~2%幅度过大会破坏颜色特征Cutout16x16p0.51~2%置零区域不宜过大RandAugmentN2, M92~3%训练时间增加约30%2.3 验证集的划分与使用CIFAR100官方给的训练集是50000张测试集是10000张。很多人直接把测试集当验证集用每轮训练完在测试集上评估然后根据测试集准确率来调参。这个做法在学术论文里可能问题不大但在实际项目中是非常危险的因为你相当于在测试集上做了模型选择最终报告的性能会偏乐观。我的做法是从50000张训练集里划出5000张作为验证集剩下的45000张用来训练。验证集只用来监控训练过程、选择最佳模型和调整超参数。测试集在最终模型确定之前完全不碰。这样得到的测试准确率才是真正可信的。划验证集的时候要注意分层采样保证每个类在验证集里都有50张否则某些类可能一张都没有验证集准确率会波动很大。3. 网络结构设计与选型考量3.1 从VGG到ResNet再到WideResNet的演进逻辑在CIFAR100上网络结构的选择直接决定了性能上限。早期大家用VGG式的堆叠卷积但VGG在CIFAR100上有个致命问题网络一深就退化训练准确率反而下降。这不是过拟合而是梯度消失导致的优化困难。ResNet通过残差连接解决了退化问题让训练超深网络成为可能。但在CIFAR100这个规模上标准的ResNet-50其实有点过大了参数量太多容易过拟合。我试过ResNet-50在CIFAR100上跑如果不加很强的正则化验证集准确率会比ResNet-18还低。原因就是50000张图撑不起50层的参数量。WideResNet是我在CIFAR100上最推荐的结构。它的核心思想是减少深度、增加宽度。比如WideResNet-28-10深度只有28层但每层的通道数是标准ResNet的10倍。这个结构在CIFAR100上表现非常稳不加任何花哨的技巧就能跑到80%以上的准确率。为什么宽比深好因为CIFAR100的图像分辨率低32x32的图经过几次下采样就变成4x4了再深下去感受野都超出图像范围了没有意义。而增加宽度可以提升每层的特征表达能力对细粒度分类特别有帮助。3.2 关键组件的设计细节第一个卷积层CIFAR100的图像是32x32的第一层卷积我一般用3x3的核步长1padding 1输出通道32或64。不要用7x7的大核那是给224x224的ImageNet用的在32x32的图上用7x7核会直接丢掉大量空间信息。下采样策略我采用三次下采样分别在网络的三个阶段。每次下采样用步长为2的3x3卷积或者2x2的最大池化。下采样后通道数翻倍。最终特征图变成4x4通道数达到256或512。这个尺寸刚好再小就丢失太多空间信息了。全局平均池化网络末端用全局平均池化代替全连接层。这个选择在CIFAR100上特别重要因为全连接层的参数量巨大在50000张训练样本下极易过拟合。全局平均池化把每个通道的4x4特征图平均成一个标量然后直接接100类的全连接输出。参数量从几百万降到几万过拟合风险大幅降低。Batch Normalization每个卷积层后面都跟BN这是标配。但要注意BN的momentum参数我一般设0.1。如果设得太小训练初期统计量更新太慢收敛会变慢设得太大统计量波动大训练不稳定。激活函数ReLU是默认选择但我最近在CIFAR100上试了GELU和SiLU发现SiLU在WideResNet上能带来约0.5%的提升。不过SiLU的计算量比ReLU大如果追求训练速度ReLU完全够用。3.3 参数量与计算量的平衡在CIFAR100上参数量不是越多越好。我做过一组对比实验用WideResNet的不同宽度配置跑同样的训练策略结构参数量验证准确率单轮训练时间WRN-16-42.7M76.3%28sWRN-28-45.8M78.1%42sWRN-28-823.4M80.5%78sWRN-28-1036.5M81.2%95sWRN-40-1055.8M81.0%130s可以看到从WRN-28-8到WRN-28-10参数量增加了56%但准确率只提升了0.7%。而WRN-40-10比WRN-28-10更深准确率反而略降说明过拟合已经开始显现。所以我的推荐是WRN-28-10作为起点如果计算资源有限WRN-28-8是性价比最高的选择。4. 训练策略与学习率调度的实战配置4.1 优化器选择与参数设置CIFAR100上我用得最多的是SGD with momentummomentum设0.9weight decay设5e-4。为什么不用AdamAdam在训练初期收敛确实快但在CIFAR100这种需要精细分类的任务上Adam最终收敛到的解往往比SGD差。我猜测是因为Adam的自适应学习率在细粒度特征学习上容易陷入局部最优而SGD的噪声反而有助于跳出局部极小值。初始学习率我一般设0.1batch size设128。这个组合在WideResNet上经过大量实验验证是稳的。如果batch size翻倍到256学习率也要相应翻倍到0.2这是线性缩放规则。但注意学习率不是越大越好0.2以上在CIFAR100上容易导致训练初期loss爆炸。Weight decay设5e-4是我在CIFAR100上的经验值。CIFAR10上一般用1e-4就够了但CIFAR100类别多、样本少需要更强的正则化来防止过拟合。如果你发现训练准确率和验证准确率差距很大可以适当增大weight decay到1e-3试试。4.2 学习率调度余弦退火为什么比阶梯下降好学习率调度是训练CIFAR100的关键。我早期用阶梯下降在epoch 60、120、160的时候各降一次每次降为原来的0.2倍。这个策略能用但需要手动调下降的时机和幅度比较麻烦。后来我换成了余弦退火学习率从初始值按照余弦曲线平滑下降到0。这个策略的好处是训练后期学习率非常小模型可以在损失曲面的底部精细搜索找到更优的解。而且余弦退火不需要手动设下降节点超参数少更省心。具体配置是总epoch数200学习率从0.1按余弦退火降到0。前5个epoch用warmup学习率从0.01线性升到0.1。Warmup的作用是防止训练初期梯度太大导致参数更新过猛特别是BN层的统计量还没稳定的时候warmup能显著提升训练稳定性。实操心得余弦退火的周期设置要和总epoch数匹配。如果你只跑100个epoch那余弦周期就是100如果跑200个epoch周期就是200。不要设一个固定的周期然后中途停止那样学习率还没降到足够小模型没有充分收敛。4.3 标签平滑与混合增强标签平滑是我在CIFAR100上必加的一个技巧。标准交叉熵用的是one-hot标签模型会拼命把正确类的logit推高把其他类的logit压低。但在CIFAR100上有些类别本身就存在语义重叠比如“男孩”和“男人”模型把“男孩”的图片预测成“男人”其实不算大错但one-hot标签会惩罚这个预测。标签平滑把目标从1和0改成0.9和0.1/99给模型一点容错空间。这个操作通常能带来0.5到1个百分点的提升而且几乎不增加计算成本。Mixup和CutMix是另外两个我常用的增强手段。Mixup把两张图按比例混合标签也按同样比例混合。CutMix则是把一张图的一块区域剪切粘贴到另一张图上标签按区域面积比例混合。这两个方法都能显著提升模型的泛化能力在CIFAR100上通常能带来1到2个百分点的提升。我一般从epoch 50开始启用Mixup前期不用因为前期模型还在学基础特征混合图像会干扰学习。5. 正则化手段与过拟合对抗5.1 Dropout放在哪里最有效Dropout在CIFAR100上的使用位置很有讲究。我见过有人在每个卷积层后面都加Dropout结果训练loss降不下去模型欠拟合了。卷积层的Dropout会破坏空间特征效果不好。我的做法是在全局平均池化之后、最后的全连接层之前加一个Dropout概率设0.3到0.5。这个位置是特征已经压缩成向量之后Dropout相当于在特征层面做集成效果最好。另外如果网络里有全连接层全连接层后面也可以加Dropout但WideResNet通常没有中间全连接层所以只需要在最后加一个。5.2 权重衰减与BN的交互Weight decay和Batch Normalization一起用的时候有个细节BN层的参数gamma和beta不应该被weight decay惩罚。因为gamma和beta是用来缩放和平移归一化后的特征的对它们做衰减会破坏BN的表达能力。在PyTorch里你需要把BN层的参数从weight decay的参数组里排除出去。这个细节很多人不知道但实测下来正确排除BN参数能带来约0.3%的准确率提升。实现方式是在优化器里分两个参数组一个组包含卷积层和全连接层的权重设weight decay另一个组包含所有BN参数和偏置设weight decay为0。5.3 早停与模型选择CIFAR100训练到后期验证集准确率会先升后降这是过拟合的典型信号。我一般每轮训练完都在验证集上评估记录最佳准确率和对应的epoch。如果连续20个epoch验证准确率没有提升就触发早停回滚到最佳模型。但要注意余弦退火的学习率在训练末期非常小验证准确率可能会在最后几个epoch突然跳一下。所以我通常不会在验证准确率一降就停而是等到学习率降到接近0的时候再看。如果最后几个epoch验证准确率确实没有回升那说明模型已经收敛到极限了早停是合理的。6. 实操过程与关键环节实现6.1 环境配置与数据加载我用的环境是PyTorch 1.12加上CUDA 11.6单卡RTX 3060 12GB。CIFAR100的数据集用torchvision自带的数据加载器就能下载但下载速度有时候不稳定我一般提前下载好放到本地目录。数据加载的num_workers我设4pin_memory设True。这两个参数对训练速度影响很大。num_workers设0的话数据加载会成为瓶颈GPU利用率可能只有50%不到。设4之后GPU利用率能到90%以上。pin_memory设True可以加速CPU到GPU的数据传输。import torch import torchvision import torchvision.transforms as transforms transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1), transforms.ToTensor(), transforms.Normalize((0.507, 0.487, 0.441), (0.267, 0.256, 0.276)), transforms.RandomErasing(p0.5, scale(0.02, 0.2), ratio(0.3, 3.3)) ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.507, 0.487, 0.441), (0.267, 0.256, 0.276)) ]) trainset torchvision.datasets.CIFAR100(root./data, trainTrue, downloadTrue, transformtransform_train) testset torchvision.datasets.CIFAR100(root./data, trainFalse, downloadTrue, transformtransform_test)6.2 WideResNet的实现要点WideResNet的核心是BasicBlock每个block包含两个3x3卷积加上BN和ReLU再加上残差连接。当输入输出通道数不一致或者特征图尺寸变化时残差连接需要一个1x1卷积来匹配维度。import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): def __init__(self, in_planes, out_planes, stride, drop_rate0.0): super(BasicBlock, self).__init__() self.bn1 nn.BatchNorm2d(in_planes) self.conv1 nn.Conv2d(in_planes, out_planes, kernel_size3, stridestride, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_planes) self.conv2 nn.Conv2d(out_planes, out_planes, kernel_size3, stride1, padding1, biasFalse) self.drop_rate drop_rate self.equal_in_out (in_planes out_planes) self.shortcut (not self.equal_in_out) and nn.Conv2d( in_planes, out_planes, kernel_size1, stridestride, padding0, biasFalse) or None def forward(self, x): out F.relu(self.bn1(x)) shortcut self.shortcut(out) if self.shortcut else x out self.conv1(out) out self.conv2(F.relu(self.bn2(out))) if self.drop_rate 0: out F.dropout(out, pself.drop_rate, trainingself.training) return out shortcut这个实现里有个细节BN放在卷积前面还是后面。我这里是BN-ReLU-Conv的顺序这是pre-activation的变体。原始ResNet是Conv-BN-ReLU但pre-activation在CIFAR上通常表现更好因为梯度可以直接通过残差连接回传不受BN的影响。6.3 训练循环与关键监控指标训练循环里我重点监控三个指标训练loss、验证loss、验证准确率。训练loss看模型是否在正常学习验证loss看是否过拟合验证准确率看最终性能。def train_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 correct 0 total 0 for inputs, targets in loader: inputs, targets inputs.to(device), targets.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() total_loss loss.item() * inputs.size(0) _, predicted outputs.max(1) total targets.size(0) correct predicted.eq(targets).sum().item() return total_loss / total, 100. * correct / total验证的时候记得把模型设成eval模式关闭Dropout和BN的统计量更新。这个切换如果忘了验证结果会波动很大而且BN的running stats会被验证集污染。6.4 完整训练配置与预期结果我跑的一组完整配置是WideResNet-28-10SGD momentum 0.9weight decay 5e-4初始学习率0.1余弦退火200 epochwarmup 5 epochbatch size 128标签平滑0.1Cutout 16x16从epoch 50开始Mixup alpha0.2。这套配置在单卡RTX 3060上大概跑6个小时最终测试准确率在81.5%左右。如果去掉Mixup和Cutout准确率会降到79%左右。如果再加上RandAugment能到82%左右但训练时间会增加30%。配置组合测试准确率训练时间基础配置79.2%5hCutout80.5%5.2hCutoutMixup81.5%5.8hCutoutMixupRandAugment82.1%7.5h7. 常见问题与排查技巧实录7.1 训练loss不下降怎么办这是最常见的问题。如果训练loss在前几个epoch几乎不动先检查学习率是不是太小了。0.1的初始学习率在WideResNet上是合适的但如果你换了一个更浅或者更深的网络可能需要调整。另外检查数据归一化是不是做对了如果输入图像的数值范围是0到255而不是0到1梯度会爆炸或者消失。还有一个容易被忽略的点是标签是否正确对齐。CIFAR100的标签是0到99的整数如果你不小心做了某种映射导致标签错位模型永远学不对。我一般会在训练前先取一个batch打印几张图的标签和对应的类别名肉眼确认一下。7.2 验证准确率波动大验证准确率每轮波动超过2个百分点通常是因为验证集太小或者BatchNorm在验证时的行为不一致。如果你只划了1000张验证集每类只有10张那波动大是正常的。我建议至少划5000张每类50张。另一个原因是验证时没有正确切换eval模式。如果模型还在train模式Dropout会随机丢弃神经元BN会用当前batch的统计量而不是running stats导致验证结果不稳定。确保在验证前调用model.eval()验证后调用model.train()。7.3 过拟合的识别与应对训练准确率到95%以上验证准确率卡在75%左右上不去这就是典型的过拟合。应对手段按优先级排序第一增加数据增强的强度特别是Cutout和Mixup第二增大weight decay从5e-4提到1e-3第三增加Dropout概率从0.3提到0.5第四减小模型宽度比如从WRN-28-10降到WRN-28-8。如果这些手段都试过了还是过拟合那可能是数据量本身就不够。CIFAR100每类500张训练图确实偏少可以考虑用半监督学习或者自监督预训练来利用测试集的10000张图。不过这就超出本文的范围了。7.4 训练速度慢的优化GPU利用率低是训练慢的主要原因。用nvidia-smi查看GPU利用率如果低于70%说明数据加载是瓶颈。把num_workers从4提到8pin_memory设True能显著提升。另外如果显存够用可以把batch size从128提到256同时学习率翻倍到0.2这样每轮训练时间能减少约30%。混合精度训练是另一个加速手段。用torch.cuda.amp自动混合精度前向和反向用float16参数更新用float32。这个操作通常能提速30%到50%而且几乎不影响准确率。但要注意混合精度下loss scaling要设对否则梯度会下溢。问题现象可能原因排查方法解决方案训练loss不降学习率过小/数据未归一化打印输入范围和学习率调整学习率检查归一化验证准确率波动大验证集太小/未切eval模式检查验证集大小和模型模式扩大验证集确保eval模式过拟合模型太大/增强不足对比训练和验证准确率增强数据增大正则化训练速度慢数据加载瓶颈查看GPU利用率增加num_workers混合精度7.5 独家避坑技巧第一个坑是余弦退火的周期设置。如果你打算跑200个epoch但中途因为某种原因停了想接着跑学习率会从停的地方继续余弦曲线但此时的学习率可能已经很小了模型几乎不再更新。我的做法是保存优化器的状态包括学习率调度器的状态这样恢复训练时学习率能正确接上。第二个坑是Mixup和Cutout同时用的时候Cutout的置零区域会被Mixup混合效果会打折扣。我的做法是先用Cutout再用Mixup或者只用其中一个。实测下来Mixup加Cutout的组合比单独用Mixup提升约0.5%但比单独用Cutout提升约1%。第三个坑是标签平滑和Mixup同时用的时候标签平滑的目标值需要调整。标准标签平滑是0.9和0.1/99但Mixup之后标签是混合的如果还用标准标签平滑目标值之和可能不等于1。我的做法是在Mixup之后不再做标签平滑或者把标签平滑的epsilon调小到0.05。第四个坑是BN的momentum在训练后期需要调整。训练初期用0.1没问题但训练后期模型参数变化很小BN的running stats更新太慢可能导致验证时统计量不匹配。我一般会在最后20个epoch把BN momentum降到0.01让running stats更贴近最终参数。8. 从CIFAR100延伸出去的几个方向CIFAR100跑通之后你可以往几个方向继续深入。一个是细粒度分类CIFAR100本身就有细粒度的特性你可以进一步用CIFAR100的层级标签做层次分类让模型不仅预测细类还预测粗类。另一个方向是自监督预训练用SimCLR或者MoCo在CIFAR100的无标签数据上预训练然后再微调通常能比从头训练提升3到5个百分点。还有一个方向是模型压缩。WideResNet-28-10有36M参数部署到边缘设备上太大了。你可以用知识蒸馏用一个大的教师模型教一个小的学生模型学生模型可能只有1M参数但准确率能保持在78%左右。这个在實際落地场景里非常有用。我个人的体会是CIFAR100是一个非常好的试炼场它足够难能暴露你训练策略里的各种问题又足够小让你能在几个小时内完成一轮实验。每次我在CIFAR100上调出一个新的准确率记录那种感觉比在ImageNet上刷点要实在得多因为你知道每一个百分点的提升都来自于对细节的精确把控而不是靠堆算力堆出来的。