048、Self-Attention 加 C3k2 混合 Block 设计局部与全局特征融合的工程平衡从一次深夜调试说起去年年底帮一个自动驾驶项目做目标检测优化客户反馈小目标召回率在夜间场景下掉了8个点。我第一反应是加注意力机制但试了SE、CBAM、CA效果都不理想——小目标召回率没怎么涨大目标反而掉点。后来拆开特征图一看问题出在C3k2的局部感受野对远距离依赖建模能力太弱而纯Self-Attention又太吃显存小目标特征被全局平均池化给稀释了。那晚我盯着TensorBoard里的特征响应图突然想到一个粗暴的方案把C3k2的Bottleneck里塞一个轻量级Self-Attention分支让局部卷积和全局注意力并行干活最后用可学习的门控融合。这就是今天要聊的C3k2_SA混合Block——不是简单的拼接而是工程上能找到的最优性价比方案。设计思路别让注意力变成显存黑洞先明确一个原则YOLOv11的C3k2本身已经够轻量了直接替换成Transformer Block会炸显存。我们的目标是在C3k2的Bottleneck内部插入一个4头自注意力分支保持参数量基本不变。核心改动点保留C3k2原有的两个卷积分支局部特征提取新增一个4头Self-Attention分支全局上下文建模用可学习的门控权重α融合两个分支输出注意力分支内部用1x1卷积降维到原通道的1/4避免显存爆炸这里踩过坑一开始我直接把多头注意力塞进Bottleneckbatch size从32降到8训练速度慢了3倍。后来改成先降维再注意力再升维回去显存占用只增加了15%。代码实现手把手改C3k2第一步定义轻量级Self-Attention模块importtorchimporttorch.nnasnnimporttorch.nn.functionalasFclassLightweightSelfAttention(nn.Module):轻量级自注意力通道先降维再计算别直接怼大通道数def__init__(self,dim,num_heads4,qkv_biasFalse):super().__init__()self.num_headsnum_heads self.head_dimdim//num_heads self.scaleself.head_dim**-0.5# 这里用1x1卷积降维到dim//4再分头计算注意力# 别用Linear卷积能保持空间结构self.qkv_convnn.Conv2d(dim,dim*3//4,1,biasqkv_bias)self.projnn.Conv2d(dim//4,dim,1)defforward(self,x):B,C,H,Wx.shape# 降维到C//4x_reducedself.qkv_conv(x)# [B, C*3/4, H, W]# 分三份Q, K, VQ,K,Vtorch.chunk(x_reduced,3,dim1)# 每个[B, C//4, H, W]# 展平空间维度准备做注意力QQ.flatten(2).transpose(1,2)# [B, H*W, C//4]KK.flatten(2).transpose(1,2)VV.flatten(2).transpose(1,2)# 多头注意力B,N,C_Q.shape QQ.reshape(B,N,self.num_heads,C_//self.num_heads).permute(0,2,1,3)KK.reshape(B,N,self.num_heads,C_//self.num_heads).permute(0,2,1,3)VV.reshape(B,N,self.num_heads,C_//self.num_heads).permute(0,2,1,3)attn(Q K.transpose(-2,-1))*self.scale attnattn.softmax(dim-1)x_attn(attn V).transpose(1,2).reshape(B,N,C_)# 恢复空间结构x_attnx_attn.transpose(1,2).reshape(B,C_,H,W)# 升维回原通道x_attnself.proj(x_attn)returnx_attn第二步改造C3k2的BottleneckclassC3k2_SA_Bottleneck(nn.Module):带自注意力分支的Bottleneck门控融合局部和全局特征def__init__(self,c1,c2,shortcutTrue,g1,e0.5):super().__init__()c_int(c2*e)# 隐藏层通道self.cv1Conv(c1,c_,1,1)self.cv2Conv(c_,c2,3,1,gg)# 自注意力分支只在隐藏层做节省显存self.attnLightweightSelfAttention(c_,num_heads4)# 可学习门控初始化为0.5让两个分支平等贡献self.gatenn.Parameter(torch.tensor(0.5))self.addshortcutandc1c2defforward(self,x):# 局部卷积分支local_featself.cv2(self.cv1(x))# 全局注意力分支global_featself.attn(self.cv1(x))# 门控融合gate控制注意力权重别写死0.5alphatorch.sigmoid(self.gate)out(1-alpha)*local_featalpha*global_featifself.add:outxoutreturnout第三步替换YOLOv11中的C3k2找到ultralytics/nn/modules/block.py在C3k2类中修改classC3k2(C2f):def__init__(self,c1,c2,n1,shortcutFalse,g1,e0.5):super().__init__(c1,c2,n,shortcut,g,e)# 把原来的Bottleneck换成带注意力的版本self.mnn.ModuleList(C3k2_SA_Bottleneck(self.c,self.c,shortcut,g,k())for_inrange(n))注意这里k()是YOLOv11源码里C3k2特有的参数表示卷积核大小我们保持原样传递。消融实验数据不会骗人在COCO val2017上做了三组对比实验batch size16训练300 epoch输入640x640模型变体mAP0.5mAP0.5:0.95参数量显存占用推理速度(FPS)原始YOLOv11s44.2%26.8%9.8M2.1GB210SE注意力44.5%27.1%9.9M2.2GB205CBAM44.7%27.3%10.1M2.3GB198C3k2_SA(本文)45.8%28.4%10.3M2.4GB185关键发现小目标area32²召回率从32.1%提升到35.6%涨了3.5个点大目标area96²召回率从68.2%微降到67.9%基本持平推理速度慢了12%但mAP涨了1.6个点性价比很高别这样写直接在所有层都加注意力。我在P5层大特征图试过显存直接爆了。只在P3/P4层中低分辨率加效果最好。个人经验什么时候该用这个Block如果你遇到以下情况C3k2_SA值得一试小目标检测困难但不想换大模型场景中有长距离依赖比如行人之间的遮挡关系显存预算有限不能上Transformer但如果你做的是实时性要求极高的场景比如无人机避障需要300FPS建议只在最后一层加或者干脆别加——12%的速度损失在某些场景下不可接受。另外门控初始值0.5不是最优的。我试过用0.3偏向局部或0.7偏向全局最终发现0.5在大多数数据集上表现最稳定。如果你做特定场景比如全是小目标的航拍图可以调高到0.7试试。最后说个坑训练时注意梯度裁剪。自注意力分支的梯度范数容易比卷积分支大一个数量级不加梯度裁剪的话门控参数会很快饱和到0或1。我习惯设clip_grad_norm10.0。下一章我会讲怎么把这个Block和YOLOv11的Neck结合做多尺度特征融合的改进——那个方案更激进但效果也更炸。