ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

遥感土地利用分类:给ResNet每个layer加CBAM注意力

遥感土地利用分类:给ResNet每个layer加CBAM注意力 简介面向遥感卫星土地利用分类任务的ResNet系列改进实战代码包覆盖resnet18/34/50/101/152五种主干。训练时可根据需要选择迁移学习或仅训练分类层优化器集成Adam与SGD损失函数采用多类别交叉熵学习率使用余弦退火策略方便进行对比消融实验。验证集在训练过程中同步评估输出loss、准确率、混淆矩阵、recall、precision、F1 score与特异度等指标并自动生成对应曲线图像训练日志以JSON格式保存便于按需绘制其他曲线。核心改进在ResNet每个layer后加入CBAM注意力模块可只保留某一层后的CBAM也可整体替换为其他注意力或模块代码结构清晰易扩展。包内共2000个文件以1994张JPEG土地分类图像为主另有3个Python脚本、readme说明、JSON日志及txt配置压缩包大小27.79MB数据覆盖21种土地目标。目前已有71人学习下载适合需要快速上手注意力机制增强图像分类模型的研究者与开发者。1. 遥感土地利用分类为何要给 ResNet 每个 layer 后加 CBAM一张 512×512 的遥感图里农田边界和裸地经常只有色调深浅的差别水体在大尺度下是一整片、切成小 patch 后又是零碎的沟渠ResNet 这种靠卷积堆深度的骨干网络做土地利用分类时总显得“看不清重点”。给 ResNet 的每个 layer 后串一个 CBAM 注意力模块是在不改主干、不换 Transformer 的前提下用很少的参数量把“该看哪些通道、该聚焦哪个位置”直接教给网络。这个方案适合手里有几千到几万张标注图、希望快速在遥感卫星土地利用分类上提点的一线工程师和研究生。2. 读懂 CBAM 的两个子模块通道先行的注意力为什么适合遥感地物2.1 通道注意力与空间注意力CBAM 在计算什么CBAM 全称是 Convolutional Block Attention Module设计上很朴素先做通道注意力再做空间注意力两个子模块串行输出一个和输入形状完全相同的重标定特征。通道注意力部分对输入特征图分别做全局平均池化和全局最大池化得到两个 1×1×C 的描述子送进一个共享的两层 MLP加和后再过 Sigmoid得到 1×1×C 的通道权重。空间注意力部分则是在通道维上对特征图做平均和最大压缩拼成一个 2×H×W 的“双通道”描述用一个 7×7 卷积降成 1×H×W再过 Sigmoid 得到空间权重。整个过程可以写成F1 Mc(F) ⊗ FF2 Ms(F1) ⊗ F1。注意 CBAM 训练初期权重都接近 1所以它不会像 BN 那样剧烈改变特征分布这也是它能直接插进预训练模型的原因之一。回到遥感土地利用场景农田、草地、林地之间纹理差异大但颜色相近的类别容易混淆通道注意力会告诉网络“这个地块更依赖红光波段还是近红外波段的响应”建筑和裸地的边界模糊空间注意力则负责把注意力集中在“地块内部”而不是路网和阴影边缘。相比只有通道注意力的 SE 模块CBAM 多出来的这一路空间注意力恰好补上了遥感地物对“位置感”的需求。参数开销上以 ResNet18 为例完整 CBAM 插在四个 stage 后新增参数约 0.4M对比模型本身的 11.7M 几乎可以忽略。2.2 “每个 layer 后加”到底是加在哪一级标题里“每个 layer 后加入 CBAM”这个说法在实操中其实有歧义。ResNet 的 layer 通常指 torchvision 实现里的 layer1 到 layer4也就是模型结构上的 4 个 stage每个 stage 里包含若干个 BasicBlock 或 Bottleneck。把 CBAM 加在 stage 尾部是参数效率最高的做法而如果理解为“每个 BasicBlock 的第二个卷积后再加”参数量和显存都会明显上涨训练时间也拉长。我一般默认“每个 layer 后”就是 stage 后。下面是三种常见加法的对比。加插位置新增参数ResNet18显存影响实测效果适用场景每个 stage 后layer1~4 尾部约 0.4M小稳定提升 0.5%~2%图像分类推荐首选每个 BasicBlock 后约 1.2M明显容易过拟合收敛不稳小数据集不建议只在 layer3、layer4 后约 0.2M很小提升幅度接近全加计算资源紧张时stage 后加还有一个好处预训练权重完全不受影响。因为 CBAM 是额外 add_module 进去的原始卷积和 BN 层的权重路径没有被改写加载 ImageNet 权重时不会出现 missing key 或 shape 不匹配。这一点对遥感这种“预训练权重决定上限”的任务尤其重要。2.3 为什么不是自注意力或 FPNCBAM 的取舍近两年图像分类模型的热点已经偏向 Transformer自注意力机制确实擅长捕捉粗粒度到细粒度的长程依赖但它在遥感土地利用任务上有一个现实门槛数据量。ViT 类模型在 ImageNet 上至少需要几千万张图才能训出好权重遥感切块数据往往只有几千到几万张直接微调很容易过拟合而且 patch 化会把地物的边界打碎位置编码也要重新适应遥感图的分布。FPN 是另一条思路但它是检测框架里的多尺度特征融合结构用于分类任务需要额外加分类头改动远大于一个 CBAM。CBAM 的好处是即插即用不改变数据流形状训练策略和原来几乎一样本质上是用局部注意力去替代全局自注意力的“大部分收益”。在样本有限、算力有限的前提下先把 CBAM 加进 ResNet 是性价比最高的第一步。3. 手写 ResNetCBAM三个代码块把注意力插进每个 layer 后3.1 定义 CBAM 模块通道注意力与空间注意力的最小实现下面是基于 PyTorch 的标准 CBAM 实现我习惯把通道注意力和空间注意力拆成两个子类调试时可以直接单独看某一层输出。import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_planes, ratio16): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) self.shared_mlp nn.Sequential( nn.Conv2d(in_planes, in_planes // ratio, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(in_planes // ratio, in_planes, 1, biasFalse), ) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out self.shared_mlp(self.avg_pool(x)) max_out self.shared_mlp(self.max_pool(x)) return self.sigmoid(avg_out max_out)in_planes 是输入特征图的通道数ratio 决定中间瓶颈维度。ResNet18 的 layer1 输出 64 通道64 // 16 4不会出现 0 维但如果你把 ratio 改成 6464 // 64 1也还成立再小就会出问题。这里共享同一个 MLP 对 avg 和 max 两个分支做映射是论文里的标准写法我自己实践时发现 max_pool 分支在遥感图里更重要因为农田边界和裸地往往是局部极值特征。空间注意力模块代码如下class SpatialAttention(nn.Module): def __init__(self, kernel_size7): super().__init__() padding kernel_size // 2 self.conv nn.Conv2d(2, 1, kernel_size, paddingpadding, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) max_out, _ torch.max(x, dim1, keepdimTrue) x_cat torch.cat([avg_out, max_out], dim1) return self.sigmoid(self.conv(x_cat))kernel_size7 是论文默认值7×7 卷积的感受野约等于 7×7 像素对 224×224 输入来说足够覆盖常见地物。如果遥感切块比较小比如 64×64 的 patch7×7 就显得过大我会改成 3见第 6 章。组合起来就是class CBAM(nn.Module): def __init__(self, in_planes, ratio16, kernel_size7): super().__init__() self.channel_attn ChannelAttention(in_planes, ratio) self.spatial_attn SpatialAttention(kernel_size) def forward(self, x): x self.channel_attn(x) * x x self.spatial_attn(x) * x return xforward 里的乘法和原特征做的是逐元素相乘CBAM 的输出形状和输入完全一样所以它能插入网络任何位置而不影响后续张量尺寸。3.2 把 CBAM 插进 ResNet先加载预训练权重再 add_module这里有一个容易翻车的顺序问题如果你先把 CBAM 写进 ResNet 结构里再去加载torchvision预训练权重load_state_dict会因为新增模块报 missing key正确做法是先用官方 API 拿到完整权重再往模型上挂模块这样 ResNet 本体的权重一条都不需要改动。import torchvision.models as models # 这一步拿到的是标准 ResNet18 ImageNet 预训练权重 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) def add_cbam_to_resnet(model, add_after_layer(True, True, True, True)): cbam_channels [64, 128, 256, 512] # resnet18/34 的四个 stage 输出通道 layers [model.layer1, model.layer2, model.layer3, model.layer4] for i, (layer, ch) in enumerate(zip(layers, cbam_channels)): if add_after_layer[i]: # 在 stage 的尾部追加 CBAM不改变任何已有层的权重 layer.add_module(fcbam_{i1}, CBAM(in_planesch, ratio16, kernel_size7)) return model model add_cbam_to_resnet(model)add_module的作用是往layer1这个Sequential容器里追加一个子模块。以 ResNet18 为例原来layer1包含两个BasicBlock追加后变成三个子模块数据流会按顺序执行 block1 - block2 - cbam_1。cbam_1 的输入输出都是 64 通道不改变 residual 结构。这样实现比修改torchvision源码里的BasicBlock干净得多也方便随时通过add_after_layer开关只加后几个 stage。对于 ResNet50四个 stage 输出通道是 256、512、1024、2048代码里只需把cbam_channels换掉其他逻辑不变。参数初始化的部分不需要额外处理add_module新增的模块会默认使用 PyTorch 的默认初始化CBAM 初始输出接近 1不会在第一个 epoch 就冲乱主干特征。3.3 验证前向与参数量跑通最小测试再进训练模型改完先别急着训用随机张量走一遍前向同时对比参数量变化这一步能拦截绝大多数“结构改错但没报错”的玄学问题。# 前向验证 model.eval() with torch.no_grad(): out model(torch.randn(1, 3, 224, 224)) print(out.shape) # 期望输出 torch.Size([1, 1000]) # 参数量对比 total_params sum(p.numel() for p in model.parameters()) trainable_params sum(p.numel() for p in model.parameters() if p.requires_grad) print(ftotal: {total_params / 1e6:.2f}M, trainable: {trainable_params / 1e6:.2f}M)如果 output 的最后一维不是 1000说明 FC 头被改动了如果参数量和原始 ResNet18 的 11.7M 差出好几倍说明 CBAM 被加到循环里重复堆叠了。正常加在四个 stage 后ResNet18 总参数量约 12.1M新增 0.4M 左右。这个测试数据也可以直接写进实验记录后面对照“加 CBAM 到底贵了多少”就不用现算。4. 遥感土地利用分类的训练配置数据目录、增强与超参数一次调到位4.1 数据目录与样本划分按图幅切块是底线土地利用分类常见类别包括农田、森林、草地、水体、建筑、裸地六类数据按 ImageFolder 组织最省事。有一点要从一开始就注意要按原始遥感图幅来划分训练集和验证集而不是把一张大图切出的所有 patch 随机打散。否则同一个地块的纹理会被模型记进权重里验证集虚高两三个点都不奇怪。data/ train/ farmland/ forest/ grassland/ water/ building/ bareland/ val/ farmland/ forest/ ...每个类别下放经过筛选的切块切块尺寸常用 224×224 或 256×256。多光谱数据输入通道不是 3 时预训练权重不能直接用要么训练时只取 RGB 三波段要么把第一个卷积层单独处理这部分细节放在第 5 章避坑清单里。数据集来源建议直接选用公开遥感分类数据集网上图像分类数据集下载渠道很多但自己抓图会引入标注不一致和传感器差异这两个问题比模型结构更难处理。4.2 数据增强强一点的几何增强对注意力更友好遥感图没有“上下颠倒”的概念所以翻转可以放开用。增强策略表如下。增强操作参数建议说明RandomResizedCropscale(0.5, 1.0), size224强制模型从不同尺度学地物RandomHorizontalFlipp0.5通用增强稳定收敛RandomVerticalFlipp0.5遥感图特有不破坏语义ColorJitterbrightness0.2, contrast0.2数值不宜过大破坏光谱特征NormalizeImageNet 均值/方差搭配预训练权重的标配ColorJitter 是双刃剑。遥感地物的光谱反射率是有物理意义的调太多会让水体发绿、植被发黄反而引入噪声。我一般把 brightness 和 contrast 都压在 0.2 以内saturation 不动。加 CBAM 之后模型对空间位置更敏感RandomResizedCrop 的尺度扰动可以适当调强一点防止注意力被“地块边缘”这种固定特征骗走。4.3 训练脚本与超参数小学习率配 cosine 足够ResNet CBAM 的训练策略和纯 ResNet 几乎一致唯一要注意的是 CBAM 是随机初始化的初始学习率不宜过大。用预训练权重时我一般用 0.005batch size 64能在前 5 个 epoch 里让 CBAM 平稳进入工作状态。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms transform_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.5, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_set datasets.ImageFolder(data/train, transformtransform_train) val_set datasets.ImageFolder(data/val, transformtransform_val) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers8, pin_memoryTrue) val_loader DataLoader(val_set, batch_size64, shuffleFalse, num_workers8, pin_memoryTrue) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer torch.optim.SGD(model.parameters(), lr0.005, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) model model.cuda() for epoch in range(30): model.train() for x, y in train_loader: x, y x.cuda(), y.cuda() out model(x) loss criterion(out, y) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 验证代码省略记录 val acc 和各类别 F1label_smoothing0.1对遥感粗标注特别有用因为真实标注里混着很多过渡地类硬标签很容易让模型变得过度自信。CosineAnnealingLR 的T_max30对应训练轮数如果你把调度改成按 iteration 更新T_max需要改成 30×len(train_loader)两种写法不要混用否则学习率会掉得太快。4.4 验证指标总体精度之外一定要看类别 F1土地利用分类只报 overall accuracy 是不够的水体、建筑这类类别样本往往偏少大类准确率高会把小类的恶化掩盖掉。我在训练时每个 epoch 都记录宏平均 F1 和每类 F1尤其关注 building 和 bareland 这对“易混淆对”。加了 CBAM 之后常见的变化是总体 acc 提升 1% 左右但 building 和 water 的 F1 拉开差距这说明空间注意力开始把边界类地物和内部均质地物分开处理了。如果只是 acc 小幅涨而 F1 没动那多半是数据划分泄露带来的虚涨不是模型的功劳。5. 加 CBAM 后训练与评估的避坑清单从掉点到 OOM 的五个现象5.1 加了 CBAM 准确率反而下跌现象完整加在四个 stage 后val acc 比纯 ResNet 还低 1~2 个点loss 曲线也不怎么降。原因最常见是 CBAM 加得太“满”。layer1 和 layer2 的浅层特征主要表达边缘、纹理这些低层特征本身空间分辨率高强注意力会把浅层特征过度调制反而干扰后续层。另一个原因是学习率偏大随机初始化的 CBAM 在初期被 SGD 大步长推偏主干也被带歪。解决先只在 layer3、layer4 后加 CBAM用第 3 章代码里的add_after_layer(False, False, True, True)学习率从 0.005 降到 0.003并加 3 个 epoch 的线性 warmup。绝大多数数据集上只加后两个 stage 的表现不比四个 stage 差训练还更快。5.2 遥感数据“地点泄露”导致验证集虚高现象随机划分时 val acc 92%看起来效果很好一旦按图幅划划分同一张原始大图的 patch 全进同一个集合准确率掉到 78%。原因随机划分把同一个地块的相邻切块同时放进了训练集和验证集模型记住的是“这张图的色调纹理”而不是“这类的通用特征”。这在遥感数据集里是普遍存在的黑匣子问题很多公开数字好看落地到新区域就翻车。解决划分数据时以图幅 ID 为最小单位。先把一张原始卫星图按滑动窗口切块记录每个 patch 的父图编号然后按“图幅”分组做 train/val split保证同一个图幅的 patch 不会同时出现在两侧。这条和 CBAM 无关但如果不先堵住所有“CBAM 提升”的结论都是假的。5.3 多光谱四通道输入怎么处理现象数据集是 RGBNIR 四波段把第一个卷积层改成输入 4 通道后loss 卡住不降训练几个 epoch 后 val acc 仍然接近随机。原因直接改model.conv1的in_channels4后这一层权重随机初始化ImageNet 预训练权重的信息全部失效等于让模型从零学第一个卷积层。CBAM 本身没问题问题出在主干入口。解决常见的做法是用 3 通道预训练权重初始化 RGB 部分新增加的第 4 个通道用 RGB 权重的均值去填。代码上可以这样处理old_conv1 model.conv1.weight.data # shape [64, 3, 7, 7] new_conv1 torch.zeros(64, 4, 7, 7) new_conv1[:, :3, :, :] old_conv1 new_conv1[:, 3, :, :] old_conv1.mean(dim1) model.conv1 nn.Conv2d(4, 64, 7, stride2, padding3, biasFalse) model.conv1.weight.data new_conv1也可以选择只训第一个 conv 层而冻结其余层跑几个 epoch再全部解冻。这种“半冻结”策略能明显减少多光谱入口带来的训练震荡。既然用到了多光谱数据增强里的 ColorJitter 更不建议开大近红外通道的数值扰动会让植被类地物特征失效。5.4 显存溢出batch 64 训不动现象ResNet18 原本 batch 64 跑得好好的加上 CBAM 后同一个 batch 直接 OOM。原因CBAM 里的 7×7 空间注意力卷积虽然参数量小但它在每个 stage 结束后都保留了一份完整的 H×W 中间激活用于反向传播。四个 stage 的特征图分辨率逐级减半浅层的 64 通道 56×56 特征图被多保留了一份显存峰值就上去了。解决把kernel_size从 7 降到 3能省一点计算但不会太多更有效的是减少插入位置只加(False, False, True, True)浅层不保留额外激活。还有一招是把 batch size 降到 32并用梯度累积模拟 64 的等效 batch。如果显存还是很紧可以考虑对 CBAM 的 forward 使用torch.utils.checkpoint用计算换显存但这个操作会拖慢训练。5.5 水体精度特别差森林却很高现象森林类别 F1 0.94水体只有 0.61训练曲线显示 water 的 loss 一直很高。原因水体在遥感图里往往是大面积均质区域经过 4 次下采样后边界信息基本丢失CBAM 空间注意力在深层拿到的已经是低分辨率特征很难恢复完整的水体轮廓。森林纹理丰富即使分辨率低也能靠纹理特征区分。解决对这类“大目标类别”可以在 layer4 后、全局池化前再补一个 CBAM让空间注意力在最高的语义特征层上再做一次区域加权。另一个更直接的办法是把训练切块改成多尺度除了 224×224 的 patch再抽一部分 448×448 的大 patch 下采样到 224 输入让模型在训练时见过“整片水体”的样子。我的实践经验里最后这种尺度策略对水体类别的 F1 提升比调 CBAM 参数更明显。6. 验证 CBAM 是否真的有效三组消融与热力图定位6.1 先跑三组对照别只盯最后一轮 acc建议至少做三组实验纯 ResNet18 基线、ResNet18 CBAM(后两个 stage)、ResNet18 CBAM(四个 stage)。下面是一个典型的记录表。模型参数量val accmacro F1单 epoch 耗时ResNet1811.7M86.2%79.5%42sResNet18 CBAM(后两 stage)11.9M87.4%81.0%47sResNet18 CBAM(四 stage)12.1M87.1%80.6%53s后两个 stage 的版本往往比四个 stage 的更好这个现象在很多遥感数据集上都能复现。判断 CBAM 有没有用还要看看训练前 10 个 epoch 的 loss 曲线CBAM 版本通常下降更快这个“前期收敛加速”比最后一轮的 acc 更能说明模块真的在学习注意力。6.2 用 Grad-CAM 看注意力落点消融实验数值上去了还想确认空间注意力到底看重哪里可以用 Grad-CAM 做可视化。下面是被简化后的核心片段。def grad_cam(model, x, target_class): model.eval() feature None gradient None def hook_f(module, input, output): nonlocal feature feature output.detach() def hook_b(module, grad_input, grad_output): nonlocal gradient gradient grad_output[0].detach() target_layer model.layer4[-1] # 注意此时 layer4 的最后一个子模块是 cbam_4 handle_f target_layer.register_forward_hook(hook_f) handle_b target_layer.register_full_backward_hook(hook_b) out model(x) model.zero_grad() out[0, target_class].backward() handle_f.remove() handle_b.remove() weight gradient.mean(dim(2, 3), keepdimTrue) cam (weight * feature).sum(dim1, keepdimTrue).relu() return cam在 torchvision 的 ResNet 里layer4[-1]不再是一个 BasicBlock 的 conv而是我们挂上去的 CBAM这会让热力图直接反映 CBAM 输出前的梯度分布反而很适合观察注意力给自己留下了哪些区域。对比纯 ResNet 的热力图CBAM 版本的热力中心通常更集中在地块内部而不是全图弥散。6.3 一个有用的调参习惯先大后小最后分享一个我自己的习惯任何新数据集上做 CBAM 实验都先把ratio固定 16、kernel_size固定 7、只放在层3和层4后跑通一轮再根据可视化结果决定要不要加到浅层。理论上 CBAM 参数不多但它们和数据集尺度、切块策略耦合在一起网格搜索“四个 stage 是否都加”的成本远高于它的收益。希望这些经验能帮你少走一段弯路祝调参顺利。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表