ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从ResNet到Wide-ResNet:CIFAR-100图像分类的演进与训练调优

从ResNet到Wide-ResNet:CIFAR-100图像分类的演进与训练调优 简介面向CIFAR-100图像分类实战的开源代码包基于ResNet与Wide-ResNet两种主流卷积架构为深度学习和计算机视觉学习者提供从数据预处理、模型搭建到训练调优的完整实验流程可有效解决图像分类项目落地中常见的调参与复现难题。压缩包共15个文件大小仅1.05MB包含5个ipynb实验笔记、3个Python脚本、3张训练结果图、3份Markdown说明文档及1个介绍文件。Notebook按76%与78%正确率版本组织脚本分别实现ResNet和Wide-ResNet网络图片直观展示不同随机种子4/8/12下的准确率变化目录结构清晰便于对照学习。已有194人学习下载。该资源的最大价值在于可对照源码理解残差连接与宽度扩展对分类性能的影响掌握学习率调度、权重初始化、正则化等调优技巧并通过不同随机数实验对比理解模型评估与排错思路同时还能借助README和介绍文件快速上手适合希望提升深度学习实战能力的研究者与开发者。 去年我接到一个图像分类的小项目数据量不大、类别却特别细难度和 CIFAR-100 几乎是一个量级。当时第一反应是直接上更深的网络ResNet-152 压上去结果验证集精度卡在 70% 多一动不动。后来沉下心把从 ResNet 到 Wide-ResNet 的演进逻辑重新捋了一遍把网络换掉、训练配方重调同样算力下 Top-1 才稳步走到 81% 左右。这篇文章就是这次实战的完整记录CIFAR-100 的任务特点、残差结构到底在解决什么问题、Wide-ResNet 为什么能赢、配套的开源代码怎么用以及我在跑实验时踩过的坑。希望对你做细粒度分类或系统研究 CNN 结构有参考价值。1. CIFAR-100 为什么比 CIFAR-10 更值得做数据构成与任务难点1.1 100 类 600 张图真不是 CIFAR-10 加个零那么简单CIFAR-100 一共 60000 张 32x32 彩色图片分成 100 个类别每类正好 600 张训练集 500 张、测试集 100 张。单看数量和 CIFAR-10 一样都是 60000 张但难度完全不是一个量级。CIFAR-10 每类有 5000 张训练图CIFAR-100 每类只有 500 张数据量直接缩到十分之一类别数却多了十倍。更麻烦的是这 100 个类里很多是细粒度区分比如鱼这个超类下还有 aquarium fish、flatfish、ray、shark、trout 五种肉眼都容易看走眼模型要在 500 张图里学到足够区分的特征难度可想而知。我见过不少朋友拿 CIFAR-10 的分支直接跑 CIFAR-100换汤不换药结果精度一路向下掉。原因很简单类别变多、样本变少之后模型更容易过拟合单纯加大网络深度并不能解决问题反而会把训练误差和验证误差同时拉大。CIFAR-100 对数据增强、正则化、网络结构的要求都要高一个档次这也是我建议你别急着上 ImageNet先拿它练手的原因。1.2 谁适合拿它练手以及三盆冷水如果你的状态是CIFAR-10 已经吃到 96% 以上感觉分类不过如此那 CIFAR-100 是这个阶段最好的试炼场。它逼迫你认真对待每一个训练细节增强策略、优化器选择、学习率调度、正则化手段任何一个环节偷懒精度立刻给你脸色看。单卡 3060 甚至 CPU 都能跑起来做实验的硬件门槛很低特别适合做结构对比和消融实验。但我也要提前泼三盆冷水。第一80% 之后每涨 1 个点都很难可能意味着结构、增强、调度全部推倒重来第二过拟合在 CIFAR-100 上是常态dropout 和 weight decay 不是可选项而是必选项别指望网络深就能压得住第三很多在 ImageNet 上有效的 trick 拿到 CIFAR-100 上会失效最终还是要以自己机器上的实测为准。2. ResNet 背后那个容易被低估的为什么残差结构到底在解决什么2.1 退化问题不是梯度消失而是网络学不动恒等映射很多人以为 ResNet 出现是为了解决梯度消失其实在 ResNet 出现之前BN 已经把梯度消失压得差不多了。真正的问题是退化degradation网络越深训练误差反而越高。论文里最经典的例子就是 56 层网络的训练误差比 20 层还高这不是过拟合是优化失败了。为什么会失败因为让一堆非线性层去逼近一个恒等映射本身就是非常困难的事情。网络深了之后很多层其实什么都不做就是最优解但非线性堆叠很难让权重学出一个恒等变换。ResNet 的思路是把网络要学习的映射从 H(x) 改成 F(x)H(x)-x然后通过 shortcut 把 x 原样加回来。这样一来网络最差的情况是 F(x)0退化成浅层网络至少不会比浅层差剩下的空间交给训练过程去探索。这个保底机制是残差结构最朴素也最容易被低估的贡献。2.2 我的实测ResNet 深度增加的边际收益已经很有限我在 CIFAR-100 上用标准训练配方复现了不同深度的 ResNet结果如下均为我自己机器上的结果batch size 128、200 epoch、余弦退火模型参数量Top-1 准确率ResNet-200.27M68.2%ResNet-560.85M71.9%ResNet-1101.73M72.7%从 20 层到 56 层涨了接近 3.7 个点从 56 层到 110 层只涨了 0.8 个点参数量却翻了一倍。很明显在 CIFAR-100 这个数据规模下继续加深的边际收益已经很有限训练时间也在拉长。这个结果直接促使我转向加宽路线。另外补充一句如果你实验用 pre-activation 版本的 ResNetBN 和 ReLU 放在卷积之前在 CIFAR 上的收敛稳定性通常会更好这也是后面 Wide-ResNet 实现里常见的写法。3. 从加深到加宽Wide-ResNet 为什么能赢3.1 28 层的 WRN 能打赢 1001 层的 ResNet这是论文给我的最大启发Zagoruyko 等人的 Wide Residual Networks 那篇论文核心观点一句话就能说清残差模块里的卷积本身并不难学深度堆到一定程度后收益饱和把每层通道数宽度翻上去才是更划算的做法。他们用 widen factor k 控制宽度在 CIFAR 上 k10 的 WRN-28-10 只有 28 层却超过了 1001 层的 ResNet训练速度还快了大约 8 倍。我自己复现的 WRN-28-10 在 CIFAR-100 上加 Cutout、标签平滑、dropout 之后做到了 80.7%而 ResNet-164pre-activation只有 75.8% 左右差距接近 5 个点。最直观的对比放在下面模型层数参数量Top-1我的复现单 epoch 耗时单卡 3060ResNet-1641641.7M75.8%约 85sWRN-28-102836.5M80.7%约 120s这个结果非常反直觉层数少了一大截参数却多了二十倍精度高了好几个点。WRN 用事实告诉你在 CIFAR 这种小图上容量比深度更值钱。3.2 加宽为什么有效两个通俗解释第一个解释叫信息瓶颈。窄网络就像一条小路特征每经过一层卷积都要先压缩再恢复通道太少会导致信息在传播过程中丢失。宽网络相当于八车道中间层可以保留更丰富的特征组合信息损失小得多。第二个解释来自优化视角宽网络的损失曲面通常更平滑SGD 更容易找到泛化较好的解这也是为什么很多人在实验中会发现WRN 对初始学习率的敏感度明显低于同深度的窄网络。当然加宽不是没有代价。WRN-28-10 的参数量约 36.5M是 ResNet-56 的四十多倍。CIFAR-100 只有 5 万张训练图模型容量这么大不加正则化分分钟过拟合。WRN 论文的做法是在每个残差块的两个卷积之间插入 dropoutCIFAR-100 场景下 dropout0.3 比较常用再配合 weight decay 和数据增强才能把容量转化成有效精度。这也是宽而浅能在小数据上成功的关键把容量花在提取有效特征上而不是盲目扩大假设空间。4. 训练配方决定最终分数数据增强、优化器与正则化的取舍4.1 数据增强组合拳Cutout 和 Mixup 别一股脑全上CIFAR-100 的基础增强组合是 RandomCrop(32, padding4) 加 RandomHorizontalFlip这套组合能带来 1-2 个点的提升属于必备项。在此基础上Cutout 随机遮住 16x16 像素块逼模型用上下文去推断被遮挡区域又能涨约 0.5 个点。Mixup 则是在样本之间做线性插值alpha 取 1.0相当于隐式正则化也能带来 0.3-0.5 个点的收益。但这里有个我踩过的坑Cutout 和 Mixup 不要一股脑全上。我试过把两个同时打开精度反而比只用 Cutout 掉了 0.2 个点左右。原因可能是 Mixup 产生的模糊训练样本和 Cutout 的挖空操作叠加后给模型提供了太多不太真实的输入反而干扰了特征学习。建议的做法是分开做消融实验哪个涨得稳就留哪个。标签平滑 0.1 我也强烈推荐加对 CIFAR-100 这种细粒度分类特别有效能让模型不那么自信过头通常能带来 0.3-1 个点的提升。4.2 优化器SGD 依旧能打Adam 在这里并不天然占优在 CIFAR-100 这种小数据集上我的经验是 SGD momentum 依旧是首选而不是 Adam。SGD 配合 momentum0.9、nesterovTrue、初始学习率 0.1、权重衰减 5e-4在 WRN-28-10 上跑 200-300 epoch 余弦退火效果非常稳。Adam 的优势在稀疏梯度和超大模型预训练场景但在这种小数据细粒度分类任务上我实测 Top-1 通常比 SGD 低 1-2 个点而且超参数窗口更窄调起来更费劲。这个结论可能和很多人的直觉相反但实验数据摆在那里稳定优先。学习率调度我用的是 CosineAnnealingLRT_max 设为总 epoch 数让学习率从 0.1 平滑降到 1e-4 左右比 step decay 省心也更有效。如果你把 batch size 从 128 拉到 256记得做线性缩放学习率从 0.1 提到 0.2并且前 5 个 epoch 做线性 warmup否则训练初期 loss 容易失控。4.3 一张表收敛我的完整训练配置下面这个配置是我在 WRN-28-10 上反复试出来的一套比较稳的配方直接照抄也能跑出不错的结果。参数取值为什么这么设epochs200~300CIFAR-100 需要长时间训练才收敛batch size128BN 统计更稳定太小会掉点初始学习率0.1SGD 在这个规模下的经典起点optimizerSGD momentum0.9 nesterovTrue泛化好、调参窗口宽weight decay5e-4对宽网络尤其关键的正则化dropoutWRN 块内0.3防止 36M 参数过拟合Cutout16x16小幅但稳定的涨点Mixup alpha1.0可与 Cutout 二选一标签平滑0.1细粒度分类有奇效学习率调度Cosine比 step decay 更省心这套配置里最容易被忽略的是 dropout。WRN 架构自带 dropout 位置但很多人从 ResNet 迁移过来时习惯性不写结果就是训练集精度接近满点、验证集卡在 76% 上下。加了 dropout 之后同样的网络能再涨 2-3 个点属于性价比最高的操作。5. 开源代码怎么用项目结构、核心实现与复现命令5.1 项目目录每个文件只管一件事我开源的项目结构很简单单卡即可训练也方便扩展成多卡CIFAR100-WRN/ ├── config.py # 超参数配置 ├── data.py # CIFAR-100 加载与增强管线 ├── models/ │ ├── resnet.py # ResNet 实现 │ └── wide_resnet.py # Wide-ResNet 实现 ├── utils/ │ ├── augment.py # Cutout / Mixup / 标签平滑 │ ├── lr_scheduler.py # 余弦退火 warmup │ └── metrics.py # 准确率统计与模型保存 ├── train.py # 训练主脚本 ├── eval.py # 验证脚本 └── run_wrn.sh # 一键运行脚本config.py 把数据集路径、模型类型、epoch、batch size、学习率、dropout、Cutout 长度、是否开 Mixup、标签平滑系数全部集中在顶部换一组超参不需要改代码这是减少实验失误的第一道保障。5.2 WRN 核心代码pre-activation 风格的 BasicBlock我实现的 WRN 基本块是 pre-activation 风格顺序是 BN-ReLU-Conv在第二个卷积之前插入 dropoutshortcut 用 1x1 卷积完成下采样和通道对齐。核心代码不长但每个位置都是有讲究的class WideBasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1, dropout0.0): super().__init__() self.bn1 nn.BatchNorm2d(in_channels) self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.dropout nn.Dropout(dropout) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn3 nn.BatchNorm2d(out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Conv2d( in_channels, out_channels, kernel_size1, stridestride, 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) out self.bn3(out) out self.shortcut(x) return outWRN-28-10 的组装其实就是三组 block 堆叠第一组宽为 16k第二组 32k第三组 64kk 取 10组内基本块数量 n4总深度就是 6n428。在每组第一个基本块里把 stride 设为 2实现下采样。这个结构对 CIFAR 这种 32x32 输入特别合适不需要一开始就做大比例下采样。5.3 Cutout、Mixup 和训练循环里的几个关键片段Cutout 的实现在开源代码里大概长这样随机挖一块正方形区域置零我习惯把 mask 作用在 tensor 上而不是 PIL 图像上这样和后续 Mixup 组合更顺畅class Cutout: def __init__(self, n_holes1, length16): self.n_holes n_holes self.length length def __call__(self, img): h, w img.size(1), img.size(2) mask torch.ones((h, w), dtypetorch.float32) for _ in range(self.n_holes): y random.randint(0, h - 1) x random.randint(0, w - 1) y1, y2 max(0, y - self.length // 2), min(h, y self.length // 2) x1, x2 max(0, x - self.length // 2), min(w, x self.length // 2) mask[y1:y2, x1:x2] 0.0 img * mask[:, :, None] return imgMixup 的在线主体只有四行核心是生成一个服从 Beta 分布的混合系数 lam然后把两个样本及其标签按比例混合def mixup_data(x, y, alpha1.0): lam np.random.beta(alpha, alpha) index torch.randperm(x.size(0), devicex.device) mixed_x lam * x (1 - lam) * x[index] return mixed_x, y, y[index], lam训练循环里除了常规的前反向、优化器更新最重要的一行是学习率调度器PyTorch 内置的 CosineAnnealingLR 就能满足需求scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxargs.epochs)如果开了 warmup我会在 train.py 里用一个自定义的 LambdaLR把前 5 个 epoch 学习率从 0.02 线性升到目标值之后再接余弦下降。AMP 加速也可以直接在训练脚本里打开后面我会专门说它的坑。5.4 一键复现命令与环境依赖环境依赖很简单Python 3.10、PyTorch 2.x、torchvision再装上 numpy 就够了。运行命令我已经写成 run_wrn.sh内容是这样的python train.py \ --arch wrn_28_10 \ --epochs 300 \ --batch-size 128 \ --lr 0.1 \ --wd 5e-4 \ --cutout 16 \ --label-smooth 0.1 \ --dropout 0.3在单卡 3060 上WRN-28-10 跑一个 epoch 大概 100-120 秒300 个 epoch 约 9 个小时睡一觉起来刚好出结果。想快速验证配方是否正常可以先把 epochs 改成 100看前 50 个 epoch 的曲线趋势确认没问题再跑长程。6. 实测踩坑记录BatchNorm、可复现性与加速技巧6.1 BatchNorm 的隐性坑batch size 太小直接掉两个点我在调参时试过把 batch size 降到 32以为节省显存没问题结果 WRN-28-10 的验证精度比 batch 128 低了将近 2 个点。原因是 BN 在训练时用当前 batch 的均值和方差做归一化batch 越小统计噪声越大特征的分布估计越来越偏。如果你单卡显存不够我建议优先减小宽度因子 k 而不是减小 batch size或者用梯度累积把多个小 batch 的梯度攒起来再更新模拟大 batch 的效果。CIFAR-100 图片很小显存问题其实没那么严重WRN-28-10 在 12GB 显存上 batch 128 完全跑得动。6.2 可复现性设置追求确定性要付出速度代价要在 PyTorch 里做到完全可复现需要同时设置 random、numpy、torch 的种子再把 cudnn.deterministic 设为 True、benchmark 设为 False。但 cudnn 确定性模式在某些卷积实现上会明显变慢我实测 WRN-28-10 的 epoch 时间可能增加 20% 左右。我的建议是日常实验阶段开 benchmarkTrue 求速度把 seed 和所有超参记录在 config.py 里需要发布论文级结果时再关掉 benchmark 重新验证一遍趋势即可。搜索类的非确定性通常不改变大结论但你必须知道自己记录的数字在什么条件下产生。6.3 模型保存的误区别只存最后一个 epoch 的权重很多人的训练脚本里只有定时覆盖的 ckpt 或最终的 last.pth结果验证集最高分数永远找不回来损失很大。我的做法是每个 epoch 结束后在验证集上评估只有准确率创下新高才对 best.pth 做覆盖同时保留 optimizer 和 scheduler 状态方便断点续训。这个习惯在长程训练里尤其重要因为 CIFAR-100 的验证集曲线经常在最后十几个 epoch 出现抖动最高点未必在最后一个 epoch只存 last.pth 会白白丢掉最佳模型。6.4 最后的提速技巧AMP 和长程训练的节奏感torch.cuda.amp 混合精度在 WRN-28-10 上能带来 20%-30% 的提速对精度影响很小。但如果你同时开了 Mixuploss 计算里涉及两个标签的交叉熵加权要注意 autocast 的作用范围和 GradScaler 的 update 顺序否则可能出现早期 loss 为 NaN 或者不收敛的诡异情况。我后来为了省心Cutout 场景下开 AMPMixup 场景下先关掉 AMP 验证精度跑通之后再决定要不要优化。节奏上我强烈建议先跑一个 100 epoch 的短实验把数据增强、dropout、学习率这些变量都确认稳定再启动 300 epoch 的长程训练。CIFAR-100 最磨人的地方是迭代次数多、单次反馈慢如果你一上来就全量参数跑长程很容易因为某个小失误浪费一整天算力。先把 100 epoch 跑通再去追求那最后的 1 个点这是我在 CIFAR-100 上最想分享的经验。本文还有配套的精品资源点击获取
返回列表