资讯详情

ResNet到Wide-ResNet:CIFAR-100分类实战与调参指南

📅 2026/10/12 15:25:07 | 华诺云谱 👁 阅读
ResNet到Wide-ResNet:CIFAR-100分类实战与调参指南
简介面向希望系统掌握图像分类实战的深度学习初学者与研究者CIFAR-100分类开源项目提供了基于ResNet与Wide-ResNet的完整解决方案。项目覆盖CIFAR-100数据的加载与预处理、数据增强策略、网络搭建、训练测试及性能评估全流程并直观展示跳跃连接与加宽残差块的设计思路便于对比两种架构在百类小图任务上的表现。资源包共15个文件压缩后仅1.05MB5个ipynb对应不同正确率版本的实验记录3个py封装模型定义与训练脚本3张png呈现准确率曲线3个md提供说明文档另有1个介绍文件目录清晰、便于按需查阅。代码涉及学习率调整、权重初始化、正则化等调优手段并保留76%与78%正确率的版本节点方便复现与对比。已有194人学习是理解残差网络调优、开展图像分类实验的实用资料。1. CIFAR-100 分类实战为什么 ResNet 只是起点不是终点在 CIFAR-100 这种 32×32 小图上做分类实战很多人第一反应是拿现成 ResNet 跑一遍收工。真跑过就会发现ResNet-50 在这种小分辨率、100 类别、每类只有 500 张训练图的数据集上练到 200 轮也就在 74%75% 附近晃而同样训练代价的 Wide-ResNet 能稳定上到 80% 左右。这个差距不是玄学而是“加深”和“加宽”在小数据集上的本质区别。这篇文章把从 ResNet 基线到 Wide-ResNet 的完整链路拆开模型结构怎么改、训练超参怎么配、哪些坑会让精度反复横跳。适合已经会用 PyTorch 跑通 ImageNet 分类、但想在 CIFAR-100 上把单模型精度做上去的开发者。2. 从 ResNet 到 Wide-ResNet模型选型与两个关键改动2.1 WRN 的核心思路加宽比加深对小图更划算CIFAR-100 的难点不在于分辨率而在于“类别多、每类样本少”。100 类、每类 500 张训练图意味着模型稍微复杂一点就开始背训练集。这时候盲目堆深度不是好选择32×32 的输入经过 ResNet 的下采样到最后一层特征图只有 8×8深层网络里大量 1×1 卷积把通道压来压去信息流动反而成了瓶颈。更麻烦的是ResNet 的 Bottleneck 结构在小数据集上容易过拟合训练集 acc 冲到 95% 以上验证集却卡在 75% 附近不动。Wide-ResNet简称 WRN标题里的 Wide-ResNet 就是它换了个思路不加深而是把每个残差块的通道数乘上一个系数 k同时用两个 3×3 卷积代替 Bottleneck 里的 1×1-3×3-1×1 三段式。两个 3×3 卷积的感受野和一个 5×5 卷积相当但参数更少、非线性更强配合大通道数信息在 block 内部流动得很充分。CIFAR 上最常用的配置是 WRN-28-10即 28 层、k10参数量大约 36M比 ResNet-50 大一圈但在小图上反而更容易训到 80% 以上。这里还有一个经常被忽略的关键改动WRN 在残差 block 内部加了 dropout概率通常取 0.3。ResNet 本身不带 dropout靠 BN 的随机性做正则WRN 通道数大过拟合风险高所以作者在第二个 3×3 卷积之前插一个 Dropout(p0.3)把正则补上。很多人复现 WRN 掉点就是把这个 dropout 漏了或者位置放错。2.2 WRN 完整模型定义一份能直接跑通的最小结构下面这份代码是我整理项目时一直用的 WRN 结构基于 PyTorch 实现不依赖额外库。它和 torchvision 里 ResNet 的最大区别就是我用WideBasicBlock替代Bottleneck并显式加入 dropout。import torch import torch.nn as nn import torch.nn.functional as F class WideBasicBlock(nn.Module): def __init__(self, in_planes, out_planes, stride, dropout_rate0.0): super().__init__() self.bn1 nn.BatchNorm2d(in_planes) self.conv1 nn.Conv2d(in_planes, out_planes, 3, stride, 1, biasFalse) self.bn2 nn.BatchNorm2d(out_planes) # 关键点dropout 放在第二个 3x3 卷积之前 self.dropout nn.Dropout(pdropout_rate) if dropout_rate 0 else nn.Identity() self.conv2 nn.Conv2d(out_planes, out_planes, 3, 1, 1, biasFalse) # shortcut分辨率或通道数变化时用 1x1 卷积对齐 self.shortcut nn.Sequential() if stride ! 1 or in_planes ! out_planes: self.shortcut nn.Conv2d(in_planes, out_planes, 1, stride, biasFalse) def forward(self, x): out F.relu(self.bn1(x)) out self.conv1(out) out F.relu(self.bn2(out)) out self.dropout(out) out self.conv2(out) return out self.shortcut(x) class WideResNet(nn.Module): def __init__(self, depth28, num_classes100, widen_factor10, dropout_rate0.3): super().__init__() # depth 必须满足 6n4CIFAR 版常用 28 / 34 / 40 assert (depth - 4) % 6 0, depth 需满足 6n4 self.in_planes 16 n (depth - 4) // 6 # 每个 stage 里的 block 数量 self.conv1 nn.Conv2d(3, 16, 3, 1, 1, biasFalse) self.layer1 self._make_layer(16 * widen_factor, n, stride1, dropout_ratedropout_rate) self.layer2 self._make_layer(32 * widen_factor, n, stride2, dropout_ratedropout_rate) self.layer3 self._make_layer(64 * widen_factor, n, stride2, dropout_ratedropout_rate) self.bn nn.BatchNorm2d(64 * widen_factor) self.avgpool nn.AvgPool2d(8) self.fc nn.Linear(64 * widen_factor, num_classes) def _make_layer(self, out_planes, blocks, stride, dropout_rate): layers [WideBasicBlock(self.in_planes, out_planes, stride, dropout_rate)] self.in_planes out_planes for _ in range(1, blocks): layers.append(WideBasicBlock(self.in_planes, out_planes, stride1, dropout_ratedropout_rate)) return nn.Sequential(*layers) def forward(self, x): x self.conv1(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x F.relu(self.bn(x)) x self.avgpool(x) # 32x32 - 1x1 x x.view(x.size(0), -1) return self.fc(x)代码逻辑很简单先用一个 3×3 卷积把输入从 3 通道变到 16 通道然后过三个 stage。每个 stage 的通道数分别是 16k、32k、64k其中第二个和第三个 stage 的第一个 block 用 stride2 下采样对应特征图从 32 降到 16 再到 8。widen_factor就是标题里 Wide-ResNet 的“宽”字所在k10 时最后一层是 640 通道。需要调的两个核心参数是depth和widen_factor。我在 CIFAR-100 上推荐的顺序是先用 28-10depth28, k10跑通流程把验证集做到 80% 左右时间紧就换 40-10精度能再高 0.30.5 个点但训练时间明显变长。dropout_rate默认 0.3这是原论文在 CIFAR 上的常见取值后面避坑章节会讲为什么不能随手改成 0.1 或 0.5。注意不同社区的 WRN 复现dropout 位置略有差异。有的放在两个 conv 之后做 post-activation有的放在 shortcut 加法之后。本文采用的是 CIFAR 任务中最常见的一种BN-ReLU-Conv-BN-ReLU-Dropout-Conv。如果你从别处拷到代码先确认这个顺序。3. 把训练跑起来数据增强、优化器与学习率调度怎么配3.1 数据增强组合对 CIFAR-100 最管用的三招CIFAR-100 的原始图像是 32×32 彩色图训练集 5 万张。数据增强不是“锦上添花”而是决定你有没有 80% 的分水岭。我实验下来收益最大的是下面三招。第一是RandomCrop(32, padding4)加RandomHorizontalFlip()。这是 CIFAR 上的标配增强padding 4 等于把图像先放大到 40×40 再随机裁回 32×32等价于引入平移扰动。这两个操作几乎不加显存、不拖慢训练但能把验证集提升 1.5 个点左右。第二是 Cutout 或 RandomErasing。每类只有 500 张图模型很容易对某些局部纹理过拟合。Cutout 随机遮掉图像上一块方形区域相当于强制模型利用全局信息。我用RandomErasing(p0.5, scale(0.02, 0.2))也就是一半概率擦除、擦除面积占原图 2%20%。这个增强对 CIFAR-100 的收益通常在 1 个点以上。第三是 Normalize 的统计量。很多人直接搬运 ImageNet 的 mean/std 到 CIFAR 上这是完全错误的。CIFAR-100 自己有一套公开统计量mean 为 (0.5071, 0.4867, 0.4408)std 为 (0.2675, 0.2565, 0.2761)。用错统计量不会导致训练崩掉但会让输入分布整体偏移验证集掉 0.30.5 个百分点属于完全没有必要的损失。import torchvision.transforms as T # 注意RandomErasing 必须放在 Normalize 之后作用在 tensor 上 train_transform T.Compose([ T.RandomCrop(32, padding4), T.RandomHorizontalFlip(), T.ToTensor(), T.Normalize(mean[0.5071, 0.4867, 0.4408], std[0.2675, 0.2565, 0.2761]), T.RandomErasing(p0.5, scale(0.02, 0.2), valuerandom), ]) val_transform T.Compose([ T.ToTensor(), T.Normalize(mean[0.5071, 0.4867, 0.4408], std[0.2675, 0.2565, 0.2761]), ])验证集的 transform 不要加 RandomCrop也不要加 RandomErasing。很多新手把训练集的 transform 原封不动套在验证集上验证集精度直接掉 10 个点往上这不是模型问题是评估口径错了。3.2 训练主循环SGD Cosine Weight Decay 的推荐配置WRN 在 CIFAR-100 上的优化器选择实践下来最稳的是带 Nesterov 的 SGD而不是 Adam。原因很简单CIFAR-100 样本少Adam 的自适应学习率在训练后期容易在小梯度上产生过大更新导致验证集震荡。SGD 配合 Cosine 学习率调度收敛更平滑最终精度普遍比 Adam 高 12 个点。import torch import torch.nn.functional as F from torch.optim import SGD from torch.optim.lr_scheduler import CosineAnnealingLR def train_one_epoch(model, train_loader, optimizer, device): model.train() total_loss, correct, total 0.0, 0, 0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss F.cross_entropy(out, y) loss.backward() optimizer.step() total_loss loss.item() * x.size(0) correct (out.argmax(dim1) y).sum().item() total x.size(0) return total_loss / total, correct / total model WideResNet(depth28, num_classes100, widen_factor10, dropout_rate0.3).cuda() optimizer SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4, nesterovTrue) scheduler CosineAnnealingLR(optimizer, T_max200, eta_min1e-4)这段代码里的超参是我在 CIFAR-100 上反复用的一组初始学习率 0.1batch size 128总训练轮数 200。T_max200表示 Cosine 周期和训练轮数完全对齐学习率从 0.1 平滑降到 1e-4。eta_min取 1e-4 而不是 0是为了防止最后几轮学习率过小导致模型在验证集附近来回抖动。weight_decay5e-4是 CIFAR 上的经验值对应 L2 正则。很多人会纠结要不要设更大我试过 1e-3训练集 acc 掉得不明显但验证集在 190 轮附近会出现一个明显的下降段说明正则过强把模型的拟合能力压住了。如果你的 WRN 用的是 k4 这种小宽度weight_decay 可以降到 3e-4小模型本身不容易过拟合。3.3 观察训练曲线从三个信号判断模型状态训练代码跑起来之后不要只盯着验证集 acc。我一般同时看三个信号训练 loss、训练 acc 与验证 acc 的差距、验证 acc 的波动幅度。第一个信号是训练 loss。WRN-28-10 在 200 轮 Cosine 调度下前 20 轮 loss 应该从 4.6 左右快速降到 2.0 附近中间段平滑下降最后 30 轮基本贴在地上。如果训练 loss 下降速度远慢于这个节奏先怀疑学习率把 lr 从 0.1 调到 0.2 对比一下如果 loss 曲线出现明显台阶多半是数据加载顺序问题或 BN 的 momentum 没设置好。第二个信号是泛化 gap。CIFAR-100 上 WRN-28-10 训练到后期训练 acc 到 99% 很正常验证 acc 在 80% 附近这个 gap 约 19 个点看起来吓人但属于正常范围。如果训练 acc 老早就到 99% 而验证 acc 还在 75% 以下说明过拟合失控优先检查 dropout 是否生效、widen_factor 是否太大。第三个信号是验证 acc 的波动。好的状态是最后 50 轮验证 acc 单次波动不超过 0.3 个点。如果验证 acc 在 78% 和 80% 之间来回跳说明学习率还是偏大或者 batch size 太小导致 BN 统计量不稳定。此时可以把eta_min从 1e-4 降到 5e-5或者把 batch size 提到 256 并同步调大 lr。4. 训练加速与结果稳定混合精度、种子与日志管理的工程细节4.1 混合精度不是免费的小 batch 下的损失缩放问题不少人在 CIFAR 上跑 WRN 也喜欢开 AMP自动混合精度理由是训练速度快。但我要提醒一点混合精度对这个小数据集来说收益有限却可能引入一个隐蔽的精度问题。AMP 的原理是部分算子用 FP16 计算梯度经过 GradScaler 放大后再缩小。问题出在 CIFAR-100 的训练 batch 通常只有 128样本少、梯度本身的方差大如果某个 batch 出现极端 lossFP16 的动态范围可能直接溢出表现为 loss 变成 NaN或者训练 acc 正常但验证 acc 比 FP32 低 0.3 个点。我遇到过两次第一次还以为是模型写错了后来把 AMP 关掉就跑回正常。scaler torch.cuda.amp.GradScaler() for x, y in train_loader: x, y x.cuda(), y.cuda() optimizer.zero_grad() with torch.cuda.amp.autocast(): out model(x) loss F.cross_entropy(out, y) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()如果一定要用 AMP记住三条一是把GradScaler()的init_scale从默认的 65536 调小到 4096减少极端情况下的溢出二是当 loss 连续出现 NaN 时不要急着调学习率先关掉 AMP 确认 FP32 下正常三是 CIFAR-100 每轮迭代很快FP32 全程跑也就多花 20%30% 时间省下的这点时间不值得用精度去换。4.2 确定性设置一份能跨机器复现的种子配置做实验最怕的是同一份代码、同一个 seed换台机器或换块卡验证集精度差 0.5 个点。这不是模型问题是训练过程里存在非确定性操作。来源主要有三个cuDNN 的卷积算法选择、多线程 DataLoader 的数据顺序、以及某些算子内部的原子操作。import random import numpy as np import torch def set_seed(seed_value42): random.seed(seed_value) np.random.seed(seed_value) torch.manual_seed(seed_value) torch.cuda.manual_seed_all(seed_value) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False这段代码里最容易漏的是torch.backends.cudnn.benchmark。默认情况下 PyTorch 会在第一次迭代后自动测试多种卷积算法并选最快的不同卡选出的算法可能不一样导致结果漂移。benchmarkFalse配合deterministicTrue能固定卷积算法代价是训练速度略有下降但在 CIFAR 这种小任务上完全可以接受。另一个隐蔽问题在 DataLoader。如果开了num_workers8每个 worker 的数据采样顺序不同即使 master seed 固定epoch 内样本顺序也不完全一致。对最终精度影响很小但如果你要对比两个实验的 diff建议把num_workers固定成同一个值并在代码注释里写明。4.3 检查点与评估策略别拿最后一轮权重交差训练 WRN 这种带 dropout 的模型最后一轮的权重几乎不可能是验证集最优。原因很简单Cosine 调度最后学习率极低模型在最后几轮可能因为 BN 统计量的微小波动把验证 acc 压低了 0.20.3 个点。所以保存权重时必须按验证 acc 做早停式的记录。我一般会在每个 epoch 结束后跑一次验证集把val_acc和对应的 state_dict 存下来只在验证 acc 创新高时覆盖旧权重。这样训练结束后直接加载best_model.pth做测试而不是用训练循环里最后一步的模型。这个习惯能稳定多拿 0.3 个点左右在 CIFAR-100 这种 100 类的任务里0.3 个点可能就意味着排名差好几个身位。best_acc 0.0 for epoch in range(200): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, device) val_acc evaluate(model, val_loader, device) scheduler.step() if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fepoch {epoch}: save best, val_acc{val_acc:.4f})evaluate函数里有个细节验证阶段必须写model.eval()并放在torch.no_grad()里面。WRN 的 BN 在训练和推理模式下行为不同如果忘记model.eval()BN 会继续用当前 batch 的统计量验证 acc 会偏低且不稳定。5. 避坑指南CIFAR-100 训练中的 5 个高频翻车点5.1 加了 Wide-ResNet 反而掉点dropout 与 widen_factor 没配好现象从 ResNet-50 切到 WRN-28-10训练充分后验证集非但没涨反而掉了 0.5 个点。原因WRN-28-10 参数量比 ResNet-50 大在小数据集上过拟合更严重。如果 dropout 没开成 0.3或者被误设成 0.1正则强度完全不够另外很多复现代码默认dropout_rate0直接复制过来等于没加正则。解决把dropout_rate明确设成 0.3确认 dropout 在 forward 路径里真实生效。然后做一次消融固定 epoch 和超参分别用 k4、k10 跑两版如果 k10 验证集低于 k4说明当前正则不足优先调 weight_decay 和 dropout而不是继续加大模型。5.2 训练集 99%、验证集 78% 且来回抖学习率过大或 batch 过小现象训练 acc 很早就到 99%验证 acc 卡在 78% 附近并且每轮波动超过 0.5 个点。这不是过拟合的典型表现因为过拟合通常是验证 acc 平滑掉头而不是剧烈震荡。原因学习率 0.1 配合 batch size 64 时模型参数更新方差过大BN 的运行统计量也被带偏。Cosine 调度后期学习率虽然降下来但前期积累的噪声让模型始终在最优解附近摆动。解决先把 batch size 提到 128 或 256学习率保持 0.1如果显存不够就把 lr 降到 0.05 并延长训练到 300 轮。另外给前 5 个 epoch 加一个 warmup让学习率从 0.02 线性升到 0.1能明显压住验证集的早期抖动。5.3 换显卡后精度对不上非确定性算法在背后作怪现象同一份代码、同一个 seed、同样的 epoch 数在一张卡上跑到 80.4%换另一张卡变成 80.1%。代码没动过数据集没动过。原因我在 4.2 节提到过cuDNN 在不同显卡上有不同的卷积实现选择即使deterministicTrue某些算子比如大矩阵乘的累加顺序依然存在浮点误差。多跑 0.3 个点的差异在 CIFAR 上属于正常浮动但如果差异超过 1 个点一定是代码层面有随机性。解决把benchmarkFalse设置写进项目入口而不是依赖默认值同时固定torch.manual_seed的调用顺序必须在模型初始化之前执行。换卡后先跑 20 个 epoch 做抽查确认验证 acc 与旧卡偏差在 0.3 以内再继续长训。5.4 AMP 训练出现 NaN损失缩放策略过于激进现象训练到 30 轮左右loss 突然变成 NaN之后再也回不来。日志里没有明显的前兆验证集在此之前一直是正常上升的。原因AMP 的 GradScaler 默认初始 scale 很大遇到极端 batch 时梯度溢出scale 反复减半也来不及纠正另一个常见原因是学习率 0.1 配合 AMPFP16 下的参数更新幅度超出可表示范围。解决先关掉 AMP 确认 FP32 能正常训练再开 AMP 并把GradScaler(init_scale4096)调低同时把梯度裁剪设成max_norm5.0等于给极端梯度加上限。如果问题依旧我建议直接放弃 AMPCIFAR-100 的训练成本很低可复现性比那一点速度重要。5.5 训练和验证的预处理不一致Normalize 统计量与随机增强写错位置现象模型在训练集上表现正常验证集 acc 只有 40%50%看起来像没收敛但训练曲线完全健康。原因最常见的是把RandomErasing或RandomCrop也加到了验证集 transform 里另一种是 Normalize 的 mean/std 写反或者直接复制了 ImageNet 的统计量。这类错误不会报错模型照常训练只是评估口径不对。解决按 3.1 节给的验证集 transform 严格检查验证集只保留ToTensor和Normalize任何带随机性的操作都要移除。我习惯在项目里写一个简单的断言打印出验证集第一张图的均值对照 CIFAR-100 的官方 mean 快速排查。6. 进阶技巧把 WRN 在 CIFAR-100 上的准确率再往上推当 WRN-28-10 稳定跑到 80% 之后还有几个手段可以继续往上走按收益从高到低排列如下。第一个是标签平滑Label Smoothing。CIFAR-100 类别多部分样本本身就有歧义比如有些类之间的视觉差异很小。把交叉熵的label_smoothing设为 0.1等于告诉模型不用太相信 hard label能缓解过拟合。在 PyTorch 里只需要改一行F.cross_entropy(out, y, label_smoothing0.1)。第二个是 EMA指数移动平均。给模型权重维护一份滑动平均版本训练时不更新它验证和测试时用平均权重代替实时权重。EMA 相当于对训练轨迹做平滑能稳定地把验证集提升 0.20.3 个点实现起来也就 20 行代码。我习惯在 Cosine 调度结束后再单独跑 20 轮 EMA 预热效果比全程平均更好。第三个是测试时增强TTA。CIFAR-100 的测试集是 32×32 原图TTA 可以简单做一次水平翻转把原图和翻转图的预测 logits 取平均再求 argmax。这个技巧只影响推理不影响训练通常能白拿 0.2 个点左右。第四个是 Cutout 和 Mixup 的组合。注意这两者不建议从第一轮就同时开。我试过先把 Cutout 固定 0.5 概率从头训然后在最后 50 轮加 Mixupalpha0.2验证集能到 82% 上下但如果全程同时开模型在训练集上的拟合速度会变慢最终精度反而下降。我第一次把 WRN-28-10 从 79.5% 往 82% 推的时候犯过一个蠢错误只加了 dropou 的宽度把 widen_factor 从 10 加到 14结果验证集掉了 0.4 个点。后来才意识到加宽之后正则必须同步加强这不是模型结构问题而是正则和容量的配平问题。从那以后我每调一个结构参数就强制自己先跑一遍小轮数消融再决定是否投入长训。这套从 ResNet 到 Wide-ResNet 的流程每一步的坑我都踩过希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