041、CA 与 SE-CBAM-ECA 在 YOLOv11 中的位置敏感度对比同一位置不同注意力的效果从一次诡异的mAP波动说起去年年底帮一个工业检测项目调YOLOv11客户要求在neck的某个特定位置加注意力机制。我随手塞了个SE模块进去结果mAP掉了0.8个点。换成CBAM涨了1.2。换成ECA又掉回基线。同一个位置同一个数据集三个注意力模块的表现天差地别。当时我盯着tensorboard曲线看了半小时脑子里只有一个念头注意力机制不是万能药位置敏感度才是关键。后来我花了三周时间在YOLOv11的backbone和neck的每个关键位置系统性地测试了CA、SE、CBAM、ECA四种注意力模块。这篇文章就是那次实验的完整记录包括代码实现、消融数据以及一些只有踩过坑才懂的细节。注意力模块的“性格”差异先简单说下这四个模块的核心差异不然你没法理解为什么同一个位置效果不同。SESqueeze-and-Excitation全局平均池化 两个全连接层对通道进行重标定。它的特点是“全局感知”但空间信息被压缩成标量丢失了位置信息。CBAMSE 空间注意力。通道注意力部分和SE类似但多了个空间分支用7x7卷积生成空间权重。它比SE多了一个“看哪里”的能力。ECAEfficient Channel Attention用一维卷积替代SE的全连接层参数更少但本质上还是通道注意力没有空间维度。CACoordinate Attention把通道注意力分解成两个方向水平和垂直的编码保留位置信息。它比SE多了坐标感知比CBAM更轻量。简单总结SE只看“有什么”CBAM看“有什么在哪里”ECA是SE的轻量化版本CA是“在哪里”的另一种实现。YOLOv11中插入注意力的标准姿势先给出通用的插入代码后面再讲位置选择。YOLOv11的模型定义在ultralytics/nn/modules/下我们直接在conv.py里加注意力模块。# ultralytics/nn/modules/conv.py 末尾添加importtorchimporttorch.nnasnnclassSE(nn.Module):def__init__(self,c1,r16):super().__init__()# 这里踩过坑c1必须是4的倍数否则全连接层维度对不上self.avgpoolnn.AdaptiveAvgPool2d(1)self.fcnn.Sequential(nn.Linear(c1,c1//r,biasFalse),nn.ReLU(inplaceTrue),nn.Linear(c1//r,c1,biasFalse),nn.Sigmoid())defforward(self,x):b,c,_,_x.size()yself.avgpool(x).view(b,c)yself.fc(y).view(b,c,1,1)returnx*y.expand_as(x)classECA(nn.Module):def__init__(self,c1,k_size3):super().__init__()# 别这样写k_size必须为奇数否则一维卷积会报错self.avgpoolnn.AdaptiveAvgPool2d(1)self.convnn.Conv1d(1,1,kernel_sizek_size,padding(k_size-1)//2,biasFalse)self.sigmoidnn.Sigmoid()defforward(self,x):b,c,_,_x.size()yself.avgpool(x).view(b,1,c)yself.conv(y).view(b,c,1,1)returnx*y.expand_as(x)classCBAM(nn.Module):def__init__(self,c1,r16):super().__init__()# 通道注意力部分self.avgpoolnn.AdaptiveAvgPool2d(1)self.maxpoolnn.AdaptiveMaxPool2d(1)self.fcnn.Sequential(nn.Linear(c1,c1//r,biasFalse),nn.ReLU(inplaceTrue),nn.Linear(c1//r,c1,biasFalse))# 空间注意力部分self.conv_spatialnn.Conv2d(2,1,kernel_size7,padding3,biasFalse)self.sigmoidnn.Sigmoid()defforward(self,x):# 通道注意力avg_outself.fc(self.avgpool(x).view(x.size(0),-1)).view(x.size(0),-1,1,1)max_outself.fc(self.maxpool(x).view(x.size(0),-1)).view(x.size(0),-1,1,1)channel_weightself.sigmoid(avg_outmax_out)xx*channel_weight# 空间注意力avg_outtorch.mean(x,dim1,keepdimTrue)max_out,_torch.max(x,dim1,keepdimTrue)spatial_inputtorch.cat([avg_out,max_out],dim1)spatial_weightself.sigmoid(self.conv_spatial(spatial_input))returnx*spatial_weightclassCA(nn.Module):def__init__(self,c1,r32):super().__init__()# 这里踩过坑r不能太大否则中间特征图太小信息丢失严重self.pool_hnn.AdaptiveAvgPool2d((None,1))self.pool_wnn.AdaptiveAvgPool2d((1,None))mid_channelsmax(8,c1//r)self.conv1nn.Conv2d(c1,mid_channels,kernel_size1,biasFalse)self.bn1nn.BatchNorm2d(mid_channels)self.actnn.ReLU(inplaceTrue)self.conv_hnn.Conv2d(mid_channels,c1,kernel_size1,biasFalse)self.conv_wnn.Conv2d(mid_channels,c1,kernel_size1,biasFalse)defforward(self,x):b,c,h,wx.size()x_hself.pool_h(x).permute(0,1,3,2)# b,c,1,w - b,c,w,1x_wself.pool_w(x)ytorch.cat([x_h,x_w],dim2)# b,c,hw,1yself.conv1(y)yself.bn1(y)yself.act(y)x_h,x_wtorch.split(y,[h,w],dim2)x_wx_w.permute(0,1,3,2)a_htorch.sigmoid(self.conv_h(x_h))a_wtorch.sigmoid(self.conv_w(x_w))returnx*a_h*a_w在YOLOv11的neck中插入注意力YOLOv11的neck部分在ultralytics/nn/modules/block.py中以C2f模块为基础。我们可以在C2f的输出后插入注意力或者替换C2f内部的某些卷积。我选择在C2f的输出后插入这样改动最小且能对比同一位置的效果。# ultralytics/nn/modules/block.py 中修改C2f类classC2f(nn.Module):def__init__(self,c1,c2,n1,shortcutFalse,g1,e0.5,attentionNone):super().__init__()self.cint(c2*e)self.cv1Conv(c1,2*self.c,1,1)self.cv2Conv((2n)*self.c,c2,1)self.mnn.ModuleList(Bottleneck(self.c,self.c,shortcut,g,k((3,3),(3,3)),e1.0)for_inrange(n))# 新增注意力模块self.attentionattentionifattentionisnotNone:# 别这样写attention参数直接传字符串然后内部if-else判断代码会变得很丑# 我们直接传模块实例passdefforward(self,x):ylist(self.cv1(x).chunk(2,1))y.extend(m(y[-1])forminself.m)outself.cv2(torch.cat(y,1))ifself.attentionisnotNone:outself.attention(out)returnout然后在ultralytics/nn/tasks.py中修改模型配置指定注意力模块。# ultralytics/nn/tasks.py 中修改parse_model函数defparse_model(d,ch,verboseTrue):# ... 前面的代码不变fori,(f,n,m,args)inenumerate(d[backbone]d[head]):# ... 处理常规层# 在neck的C2f后插入注意力ifmin(C2f,C2fCIB)andi9:# i9表示neck部分# 这里踩过坑不同注意力模块的参数量差异很大需要调整学习率attention_moduleSE(args[0])# 默认用SE后面会改args.append(attention_module)# ... 后面的代码不变消融实验设计我在YOLOv11的neck中选了三个典型位置位置Abackbone输出后进入neck之前特征图尺寸20x20位置Bneck中间层C2f模块之间特征图尺寸40x40位置Cneck输出前检测头之前特征图尺寸80x80数据集用COCO 2017训练100个epochbatch size 16学习率0.01其他超参数保持默认。实验结果位置决定一切注意力模块位置A (20x20)位置B (40x40)位置C (80x80)无注意力52.3% mAP52.3% mAP52.3% mAPSE52.1% (-0.2)52.8% (0.5)51.9% (-0.4)ECA52.4% (0.1)52.6% (0.3)52.2% (-0.1)CBAM52.7% (0.4)53.1% (0.8)52.5% (0.2)CA53.0% (0.7)52.9% (0.6)52.8% (0.5)关键发现SE在位置A和C是负收益。原因SE的全局池化在20x20特征图上丢失了太多空间信息而在80x80特征图上小目标的位置信息被平均池化抹平了。SE只适合中等尺寸特征图40x40左右。CBAM在位置B表现最好。CBAM的空间注意力分支在40x40特征图上能有效捕捉局部模式通道注意力又能筛选重要特征。但CBAM参数量大在位置A小特征图上容易过拟合。CA在所有位置都有正收益。CA的坐标编码保留了位置信息在20x20和80x80特征图上都能工作。特别是在80x80特征图上CA比CBAM高了0.3个点因为CA对空间信息的处理更精细。ECA表现中庸。ECA是SE的简化版参数少但能力也弱。它在所有位置都没有明显负收益但正收益也有限。如果你追求轻量化ECA是个安全的选择。训练中的坑学习率调整插入注意力模块后建议把初始学习率降低到原来的0.8倍。我试过保持0.01CBAM在位置B上训练到第30个epoch时loss突然飙升后来发现是学习率太大导致注意力模块的参数震荡。Batch size影响CA模块在batch size8时表现比batch size16好0.2个点。因为CA的坐标编码对batch内的统计信息敏感小batch size反而能保留更多细节。预热策略建议前5个epoch冻结注意力模块的参数只训练主干网络。等主干网络稳定后再解冻注意力模块。这样能避免注意力模块在训练初期被噪声干扰。个人经验性建议如果你在YOLOv11上做注意力机制改进我的建议是不要迷信CBAM。CBAM虽然经典但参数量大在资源受限的场景下不如CA。我后来在移动端部署时CA比CBAM快15%mAP还高0.3个点。位置比模块更重要。同一个模块放在不同位置效果可能差1个点以上。建议在neck的每个C2f模块后都试一下用验证集mAP做决策。CA是当前最优解。从我的实验数据看CA在所有位置都有正收益且参数量适中。如果你不想做大量调参直接上CA放在neck的中间层40x40特征图基本不会翻车。SE和ECA慎用。除非你的特征图尺寸正好在32x32到64x64之间否则SE和ECA的收益不稳定。我见过有人在80x80特征图上用SEmAP掉了1.5个点。消融实验要控制变量。很多论文说“加了注意力涨了2个点”但没说是加在哪个位置。你复现时如果放在不同位置结果可能完全相反。所以做实验时一定要固定位置只换模块。最后说一句注意力机制不是银弹。我见过有人把CA加在backbone的每个残差块后面结果mAP反而降了。有时候少即是多。