4. 图像分类实战技巧

图像分类这个方向,说实话,入门容易,精通难。很多人跑个 ResNet 在 CIFAR-10 上能到 90% 多的准确率,就觉得差不多了。但真到了工业级项目,你会发现那点精度根本不够用。我这些年踩过的坑,总结下来,真正能拉开差距的,往往就是数据增强、学习率调度和标签平滑这三个点。今天咱们就掰开揉碎了聊聊。

核心观点: 这三个技巧不是锦上添花,而是雪中送炭。它们分别解决了过拟合、收敛困难和过度自信这三个核心问题。

4.1 数据增强:不只是翻转和裁剪

传统的随机翻转、随机裁剪,现在只能算基本功。真正让模型泛化能力上一个台阶的,是 CutMix 和 MixUp 这类“混合式”增强。它们的思路其实很暴力——把两张图混在一起训练。

4.1.1 MixUp:线性插值

MixUp 的做法很简单。从训练集里随机抽两张图,比如一张猫、一张狗。然后按一个随机比例 λ 把它们像素混在一起,标签也按同样的比例混。比如 λ=0.7,那这张混合图就 70% 像猫,30% 像狗,标签也变成 [0.7, 0.3] 这样的软标签。

# MixUp 核心代码片段
def mixup_data(x, y, alpha=1.0):
    lam = np.random.beta(alpha, alpha)
    batch_size = x.size()[0]
    index = torch.randperm(batch_size)

    mixed_x = lam * x + (1 - lam) * x[index, :]
    mixed_y = (lam * y + (1 - lam) * y[index])

    return mixed_x, mixed_y, lam

我刚开始用 MixUp 的时候,总觉得这方法有点“不讲武德”。两张图叠在一起,人眼都看不清,模型能学到啥?后来发现,正是这种模糊性,强迫模型去关注更鲁棒的特征,而不是死记硬背那些纹理和背景。

我的经验: MixUp 的 α 参数控制混合强度。α 越小,混合越极端(λ 接近 0 或 1);α 越大,混合越温和。我个人习惯从 α=0.2 开始调,效果通常不错。

4.1.2 CutMix:区域裁剪粘贴

CutMix 是 MixUp 的“暴力升级版”。它不搞像素级混合,而是直接从一张图上裁一块矩形区域,贴到另一张图上。标签也按面积比例混合。

# CutMix 核心思路
def cutmix_data(x, y, alpha=1.0):
    lam = np.random.beta(alpha, alpha)
    batch_size = x.size()[0]
    index = torch.randperm(batch_size)

    # 随机生成裁剪区域
    bbx1, bby1, bbx2, bby2 = rand_bbox(x.size(), lam)
    # 粘贴
    x[:, :, bbx1:bbx2, bby1:bby2] = x[index, :, bbx1:bbx2, bby1:bby2]
    # 调整 lam 为实际面积比例
    lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (x.size()[-1] * x.size()[-2]))

    return x, (y, y[index], lam)

你想想看,CutMix 比 MixUp 好在哪?它保留了局部区域的完整信息。模型既要学会识别“贴上去”的那块区域,又要学会利用“背景”区域。这相当于让模型同时处理两个目标,变相增加了训练难度,也提升了泛化能力。

方法 混合方式 标签处理 适用场景
MixUp 像素级线性插值 线性插值 通用,对小数据集效果好
CutMix 区域裁剪粘贴 面积比例混合 目标检测、细粒度分类
传统增强 翻转/裁剪/颜色抖动 标签不变 基础场景

避坑指南: 我曾经在一个细粒度分类项目里同时用了 MixUp 和 CutMix,结果精度反而下降了。后来发现,这两种方法本质上是冲突的——一个模糊全局,一个保留局部。建议二选一,或者用一些自适应策略(比如 CutMix 的升级版 CutOut)。

4.2 学习率调度:Cosine Annealing 的妙用

学习率怎么调,是训练模型的核心手艺。Step Decay 太死板,ReduceLROnPlateau 又太被动。我个人最常用的,是 Cosine Annealing(余弦退火)。

