假设我们想让模型识别一张照片中的花卉类别。模型需要从花瓣、叶片和形态等图像信息中区分类别,但可用训练样本可能并不多。这个场景很适合学习迁移学习:我们借用预训练骨干已有的图像表示,再让它适应当前分类任务。

本课使用 Oxford Flowers-102作为项目背景。数据集提供 102 类花卉图像、标签和划分文件。我们先按照选定划分建立分类基线,再比较注意力模块是否带来收益,这样每一步都有明确的对照。

读者需要先理解卷积与特征图和完整训练步骤。本课会解释项目流程,并提供可运行的小练习;课程中的完整 GPU 训练尚未在本地复现。

先检查一个批次

数据加载程序会读取图像文件和标签,再把多个样本组成批次。我们应当能从批次中的任一样本追溯到原始图像、类别及其所属集合。对于常见分类网络,输入形状是 B×C×H×W,目标则是 B 个类别索引;颜色通道、尺寸、数值范围与类别编号都需要对应正确。

训练程序可以使用随机裁剪、翻转或样本混合来增加输入变化。验证与常规测试采用固定变换,研究者才容易比较不同模型的结果。如果实验还使用测试时增强,就应当单独记录这种设置。

预训练权重也有自己的输入处理要求。TorchVision 的权重对象附带相应推理变换,我们可以据此核对尺寸与归一化,避免把一套常数不加区分地用于所有模型。模型与权重

从预训练骨干开始

骨干网络负责把图像转换成特征,分类头负责根据这些特征预测花卉类别。我们通常会把原分类头替换成适合 102 个类别的新分类头,再根据样本量、训练预算和验证表现,决定冻结或微调哪些层。

所谓冻结,是让指定参数在训练中保持不变。不过,一个可用的训练设置还涉及优化器和模块模式,下面三项需要分别理解:

设置 控制的内容 要核对的地方
requires_grad 参数是否求梯度 冻结范围是否符合计划
优化器参数组 哪些参数实际更新 新分类头是否加入,有无遗漏或重复
train() / eval() BN、Dropout 等行为 冻结骨干时,是否希望更新 BN 统计

例如,我们可能希望只更新分类头,同时保持骨干的 BatchNorm 统计不变。此时,程序需要同时维护参数冻结与模块模式;如果后续调用整个模型的 train(),骨干也可能重新进入训练模式。程序逐层解冻时,还要把新解冻参数加入相应优化器参数组。迁移学习教程

下面的名字清单演示了一个常见检查:每个需要更新的参数是否恰好登记了一次。真实项目中,我们应当比较参数对象本身。

from collections import Counter

trainable = {"conv1.weight", "bn1.weight", "bn1.bias",
             "layer1.weight", "layer4.weight", "fc.weight", "fc.bias"}
groups = {
    "backbone": [name for name in trainable if not name.startswith("fc.")],
    "head": [name for name in trainable if name.startswith("fc.")],
}
counts = Counter(name for group in groups.values() for name in group)
assert set(counts) == trainable
assert all(value == 1 for value in counts.values())
print("all trainable parameters registered once")

SE、CBAM 和 ECA 放在哪里

模型的不同通道可能响应不同的图像模式,注意力模块会根据当前特征生成权重,调整各部分的相对作用。我们以 B×C×H×W 特征为例,看看三种常见模块怎样计算:

  • SE 汇总空间信息,为各通道产生门控,权重常为 B×C×1×1。
  • CBAM 依次调整通道与空间位置。
  • ECA 汇总后用通道方向的一维卷积建模局部交互。

读者接入这些模块时,可以先画出权重形状,再观察程序沿哪些轴广播。它们通过门控调整 CNN 特征,而 Transformer 的 QKV 注意力是在 Token 之间汇总信息,两者的计算对象不同。SE、CBAM、ECA

我们还可以估算模块的额外参数。忽略偏置时,SE 两层映射约有 2C²/r 个参数;单输入、单输出的一维 ECA 卷积有 k 个核参数。这些数字只描述模块自身,实际运行开销仍要通过整网显存与时延测量来判断。

