遥感图像里的小目标大概是计算机视觉里最让人头疼的一类对象。一张1024×1024的高分影像中目标可能只占据十几个像素背景却堆满楼宇、植被、云影检测头稍微做一次下采样目标响应就被稀释到几乎看不见。更麻烦的是很多模型加了注意力机制之后不仅没变强反而因为算力被冗余计算吃掉了精度和速度双双下滑。这种情况在行业里被总结成一个词空间注意力与通道注意力双重冗余。LWGANet就是冲着这个问题来的。它是一个轻量级注意力模块核心思路很简单在通道维度和空间维度上分别做低秩、分组的注意力生成再把两路信息引导融合从而把注意力模块自身带来的冗余降到最低。因为设计成即插即用结构它可以直接塞进主流检测和分割框架的Backbone、FPN甚至检测头前面不需要改动主干结构也不需要对预训练权重做特殊处理。对正在折腾遥感小目标检测、无人机视角目标分析以及想控制参数量做端侧部署的工程师来说这类模块会是比通用注意力更省心的一种选择。1. 遥感小目标检测的痛点注意力双重冗余是怎么产生的1.1 小目标检测面临的三个“天生难题”做遥感小目标检测的人最先感受到的往往是这三个坎。第一目标像素占比极低。一个常见场景里飞机、车辆、小船在原始影像里可能只有5×5到20×20个像素。而检测模型里一个普通的下采样操作每经过一次就能把特征图缩小一倍等传到高层特征时小目标的响应往往只剩几个像素甚至完全消失。这就导致很多模型对中大型目标效果不错一到密集小目标场景就疯狂漏检。第二背景极其复杂。遥感影像不像自然图像那样主体突出地面上的道路、屋顶、阴影、云层、水体在纹理上经常比目标本身还要丰富。模型需要从海量干扰里把微弱的目标信号捞出来注意力机制因此被寄予厚望但通用注意力模块在复杂背景下很容易“分心”把大量权重投到背景细节上。第三小目标自身纹理信息有限。一个飞机目标的形状信息可能只有轮廓和发动机亮点灰度分布和周围环境差别不大。一旦特征提取不够锐利后续无论分类还是回归都很难做出准确判断。这也是为什么小目标任务特别依赖高质量、强对比的特征表示。这三个难题叠加起来让小目标检测任务对注意力机制的需求比普通检测更迫切。但有趣的是把SE、CBAM、CoordAtt这类通用注意力模块直接搬过来效果往往不如预期一个重要原因就是接下来要说的冗余问题。1.2 什么是通道注意力冗余和空间注意力冗余先说说通道注意力冗余。一个骨干网络输出的特征图通常有256、512甚至2048个通道但真正“有效”的语义类别远没有这么多。很多通道其实在响应相近的模式比如多个通道都在对屋顶边缘响应另一些通道都在对植被纹理响应。通道注意力机制要做的事情是给每个通道打分可如果几十个通道的语义几乎一致那么打分结果本身就高度相关相当于用了几十份算力去表达同一件事。打个比方开会时每个人手上都有同一份材料却按顺序轮流念一遍最后只有一两句话是有用的。通道注意力冗余就是这么个状态计算量不小但信息增益很低。度量方式也很直观——把通道注意力权重向量两两之间算相似度或者对通道特征矩阵做特征值分解看有效秩你会发现有效维度远小于名义上的通道数。再说空间注意力冗余。空间注意力通常要为特征图的每个位置生成一个权重值希望告诉网络“看这里、别看那边”。但对遥感影像来说一张特征图上大部分区域属于背景真正需要精细打分的位置可能只占几个百分点。更关键的是相邻像素的注意力分数往往高度平滑一块50×50的区域里得分几乎相同。这时候逐像素生成attention map实际上是在给一大片背景做毫米级精度的测量测出来的结果却几乎没有变化。双重冗余放到一起就是这样一个场景通道维度重复打分空间维度重复打分两路结果还存在一定程度的重叠。于是模型又慢又肿却没能把有限的算力集中在小目标真正需要的特征上。LWGANet要解决的正是这种“双重冗余”带来的注意力效率问题。1.3 常见通用注意力模块在遥感小目标场景下的局限回过头看几个常见模块你会发现它们各有各的别扭。SE模块的通道压缩做得不错但它是纯通道注意力完全忽略了空间位置信息。遥感小目标本身位置稀疏缺少空间维度引导SE在小目标场景的收益天然有限。CBAM虽然把通道注意力和空间注意力串起来了但它的空间分支用的是一个大卷积核直接在大尺寸特征图上生成attention map空间维度上的冗余计算并没有被针对性压缩。CoordAtt引入了方向感知让注意力拥有了坐标信息在很多中目标场景表现不错但它的实现本质上仍然是全图范围生成注意力编码放到密集小目标场景空间冗余依然存在。通用注意力模块“够用但不够轻、不够准”的尴尬在轻量化部署时会被放大。移动端或嵌入式平台对参数量和计算量极其敏感一个额外的7×7卷积或者两个大全连接层都可能导致推理延迟飙升。LWGANet的设计思路正是从这里切入与其在通用模块上打补丁不如重新设计一个轻量级注意力分支把通道和空间两路冗余都压下去。2. LWGANet的核心设计把注意力分支本身做“瘦身”2.1 从解决思路到模块结构理解了痛点之后再看LWGANet的模块结构就顺理成章了。它有三条核心设计原则。第一通道维度的注意力权重不应当被当作C个完全独立的值来处理。真实场景里通道之间有大量语义重叠所以完全可以让一组通道共享同一个低秩变换用分组和压缩的方式把重复表达去掉。第二空间注意力图并不需要逐像素独立的自由度。一张H×W的attention map看似提供了H×W个可学习的空间权重实际有效信息可能只集中在少数区域。用少量“锚点”加低秩编码去近似整张图既能保留空间选择性又能砍掉大量冗余计算。第三通道和空间两路注意力不能是简单串联或并联叠加。串联容易让第二次注意力继续强调第一次已经选中的特征产生新的重复响应单纯并联相加又缺乏交互。LWGANet把两路信息做引导融合让通道权重影响空间图生成、空间图反过来约束通道权重在机制上避免二次冗余。沿着这三条原则模块内部被拆成了通道分支、空间分支和一个融合门控。下面逐个看。2.2 通道分支分组低秩权重生成通道分支的设计思路是在SE的全局平均池化基础上增加一个分组共享机制。具体做法是把输入特征图按通道分成G个组每组内先做全局平均池化得到一个长度为C/G的组描述向量然后把这个组描述向量送进一个1×1卷积压缩到很小的维度再用另一个1×1卷积恢复回C/G最后把所有组的权重拼起来经过sigmoid得到全局通道权重。我写了一份简化版的PyTorch风格伪代码方便对照看结构。import torch import torch.nn as nn class ChannelBranch(nn.Module): def __init__(self, channels, groups4, reduction8): super().__init__() assert channels % groups 0, channels必须能被groups整除 self.groups groups self.group_c channels // groups hidden_c max(self.group_c // reduction, 4) # 注意这里的1x1卷积在所有分组之间共享权重 self.fc1 nn.Conv2d(self.group_c, hidden_c, 1, biasFalse) self.fc2 nn.Conv2d(hidden_c, self.group_c, 1, biasFalse) def forward(self, x): B, C, H, W x.shape # 按分组拆开 xg x.view(B, self.groups, self.group_c, H, W) # 每组做全局平均池化 v xg.mean(dim(3, 4)) # (B, groups, group_c) v v.reshape(B * self.groups, self.group_c, 1, 1) v torch.relu(self.fc1(v)) v self.fc2(v) # (B*groups, group_c, 1, 1) v v.reshape(B, self.groups * self.group_c) return torch.sigmoid(v).view(B, C, 1, 1)为什么这样做能缓解通道冗余关键在于分组共享。同一组内的通道共享同一套低秩变换参数迫使它们先被压缩成共享的语义描述再恢复成各自权重。那些高度相似的通道权重生成路径几乎相同网络就能意识到“它们本来就代表同一个信息”从而降低重复打分的可能。相比SE里的大型全连接层这种设计的参数量大致减少G倍在通道数很大的深层特征上收益非常明显。2.3 空间分支锚点低秩编码与上采样空间分支要处理的问题是如何用尽量少的自由度生成一张有效的空间注意力图。LWGANet的做法是引入“锚点编码”。首先用一组1×1卷积把输入特征投影到很低维的rank通道上得到轻量特征然后生成一组可学习的锚点每个锚点代表一种空间模式最后把轻量特征和锚点拼接起来经过一个1×1卷积生成最终的空间注意力图。因为锚点数量很少整个空间分支的自由度被压到非常低的量级但依然保留了“某个位置更像哪种空间模式”的判别能力。class SpatialBranch(nn.Module): def __init__(self, channels, anchors8, rank4): super().__init__() self.anchors anchors self.proj nn.Conv2d(channels, rank, 1, biasFalse) # 可学习锚点每个锚点是一个尺度参数向量 self.anchor_params nn.Parameter(torch.randn(1, anchors, 1, 1) * 0.1) self.gen nn.Conv2d(rank anchors, 1, 1, biasFalse) def forward(self, x): B, C, H, W x.shape feat self.proj(x) # (B, rank, H, W) anchors self.anchor_params.expand(B, -1, H, W) attn self.gen(torch.cat([feat, anchors], dim1)) return torch.sigmoid(attn) # (B, 1, H, W)锚点数量的选择很有意思。锚点太少空间分支缺乏足够的位置判别能力锚点太多又会把空间分支重新推向冗余。按我在本地复现实验的观察锚点数取4到8已经能覆盖大多数遥感小目标场景取16以后收益很小显存开销反而开始上涨。空间分支的rank通常在4到8之间再往上抬不仅费算力还会让训练前期明显变慢。2.4 融合策略门控互引导与恒等映射通道分支输出的是一个C×1×1的向量空间分支输出的是一个1×H×W的图怎么把两路信息合起来直接影响最终效果。LWGANet的做法是让两路结果先做外积式结合通道权重先广播到每个空间位置上再与空间图相乘得到一个同时包含通道和空间选择的调制张量。然后用一个可学习的门控系数去控制这个调制张量的强度最终通过残差形式加到原特征上。这里的关键是残差连接。注意力模块只负责对原特征做“微调”而不是直接重写特征图。门控系数初始化为0.5训练阶段由网络自己决定到底该给调制结果多大信任。这样一来即使注意力机制在某个阶段完全失效残差连接也能让输出退化为恒等映射不会破坏原本已经训练好的特征表达能力。这一点对即插即用模块特别重要因为插入预训练网络时我们不希望一上来就把主干特征带跑偏。3. 即插即用实操把LWGANet接进现有检测框架3.1 模块的插入位置选择“即插即用”听起来简单实际上插入位置直接决定了模块收益。我推荐先在三个典型位置做对比实验。插入位置建议程度主要作用额外开销Backbone的C3/C4/C5输出之后推荐优先尝试增强基础特征表达抑制背景干扰低FPN/PANet各层上采样之前推荐提高多尺度特征融合时小目标的响应中检测头分类/回归分支之前可选直接作用于最终预测但延迟更高中高从经验来看先把模块放在Backbone最后两层的输出之后效果和成本最平衡。也许有同学习惯在每个FPN层都插入模块但实测下来插入层数过多并不总是带来精度提升反而可能让训练收敛变慢、显存上升。对小目标任务FPN的浅层特征本身就保留了大量空间细节再叠一个注意力模块容易重复强化边缘类特征所以建议从一层开始试确认有效再逐步扩散。3.2 通道对齐和初始化很多即插即用模块在接进网络时最头疼的就是通道数对不上。LWGANet的设计把这个问题绕开了。模块内部的通道分支和空间分支都只依赖输入张量的通道数C通过对输入特征做动态reshape可以自动适配任意通道数。比如插入到FPN层时输入通常是256通道插入到Backbone输出时可能是1024或2048通道代码不需要改动只需要在初始化时传入对应的C、分组数G和压缩比r。初始化方面有几个实打实的建议。通道分支最后的1×1卷积权重建议用较小方差初始化避免sigmoid前输出过大导致权重一开始就饱和到0或1。空间分支的锚点参数建议用0.1倍缩放偏置不设或置0。门控系数初始化为0.5比较稳如果发现插入后训练初期损失震荡明显可以临时降到0.1甚至0等主干适应了再恢复。3.3 基于PyTorch的完整模块封装把两个分支和融合逻辑包起来就能得到一个完整可用的模块。下面这段是我在本地项目里用过的参考实现整理了一下注释。class LWGA(nn.Module): def __init__(self, channels, groups4, reduction8, anchors8, rank4, gate_init0.5): super().__init__() self.channel_branch ChannelBranch(channels, groups, reduction) self.spatial_branch SpatialBranch(channels, anchors, rank) self.gate nn.Parameter(torch.tensor(gate_init)) def forward(self, x): cw self.channel_branch(x) # (B, C, 1, 1) sw self.spatial_branch(x) # (B, 1, H, W) modulated cw * sw # 外积式结合 return x * (1 torch.sigmoid(self.gate) * modulated)接进一个检测器也很直接。假设原来FPN层输出的变量叫fpn_out只需要在送入检测头之前加一行fpn_out lwga(fpn_out)即可。如果对原始骨干比较熟悉也可以把模块嵌到C4、C5之后直接在构建Backbone时同步调用。注意模块里每个分支都用到了reshape和view。在PyTorch里输入张量需要是连续内存否则view会报错或者触发额外的内存拷贝。如果放在自定义模型里使用建议在forward开头加一句x x.contiguous()代价几乎为零但能避免很多莫名其妙的运行时错误。3.4 参数量和FLOPs的估算方法轻量级模块到底轻在哪需要用数字说话。拿一个典型的输入做例子单张图、C256通道、特征图尺寸56×56。通道分支主要由两个1×1卷积构成。设分组数G4组内通道数64压缩比r8中间通道数为8则每组的参数量是64×8 8×64 1024四组总共约4096个参数。空间分支的投影卷积参数量是256×4 1024生成卷积参数量是(48)×1 12锚点参数可忽略。整个模块加起来大约5K个参数相比ResNet系列动辄几百万参数几乎是零头。再看FLOPs。1×1卷积的计算量可以近似用公式2 × H × W × C_in × C_out估算。空间分支第一步投影卷积的计算量是2×56×56×256×4 ≈ 6.4MFLOPs生成卷积的计算量是2×56×56×12×1 ≈ 75KFLOPs再加上通道分支在池化向量上的开销合计大约是6.5MFLOPs。这个数字放在检测模型动辄几十上百GFLOPs的整体计算量里确实可以忽略。想控制算力的场景优先调小rank和anchors比调小分组数更直接。4. 训练与调参如何让注意力模块真正发挥效果4.1 训练配置建议模块插进去之后训练配置不能完全照搬原网络。学习率方面如果直接在预训练模型上微调建议整体学习率按原方案不变但注意力分支的初始化权重比较随机可以单独设置一个缩放系数。我通常会把注意力分支参数的学习率设为主干的0.1倍避免新模块在训练早期剧烈扰动预训练特征。如果是从头训练就无所谓了按正常策略走。预热策略很关键。插入注意力模块会改变特征分布BN层的统计量在训练初期需要重新适应。建议增加5到10个epoch的线性预热前几个epoch把学习率从很低的数值慢慢抬到设定值。我踩过几次坑之后发现不预热直接跑完整学习率训练损失前几十个batch会异常波动后期精度比预热版本低好几个点。如果发现注意力分支在训练中后期才开始起作用可以尝试在sigmoid输出前增加一个温度参数公式变成sigmoid(z / T)。T小于1会让注意力图更尖锐更聚焦小目标T大于1则让注意力分布更平滑防止过拟合。默认T1.0具体取值可以加入消融。4.2 不同检测器的接入差异一阶段检测器比如常见的YOLO系列或CenterNet结构注意力模块放在Neck每个尺度输出层之前效果比较直接。因为不同尺度的特征图尺寸不同模块需要支持多种空间分辨率PyTorch的动态计算天然满足这一点不用额外改结构。二阶段检测器更讲究。如果放在整张图经过的Backbone输出之后模块只需要推理一次成本很低收益也最稳定。如果放在RoI Pooling之后的检测头里每个候选区域都要过一遍模块计算成本会成倍增加。我的建议是二阶段模型优先放Backbone输出后别放在Head内部。分割任务同理模块放在Decoder每个上采样阶段之前即可。由于遥感图像建筑分割和道路提取同样存在背景冗余问题这类模块的收益表现甚至比检测更明显因为分割任务要求逐像素输出空间注意力的作用能直接体现在边界细化上。4.3 消融实验设计想把模块调好消融实验别只做“有模块 vs 没模块”这种粗糙对比。我在本地复现类似的轻量级注意力设计时通常会固定四组变量分组数G、通道压缩比r、锚点数A、空间分支rank。参考的观察趋势如下。超参数测试范围观察到的趋势分组数G2 / 4 / 8G4效果最稳G8时组间信息割裂精度微降压缩比r4 / 8 / 16r8性价比最高r16时通道分支信息损失明显锚点数A2 / 4 / 8 / 16A4到8足够A16精度收益小显存上涨明显rank2 / 4 / 8rank4最合适rank8训练明显变慢如果你是第一次在自己数据集上复现建议先固定G4、r8、A8、rank4跑一组再单独调A。锚点数对结果的影响比较敏感小目标越密集锚点数适当增加有帮助但超过16一般就开始边际递减了。4.4 轻量化部署的考量模块能在训练框架里跑通不意味着在部署环境一样顺利。好在LWGANet的组成基本都是1×1卷积、池化和逐点乘法没有自定义算子理论上对ONNX导出非常友好。实际导出时留意两点自适应平均池化在ONNX不同版本里的实现差异有时候会导致shape推断失败最好显式传入与特征图尺寸匹配的kernel_sizereshape/view操作在TensorRT里有时会被替换成更耗时的内存拷贝建议导出前用静态维度跑一次确认没有额外延迟。如果目标是FPGA或单片机级别的部署可以把通道分支的1×1卷积等效换算成一组小矩阵乘把空间分支的锚点部分提前离线展开推理时只需要做少量卷积运算。整体来看这个模块在端侧落地并不会比一个标准1×1卷积复杂太多。5. 常见问题与排查思路实录5.1 加了模块精度不升反降这是即插即用模块最常见的翻车现场。我在本地复现时也遇到过几次总结下来原因多半集中在四类。一类是插入位置不对比如把模块插进了原本已经很深的高层特征层这时候特征图分辨率过低空间注意力基本失效。一类是门控初始值太大0.5不算大但如果手动设成1.0训练前期就会剧烈改写预训练特征。一类是学习率没调新模块跟着主干一起用大学习率几个迭代就把特征分布冲乱了。还有一类比较隐蔽数据集里小目标数量本身就很少注意力模块学到了一个接近恒等的映射等于白加。排查时先做两件事第一可视化模块输出前后的特征差异确认模块确实在改特征而不是输出恒等第二把门控初始值临时降到0.1甚至0观察几个epoch的损失是否比之前更稳定。如果降门控有效说明问题出在训练前期扰动过大而不是模块结构本身不行。5.2 训练时显存不降反升LWGANet设计上很轻但如果显存反而涨了先别急着怀疑模块大概率是使用方式出了问题。锚点数A和rank设置得太大会直接增加中间特征图的内存占用尤其是输入特征分辨率较大时。还有一个容易被忽略的点通道分支里的view和reshape如果作用在不连续内存的张量上PyTorch会触发一次额外拷贝这个拷贝在梯度回传时会变成两份内存占用。另外有些工程实现里把模块插入到了网络每个block里这相当于在每个尺度都保存了中间变量用于反向传播显存自然快速上涨。排查建议是先只在一层插入跑通确认显存增量正常再逐步增加插入层数。如果只插一层显存仍然异常把锚点数降到4、rank降到2基本能定位到瓶颈。5.3 注意力权重容易“塌缩”到恒等训练一段时间后如果模块输出的attention map所有位置得分都接近1说明注意力模块已经塌缩成恒等映射。这种问题在小数据集上特别常见。塌缩的本质是网络发现维持现状就够了注意力分支没有提供足够的额外收益。解决办法可以从两个方向入手。一是增强唯一性约束比如在计算通道权重时对权重向量做一次标准化或者对门控系数加一个很小的L2惩罚防止它无限制增大让调制失效。二是调整分支容量把rank从4调到8锚点数从8调到16让分支有更多表达能力。如果这些都没效果换个角度考虑会不会数据集本身没给模型足够的压力增加训练轮数、使用更强的数据增强、把小目标过采样生成更多样本往往比调模块参数更有效。5.4 复现与对比实验要避开的坑做算法对比实验最怕的不是效果差而是复现的不确定性。有几个坑我印象很深。随机种子不统一是最大的坑。注意力模块引入了额外的随机初始化不同种子下精度波动可能超过1个点而小目标检测本身对种子敏感度又高。每次对比实验必须固定同一个随机种子最好在主训练脚本开头统一设置Python、NumPy和PyTorch三处种子。BN统计量在多卡训练和单卡推理之间会有偏差。如果训练用了SyncBN验证时最好也保持相同模式否则注意力分支在推理阶段可能因为统计量不匹配产生明显性能抖动。验证前用全局统计代替batch统计通常能恢复一部分精度。还有一个容易忽略的点是特征图尺寸。有些工程实现为了省显存会把输入图缩到512以下对小目标任务来说这等于直接把目标信息扔掉了。注意力模块再好也救不回过小分辨率下丢失的空间细节。除非做多尺度测试否则建议训练和推理都用同一分辨率至少保证短边不低于原始标注设计尺寸。最后的一点个人体会我在实际项目里用这类轻量级注意力模块时最大的感受是“别贪多”。不要在每一层都堆模块先选一两个关键位置做实验确认收益后再说。还有个小技巧训练结束后用一个较小的batch size重新向前传播一遍收集全局的BN均值和方差再切换到推理模式能明显减少注意力分支带来的域偏移尤其是从大batch训练切到单卡推理的场景。这套模块的思路后续还可以跟知识蒸馏、NAS搜索锚点数量等方法结合起来也能扩展到视频小目标检测这类时序场景算是轻量级注意力设计里一个值得长期跟进的尝试方向。
阅读完成 · 觉得有帮助?