简介:这份资源是一套面向遥感卫星土地利用场景的 ResNet 系列图像分类改进实战代码包,适合有一定深度学习基础、希望将注意力机制融入主干网络做对比实验的研究者或开发者。工程完整覆盖 resnet18/34/50/101/152 五个系列,支持迁移学习或仅训练分类层,并内置 Adam 与 SGD 优化器、交叉熵损失、余弦退火学习率策略;训练阶段同步输出 loss、准确率、混淆矩阵、召回率、精确率、F1 与特异度等指标及曲线图。改进亮点是在每个 layer 后加入 CBAM 模块,且可灵活选择仅在某层后启用,便于替换其他注意力模块做消融研究。资源共 2000 个文件,以 1994 张 jpg 图像数据集为主,另有 3 个 Python 脚本、1 个 readme、1 个 json 日志说明及 1 个 txt 标注/说明文件,压缩包大小约 27.79MB。数据集包含 21 种土地目标类别,按 readme 要求整理即可更换数据。目前已有 71 人学习,适合用于分类任务迁移、注意力机制对比及遥感影像落地实践。
1. 为什么遥感土地利用分类需要在每个 ResNet layer 后加 CBAM
当你要把遥感卫星影像切成农田、建设用地、水体、林地和裸地,第一版基线往往就是 ResNet。ResNet 的残差连接解决了深度网络退化,ImageNet 预训练权重也足以应付很多自然图像任务,但遥感图像不是普通照片。遥感土地利用图像分类里,最典型的失败不是“认不出物体”,而是区域边界模糊:水体被阴影带偏,道路和裸地混在一起,建设用地与农田在纹理统计上高度重叠。CBAM 注意力机制通过通道权重和空间权重对特征图做显式重标定,把它放到每个 ResNet layer 后,相当于让网络在下采样后反复问一次“当前哪些通道重要、哪个位置值得看”。分类头最终拿到的特征会更贴近土地利用的决策边界。下面这条链路按“原理 → 代码 → 训练 → 验证”展开,最后你能得到一份可以直接在 PyTorch 上跑的 ResNet50+CBAM 实现。
2. 动手前先读懂 CBAM:通道权重和空间权重如何与 ResNet layer 产出对齐
2.1 CBAM 注意力机制的构成:通道注意力与空间注意力
CBAM 的完整结构由两个串行子模块组成。通道注意力首先对输入特征图 F∈R^{C×H×W} 同时做全局平均池化和全局最大池化,得到两个 C×1×1 的向量。平均池化保留整体响应水平,最大池化保留最强激活,这两个视角对遥感数据尤其重要:某一类地物可能整体色调平均,但会在某个波段出现尖峰响应。两个向量进入同一个共享 MLP,先把维度压到 C/r,再恢复到 C,相加后过 sigmoid 得到通道权重,输出为 C×1×1。通道注意力之后是空间注意力,输入特征图经过通道维度上的平均和最大操作,拼接成 2×H×W 的映射,再用一个 7×7 卷积压缩到 1×H×W,sigmoid 后得到空间权重。两个子模块串行连接,理由很直接:通道加权改变了各位置上的特征幅度,空间注意力再在这个更干净的特征上选择位置,两个动作解耦后反向传播路径更稳定。
通道注意力在 ResNet 中的参数体量主要由当前层输出通道数 C 决定。以 ResNet50 为例,layer1 到 layer4 的输出通道数分别是 256、512、1024、2048。每个 CBAM 模块增加参数量大约是 2×(C×C/r×2),reduction 取 16 时,layer4 后增加的参数量约 50 万,四个模块合计约为原始 ResNet50 的 2% 到 3%,可以接受。真正昂贵的不是参数量,而是特征图分辨率。layer1 输出的是 56×56 的特征,空间注意力在那里的乘法和卷积会带来额外算力,但因为每一次操作都是逐元素乘法,实际训练时间增加一般在 10% 到 20%。如果遥感影像分辨率很高,可以先把 CBAM 的 kernel_size 从 7 降到 5,减少空间卷积开销。
2.2 ResNet 的 layer 边界如何定位:四个 stage 与下采样
PyTorch torchvision 里的 ResNet 对“layer”有明确命名。先实例化一个不加载权重的模型,打印子模块名会得到:
import torchvision.models as models model = models.resnet50(weights=None) for name, module in model.named_children(): print(name)输出依次是 conv1、bn1、relu、maxpool、layer1、layer2、layer3、layer4、avgpool、fc。这里的 layer1 到 layer4 实际对应论文里的四个 stage,每个 stage 内部由若干 BasicBlock 或 Bottleneck 组成。题目中说的“每个 layer 后”,合理读法就是在 layer1、layer2、layer3、layer4 的输出之后分别接入 CBAM。如果你把粒度理解成每个 block 后也完全可行,四个 stage 分别有 3、4、6、3 个瓶颈块,那就要插入 16 个 CBAM,参数量和训练时长都会明显上升。在做遥感土地利用图像分类时,我的经验是先做 stage-level 的插入,这已经能覆盖不同尺度特征。CBAM 放在 layer 之后,意味着输入层已经从上一个下采样压缩过,空间分辨率减半但通道数翻倍,通道注意力的收益比放在 block 内部更明显。
2.3 插入位置与计算代价的不同选择
把 CBAM 放在哪里本质上是在权衡“注意力应该作用在多大尺度的特征上”。下面这张表概括了三种常见策略:
| 策略 | 插入模块数 | 作用特征图尺寸 | 适合场景 |
|---|---|---|---|
| 仅放在 layer4 后 | 1 | 2048×7×7 | 轻量快速验证,长尾类别感受野过大 |
| 每个 layer 后 | 4 | 256×56×56 到 2048×7×7 | 本文采用的默认配置,平衡精度与开销 |
| 每个 Bottleneck 后 | 16 | 全部尺度 | 高精度需求且算力充足,适合细碎地块 |
只放 layer4 后的缺点很典型:此时特征图只有 7×7,空间注意力几乎没有像素级定位能力,只能提供全局位置提示。对于遥感影像里大量存在的狭长地块,比如道路、河流,7×7 的掩膜根本不足以表达线性结构。放在每个 layer 后能让 layer1 和 layer2 的高分辨率特征也参与空间注意力,线性地物在 56×56 甚至 28×28 尺寸上仍保留较强的空间连续性。这也是遥感土地利用分类比一般 ImageNet 分类更需要中间层注意力的原因。
3. 用 PyTorch 在 ResNet 的每个 layer 后接入 CBAM 模块:代码与参数
3.1 最小可复现的 CBAM 模块实现
下面的代码是可直接使用的 CBAM 实现,包含通道注意力和空间注意力两部分,没有依赖额外库。注意通道注意力里用 1×1 卷积代替线性层,这样既能处理任意输入尺寸,也方便直接替换 ResNet 的中间层。
import torch from torch import nn class ChannelAttention(nn.Module): def __init__(self, channels, reduction=16): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.mlp = nn.Sequential( nn.Conv2d(channels, channels // reduction, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(channels // reduction, channels, 1, bias=False), ) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = self.mlp(self.avg_pool(x)) max_out = self.mlp(self.max_pool(x)) weight = self.sigmoid(avg_out + max_out) return x * weight class SpatialAttention(nn.Module): def __init__(self, kernel_size=7): super().__init__() self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) max_out, _ = torch.max(x, dim=1, keepdim=True) concat = torch.cat([avg_out, max_out], dim=1) weight = self.sigmoid(self.conv(concat)) return x * weight class CBAM(nn.Module): def __init__(self, channels, reduction=16, kernel_size=7): super().__init__() self.channel_attention = ChannelAttention(channels, reduction) self.spatial_attention = SpatialAttention(kernel_size) def forward(self, x): x = self.channel_attention(x) x = self.spatial_attention(x) return x参数说明:channels必须等于上一层输出的通道数;reduction=16是压缩比例,通道数越多压缩收益越大;kernel_size控制空间注意力感受野,7 是对自然图像常用的默认值,遥感图像里如果要捕获河流或公路的细长结构,7 仍然够用。AdaptiveMaxPool2d会让梯度流经最大值位置,这与平均池化提供的梯度形成互补,是 CBAM 在通道权重上优于 SE 模块的关键点。
3.2 修改 ResNet 前向传播:在 Layer1 到 Layer4 后插入调用
我一般不对 torchvision 的源码做硬改,而是用一个包装类重新组装 ResNet50。这样做的好处是预训练权重可以直接从backbone继承,CBAM 模块作为新增参数独立存在,训练时也能单独设置学习率。
import torchvision.models as models class ResNet50CBAM(nn.Module): def __init__(self, num_classes=6): super().__init__() backbone = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) self.conv1 = backbone.conv1 self.bn1 = backbone.bn1 self.relu = backbone.relu self.maxpool = backbone.maxpool self.layer1 = backbone.layer1 self.cbam1 = CBAM(256, reduction=16) self.layer2 = backbone.layer2 self.cbam2 = CBAM(512, reduction=16) self.layer3 = backbone.layer3 self.cbam3 = CBAM(1024, reduction=16) self.layer4 = backbone.layer4 self.cbam4 = CBAM(2048, reduction=16) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(2048, num_classes) def forward(self, x): x = self.maxpool(self.relu(self.bn1(self.conv1(x)))) x = self.cbam1(self.layer1(x)) x = self.cbam2(self.layer2(x)) x = self.cbam3(self.layer3(x)) x = self.cbam4(self.layer4(x)) x = self.avgpool(x) x = torch.flatten(x, 1) return self.fc(x)前向逻辑里的关键点是顺序:先跑完一个 stage,再进入 CBAM。self.layer1(x)的输出会交给cbam1,此时特征仍是高分辨率,空间注意力可以对 56×56 的每个位置分配权重。cbam4则作用在 2048×7×7 上,主要调整全局语义信息。如果把 CBAM 放在layer内部与残差分支并联,还要考虑恒等映射是否也被加权,这会让训练初期梯度更容易震荡。放在外部则不需要改动原始残差连接,是最容易稳定复现的接入方式。
3.3 预训练模型加载与新增层参数裁剪
上面的实现直接通过backbone获取预训练卷积层,load_state_dict不存在键冲突。如果用了strict=True加载,CBAM 和 fc 的缺失键会报错,正确的姿势是去掉与cbam或fc相关的参数名。另一种常见做法是先构建 torchvision 的原始模型,加载权重后把layer1到layer4赋值给自定义模型,同时保留conv1、bn1等结构。这样 Imagenet 上训练出的低级边缘纹理特征不会丢失。
CBAM 的初始化也值得注意:新模块没有预训练,启动时输出的空间权重会被 sigmoid 压到接近 0.5,通道权重接近 1,因此初始阶段 CBAM 几乎不改变原 ResNet 输出,这是安全的行为。若希望模块更快进入训练状态,可在SpatialAttention卷积层初始化里给 conv 参数乘一个小系数,但一般不需要。
4. 遥感土地利用图像分类的训练配置与评估指标
4.1 数据集组织与类别平衡策略
遥感土地利用图像分类的项目目录一般按train和val拆分,每个类别一个子文件夹。影像来源可能是高分二号、Sentinel-2 或者无人机航片,统一裁剪到 224×224 或 256×256 作为输入。没有预先切好的数据集时,最先要检查的是类别面积占比。建设用地、水体常常占比较大,而裸地、道路占比较小,直接用 CrossEntropyLoss 会导致小类别被忽略。CBAM 模块能缓解这个问题,但没有办法完全替代损失函数层面的处理。我一般会先统计训练集每类样本数,计算中位数与最小类别之间的比例,超过 5:1 就采用加权采样或给损失函数添加类别权重。遥感数据往往带有地理位置相关性,切割训练集时一定要按地块或图幅划分,不能把同一块地的 patch 同时放进训练和验证,否则验证指标虚高。
4.2 训练超参数:冻结策略、优化器和学习率
使用预训练 ResNet 后,backbone 参数不需要从零训练。载入预训练模型后,可冻结 backbone 前几层固定 low-level 特征,只训练 CBAM 和分类头。对于 6 类土地利用分类这种中小型任务,常见配置如下:
| 超参数 | 建议值 | 影响 |
|---|---|---|
| optimizer | SGD momentum=0.9 | 遥感特征噪声较大,SGD 比 Adam 更易收敛到平坦极值 |
| lr | 0.001 骨干,0.001 CBAM | 从 0.01 起步容易让 CBAM 梯度突变 |
| weight_decay | 5e-4 | 在空间注意力卷积上倾向小权重 |
| batch_size | 32 或 64 | 受影像通道数与裁切大小影响 |
| epochs | 30 到 50 | CBAM 前 10 轮基本完成通道筛选 |
为了处理新模块与预训练骨干的行为不一致,可以对参数做分组:
backbone_params = [] cbam_params = [] baseline_params = [] for name, param in model.named_parameters(): if 'cbam' in name: cbam_params.append(param) else: backbone_params.append(param) optimizer = torch.optim.SGD([ {'params': backbone_params, 'lr': 1e-3}, {'params': cbam_params, 'lr': 1e-3}, ], momentum=0.9, weight_decay=5e-4)如果希望骨干更稳定,可以将第一个 layer 的学习率设为其他层的 0.1 倍,我一般只在数据量为几千张这种规模下才启用这一设置。训练中关注第一个 epoch 的 loss:CBAM 初始权重接近恒等,loss 应和原始 ResNet 近似,如果明显高出,说明输入归一化或学习率设置异常。
4.3 用混淆矩阵和 Kappa 系数验证改进效果
分类准确率在遥感土地利用上不足以说明问题,特别是样本类别不均衡时。我会额外计算混淆矩阵和 Kappa 系数。Kappa 衡量的是分类结果与随机分类之间的差异,数值在 0 到 1 之间,遥感领域一般要求达到 0.75 以上才算模型可用。用测试集跑一轮预测,再统计:
import numpy as np from sklearn.metrics import cohen_kappa_score, confusion_matrix preds = [] labels = [] model.eval() with torch.no_grad(): for inputs, targets in val_loader: inputs = inputs.cuda() outputs = model(inputs) _, predicted = torch.max(outputs, 1) preds.extend(predicted.cpu().numpy()) labels.extend(targets.numpy()) kappa = cohen_kappa_score(labels, preds) cm = confusion_matrix(labels, preds)在对比实验中,base_result 是只使用 ResNet50 的混淆矩阵,cbam_result 是加入四个 CBAM 后的矩阵。加入 CBAM 后最明显的改观通常体现在水体和阴影的混分、道路与裸地的错贴上。肉眼逐类检查矩阵中的 off-diagonal 元素,如果这些长期混淆的对角邻域值明显下降,说明空间注意力确实在偏分位置起了作用,而不只是整体准确率微涨。训练日志建议保留每个 epoch 的 Kappa,因为准确率可能小幅波动,而 Kappa 对偶然一致性更敏感,更能反映空间注意力的稳定性。
5. 验证改进效果与常见坑:用 Grad-CAM 判断 CBAM 是否落在正确图层
加入 CBAM 后最怕的并不是模型变差,而是模块根本没有被训练起来,只是挂在网络结构里形成了一个恒等分支。验证它是否真正发挥作用的直接方式是观察梯度、激活和注意力掩膜。一个前端技巧是注册 forward hook,把 CBAM 中空间注意力输出的权重保存下来,看看不同土地类别上权重分布有没有差异。遥感图像里的水体通常呈线性,空间注意力应该沿水体轮廓产生长条高亮;建设用地是连续面状区域,注意力掩膜应该更平滑且集中在中心地块。如果所有类别的掩膜都几乎均匀,说明空间注意力退化为全 1 映射。
还可以用 Grad-CAM 对类别输出做可视化,确认 CBAM 是否把分类头最后的最近邻调整到正确的纹理边界上。以下代码放在验证阶段,对 layer4 后最后一层特征图求梯度:
def grad_cam(model, input_tensor, target_class): model.eval() input_tensor = input_tensor.unsqueeze(0).cuda() output = model(input_tensor) model.zero_grad() output[0, target_class].backward() conv_layer = model.layer4[-1].conv3 feature = conv_layer.output gradient = conv_layer.weight.grad weights = gradient.mean(dim=(2, 3), keepdim=True) cam = (weights * feature).sum(dim=1, keepdim=True) cam = torch.relu(cam) return cam实际运行时需要先给目标卷积层注册 forward hook 保存正向特征。将 cam 放大到输入尺寸后叠加到原始遥感图上,如果高亮区域和目标地物重合度过低,而你确认分类标签正确,那问题往往出在 backbone 下采样太早,CBAM 没有机会在早期特征图上形成空间分辨率的注意力。解决方法是把 layer1 或 layer2 后面的 CBAM kernel_size 加大到 7,并且检查通道注意力的 max_path 是否有梯度消失。小批量训练时torch.max的梯度流在空间维度上会退化为单点,建议用mean + max融合空间注意力输入,或者把批量大小提高到 16 以上,避免单张样本主导梯度。
使用一幅验证好的热力图作为快速测试,检查 water 类别激活集中在河道而非岸边林地,这就是 ResNet+CBAM 相对普通 ResNet 可观察到的真实改进。
本文还有配套的精品资源,点击获取