它的思路很优雅:学习率按照余弦函数周期性地从最大值下降到最小值,然后重置。这种“热重启”机制,能让模型跳出局部最优,找到更平坦的极值点。

# PyTorch 实现 Cosine Annealing
import torch.optim.lr_scheduler as scheduler

optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
cosine_scheduler = scheduler.CosineAnnealingLR(
    optimizer, 
    T_max=50,       # 半个周期的步数
    eta_min=1e-6    # 最小学习率
)

# 训练循环中
for epoch in range(100):
    train_one_epoch()
    cosine_scheduler.step()

为什么 Cosine Annealing 效果好?说白了,它模拟了“先粗调、后微调”的过程。学习率大的时候,模型大步探索;学习率小的时候,模型精细调整。而热重启又给了模型第二次机会,避免困在某个鞍点。

我的习惯: 我会把 T_max 设为总 epoch 数的一半,然后配合 warmup 使用。前 5 个 epoch 线性升温到最大学习率,再开始余弦下降。这样能避免一开始学习率太大导致梯度爆炸。

4.3 标签平滑:让模型别太“自信”

你有没有遇到过这种情况?模型在训练集上准确率 99%,一到测试集就掉到 80%。这通常是因为模型对训练标签过于自信,把一些噪声也学进去了。

标签平滑就是来解决这个问题的。它把原本的 one-hot 标签(比如 [0, 1, 0])稍微“揉”一下,变成 [ε, 1-2ε, ε]。ε 通常取 0.1,意思就是:模型别那么肯定,留点余地给其他类别。

# 标签平滑实现
def label_smoothing(y, num_classes, epsilon=0.1):
    # y: 原始标签 [batch_size]
    # 返回平滑后的标签
    smooth_label = torch.full((y.size(0), num_classes), epsilon / (num_classes - 1))
    smooth_label.scatter_(1, y.unsqueeze(1), 1 - epsilon)
    return smooth_label

你想想看,这背后的逻辑是什么?它相当于告诉模型:“即使你 99% 确定这是猫,也保留 1% 的可能性是狗。” 这种“不确定性”反而让模型学到的特征更鲁棒,不会因为某个像素的微小变化就改变判断。

关键参数: ε 一般取 0.1,太大(比如 0.3)会让模型学不动,太小(比如 0.01)又没效果。我在 ImageNet 级别的任务上,通常固定 ε=0.1,效果稳定。

4.4 知识体系总览

这三个技巧不是孤立的,它们共同构成了一个完整的训练优化体系。下面这张图能帮你理清它们之间的关系:

图像分类实战技巧知识体系 训练优化核心 数据增强 学习率调度 标签平滑 MixUp CutMix Cosine Annealing Warmup ε=0.1 三者协同效果 • 数据增强:增加数据多样性,防止过拟合 • 学习率调度:优化收敛路径,跳出局部最优 • 标签平滑:降低模型过度自信,提升泛化能力

4.5 实战组合策略

这三个技巧怎么搭配?我一般按这个流程来:

  1. 先上数据增强: 基础增强(翻转、裁剪)+ CutMix 或 MixUp 二选一。小数据集优先 MixUp,大数据集优先 CutMix。
  2. 再配学习率调度: Cosine Annealing + Warmup,T_max 设为总 epoch 的 60%-80%。
  3. 最后加标签平滑: ε=0.1,直接加到损失函数里。

我曾经踩过的坑: 有一次在 CIFAR-100 上做实验,我把 CutMix、Cosine Annealing 和标签平滑全用上了,结果训练 loss 死活降不下去。排查了半天,发现是 CutMix 的 α 设得太小(0.1),导致混合太极端,模型根本学不到有效特征。后来把 α 调到 1.0,问题就解决了。所以,参数调优一定要从默认值开始,别一上来就搞极端。

好了,这三个技巧讲完了。它们看起来简单,但每个都值得你花时间去深入理解。面试的时候,面试官问数据增强,你如果能从 MixUp 讲到 CutMix,再对比一下标签平滑的原理,那基本就稳了。记住,面试官想听的不是你知道什么,而是你用过什么、踩过什么坑、怎么解决的。