008、SE 通道注意力插入 Neck 上采样后(位置三):代码修改与 mAP 消融
008、SE 通道注意力插入 Neck 上采样后位置三代码修改与 mAP 消融从一次诡异的 mAP 波动说起上周调 YOLOv11 的改进方案在 Neck 部分塞了个 SE 模块结果发现 mAP 在验证集上忽高忽低有时候甚至比 baseline 还低 0.3 个点。排查了半天发现是 SE 模块放的位置不对——我一开始塞在了上采样之前结果特征图分辨率还没对齐注意力权重全学歪了。后来挪到上采样之后mAP 直接涨了 1.2 个点。这个坑踩得我印象深刻今天就把这个位置三的改进方案掰开揉碎讲清楚。为什么是上采样后位置三的直觉YOLOv11 的 Neck 结构里上采样操作负责把低分辨率特征图放大然后和 skip connection 过来的高分辨率特征图做拼接。如果你把 SE 模块放在上采样之前相当于在低分辨率空间里算注意力然后上采样——这会导致注意力权重被插值模糊掉等于白算。放在上采样之后注意力是在分辨率对齐后的特征图上计算的每个空间位置都能精准地关注到该关注的东西。位置三具体指的就是上采样操作之后、拼接操作之前。这个位置的好处是SE 模块的输入特征图已经和后续要拼接的特征图分辨率一致注意力权重不会因为插值而产生空间错位。代码修改别踩我踩过的坑第一步找到 Neck 的入口YOLOv11 的 Neck 定义在ultralytics/nn/modules/head.py或者ultralytics/nn/tasks.py里具体看你的版本。我习惯直接改ultralytics/nn/modules/conv.py把 SE 模块定义好然后在 Neck 里调用。# ultralytics/nn/modules/conv.py 末尾添加classSE(nn.Module):Squeeze-and-Excitation 通道注意力位置三专用版def__init__(self,c1,c2,r16):super().__init__()# 这里踩过坑c1和c2必须一致否则后面拼接维度对不上assertc1c2,fSE输入输出通道数必须相等当前c1{c1}, c2{c2}self.avg_poolnn.AdaptiveAvgPool2d(1)self.fcnn.Sequential(nn.Linear(c2,c2//r,biasFalse),# 别这样写用biasTrue虽然省参数但容易过拟合nn.ReLU(inplaceTrue),nn.Linear(c2//r,c2,biasFalse),nn.Sigmoid())defforward(self,x):b,c,_,_x.size()yself.avg_pool(x).view(b,c)yself.fc(y).view(b,c,1,1)returnx*y.expand_as(x)这里有个细节r16是压缩比我试过 8 和 3216 在 YOLOv11 上效果最稳。别用 4参数量涨太多mAP 提升不明显。第二步修改 Neck 的 forward 逻辑找到 Neck 的 forward 函数一般在ultralytics/nn/modules/head.py的Detect类或者单独的Neck类里。YOLOv11 的 Neck 结构大致是P5 - Conv - Upsample - 拼接P4 - Conv - Upsample - 拼接P3 - Conv - 输出我们要在每次上采样之后、拼接之前插入 SE。以 P5 上采样后拼接 P4 为例# 在 forward 函数中找到类似这样的代码块# 别这样写直接在原代码里改容易搞乱版本建议新建一个类继承classYOLOv11NeckWithSE(nn.Module):def__init__(self,...):super().__init__()# 原有初始化代码不变# 添加三个SE模块对应三个上采样位置self.se1SE(256,256)# 假设P5通道256self.se2SE(128,128)# 假设P4通道128self.se3SE(64,64)# 假设P3通道64defforward(self,x):# x 是 [P3, P4, P5] 或者 [P5, P4, P3]看具体实现# 这里假设输入是 [P5, P4, P3]p5,p4,p3x# 上采样P5xself.upsample(p5)# 假设有upsample层# 这里踩过坑上采样后一定要检查分辨率是否和P4一致# 别这样写直接拼接先过SE再拼接xself.se1(x)# 位置三上采样后、拼接前xtorch.cat([x,p4],dim1)xself.conv1(x)# 第二次上采样xself.upsample(x)xself.se2(x)# 位置三xtorch.cat([x,p3],dim1)xself.conv2(x)# 第三次上采样如果有的话# ...returnx注意通道数要根据你的模型实际配置来。YOLOv11 的不同版本n/s/m/l/x通道数不一样别直接抄我的数字。我习惯在__init__里打印一下各层通道数确认无误再改。第三步注册模块并修改配置文件在ultralytics/nn/tasks.py的parse_model函数里找到解析 Neck 的部分把 SE 模块加进去。或者更简单的方法直接在 yaml 配置文件里加一行。# yolov11-se.yaml# 在 Neck 部分每个上采样后面加一个 SEneck:-[-1,1,Conv,[256,3,2]]# 原有-[-1,1,nn.Upsample,[None,2,nearest]]# 上采样-[-1,1,SE,[256]]# 新增位置三-[[-1,6],1,Concat,[1]]# 拼接# 后续层同理这里有个坑yaml 里写SE的时候要确保tasks.py里已经 import 了 SE 类否则会报ModuleNotFoundError。我一般直接在tasks.py顶部加一行fromultralytics.nn.modules.convimportSE消融实验mAP 到底涨了多少我在 COCO val2017 上跑了 300 个 epochbatch size 16输入 640x640优化器 SGDlr 0.01。对比了三个位置配置mAP0.5mAP0.5:0.95参数量推理速度(ms)Baseline (无SE)52.337.111.2M2.1位置一 (上采样前)52.136.911.4M2.3位置二 (拼接后)52.837.511.4M2.4位置三 (上采样后)53.538.211.4M2.4位置三比 baseline 涨了 1.2 个 mAP0.50.9 个 mAP0.5:0.95。位置一反而降了 0.2说明我一开始的直觉是对的——上采样前做注意力确实会损失信息。另外我试了在三个上采样位置都加 SE结果 mAP 涨到 53.8但参数量多了 0.6M推理慢了 0.3ms。性价比不高建议只在最大的两个上采样后加 SE。个人经验别盲目堆模块SE 通道注意力在 YOLOv11 上确实有效但位置选不对就是负优化。我见过有人把 SE 塞在 backbone 的每个 stage 后面结果 mAP 反而降了——因为 backbone 的特征图分辨率高SE 的全局平均池化会丢失空间信息对小目标检测不利。位置三之所以效果好是因为上采样后的特征图已经包含了多尺度信息SE 能帮模型决定哪些通道更重要。如果你用的是 YOLOv11 的 nano 版本建议把压缩比 r 改成 8因为 nano 的通道数少r16 压缩太狠注意力学不到位。最后提醒一句改完代码一定要跑一遍torch.jit.trace或者torch.onnx.export检查一下SE 模块的expand_as操作在 ONNX 导出时可能会报错换成x * y的广播形式更稳妥。