import numpy as np

features = np.arange(24, dtype=float).reshape(2, 3, 2, 2)
gates = np.array([[0.2, 0.5, 0.8], [0.7, 0.4, 0.1]])[:, :, None, None]
scaled = features * gates
assert gates.shape == (2, 3, 1, 1)
assert scaled.shape == features.shape
np.testing.assert_allclose(scaled[0, 1], features[0, 1] * 0.5)
assert 2 * 64 * (64 // 16) == 512
print("channel gates and SE parameter count checked")

CutMix 同时改变图像和标签

训练样本较少时,我们还可以使用数据增强。CutMix 会把另一张图的一块区域贴入当前图像,并按面积比例组合两张图的监督。例如,一张图有四分之一面积来自另一类别,目标分布也会反映这部分混合。

程序实际粘贴时,贴片可能因为越界而被裁小,因此标签比例应依据最终贴入面积计算。下面的例子让我们同时核对贴片面积与损失写法:

import numpy as np

h, w = 12, 16
image = np.zeros((h, w))
x1, x2 = np.clip([12, 20], 0, w)
y1, y2 = np.clip([8, 16], 0, h)
image[y1:y2, x1:x2] = 1
area = int((x2 - x1) * (y2 - y1))
lam = 1 - area / (h * w)
assert area == 16
assert np.isclose(lam, 11 / 12)
assert np.count_nonzero(image) == area

logits = np.array([2.0, 0.5, -1.0])
shifted = logits - logits.max()
log_probs = shifted - np.log(np.exp(shifted).sum())
soft_target = np.array([lam, 0., 1 - lam])
weighted = -lam * log_probs[0] - (1 - lam) * log_probs[2]
assert np.isclose(weighted, -(soft_target * log_probs).sum())
print("clipped area and mixed-label loss checked")

代码分别计算了两个硬标签损失的加权和,以及相应软标签的交叉熵,两者在相同定义下得到一致结果。我们在框架中使用这一方法时,还应查清软目标、类别权重与 Label Smoothing 的组合方式。

实验一次只回答一个问题

如果注意力组既换了模块,又增加了 CutMix 和训练轮数,我们就很难判断哪项改动带来了收益。比较清楚的做法是先把基础配方与增强配方分开,再在相同增强配方下比较模块。下面这张表是一份实验计划,结果需要等实际训练后填写:

组别 模块 训练配方 比较目的
A 无 基础配方 建立基线
B 无 增强配方 看增强本身的作用
C SE 与 B 相同 测 SE 的额外收益
D ECA 与 B 相同 测 ECA 的额外收益

研究者需要比较 CBAM 时,可以增加一组同配方实验。各组使用相同划分、骨干、预算、选模规则与种子集合,我们才能较清楚地分析模块的额外作用。结果记录还应包括多次运行的波动、参数量和实测时延。

训练程序应当根据验证表现保存对应轮次的权重。研究者确定方案后,再用固定测试集计算 Top-1、逐类召回、Macro-F1 和混淆矩阵,观察模型对不同花卉类别的识别情况。

用错误样本回头检查模型

模型把某些花卉分错以后,我们可以用 Grad-CAM 观察哪些图像区域与目标类别输出相关。研究者比较不同模型时,需要固定图像、类别、目标层和显示设置,并同时展示正确与错误样本。热图提供分析线索,遮挡检查、定量结果与失败案例则帮助我们继续检验解释。Grad-CAM

项目结束后,一套可复查的材料应当包括划分、配置、代码版本、检查点、结果表与错误样本。我们沿着基线、增强、模块比较这条顺序推进,就能逐步说明每项改动的作用。

练习:读者可以完成上面的三个小例子,并设计自己的 A/B/C 实验表。具备训练环境后,请先用少量数据确认损失和参数更新正常,再运行完整训练。

下一篇:目标检测的框、分数与评估。

资料:疏锦行计算机视觉专题《经典骨干网络》第 29–34 页、《即插即用注意力机制》及《基于注意力机制的花卉图像分类》第 3–12、15–18 页。课堂结果只作为参考,不计入本地实验成绩。