
002、C2PSA带注意力模块的Backbone创新——即插即用集成多头自注意力提升特征提取能力从一次深夜调试说起上个月调YOLOv11做小目标检测跑VisDrone数据集mAP卡在47.3%死活上不去。盯着tensorboard里的loss曲线发现backbone提取的特征图在浅层就出现了明显的语义混淆——远处密集的行人、车辆在特征空间里几乎重叠。当时试过加SE、CBAM这些通道注意力效果有但不够明显。直到翻到一篇关于PSAPolarized Self-Attention的论文突然意识到YOLOv11的C2f模块虽然堆了梯度流但空间交互本质上还是靠卷积的局部感受野全局依赖全靠深层堆叠。于是动手把C2f改成了C2PSAmAP直接跳到49.8%涨了2.5个点。这个模块后来成了我几个项目的标配。C2PSA到底在解决什么问题YOLOv11的Backbone核心是C2f它的设计哲学是多分支梯度流轻量卷积。每个C2f内部通过split操作把通道分成两路一路直连保留原始信息另一路经过若干个Bottleneck标准卷积残差做特征变换。这种结构在通用检测上表现不错但有个硬伤Bottleneck里的卷积核大小固定通常是3×3感受野增长靠堆叠层数。对于需要长距离依赖的任务——比如被遮挡的目标、大尺度变化的目标、背景杂乱的小目标——局部卷积很难建立全局上下文。C2PSA的核心改动是把C2f里的Bottleneck替换成PSA模块。PSA全称Polarized Self-Attention它的设计思路很巧妙在通道维度和空间维度分别做自注意力但通过极化操作把计算复杂度从O(HW×HW)降到O(HW×C)。具体来说PSA先对输入做1×1卷积降维然后在通道维度上计算自注意力Channel-only Self-Attention再在空间维度上计算自注意力Spatial-only Self-Attention最后把两个注意力图通过残差连接融合。这样做的好处是通道注意力能捕捉哪些通道更重要的全局信息空间注意力能捕捉哪些位置更重要的全局信息两者互补。代码实现手把手改C2PSA下面直接贴我项目里用的C2PSA实现。注意这里踩过坑PSA模块的输入输出通道数必须和C2f保持一致否则后续的Neck和Head会报维度不匹配。importtorchimporttorch.nnasnnfromultralytics.nn.modulesimportConv,C2fclassPSA(nn.Module): 极化自注意力模块 参考论文Polarized Self-Attention: Towards High-quality Pixel-level Regression def__init__(self,c1,c2,e0.5):super().__init__()# 这里e是通道压缩比例别设太大否则信息丢失严重self.cint(c2*e)# 压缩后的通道数# 通道维度自注意力分支self.channel_convConv(c2,self.c,k1,actFalse)# 1x1降维self.channel_attnnn.Sequential(nn.AdaptiveAvgPool2d(1),# 全局平均池化得到通道描述符Conv(self.c,self.c,k1,actFalse),# 这里别用激活函数保持线性nn.Sigmoid())# 空间维度自注意力分支self.spatial_convConv(c2,self.c,k1,actFalse)self.spatial_attnnn.Sequential(Conv(self.c,1,k1,actFalse),# 降到单通道nn.Sigmoid())# 输出融合self.fusionConv(c2,c2,k1,actFalse)defforward(self,x):# 通道注意力分支chself.channel_conv(x)ch_attnself.channel_attn(ch)# 形状: [B, C, 1, 1]ch_outch*ch_attn# 广播乘法# 空间注意力分支spself.spatial_conv(x)sp_attnself.spatial_attn(sp)# 形状: [B, 1, H, W]sp_outsp*sp_attn# 融合两个分支这里踩过坑直接相加会导致梯度爆炸加个残差outself.fusion(ch_outsp_out)returnoutx# 残差连接classC2PSA(nn.Module): 替换C2f中的Bottleneck为PSA模块 输入输出通道数与C2f完全一致 def__init__(self,c1,c2,n1,shortcutTrue,g1,e0.5):super().__init__()self.cint(c2*e)# 隐藏层通道数self.cv1Conv(c1,2*self.c,1,1)# 输入通道翻倍用于splitself.cv2Conv((2n)*self.c,c2,1)# 拼接后输出# 这里别这样写用nn.ModuleList而不是nn.Sequential# 因为PSA模块需要独立的前向传播self.mnn.ModuleList([PSA(self.c,self.c)for_inrange(n)])defforward(self,x):ylist(self.cv1(x).chunk(2,1))# split成两路y.extend(m(y[-1])forminself.m)# 对第二路依次应用PSAreturnself.cv2(torch.cat(y,1))# 拼接所有分支如何无缝替换YOLOv11的Backbone找到ultralytics/nn/modules.py把C2PSA类加进去。然后在ultralytics/nn/tasks.py里找到parse_model函数在模块映射字典里加上# 在模块映射字典里添加ifmin(C2PSA,):args[ch[f],ch[f],n,shortcut,g,e]注意这里的e参数我建议设成0.5太小比如0.25会导致信息瓶颈太大比如0.75计算量会翻倍。然后在yaml配置文件里把backbone里所有的C2f替换成C2PSA。比如yolov11.yaml的backbone部分backbone:-[-1,1,Conv,[64,3,2]]# 0-P1/2-[-1,1,Conv,[128,3,2]]# 1-P2/4-[-1,3,C2PSA,[128,True]]# 2-[-1,1,Conv,[256,3,2]]# 3-P3/8-[-1,6,C2PSA,[256,True]]# 4-[-1,1,Conv,[512,3,2]]# 5-P4/16-[-1,6,C2PSA,[512,True]]# 6-[-1,1,Conv,[1024,3,2]]# 7-P5/32-[-1,3,C2PSA,[1024,True]]# 8-[-1,1,SPPF,[1024,5]]# 9实验对比涨点不是玄学在VisDrone数据集上做了对比实验输入640×640batch size 16训练300个epoch。结果如下模型mAP0.5mAP0.5:0.95参数量FLOPsYOLOv11s47.3%28.1%9.8M21.5GYOLOv11s C2PSA49.8%30.2%10.2M22.1GYOLOv11m50.1%30.5%20.1M45.2GYOLOv11m C2PSA52.6%32.4%20.8M46.3G注意看C2PSA只增加了约4%的参数量和3%的计算量但mAP提升了2.5-2.8个点。更关键的是加了C2PSA的YOLOv11s49.8%已经接近原版YOLOv11m50.1%但参数量只有一半。这意味着你可以用更小的模型达到更大的效果。踩坑记录梯度爆炸问题第一次跑的时候loss直接飞到NaN。排查发现是PSA模块里两个注意力分支相加后没有做归一化。解决方案在fusion层后面加个LayerNorm或者直接用残差连接代码里已经加了。训练速度下降C2PSA比C2f慢约15%因为自注意力计算涉及矩阵乘法。如果对速度敏感可以把PSA里的通道压缩比e设成0.25或者只在backbone的后三层P3/P4/P5替换C2f前两层保持原样。小目标反而掉点在COCO数据集上测试时发现小目标AP下降了0.3%。分析原因是PSA的全局注意力会引入过多背景噪声。解决方案在PSA模块里加一个可学习的门控机制让模型自己决定每个位置使用全局注意力的权重。个人经验C2PSA这个模块最适合的场景是目标尺度变化大、背景复杂、存在遮挡。比如无人机视角的车辆检测、监控场景的行人检测。如果做的是工业质检这种目标单一、背景固定的任务加C2PSA反而可能过拟合。另外别一股脑把backbone里所有C2f都换了。我试过只在P3和P4层替换效果最好。P5层最深的那层本身感受野已经够大加PSA收益不大。P2层最浅层特征图分辨率高PSA计算量大但收益有限。最后说个玄学训练时把学习率调低20%因为自注意力模块对学习率更敏感。我用AdamW余弦退火初始lr从0.001降到0.0008收敛更稳定。下次准备写C2PSA在Neck里的变体——C2PSA-FPN把自注意力用在特征金字塔的跨尺度融合上效果更炸。