用 Wide-ResNet 打出 79% 准确率!完整源码公开,一文带你复现 CIFAR-100 分类实验

1. 实验背景

CIFAR-100 是图像识别领域常用的一个小型数据集,包含了 60,000 张 32×32 的彩色图像,共有 100 个类别(也可归为 20 个超级类别)。训练集有 50,000 张图像,测试集有 10,000 张图像,每个类别各有 600 张图像。由于 CIFAR-100 的类间相似度较高,且图像尺寸较小,给模型的泛化能力带来了一定挑战。它也因此常被用来做深度学习模型的快速验证和对比实验。

本次实验的目标是使用经典的 ResNet 和它的变体 Wide-ResNet 模型在 CIFAR-100 上进行图像分类,并分析模型的表现。在结果评估方面,我们主要关注:

  • Top-1 Accuracy:预测结果的第一候选是否与标签相匹配。
  • Top-5 Accuracy:预测结果中前 5 个候选是否包含正确标签。
  • Super-Class Accuracy:以 20 个超级类别为目标,判断更高层级的分类准确率。

以下重点介绍使用 Wide-ResNet 的部分实验过程和结果。


2. 模型介绍

2.1 ResNet 回顾

ResNet(Residual Network)最早由微软研究院提出,通过使用残差结构(Residual Block)有效缓解了深度神经网络中出现的梯度消失或梯度爆炸等问题。同时,残差结构也让网络能在保持较深层数的同时依旧有良好的训练效果。

2.2 Wide-ResNet 简介

Wide-ResNet 是对 ResNet 的改进版本,论文 Wide Residual Networks 提出可以在增加网络宽度(即 channel 数)而不是盲目加深网络深度的情况下,获得更强的表达能力和更好的性能表现。相比于很深的 ResNet,Wide-ResNet 通常能在较少的训练时间内取得相当甚至更高的准确率。

Wide-ResNet 结构的主要变化有:

  • 将每一层的通道数乘以一个 widen factor(例如 2、4、8等),实现网络变宽;
  • 在一些实现中,还会配合使用 Dropout 来进一步缓解过拟合。

3. 代码结构

下面展示了本次实验使用的 WideResNet 模型的主要代码。可以看到,对于 Wide ResNet,关键在于残差块 WideBlock 中同时包含了 Batch Normalization、ReLU、卷积操作以及 Dropout,并且通过 shortcut 来实现残差连接。在构建网络时,我们根据 depth 来计算每个阶段的层数,并通过 widen_factor 来指定通道增宽系数。

import torch
import torch.nn as nn
import torch.nn.functional as F

# 权重初始化函数
def weights_init(m):
    if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear):
        nn.init.xavier_normal_(m.weight)  # 使用 Xavier 初始化
    elif isinstance(m, nn.BatchNorm2d):
        nn.init.ones_(m.weight)
        nn.init.zeros_(m.bias)

# Wide ResNet 的基本块
class WideBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, dropout_rate=0.3):
        super(WideBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, 
                               stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.dropout1 = nn.Dropout(dropout_rate)
        
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, 
                               padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.dropout2 = nn.Dropout(dropout_rate)

        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, 
                          stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.dropout1(out)
        out = self.bn2(self.conv2(out))
        out = self.dropout2(out)
        out += self.shortcut(x)
        out = F.relu(out)
        return out

# Wide ResNet 模型
class WideResNet(nn.Module):
    def __init__(self, depth, widen_factor, num_classes=100):
        super(WideResNet, self).__init__()
        self.in_channels = 16
        n = (depth - 4) // 6  # 每个 stage 的层数

        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(16)

        self.layer1 = self._make_layer(WideBlock, 16 * widen_factor, n, stride=1)
        self.layer2 = self._make_layer(WideBlock, 32 * widen_factor, n, stride=2)
        self.layer3 = self._make_layer(WideBlock, 64 * widen_factor, n, stride=2)

        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Linear(64 * widen_factor, num_classes)

        # 参数初始化
        self.apply(weights_init)

    def _make_layer(self, block, out_channels, n, stride):
        layers = []
        layers.append(block(self.in_channels, out_channels, stride))
        self.in_channels = out_channels
        for _ in range(1, n):
            layers.append(block(out_channels, out_channels))
        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.avg_pool(out)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out

在代码实现中:

  • WideBlock 是基础残差结构,每个 Block 中包含两个卷积层、BN 以及 Dropout,用于增宽的部分体现在 out_channels 的设计中。
  • WideResNet 按照 (depth-4)/6 计算每个阶段重复的残差块数量,并且通过 widen_factor 调整每个阶段的通道数(默认为 [16, 32, 64] 变成 [16×widen_factor, 32×widen_factor, 64×widen_factor])。

4. 实验过程

4.1 数据预处理

CIFAR-100 的图像分辨率较小(32×32),我们通常会对训练集做一些数据增强,常见手段包括:

  • 随机裁剪(RandomCrop)
  • 随机水平翻转(RandomHorizontalFlip)
  • 均值和标准差归一化(Normalize)
import torchvision.transforms as transforms

transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize([0.5071, 0.4867, 0.4408], [0.2675, 0.2565, 0.2761]) 
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize([0.5071, 0.4867, 0.4408], [0.2675, 0.2565, 0.2761])
])

