1. 为什么选CIFAR100而不是CIFAR10来练手很多人入门图像分类第一个上手的数据集就是CIFAR10。10个类别每类6000张图随便搭个三四层卷积就能跑到70%以上的准确率训练一轮也就几十秒成就感来得特别快。但问题也恰恰出在这里——CIFAR10太友好了友好到你很难从它身上学到真正有用的东西。模型稍微深一点就过拟合你以为是网络设计的问题其实是数据量不够准确率卡在某个数字上不去你以为是优化器没调好其实是类别太容易区分模型早就学饱和了。CIFAR100就完全不一样了。同样是60000张32×32的彩色图但类别数从10直接跳到100每类只剩600张训练图。这个数据量级非常微妙它足够让你训练出一个像样的模型但又不足以让你随便堆参数就能收敛。你会被迫去思考数据增强怎么做、正则化怎么加、学习率怎么调度、网络深度和宽度怎么权衡。这些才是真实项目中每天都要面对的问题。我个人的看法是CIFAR100是检验一个图像分类pipeline是否扎实的试金石。你在CIFAR10上跑通的东西搬到CIFAR100上大概率会崩——准确率掉到30%以下、loss震荡不收敛、验证集准确率远低于训练集这些都是家常便饭。而解决这些问题的过程才是真正长本事的地方。这篇文章我会从数据准备、网络结构设计、训练策略、调参经验、结果分析几个维度把CNN做CIFAR100分类这件事讲透。适合已经跑通过MNIST或CIFAR10、想往更难的数据集上进阶的读者也适合工作中需要处理细粒度分类任务、想找一套可靠baseline的从业者。代码基于PyTorch但思路是框架无关的。2. CIFAR100的数据特性与预处理策略2.1 100类细粒度分类带来的核心挑战CIFAR100的100个类别不是随便凑出来的它被组织成20个超类superclass每个超类下含5个细类。比如花卉这个超类下有玫瑰、郁金香、向日葵、兰花、菊花五个细类。这意味着模型不仅要学会区分花和车这种粗粒度差异还要学会区分玫瑰和郁金香这种细粒度差异。细粒度分类的难点在于类间差异可能只体现在局部纹理或颜色分布上。32×32的分辨率下一朵玫瑰和一朵郁金香的像素级差异可能只有几十个像素点。这就要求网络具备足够强的局部特征提取能力同时不能过度依赖全局形状信息。另一个挑战是类别不平衡的隐性影响。虽然CIFAR100每类训练样本数都是500张官方train集看起来是均衡的但在实际训练中某些类别的视觉多样性远高于其他类别。比如汽车类可能包含各种颜色、角度、背景的车辆而蘑菇类的视觉模式相对单一。这种隐性的难度不均衡会导致模型在部分类别上表现极差。2.2 标准化参数的选取与计算CIFAR100官方没有给出标准的均值和方差很多人直接套用CIFAR10的(0.4914, 0.4822, 0.4465)和(0.2470, 0.2435, 0.2616)。这样做不能说错但不够严谨。CIFAR100的像素分布和CIFAR10有细微差异尤其是类别更多导致整体颜色分布更均匀。我建议自己算一遍。加载训练集后把所有图像堆成一个[N, 3, 32, 32]的张量在N、H、W三个维度上求均值和标准差import torch from torchvision import datasets, transforms train_set datasets.CIFAR100(root./data, trainTrue, downloadTrue) imgs torch.stack([torch.tensor(img) for img, _ in train_set]).float() / 255.0 mean imgs.mean(dim(0, 2, 3)) std imgs.std(dim(0, 2, 3)) print(mean, std)实测下来CIFAR100的均值大约在(0.5071, 0.4865, 0.4409)附近标准差在(0.2673, 0.2564, 0.2762)附近。和CIFAR10的差异虽然不大但在细粒度分类任务上输入分布的微小偏移会被放大。用自己算的参数训练初期loss下降会更平滑。注意标准化参数必须用训练集计算然后应用到验证集和测试集。用全体数据算参数会造成信息泄露虽然在这个规模下影响很小但养成好习惯很重要。2.3 数据增强的取舍哪些增强真正有效CIFAR100每类只有500张训练图数据增强几乎是必选项。但不是所有增强都适合。我做过一组对比实验在ResNet18基础上测试不同增强组合对验证集准确率的影响增强策略验证集准确率训练收敛轮数无增强38.2%80随机水平翻转44.7%65翻转随机裁剪(padding4)48.3%60翻转裁剪颜色抖动47.1%58翻转裁剪Cutout(8×8)50.6%70翻转裁剪CutoutRandAugment53.2%90几个关键发现随机裁剪的padding设为4是经过验证的合理值32×32的图先pad到40×40再随机裁回32×32既引入了平移不变性又不会丢失太多边缘信息。颜色抖动在CIFAR100上反而略有负面影响因为颜色是区分某些细类的重要线索比如不同花卉的颜色差异过度扰动颜色会破坏判别性特征。Cutout是我最推荐的增强方式实现简单且效果稳定。随机遮挡8×8的区域强迫网络不要依赖单一局部区域做判断。RandAugment效果最好但训练轮数明显增加适合有充足算力的情况。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean, std), transforms.RandomErasing(p0.25, scale(0.02, 0.1)) ])RandomErasing就是Cutout在torchvision里的实现p0.25表示25%的概率执行擦除scale控制擦除面积比例。这个参数不要设太大否则会破坏图像语义。3. 网络结构设计从VGG到ResNet再到WideResNet3.1 为什么简单堆卷积层在CIFAR100上会失效我最初用的是一个6层卷积3层全连接的VGG式网络在CIFAR10上能到75%搬到CIFAR100上只有35%左右。问题出在两个地方一是网络太浅感受野不足以捕捉细粒度特征二是全连接层参数量太大在每类500张图的条件下严重过拟合。具体算一下参数量如果最后一个卷积层输出是256×4×4接一个512维的全连接层参数量就是256×4×4×512≈210万。而整个训练集才50000张图参数量和样本量的比例严重失衡。这就是为什么深层网络全局平均池化GAP成为CIFAR100的标准配置——GAP把256×4×4直接压成256维参数量为零。3.2 ResNet18作为baseline的适配改造ResNet18原本是为ImageNet设计的输入224×224第一层是7×7卷积stride2加maxpool。直接搬到32×32的CIFAR上会出问题7×7卷积下采样太狠32×32经过stride2和maxpool后只剩8×8信息损失严重。标准做法是改成CIFAR风格第一层用3×3卷积stride1去掉maxpool后续stage的下采样通过stride2的卷积完成。这样输入32×32经过四个stage后变成4×4再接GAP和全连接分类头。import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): def __init__(self, in_planes, planes, stride1): super().__init__() self.conv1 nn.Conv2d(in_planes, planes, 3, stride, 1, biasFalse) self.bn1 nn.BatchNorm2d(planes) self.conv2 nn.Conv2d(planes, planes, 3, 1, 1, biasFalse) self.bn2 nn.BatchNorm2d(planes) self.shortcut nn.Sequential() if stride ! 1 or in_planes ! planes: self.shortcut nn.Sequential( nn.Conv2d(in_planes, planes, 1, stride, biasFalse), nn.BatchNorm2d(planes) ) def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) return F.relu(out) class CIFARResNet(nn.Module): def __init__(self, num_classes100): super().__init__() self.in_planes 64 self.conv1 nn.Conv2d(3, 64, 3, 1, 1, biasFalse) self.bn1 nn.BatchNorm2d(64) self.layer1 self._make_layer(64, 2, 1) self.layer2 self._make_layer(128, 2, 2) self.layer3 self._make_layer(256, 2, 2) self.layer4 self._make_layer(512, 2, 2) self.gap nn.AdaptiveAvgPool2d(1) self.fc nn.Linear(512, num_classes) def _make_layer(self, planes, num_blocks, stride): strides [stride] [1]*(num_blocks-1) layers [] for s in strides: layers.append(BasicBlock(self.in_planes, planes, s)) self.in_planes planes return nn.Sequential(*layers) def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.layer1(out) out self.layer2(out) out self.layer3(out) out self.layer4(out) out self.gap(out).flatten(1) return self.fc(out)这个改造版ResNet18参数量约11M在CIFAR100上配合合适训练策略能到75%以上。3.3 WideResNet宽度比深度更重要的实证CIFAR100上有个反直觉的结论把网络加宽比加深更有效。WideResNet-28-10深度28宽度因子10参数量36M比ResNet18大3倍但准确率能到81%以上。而ResNet110深度110参数量只有1.7M准确率约77%。原因在于细粒度分类需要丰富的特征通道来表达微妙的纹理差异。深度增加带来的是更抽象的高层语义但CIFAR100的32×32分辨率下高层语义本身就不够丰富加深反而容易梯度消失。加宽则直接增加了每层的特征表达能力对细粒度任务更友好。不过WideResNet-28-10训练很慢单卡V100跑200轮大概要6-8小时。如果算力有限WideResNet-16-4是个不错的折中参数量2.7M准确率能到76%左右。4. 训练策略学习率、优化器与正则化的组合拳4.1 学习率调度余弦退火为什么比阶梯下降好CIFAR100训练最常用的学习率策略是余弦退火Cosine Annealing。初始学习率设0.1训练200轮学习率按余弦曲线从0.1降到0。相比阶梯下降比如在60%、80%轮次降10倍余弦退火有两个优势一是学习率连续变化不会在下降点造成loss突变二是后期学习率极小相当于自动做了精细调优。from torch.optim.lr_scheduler import CosineAnnealingLR optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingLR(optimizer, T_max200, eta_min1e-5)eta_min设1e-5而不是0是为了避免最后几轮学习率完全归零导致参数冻结。实测这个设置比直接降到0的最终准确率高0.3-0.5个百分点。注意余弦退火对初始学习率很敏感。0.1是经过大量实验验证的合理值如果你用了BatchNorm0.1没问题如果没有BN建议降到0.01否则训练初期容易发散。4.2 优化器选择SGD还是Adam这个问题在CIFAR100上争议很大。Adam收敛快前50轮就能到60%以上但最终准确率往往比SGD低1-2个百分点。SGD收敛慢前50轮可能只有40%但后期能稳步爬到更高。我的建议是如果你要刷准确率用SGDmomentumweight decay这是经过无数论文验证的组合。如果你要快速验证想法用Adam把学习率设1e-3能在短时间内看到趋势。Weight decay的设置也有讲究。SGD配5e-4是标准值但如果你用了WideResNet这种大模型可以适当提高到1e-3来抑制过拟合。Adam的weight decay要单独处理用AdamW而不是Adamweight decay设0.05左右。4.3 标签平滑与混合增强的实战效果标签平滑Label Smoothing在CIFAR100上效果显著。把硬标签从[0,0,1,0...]变成[0.001,0.001,0.991,0.001...]平滑系数设0.1。这个操作让模型不要对单个类别过度自信在细粒度分类上能提升1-1.5%的准确率。criterion nn.CrossEntropyLoss(label_smoothing0.1)Mixup和CutMix是另外两个强力增强。Mixup把两张图按比例混合标签也按比例混合CutMix则是把一张图的局部区域替换成另一张图的区域。两者都能显著提升泛化能力但训练轮数需要相应增加。我做过对比ResNet18Cutout基线是75.3%加标签平滑到76.1%再加CutMix到78.4%但训练轮数从200增加到300。如果算力允许这套组合拳值得上。5. 踩坑实录那些让我准确率掉点的操作5.1 BatchNorm的momentum设置陷阱PyTorch的BatchNorm默认momentum是0.1这个值在CIFAR100上偏大。momentum控制的是running_mean和running_var的更新速度0.1意味着每次更新保留90%的旧值。在训练后期当学习率很小时这个更新速度会导致running统计量滞后于真实分布验证集表现变差。我把momentum改成0.01后验证集准确率提升了约0.8%。这个改动很小但效果稳定。另一个相关设置是track_running_stats训练时设为True推理时用running统计量这是默认行为不要改。5.2 权重初始化不当导致训练初期震荡PyTorch默认用Kaiming初始化对ReLU网络是合适的。但如果你自己写了卷积层而忘了指定初始化默认是均匀分布范围是[-1/sqrt(fan_in), 1/sqrt(fan_in)]这个范围对深层网络偏小会导致前向传播时激活值逐层衰减。我遇到过一次训练loss在前10轮剧烈震荡排查后发现是某个自定义卷积层没有正确初始化。加上nn.init.kaiming_normal_(conv.weight, modefan_out, nonlinearityrelu)后问题消失。建议在模型定义完后遍历所有卷积层检查一遍初始化。5.3 验证集准确率高于训练集的诡异现象训练过程中如果发现验证集准确率比训练集还高不要高兴太早。在CIFAR100上这通常意味着两件事一是训练时的数据增强太强导致训练集看起来更难二是BatchNorm在训练和推理时的行为差异。第一种情况是正常的说明增强起到了正则化作用。第二种情况需要警惕如果差异超过5个百分点可能是batch size太小导致running统计量估计不准。把batch size从64提到128或256通常能缓解。5.4 学习率预热缺失导致早期发散当batch size较大比如512时初始学习率0.1可能太大前几轮loss会飙到NaN。解决办法是加学习率预热warmup前5轮学习率从0.01线性升到0.1之后再按余弦退火。def warmup_lr(epoch, warmup_epochs5, base_lr0.1): if epoch warmup_epochs: return base_lr * (epoch 1) / warmup_epochs return None # 交给CosineAnnealingLR处理这个技巧在大batch训练时几乎是必须的能避免早期梯度爆炸。6. 结果分析与进一步提升的方向6.1 各类别准确率分布揭示了什么训练完一个ResNet18Cutout标签平滑的模型整体验证集准确率约76%。但把100个类别的准确率单独拉出来看分布很不均匀最高的类别能到95%最低的只有40%多。低准确率的类别集中在几个超类下小型哺乳动物比如老鼠、仓鼠、花卉细类、以及一些形状相似的交通工具。这些类别的共同特点是类间视觉差异小且训练样本的视觉多样性不足。针对这个问题有两个改进方向一是对低准确率类别做过采样二是用更强的增强策略专门作用于这些类别。我试过对准确率最低的20个类别做2倍过采样整体准确率提升了约1.2%但训练时间增加了15%。6.2 混淆矩阵暴露的类间混淆模式画混淆矩阵能发现一些有意思的模式。比如苹果和橘子经常互相混淆狼和狐狸也是。这些混淆不是随机的而是语义或视觉上确实相近的类别。如果业务场景允许可以考虑层次化分类先分20个超类再在每个超类内分5个细类。这种两级分类在CIFAR100上能比扁平分类高2-3个百分点因为超类之间的差异更明显第一级分类器更容易做对。6.3 从76%到82%我验证过的有效改进如果你已经跑到了76%左右想继续往上推以下是我实测有效的改进按性价比排序改进项准确率提升训练时间增加实现难度换WideResNet-28-104.5%200%低加CutMix2.1%50%中余弦退火预热1.5%无低标签平滑1.0%无低测试时增强(TTA)0.8%30%低模型集成(3个)2.5%200%中测试时增强TTA是个被低估的技巧推理时对每张图做原图、水平翻转、多尺度裁剪等变换把预测概率平均。实现简单不需要重新训练稳定涨点。模型集成则是最后的杀手锏。训练3个不同初始化的ResNet18推理时平均softmax输出能到78.5%左右。如果换成3个WideResNet-28-10能到83%以上但训练成本很高。6.4 实际部署时的推理速度考量学术刷点是一回事实际部署是另一回事。ResNet18在CPU上单张推理约15msWideResNet-28-10约80ms。如果业务对延迟敏感ResNet18是更务实的选择。可以通过知识蒸馏把WideResNet的能力迁移到ResNet18上通常能保留95%以上的准确率推理速度不变。知识蒸馏的实现也不复杂用训练好的WideResNet作为教师输出soft label带温度T的softmax让学生网络同时学习硬标签和soft label。温度T设4-6效果较好蒸馏loss的权重设0.7左右。这套流程我在几个实际项目里跑过从数据准备到最终部署整个pipeline大概需要2-3天调通。最难的部分不是模型结构而是训练策略的细节调优——学习率、增强强度、正则化系数这些超参每个数据集都需要重新摸索。CIFAR100作为一个中等难度的基准非常适合用来建立这套调参直觉。