022、CBAM 插入 Neck 的三个位置与 Head 前的配置:哪一层对分类分支最有利
022、CBAM 插入 Neck 的三个位置与 Head 前的配置哪一层对分类分支最有利一个让我熬夜三天的调试问题去年秋天接了个项目要在无人机航拍数据集上做小目标检测。YOLOv11 baseline 跑出来 mAP 卡在 68.3%分类精度尤其拉胯——把“人”和“背包”搞混了十几次。直觉告诉我特征图里语义信息不够干净得加注意力机制。CBAM 是经典方案但问题来了插在 Neck 的哪个位置是 FPN 的每一层输出后都加还是只在 P3/P4/P5 的某个特定层加Head 前要不要再加一次更关键的是——分类分支和回归分支对注意力的敏感度完全不同不加区分地乱插反而会掉点。我花了三天在 VisDrone 和 COCO 子集上做了 12 组消融实验最后发现CBAM 插在 Neck 的 P4 层输出后对分类分支的提升最显著而 Head 前加 CBAM 反而会干扰回归分支的定位精度。下面把完整的踩坑过程和代码实现拆开讲。先搞清楚 YOLOv11 的 Neck 结构别被源码绕晕YOLOv11 的 Neck 是典型的 FPNPAN 结构但和 v8 有个关键区别v11 在 PAN 的上采样路径中多了一层特征融合具体来说P3小特征图8倍下采样P4中特征图16倍下采样P5大特征图32倍下采样FPN 路径P5 → 上采样 → 与 P4 融合 → 再上采样 → 与 P3 融合PAN 路径P3 → 下采样 → 与 P4 融合 → 再下采样 → 与 P5 融合这里踩过坑很多人以为 Neck 只有 FPN 的输出层实际上 PAN 路径的每一层也会输出到 Head。所以 CBAM 可以插在三个位置FPN 输出后P3_out, P4_out, P5_outPAN 输出后P3_pan, P4_pan, P5_panHead 输入前所有分支共享的特征图我一开始图省事直接在 PAN 的所有输出后都加了 CBAM结果训练 loss 降不下去——因为 PAN 路径的特征图已经经过两次融合再加注意力会导致梯度信号被过度压缩。代码实现三种插入方式的 PyTorch 写法第一步定义 CBAM 模块别用官方那个慢版本importtorchimporttorch.nnasnnclassChannelAttention(nn.Module):# 通道注意力全局平均池化 两个全连接# 注意这里用 1x1 卷积代替全连接避免破坏 batch 维度def__init__(self,in_channels,reduction16):super().__init__()# 别这样写nn.AdaptiveAvgPool2d(1) 会丢失空间信息但这里就是要全局的self.avg_poolnn.AdaptiveAvgPool2d(1)self.max_poolnn.AdaptiveMaxPool2d(1)# 用 Conv2d 代替 Linear保持 4D 张量self.fc1nn.Conv2d(in_channels,in_channels//reduction,1,biasFalse)self.relunn.ReLU(inplaceTrue)self.fc2nn.Conv2d(in_channels//reduction,in_channels,1,biasFalse)self.sigmoidnn.Sigmoid()defforward(self,x):avg_outself.fc2(self.relu(self.fc1(self.avg_pool(x))))max_outself.fc2(self.relu(self.fc1(self.max_pool(x))))outself.sigmoid(avg_outmax_out)returnx*outclassSpatialAttention(nn.Module):# 空间注意力通道维度上做 concat 然后卷积def__init__(self,kernel_size7):super().__init__()# 这里踩过坑kernel_size 必须为奇数否则 padding 不对称assertkernel_size%21,kernel_size must be oddself.convnn.Conv2d(2,1,kernel_size,paddingkernel_size//2,biasFalse)self.sigmoidnn.Sigmoid()defforward(self,x):avg_outtorch.mean(x,dim1,keepdimTrue)max_out,_torch.max(x,dim1,keepdimTrue)outtorch.cat([avg_out,max_out],dim1)outself.sigmoid(self.conv(out))returnx*outclassCBAM(nn.Module):def__init__(self,in_channels,reduction16,kernel_size7):super().__init__()self.channel_attChannelAttention(in_channels,reduction)self.spatial_attSpatialAttention(kernel_size)defforward(self,x):# 先通道注意力再空间注意力xself.channel_att(x)xself.spatial_att(x)returnx第二步修改 YOLOv11 的 Neck 代码找到关键插入点YOLOv11 的 Neck 定义在ultralytics/nn/modules/head.py的Detect类中但实际特征图处理在ultralytics/nn/tasks.py的BaseModel里。别直接改 head.py那会破坏整个 forward 流程。正确做法在tasks.py的_predict_once方法中找到 Neck 输出后、Head 输入前的特征图列表。# 在 ultralytics/nn/tasks.py 中找到类似这样的代码段# 原代码# x self.model(x, profileprofile, visualizevisualize)# 修改为def_predict_once(self,x,profileFalse,visualizeFalse):# ... 前面的 backbone 部分不变 ...# 获取 Neck 输出的特征图列表# 注意v11 的 Neck 输出是 [P3_pan, P4_pan, P5_pan] 的顺序neck_outputs[]fori,layerinenumerate(self.model):xlayer(x)# 这里踩过坑不能直接用 isinstance 判断因为有些层是 Sequential# 正确做法在模型定义时给 Neck 输出层打标签ifhasattr(layer,is_neck_output)andlayer.is_neck_output:neck_outputs.append(x)# 插入 CBAM 的三种方式# 方式一在 FPN 输出后加需要修改模型定义这里不展开# 方式二在 PAN 输出后加推荐下面详细写# 方式三在 Head 前加所有特征图拼接前# 方式二实现对 PAN 的每一层输出加 CBAMcbam_modulesself.cbam_list# 预先定义好的 CBAM 模块列表processed_outputs[]forfeat,cbaminzip(neck_outputs,cbam_modules):processed_outputs.append(cbam(feat))# 方式三实现在 Head 的 forward 中加见下一步returnprocessed_outputs第三步在 Head 前插入 CBAM最容易被忽视的位置Head 的输入是三个尺度的特征图经过cv2分类分支和cv3回归分支分别处理。这里有个关键细节分类分支和回归分支共享同一个特征图输入但注意力对两者的影响不同。# 在 ultralytics/nn/modules/head.py 的 Detect 类中classDetect(nn.Module):def__init__(self,nc80,ch()):super().__init__()self.ncnc self.nllen(ch)# 检测层数通常是 3# 定义分类和回归分支self.cv2nn.ModuleList(nn.Sequential(Conv(x,c2,3),Conv(c2,c2,3),nn.Conv2d(c2,4*self.reg_max,1))forxinch)self.cv3nn.ModuleList(nn.Sequential(Conv(x,c2,3),Conv(c2,c2,3),nn.Conv2d(c2,self.nc,1))forxinch)# 可选在 Head 前加 CBAM方式三# 注意这里只对分类分支加回归分支不加self.cbam_for_clsnn.ModuleList([CBAM(x)forxinch]ifself.use_cbam_for_clselse[nn.Identity()for_inch])defforward(self,x):# x 是三个尺度的特征图列表shapex[0].shape# 方式三只对分类分支的输入加 CBAMforiinrange(self.nl):# 回归分支用原始特征x_regself.cv2[i](x[i])# 分类分支用经过 CBAM 的特征x_clsself.cv3[i](self.cbam_for_cls[i](x[i]))# 这里别这样写把 x 直接覆盖会丢失回归分支的原始特征# 正确做法分别处理# ... 后续的 decode 和拼接 ...消融实验12 组配置的硬核对比实验设置数据集VisDrone 201910 类含小目标模型YOLOv11n轻量版方便快速迭代训练300 epochsbatch size 16输入 640x640评估指标mAP0.5分类精度、mAP0.5:0.95综合精度配置编号CBAM 插入位置分类 mAP回归 mAP综合 mAP0 (baseline)无68.372.165.81FPN 的 P3 输出后69.171.866.22FPN 的 P4 输出后70.572.067.43FPN 的 P5 输出后69.871.566.94PAN 的 P3 输出后68.971.265.55PAN 的 P4 输出后71.271.968.16PAN 的 P5 输出后70.171.367.07所有 PAN 输出后70.870.567.38Head 前分类回归都加70.269.866.49Head 前仅分类分支加70.972.067.810FPN P4 PAN P471.071.667.911PAN P4 Head 前仅分类71.571.768.3关键发现PAN 的 P4 层是黄金位置配置 5分类 mAP 提升 2.9 个点回归几乎不变。P4 对应 16 倍下采样特征图大小适中既保留了足够的空间信息又不会像 P3 那样噪声太多。Head 前加 CBAM 要谨慎配置 8分类和回归都加回归掉了 2.3 个点。因为回归分支需要精确的空间位置信息CBAM 的空间注意力会模糊边界。只对分类分支加 CBAM 是安全牌配置 9回归不掉点分类还涨了 2.6 个点。实现起来也简单只需要在cv3前插一个 CBAM。叠加两个位置效果最好配置 11PAN P4 Head 前仅分类综合 mAP 达到 68.3比 baseline 高 2.5 个点。但要注意这个配置参数量增加了约 8%在移动端部署时需要考虑。个人经验别盲目堆注意力做了这么多实验最深的体会是注意力机制不是越多越好关键是要找对位置。如果你只关心分类精度比如做图像分类任务迁移优先在 PAN 的 P4 层加 CBAM这是性价比最高的选择。如果你需要同时保持回归精度比如做检测只在分类分支的 Head 前加 CBAM回归分支保持原样。如果你有算力冗余可以尝试 PAN P4 Head 前仅分类的组合但要注意训练时学习率要调低 0.1 倍否则容易过拟合。还有一个容易忽略的点CBAM 的 reduction 参数。我试过 8、16、32发现 16 是最稳的。reduction8 时参数量太大容易在小数据集上过拟合reduction32 时注意力太弱效果不明显。最后说一句别在 FPN 的 P3 层加 CBAM那个位置的特征图分辨率高但语义弱加了注意力反而会放大噪声。我一开始就是在这个坑里浪费了两天。下次遇到分类精度上不去的问题先试试 PAN P4 加 CBAM大概率能救回来。

相关新闻