这里的 [0.5071, 0.4867, 0.4408][0.2675, 0.2565, 0.2761] 分别是 CIFAR-100 统计得到的 RGB 三通道的均值和标准差。

4.2 训练设置

  • 优化器:常用的是 SGD(带动量 Momentum=0.9)或者 Adam。如果使用 SGD,学习率衰减策略可采用多步衰减Cosine Annealing
  • 批大小 (batch_size):128 或 256 均可,根据实际显存情况决定。
  • 学习率 (lr):从 0.1 或 0.01 开始,有时搭配余弦退火(Cosine Annealing)取得较好效果。
  • 训练 epoch:通常 200 次左右即可在 CIFAR-100 上看到较稳定的结果。

4.3 训练流程

大致训练流程如下:

  1. 加载数据:使用 DataLoader 搭配前述的数据增强方法。
  2. 实例化模型:如 model = WideResNet(depth=28, widen_factor=10, num_classes=100)
  3. 定义损失函数和优化器criterion = nn.CrossEntropyLoss()optimizer = torch.optim.SGD(...)
  4. 训练循环
    • 将模型设为训练模式 model.train()
    • 前向传播计算损失;
    • 反向传播更新权重;
    • (可选)在训练集或验证集上查看中间结果。
  5. 评估:在测试集上评估 model.eval() 模式,得到 Top-1/Top-5 Accuracy 等。

5. 实验结果与分析

以下是在不同随机种子(Random Seed)下取得的结果表格。可以看到,对 CIFAR-100 的测试准确率(Test Acc)、Top-5 准确率以及超级类别准确率如下:

Random SeedTest AccTop-5 AccSuper-Class Acc
479.05%95.33%87.21%
879.44%95.25%87.28%
1279.14%95.27%87.74%

可以观察到:

  • Test Acc 在三个随机种子下都稳定在 79% 左右,Top-5 Acc 大约在 95%,表明模型在 CIFAR-100 上有相当的区分能力。
  • Super-Class Acc 更高一些,因为超级类别只有 20 个,类别难度相对较小,所以准确率在 87%~88% 左右。
  • 不同随机种子带来的性能波动很小,说明我们的训练流程相对稳定。

5.1 对比分析

  1. ResNet vs Wide-ResNet:通常情况下,相同深度下,Wide-ResNet 在 CIFAR-100 上往往能获得略好于标准 ResNet 的精度,且收敛更快。
  2. 模型深度与宽度:从实验证明,盲目加深网络可能效果并不明显,但增宽通道(适度加大网络容量)往往能带来更显著的效果提升。
  3. Dropout 影响:在 CIFAR-100 这种数据量有限且类别多的情况下,适当使用 Dropout(如 0.3)能够减少过拟合,提升泛化性能。

5.2最终结果图


6. 心得体会

  1. 宽度扩展的好处:Wide-ResNet 在相对浅层(如 depth=28)并采用较大宽度(如 widen_factor=10)时,就能取得非常不错的分类结果,而且训练速度更快,与非常深的 ResNet(如 ResNet-100+)相比效率更高。

  2. 训练策略的重要性:在 CIFAR-100 上,恰当的学习率初始值与衰减策略能够带来较大提升。特别是 余弦退火 (Cosine Annealing) 常常能比简单的 Step LR 取得更好或者至少相当的效果。对小数据集来说,数据增强也是关键环节。

  3. 随机种子:多次实验对于验证模型效果的稳定性很重要,建议在汇报或撰写文章时,至少使用 3 个以上不同的随机种子(或多次重复实验)来验证模型的平均性能和波动区间。

  4. 模型可解释性与可视化:尽管我们更关注数值指标,但在实际应用中,可视化中间特征、观察网络对不同类别的判别重点,对于深入理解模型也很有帮助,可以结合 Grad-CAM 等方法进行分析。


7. 总结

在本次实验中,采用 Wide-ResNet 模型对 CIFAR-100 进行了分类任务,取得了接近 79% 的 Top-1 测试准确率和 95% 左右的 Top-5 准确率,超级类别准确率约 87%~88%。实验中我们看到了 增宽网络(而非一味加深)在小尺寸数据上的优势,以及合适的 Dropout数据增强学习率调度 对模型效果的重要影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X.AI666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值