ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ResNet+CBAM:遥感土地利用分类注意力机制改进实践指南

ResNet+CBAM:遥感土地利用分类注意力机制改进实践指南 简介一份面向遥感图像分类与ResNet改进研究的实战代码包针对21种土地利用目标分类任务完整覆盖ResNet18、34、50、101、152五个系列网络。核心改进是在每个残差layer后嵌入CBAM注意力模块并允许按需仅保留某一层CBAM方便做消融对比训练时支持迁移学习或只训练分类层优化器可在Adam与SGD间切换配合多类别交叉熵损失与余弦退火学习率策略。验证与训练同步进行自动输出loss、准确率、混淆矩阵、recall、precision、F1分数、特异度等指标曲线并以JSON日志保存全部结果便于二次分析与绘图。包内共2000个文件包含1994张JPG遥感样本、3个Python训练/验证/推理脚本、1个Readme说明、1个TXT配置与1个JSON记录压缩包27.79MB结构紧凑下载后按Readme摆放数据即可运行。已有71人浏览学习适合需要遥感图像分类完整流程并在ResNet上尝试注意力机制改进的研究者与学生。1. ResNetCBAM 动手前的判断遥感土地利用分类卡在哪遥感卫星拍下来的土地利用图像分类很多团队第一反应就是拿 ResNet 预训练模型跑基线公开数据集上往往能到 85%~88%看着还行。但真正想冲 90% 以上时问题就来了耕地和裸地色块接近、小片水域被云影盖住、居民区和厂房边界在深层特征图里早被池化糊掉了。与其换更大的分类网络从头训不如在 ResNet 每个 layer 之后插一个 CBAM 模块——通道注意力告诉网络哪些特征通道值得放大空间注意力告诉网络哪个像素位置更关键。主干结构不动只增加几十万参数和少量计算量就能在土地利用、森林图像分类这类场景里拿到稳定提升。下面把选型理由、代码改法、训练参数和踩过的坑一次讲透。2. CBAM 的结构与选型通道注意力管看什么空间注意力管看哪里先说结论CBAM 是一个即插即用的注意力模块串行地把通道注意力和空间注意力接在一起输出和输入形状完全一致所以能直接塞进 ResNet 任意一层后面而不影响主干结构。2.1 为什么是“每个 layer 之后”而不是 block 内部或全连接之前ResNet 的四个 layer 是特征语义层次的分界点。layer1 学的是边缘、纹理、灰阶layer3、layer4 学的是“建设用地”“水体”“农田”这种高一层语义。把 CBAM 插在每个 layer 输出后相当于在每一级语义刚形成的瞬间做一次重标定让网络把当前阶段最有用的特征放大、把干扰特征压下去。这个位置不改变残差分支的梯度流向也不需要动原来训练好的权重预训练模型可以完整复用。如果把 CBAM 塞进 block 内部残差相加的顺序要重新设计反向传播路径变长训练初期容易不稳定。如果放在全连接之前空间注意力面对的是 1x1 尺寸卷积核完全派不上用场等于白加。我见过有人在全局平均池化之后接 CBAM训练完把空间注意力权重打出来看几乎全接近同一个小数模块基本失效。所以“每个 layer 之后”并不是随意定的而是性能和落地方案之间的平衡点。2.2 通道注意力让网络知道哪些特征通道值得看通道注意力对每一张 feature map 做全局平均池化和全局最大池化得到两个 C 维描述向量然后送进同一个两层 MLP。第一层把通道压缩到 C/r 再还原r 默认取 16两组输出相加后过 Sigmoid得到 0 到 1 的通道权重最后乘回原特征图。为什么要同时用平均池化和最大池化而不是只挑一个平均池化描述的是整体响应最大池化描述的是最显著响应。遥感图像里如果某个通道只在一块很小的建筑区有强响应全局平均会把信号稀释掉最大池化正好能把它捞回来。这个设计对地物尺度跨度大的土地利用分类尤其重要我实际用下来只保留平均池化的版本在小块地物识别上会差一些。r16 是最常见的压缩比。如果你的类别特别多、样本差异大可以试着把 r 改成 8参数会多一点但收敛更稳。反过来样本很少时把 r 调到 32能降低过拟合风险。2.3 空间注意力告诉网络“在哪看”空间注意力对特征图在通道维度上取平均值和最大值得到两张 HxW 的特征图拼接成 2xHxW 的张量通过一个 7x7 卷积映射成单通道再经过 Sigmoid 得到空间权重最后乘回原特征图。7x7 是原作者设置的默认值我在遥感任务里没有大改过。这个感受野在中等分辨率下刚好覆盖一块地物碎块能感知“这个像素周围是建筑物边缘还是平坦农田”。换 3x3 会把权重打散换 11x11 又容易在小数据集上过拟合。有一点要注意空间注意力本质上是给每个位置重新加权如果输入特征图分辨率特别低比如 7x7那 7x7 卷积能看到的上下文非常有限空间权重的区分度会下降这个在第 5 章会展开讲。2.4 CBAM 和 SE 的差别只有通道注意力位置信息会被丢掉很多人会问加 SE 模块是不是就够了。SE 只做通道注意力在 ImageNet 上收益不错但在土地利用分类里有个典型盲区两块颜色分布几乎一样的区域一块是耕地、一块是建设用地通道级别的权重完全无法区分它们只有空间注意力能根据周围环境把位置信息捞回来。这也是我在遥感场景里坚持用 CBAM 而不是只加 SE 的原因。通道注意力和空间注意力的顺序也有讲究。官方结构是“先通道、后空间”我反向试过在遥感数据集上掉了零点几个点。原因也好解释先做空间缩放会把背景区域一并放大通道重标定就没那么干净了。模块选型认准这个顺序别乱改。3. 代码实操把 CBAM 插进 ResNet 每个 layer 后的最小改法在 PyTorch 里CBAM 没有现成封装需要自己写一个模块。完整改法分三步先实现 CBAM再把 ResNet 的 layer 拆开重组最后处理预训练权重的加载。3.1 手写一个 CBAM通道注意力加空间注意力import torch from torch import nn class ChannelAttention(nn.Module): 通道注意力全局池化 - 共享MLP - 通道权重 def __init__(self, in_channels, reduction16): super().__init__() # 用两个 1x1 卷积代替全连接好处是能直接处理 NCHW 张量 self.mlp nn.Sequential( nn.Conv2d(in_channels, in_channels // reduction, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(in_channels // reduction, in_channels, 1, biasFalse), ) self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) def forward(self, x): avg_out self.mlp(self.avg_pool(x)) max_out self.mlp(self.max_pool(x)) weight torch.sigmoid(avg_out max_out) return x * weight # 权重广播到 H、W 维度 class SpatialAttention(nn.Module): 空间注意力通道维取均值/最大值 - 7x7卷积 - 空间权重 def __init__(self, kernel_size7): super().__init__() # 输入是 avg 和 max 拼接出来的 2 通道特征 self.conv nn.Conv2d(2, 1, kernel_size, paddingkernel_size // 2, biasFalse) def forward(self, x): avg_out x.mean(dim1, keepdimTrue) max_out x.max(dim1, keepdimTrue)[0] feat torch.cat([avg_out, max_out], dim1) weight torch.sigmoid(self.conv(feat)) return x * weight class CBAM(nn.Module): 先通道后空间串行组装 def __init__(self, in_channels, reduction16, kernel_size7): super().__init__() self.ca ChannelAttention(in_channels, reduction) self.sa SpatialAttention(kernel_size) def forward(self, x): x self.ca(x) x self.sa(x) return x这段代码里最关键的是通道注意力的 MLP 没有做“权重共享”严格说两个 1x1 卷积是独立参数但作用等价于共享 MLP实际效果一致。in_channels 是进入该层 CBAM 的 feature map 通道数不能拍脑袋填要和对应 ResNet layer 的输出通道完全一致。reduction 默认 16显存吃紧或者通道数很小时可以不降。kernel_size 默认 7对应空间注意力卷积核大小。如果写完发现输出维度不对先检查 AdaptiveAvgPool2d 是否把空间维度压到了 1再检查空间注意力里 cat 之后的通道数是不是 2。这两处是手写 CBAM 最常见的翻车点。3.2 把 CBAM 接进 ResNet拆解 layer 并重组 forward改 ResNet 最稳妥的方式不是去动 torchvision 源码而是把网络拆出来重新组装。下面以 ResNet50 为例把 layer1 到 layer4 分别取出来每一层后面接一个通道数匹配的 CBAM。from torchvision.models import resnet50, ResNet50_Weights class ResNet50_CBAM(nn.Module): ResNet50 每层后 CBAM支持加载 ImageNet 预训练权重 def __init__(self, num_classes45): super().__init__() backbone resnet50(weightsResNet50_Weights.IMAGENET1K_V2) # 拆出 stem 部分 self.conv1 backbone.conv1 self.bn1 backbone.bn1 self.relu backbone.relu self.maxpool backbone.maxpool # 拆出四个 layer self.layer1 backbone.layer1 # 输出 256 通道 self.layer2 backbone.layer2 # 输出 512 通道 self.layer3 backbone.layer3 # 输出 1024 通道 self.layer4 backbone.layer4 # 输出 2048 通道 # 每个 layer 后插一个 CBAM注意通道数必须和 layer 输出一致 self.cbam1 CBAM(256) self.cbam2 CBAM(512) self.cbam3 CBAM(1024) self.cbam4 CBAM(2048) self.avgpool nn.AdaptiveAvgPool2d(1) self.fc nn.Linear(2048, num_classes) def forward(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).flatten(1) x self.fc(x) return x这段代码的核心技巧是把原网络拆成子模块再重新拼装而不是在网络内部改写 forward这样加载预训练权重时layer1 到 layer4 的参数 key 和原来完全一致不会被破坏。CBAM 模块是新增的初始化为随机状态加载权重时要用 strictFalse否则会报缺失 key。新增模块需要单独设置学习率。一般做法是给 CXAM 参数一个较大的学习率让随机初始化的模块快速收敛骨干网络保持较小的学习率微调。注意 resnet50 也能换成 resnet18 或 resnet101但后面 CBAM 的输入通道要跟着 layer 输出改变。3.3 通道数对照表与最容易写错的三个细节不同 ResNet 系列的 layer 输出通道差异很大尤其是 ResNet50 的 layer1 输出不是 64。很多初学者在这里填错通道数导致训练直接报维度不匹配错误。模型layer1layer2layer3layer4ResNet18 / ResNet3464128256512ResNet50 / ResNet10125651210242048第一个细节ResNet50 的 layer1 输出是 256因为 Bottleneck 结构会把通道数扩大四倍。第二个细节CBAM 必须放在 layer 之后、下一个 layer 之前别放到池化层后面。第三个细节如果想在 ImageNet 预训练权重基础上继续训练加载权重时一定要用 strictFalse否则新增的 cbam1 到 cbam4 会让 load_state_dict 直接报错。4. 训练与对比用 resnet 预训练模型把土地利用分类跑稳网络结构改完之后剩下的问题是数据怎么整、训练参数怎么设、以及如何公平地和原版 ResNet 对比。遥感影像的分布和 ImageNet 差得很远这一章的操作决定了 CBAM 到底能不能发挥出来。4.1 遥感土地利用数据怎么整理成训练集公开遥感土地利用数据集里每张样本通常是 256x256 或 224x224 的影像块已经做好了类别标签可以直接用 ImageFolder 读取。但如果你拿到的是一张完整的大幅卫星影像加地块矢量标签就需要先裁剪成块。我的习惯是切 256x256 的图块滑窗步长取 224让相邻图块保留 32 像素重叠避免建筑物正好被切在边缘。import numpy as np def tile_image(image, tile_size256, stride224, labelNone): 把大幅遥感影像切成训练图块 tiles [] labels [] h, w image.shape[:2] for y in range(0, h - tile_size 1, stride): for x in range(0, w - tile_size 1, stride): tile image[y:y tile_size, x:x tile_size] tiles.append(tile) if label is not None: labels.append(label) return tiles, labelstile_size 设 256 是因为大多数预训练分类网络在这个分辨率下效果最好而且显存占用适中。stride 设 224 是为了保留重叠。如果你的任务是大块地物比如一整片森林stride 可以放大到 256 减少重复样本如果地块很碎比如城中村stride 需要缩到 128。类别不平衡是土地利用分类的常态。农田、林地样本常常是水体、裸地的十几倍直接用 CrossEntropyLoss 会让模型偏向大类别。常见做法是用 WeightedRandomSampler 按类别样本数的倒数采样让每个 batch 里各类别尽量均衡。from torch.utils.data import WeightedRandomSampler class_counts torch.tensor([500, 480, 45, 210, ...]) # 各类别样本数 class_weights 1.0 / class_counts.float() sample_weights class_weights[all_labels] # all_labels 是所有样本的类别索引 sampler WeightedRandomSampler(sample_weights, num_sampleslen(all_labels), replacementTrue)采样器会让训练 loss 波动更大但最终验证集上的小类 F1 会明显好看。如果你发现训练震荡得厉害可以把 num_samples 调成类别数的 2 到 3 倍而不是和总样本数一样。4.2 训练参数CBAM 单独给学习率别让新模块拖慢收敛CBAM 是随机初始化的ResNet 骨干是 ImageNet 预训练的两种参数用同一个学习率会出问题。学习率太小CBAM 一直学不进去学习率太大骨干预训练权重被冲乱。常见做法是把参数分成两组CBAM 的学习率调成骨干的 5 到 10 倍。import torch 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 torch.optim.SGD([ {params: backbone_params, lr: 0.001, momentum: 0.9, weight_decay: 1e-4}, {params: cbam_params, lr: 0.01, momentum: 0.9, weight_decay: 1e-4}, ]) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50)骨干用 1e-3CBAM 用 1e-2是遥感分类里比较稳的起点。如果你的数据集很小骨干学习率降到 5e-4 更安全。我这里用 SGD 而不是 Adam因为 SGD 加动量在微调预训练网络时收敛更稳泛化也更好。weight_decay 用 1e-4太大容易让通道注意力学到的权重偏小。epoch 设 50 轮起步。CBAM 参数少收敛很快真正慢的是骨干网络的微调。每 5 个 epoch 保存一次模型记录验证集 top-1 和 top-5方便后面画曲线判断有没有过拟合。4.3 对比实验固定随机种子基线和改进版一次跑成加注意力模块之后最怕的一件事是说不清提升来自 CBAM 还是来自运气。所以要固定随机种子让 baseline 和加 CBAM 的版本在同样的数据划分、同样的初始化下训练。def set_seed(seed42): import random random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)我用 42 作为固定种子但光有 seed 还不够数据加载时要把 shuffle 的随机数也固定住PyTorch 的 DataLoader 加上 generator 参数才能完全复现。比较 rough 的验证方式是跑三次取均值三次只改 seed其他全部相同。拿你本地的对比表来说一般会看到这样的现象ResNet50 在土地利用分类上 top-1 大概 86% 到 88%加了四层 CBAM 之后普遍涨 1 到 2 个点涨幅最明显的是水体、裸地、草地这些纹理不强的类别因为它们恰恰最依赖通道组合和位置信息。如果加完 CBAM 一点没涨直接跳到第 5 章查问题。5. 避坑与排错加了 CBAM 反而翻车的 4 个常见原因CBAM 不好用的案例比想象中更多但绝大多数不是模块本身的问题而是接入位置、训练配置或者权重加载出了问题。下面四条是我实际调参时反复遇到的。5.1 加了 CBAM 准确率不升反降现象baseline 验证集准确率 87%加完 CBAM 变成 85%训练 loss 还比原来降得更慢。原因最常见的是新加的 CBAM 参数没有更新。如果优化器没有把 CBAM 参数加进 param_group或者代码里误用了param.requires_grad FalseCBAM 就会一直停在随机初始化状态等于在特征图上乘了一组随机噪声。另一种原因是预训练权重加载时用了 strictTrue导致整个模型没有加载成功骨干网络从零训练准确率自然全面崩盘。解决训练前打印一下每个模块的 requires_grad确认 cbam1 到 cbam4 都是 True。优化器初始化后打印optimizer.param_groups确认两个 param_group 的参数量不为 0。加载预训练权重时用model.load_state_dict(checkpoint, strictFalse)然后手动检查 missing keys 里应该只包含 cbam 相关参数。5.2 空间注意力在低分辨率层上作用被稀释现象训练正常总体准确率也涨了但打印 layer4 之后的空间注意力权重统计发现标准差很小sigmoid 输出大量集中在 0.5 附近。原因layer4 的输出特征图只有 7x7 分辨率空间注意力在这个尺度上能看到的信息非常有限。7x7 卷积在 7x7 的特征图上只能覆盖局部几个像素很难产生全局区分度所以空间注意力退化成近似常数。解决这不是模型坏了而是高层的空间信息本身已经被压缩没了。常见做法是把 layer4 后面的 CBAM 中空间注意力去掉只保留通道注意力或者调整特征图分辨率在进入 layer4 之前把 stride 改小。对于图层说“每个 layer 都加”但实际调优时应该观察每层 CBAM 的统计量不要盲目保留全部模块。5.3 显存暴涨batch size 直接腰斩现象原版 ResNet50 batch size 64 能跑插入四个 CBAM 后 OOM只能降到 32。原因CBAM 不是单纯的参数增加它的前向过程会保留大量中间张量用于反向传播。空间注意力里拼接后的 2xHxW 特征、通道注意力里两个池化结果都会占用显存。层输出特征图分辨率越大占用越明显layer1 和 layer2 后面的 CBAM 是显存大头。解决如果只是做实验把 batch size 降到 32 就能跑通。如果要在生产环境大批量推理可以换一个思路把 CBAM 只插在 layer3 和 layer4 之后或者在 layer1、layer2 后面使用 gradient checkpointing 包装 CBAM用少量训练时间换显存。显存敏感的任务优先保住 backbone 预训练权重而不是硬上完整版 CBAM。5.4 预训练权重加载报错missing key 和 unexpected key现象model.load_state_dict(state_dict)直接抛异常错误信息里出现大量Missing key(s): cbam1.ca.mlp.0.weight。原因CBAM 是新增模块训练好的标准 ResNet 权重里根本没有这些参数。反过来如果你的 checkpoint 是加了 CBAM 之后保存的再用原版 ResNet 去加载也会报 unexpected key。解决统一用strictFalse加载权重然后分两步验证。第一步打印missing_keys确认漏掉的都是带 cbam 的 prefix第二步把 backbone 的参数单独抽取出来加载避免 CBAM 参数被意外覆盖。这个操作看着啰嗦但能省下大量试错时间。6. 验证与下一步CBAM 生效了没有再往哪个方向调判断 CBAM 是否真的起作用不能只看一个 top-1 数字。我习惯把混淆矩阵拉出来对比 baseline 和加 CBAM 的版本重点看哪些类别在涨。通常涨得最多的是纹理接近、边界模糊的地类比如水体与阴影、草地与耕地。如果所有类别都均匀涨零点几个点可能是训练配置变好了而不是 CBAM 的作用如果是几个具体类别明显上涨那才是注意力模块真正找到了关键位置。进阶一点的验证方法是看通道注意力权重分布。训练完把 cbam3 的权重导出来挑权重最大的几个通道把对应的特征图可视化你会发现它们集中在地物边缘或特定光谱组合上说明注意力在帮网络做特征选择。配合 Grad-CAM 看最后一层卷积的激活热图加 CBAM 的版本对地物边界的聚焦明显更紧而不是散成一片。再往前走一步可以在完整方案上做消融实验只保留通道注意力去掉空间注意力观察哪些类别的准确率回落就能判断这两部分各自的价值。我自己的经验是layer4 后的空间注意力常常“出力不讨好”因为 7x7 输入本身没有太多空间细节可学。这时候我会把该层 CBAM 改成通道注意力多半还能再稳一点。视线从“必须保留全部模块”转到“哪些层值得放注意力”才是这一类改进真正开始起作用的地方。希望这些实测思路能帮你在自己的数据集上少走两步弯路。本文还有配套的精品资源点击获取
返回列表