资讯动态

遥感土地利用分类:在ResNet每个Layer后加CBAM注意力机制

发布时间:2026/10/1 21:21:10 来源:尧图企业网站定制
简介这是针对遥感卫星土地利用图像分类的ResNet改进实战资源支持ResNet18/34/50/101/152全系列适合有一定深度学习基础、希望快速上手注意力机制改进图像分类任务的开发者。资源包含1994张JPG图片、3个Python脚本、1个Readme说明、1个训练日志JSON和1个TXT标注文件整体打包约27.79MB。代码在ResNet每个layer后加入CBAM模块且可通过注释灵活控制加入位置也可将CBAM替换为其他注意力模块以做消融实验训练侧提供迁移学习、Adam/SGD优化器、余弦退火学习率和多类别交叉熵损失验证侧自动产出准确率、混淆矩阵、Recall、Precision、F1与特异度等指标及曲线。数据集覆盖21种土地目标更换自定义数据集时依Readme整理目录即可推理阶段只需将图片放入指定文件夹。资源共2000个文件已有71人学习是一份可用于对比实验和论文复现的完整工程包。1. 遥感土地利用分类为什么值得在 ResNet 每个 layer 后加 CBAM同样是分类自然照片里猫在画面中央模型抓住猫头就够了遥感卫星图完全不是这样一张 512×512 的影像里可能同时有农田、林地、水体和成片阴影类别边界模糊像素占比差别很大。传统 ResNet 直接训练很容易被大块均匀纹理带偏把注意力平均撒在全图上。CBAM 要解决的就是这件事在每个 layer 输出处按通道和空间重新标定特征让网络知道先看哪里、再看什么。下面按 ResNet18/50 的 layer1~layer4 结构手写 CBAM 并接到每个 layer 后面配上遥感土地利用分类的训练配置和踩坑记录。适合正在调遥感分类模型、被 accuracy 虚高或注意力位置找不准的工程师。2. 为什么 CBAM 要加在“每个 layer 后”注意力机制与 ResNet 的边界2.1 通道注意力与空间注意力在遥感图上分别干什么CBAM 由通道注意力Channel Attention和空间注意力Spatial Attention两个子模块串成。通道注意力对每个特征图做全局平均池化和全局最大池化把 C 个通道压成一组统计量经过共享 MLP 后 sigmoid得到一组 0 到 1 的通道权重空间注意力则在通道维度上做平均池化和最大池化拼成一张 2 通道的图再过一层 7×7 卷积sigmoid 后得到 H×W 的权重图。两部分先后与输入特征逐元素相乘实现重标定。放到遥感土地利用分类里这两个子模块的语义非常具体。通道注意力回答“什么地物重要”植被茂密区在绿波段和近红外波段的响应强CBAM 会把对应通道权重放大水体、裸地这类大块均匀纹理的区域则被压下去。空间注意力回答“图像哪块重要”农田地块边界、建筑密集区的纹理会得到比大片空白区域更高的权重森林这种大纹理类别也能保住自己的判别区域。两者相乘之后每个 stage 的输出已经不是朴素的卷积特征而是被显式引导过的特征后续 stage 拿到的输入干净得多。CBAM 不是自注意力没有 query/key/value 那套计算也没有位置编码训练成本很低。遥感分类数据集通常不够大与其直接换 transformer 结构去折腾位置编码方案不如先把 ResNet 基线做好用 CBAM 这种轻量重标定把主干榨干收益往往更稳。2.2 “每个 layer 后加”和“只在最后加”的差别很多改进 ResNet 的尝试只把注意力模块放在 global pooling 之前也就是最后一个 layer 的输出上。这个方案不是不行但它把所有卷积层的信息混在一起后只做一次补救早期 stage 里被无关纹理污染的特征已经不可能被重新组织。低层的细粒度特征与高层的粗粒度语义在这个方案里都没有得到中途纠正真正能吃到的注意力信息非常有限。把 CBAM 放到每个 layer 后情况就不一样。layer1、layer2 的特征图分辨率还高空间注意力此时介入可以尽早压制大面积水域、裸地等无效响应layer3、layer4 的特征图语义更强通道注意力此时对类别相关的特征做增强。每个 stage 的输出都先重标定再进下一个 stage后层看到的内容不会被前层的错误偏好带偏。若想做消融可以分别只在 layer1 后和只在 layer4 后加 CBAM 跑一轮大多数遥感土地利用分类数据集上“每层都加”比“只加一层”的稳定性更好。2.3 ResNet 的 layer 到底指哪一段先看 torchvision 结构动手之前要澄清一个概念免得改错地方。ResNet 代码里的 layer1~layer4 不是单个卷积层而是四个 stage每个 stage 由若干个 BasicBlock 或 Bottleneck 串成。torchvision 里 ResNet18 的 layer1 实际是 2 个 BasicBlock 组成的 nn.Sequential层间空间尺寸与通道数有明确约定。把 CBAM 接在 layer 后指的是 stage 最后一个 block 的输出上不是接在每个 block 内部。先用几行代码把模型主线打印出来import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for name, module in model.named_children(): print(name, -, module.__class__.__name__)运行后会看到 conv1、bn1、relu、maxpool、layer1、layer2、layer3、layer4、avgpool、fc 这条主线。真正要插 CBAM 的就是 layer1 到 layer4 四个位置。ResNet18 四个 stage 的输出尺寸如下位置输出特征图尺寸输出通道数layer156×5664layer228×28128layer314×14256layer47×7512ResNet50 的结构类似只是每个 stage 由 Bottleneck 组成输出通道数为 256、512、1024、2048。这个差异在后面的模型改造代码里很容易埋坑建议直接用 hook 从模型里读取通道数而不是把数字写死在代码里。3. 在 ResNet 每个 layer 后加 CBAM模型改造与训练配置3.1 手写 CBAM 模块PyTorch 实现两个子模块CBAM 原论文给出的是串行结构先通道注意力后空间注意力。下面是我在分类任务里一直用的实现参数与原论文一致。import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction16): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) hidden max(in_channels // reduction, 16) self.mlp nn.Sequential( nn.Conv2d(in_channels, hidden, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(hidden, in_channels, 1, biasFalse), ) def forward(self, x): avg_out self.mlp(self.avg_pool(x)) # 平均池化分支 max_out self.mlp(self.max_pool(x)) # 最大池化分支 return torch.sigmoid(avg_out max_out) class SpatialAttention(nn.Module): def __init__(self, kernel_size7): super().__init__() self.conv nn.Conv2d(2, 1, kernel_size, paddingkernel_size // 2, biasFalse) def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) # 通道均值 max_out torch.max(x, dim1, keepdimTrue)[0] # 通道最大值 out torch.cat([avg_out, max_out], dim1) return torch.sigmoid(self.conv(out)) class CBAM(nn.Module): def __init__(self, in_channels, reduction16, kernel_size7): super().__init__() self.channel_att ChannelAttention(in_channels, reduction) self.spatial_att SpatialAttention(kernel_size) def forward(self, x): x x * self.channel_att(x) # 先通道重标定 x x * self.spatial_att(x) # 再空间重标定 return x参数说明reduction 控制通道注意力的压缩比默认 16当输入通道较窄时max(in_channels // reduction, 16) 保证隐藏层不至于坍缩到 1 个通道。这里用 1×1 Conv 替代原论文的 Linear因为池化后的特征已经变成 1×1×C两者作用等价写成 Conv2d 可以直接在 4D 张量上算省去 flatten。平均池化与最大池化共用同一个 MLP这是原论文 shared MLP 的约定不要分别建两个 MLP。kernel_size 默认取 7。CBAM 论文里比较过 3 和 7遥感图像空间范围大7×7 的感受野更贴合地块级结构。3.2 把 CBAM 接到 ResNet 的 layer1~layer4 之后不改动 torchvision 源码用模块注册加 forward 包装的方式把 CBAM 接进去。下面这段对 ResNet18/34/50/101 都通用。def apply_cbam_to_resnet(model, reduction16, kernel_size7): layer_channels [] # 用 forward hook 抓各 stage 实际输出的通道数 def collect(module, inputs, outputs): layer_channels.append(outputs.shape[1]) hooks [] for i in range(1, 5): layer getattr(model, flayer{i}) hooks.append(layer.register_forward_hook(collect)) model.eval() with torch.no_grad(): model(torch.randn(1, 3, 224, 224)) for h in hooks: h.remove() # 按实际通道数注册 CBAM 子模块 for i, ch in enumerate(layer_channels, start1): model.__setattr__(fcbam{i}, CBAM(ch, reduction, kernel_size)) def forward_with_cbam(self, x): x self.conv1(x) x self.bn1(x) x self.relu(x) x self.maxpool(x) x self.layer1(x) x self.cbam1(x) x self.layer2(x) x self.cbam2(x) x self.layer3(x) x self.cbam3(x) x self.layer4(x) x self.cbam4(x) x self.avgpool(x) x torch.flatten(x, 1) return self.fc(x) model.forward forward_with_cbam.__get__(model, type(model)) return model逻辑说明先用一次 dummy forward 跑出 layer1~layer4 的真实输出通道数按这个数字创建 4 个 CBAM 模块再用setattr注册为 model 的 submodule这样 nn.Module 的属性机制会让它们跟随 model 一起 cuda、一起进入 state_dict。最后把绑定的 forward 覆盖到实例上原有的 parameters 全部保留。参数与边界layer_channels 来自 hook 抓到的 output.shape[1]所以 ResNet18 读到 64/128/256/512ResNet50 读到 256/512/1024/2048代码不用为不同版本改数字。如果已经包了 nn.DataParallel需要改成对 model.module 调用本函数否则 cbam 会注册到 DataParallel 外壳上加载权重时会报 key 不匹配。这套改法不改动原有的 stage 内部结构因此 ImageNet 预训练权重里除了新增的 cbam 参数外全部可以复用。3.3 训练配置分层学习率与类别权重模型改完后训练配置也要跟上。常见做法是主干继续用低学习率微调新增的 CBAM 部分用更大学习率快速收敛。遥感土地利用分类的类别分布通常很不均衡损失函数上要做补偿。import torch.optim as optim from torch import nn backbone_params [] cbam_params [] for name, param in model.named_parameters(): if cbam in name: cbam_params.append(param) else: backbone_params.append(param) optimizer optim.SGD([ {params: backbone_params, lr: 1e-3}, {params: cbam_params, lr: 1e-2}, ], momentum0.9, weight_decay1e-4) # 类别权重按训练集样本数反比计算这里用示例数值 class_counts torch.tensor([120000, 50000, 80000, 20000, 60000], dtypetorch.float32) class_weights 1.0 / class_counts class_weights class_weights / class_weights.sum() * len(class_counts) criterion nn.CrossEntropyLoss(weightclass_weights)参数说明CBAM 是随机初始化的主干继续用 ImageNet 预训练所以 CBAM 部分的学习率可以比主干大一个数量级如果两者同 lrCBAM 收敛会明显拖慢。class_weights 按 1/样本数 计算后再归一化目的是让小类别的单样本 loss 贡献放大。真实工程里这个权重应该在每个 epoch 前按当前训练集重新统计上面的数值只是演示写法。如果显存有限可以先把主干参数冻结只训练 CBAM 5 个 epoch等注意力先学会在预训练特征上做重标定再放开主干一起微调。这也是处理遥感大图的常用省显存手段。4. 避坑清单遥感图像分类训练里最容易翻车的五个细节4.1 加载预训练权重报错strictFalse 后 CBAM 参数哪去了现象用 model.load_state_dict(checkpoint, strictFalse) 加载预训练权重训练过程中 loss 始终不降或者打印日志发现 cbam 相关参数一直是初始值。原因strictFalse 遇到缺失的 key 会直接忽略不会报错而 CBAM 模块如果没被正确注册它的参数就会出现在 missing_keys 里被静默跳过。解决加载后一定把 missing_keys 和 unexpected_keys 都打印出来确认。checkpoint torch.load(resnet50_imagenet.pth, map_locationcpu) result model.load_state_dict(checkpoint, strictFalse) print(missing:, result.missing_keys) print(unexpected:, result.unexpected_keys)正常情况是 missing 里只有 cbam1~cbam4 相关参数unexpected 为空。unexpected 有内容说明预训练权重和改造后的结构对不上missing 里出现主干 conv1 或 layer1 的参数说明 forward 包装或模块注册出了问题要回去检查而不是继续训练。另外如果用了 DataParalleltorch.save 时权重 key 会带 module. 前缀加载前要先 strip 掉否则 unexpected_keys 会堆满整个模型。4.2 类别不平衡Loss 被大块背景地物拉平现象训练过程中 loss 持续下降但每个类别单独看召回率养殖塘、裸土、小块建设用地几乎全错水面和农田倒是非常准。原因遥感影像里背景类别的像素占比天然占优CrossEntropyLoss 的均值被大类别主导CBAM 只是特征层面的重标定解决不了损失层面的倾斜。解决按 3.3 的方式加 class_weights或者改用 Focal Loss 这类难样本加权损失。每个 epoch 记录混淆矩阵和各类别 recall不要只盯 accuracy。小类别的样本量如果实在太少还可以在数据加载时对小类别做过采样让每个 batch 里不会一整批都是水域图。CBAM 负责注意力重标定类别权重负责损失倾斜两者各管一段缺哪个都不行。4.3 多光谱影像的 4 通道与预训练权重第一层对不上现象输入是 R、G、B、NIR 四波段影像张量形状为 4×H×W 或 B×4×H×Wforward 直接报错说 conv1 期望 3 通道。原因ImageNet 预训练权重第一层是 3 通道卷积不能直接吃 4 通道输入。解决常见做法是把 NIR 作为第四通道第一层卷积用预训练权重扩展初始化。conv1 model.conv1 w conv1.weight.data # 形状 (C_out, 3, k, k) model.conv1 nn.Conv2d(4, w.shape[0], kernel_sizew.shape[2], strideconv1.stride, paddingconv1.padding, biasFalse) model.conv1.weight.data[:, :3] w model.conv1.weight.data[:, 3] w.mean(dim1)逻辑说明NIR 通道没有可靠的预训练先验用 RGB 权重的均值去初始化至少保证它一开始不产生破坏性的响应后续训练会让它自己学出来。如果不想动 conv1也可以把 NIR 波段的信息编码成伪彩色直接喂 RGB效果会差一些。要注意 4 通道 Geo 影像读取后通道顺序一定要确认部分国产卫星数据的波段排列和 Landsat 不一样先打印影像 metadata 再动手。4.4 CBAM 加在 shortcut 上还是 residual 分支上现象有人不满足于 stage 后加 CBAM自己魔改 Bottleneck把通道注意力乘到了 shortcut 路径上结果 acc 不升反降。原因shortcut 是恒等映射承载的是跨层直连信息对它做逐元素缩放等于强行修改原始特征回流高层语义被扭曲。解决严格按标题做法放在 layer 后或者在 residual 分支内部、相加之前加不要动 shortcut。放在 layer 后是稳定性最好的位置空间尺寸和通道数天然对齐不需要额外投影层。如果在 block 内部做实验也要放在最后一个卷积输出之后、与 shortcut 相加之前并且注意步长为 2 时 feature 图尺寸已经变了空间注意力不能直接在原分辨率上相乘。4.5 测试时精度和训练时差距过大BN 与 eval 模式的坑现象训练集上 acc 已经很高验证集结果差一截且每次推理结果不稳定。原因遥感数据少时 batch size 往往取得很小BN 的 running_mean 和 running_var 在训练中振荡CBAM 本身没有 BN但主干 BN 统计量在 eval 模式下会切换成全局统计两者统计口径不一致。解决推理前调用 model.eval()训练时 batch size 不要小于 8太小时把 BN 换成 GroupNorm 再微调。这也是遥感分类工程里很常见的一处翻车点尤其在影像特别大、单卡只能放 4 张图的情况下最容易踩到。换 GroupNorm 不用重构 forward只把模型里的 BatchNorm2d 替换成 GroupNorm 即可channel 数 64 的 layer 用 num_groups4 或 8 都行可以先用验证集试一组。5. 验证 CBAM 到底起没起作用注意力热图与评估指标5.1 用 hook 把每层注意力图导出来看只靠指标提升说明不了问题还得看 CBAM 的注意力权重落在哪里。下面这段代码把每个 stage 后的空间注意力图导出来。att_outputs {} def make_hook(name): def hook_fn(module, inputs, outputs): att_outputs[name] outputs.detach().cpu() return hook_fn for i in range(1, 5): getattr(model, fcbam{i}).spatial_att.register_forward_hook(make_hook(fcbam{i})) model.eval() x preprocess(img).unsqueeze(0) with torch.no_grad(): pred model(x) # att_outputs[cbam3] 就是 layer3 之后的空间注意力权重图注意 hook 注册在 spatial_att 子模块而不是 CBAM 整体上这样拿到的就是 H×W 的权重图。实际排查时重点看 layer3 和 layer4 的输出高权重位置如果集中在农田边界、建筑群而不是大块水域或裸地说明注意力在学习有效的判别区域如果高权重位置和标注语义明显错位先怀疑训练不充分再看数据标签是否噪声太大。5.2 评估指标别只看 accuracymIoU 才是遥感分类的账本遥感土地利用分类的 acc 虚高是常态一块图里水面占 60%全预测成水面 acc 也有 60 上下。工程上我会按类别记录 IoU报告 mIoU 和加权 F1每个 epoch 存一次混淆矩阵。训练结束后挑出 IoU 最低的三类回到 5.1 的热图里看对应样本判断是特征没学好还是标签本身画错。FPN 那类多尺度融合结构先不急着加等 ResNet 主干加 CBAM 的基线稳定了再考虑把 layer3、layer4 的特征送去 FPN 做多尺度特征融合这时候融合才有意义。这是我做遥感分类项目攒下的习惯每次改骨干结构先导出一批热图和混淆矩阵再谈指标提升不然分不清收益来自 CBAM 还是随机种子。希望这些实现细节和踩坑记录能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