008、SE注意力机制改进在YOLOv8中集成Squeeze-and-Excitation模块的代码实现从一次模型部署的翻车说起去年有个项目要在边缘设备上跑目标检测客户要求模型大小控制在5MB以内mAP还不能低于0.75。我试了YOLOv8n精度差一点换YOLOv8s模型超了。正愁着突然想到SE模块——这玩意儿参数量极小但能通过通道注意力让模型“学会”关注重要特征。结果一跑YOLOv8n加SE后mAP从0.72跳到0.78模型只大了0.3MB。今天就把这个“白嫖”精度的技巧拆开揉碎讲清楚。SE模块到底在干什么Squeeze-and-Excitation说白了就是给每个通道打分。打个比方你有一堆特征图有的通道包含关键信息比如人的轮廓有的全是背景噪声。SE模块先通过全局平均池化把每个通道压缩成一个数值Squeeze再用两个全连接层学习通道间的依赖关系最后用Sigmoid输出0到1的权重Excitation乘回原特征图。这里有个关键点SE的压缩比rreduction ratio默认是16意味着中间层的神经元数量是输入通道数的1/16。别小看这个参数后面会讲它怎么坑人。在YOLOv8中插入SE的三种姿势姿势一替换C2f中的Bottleneck推荐YOLOv8的骨干网络核心是C2f模块里面包含多个Bottleneck。最直接的做法是在每个Bottleneck的3x3卷积后面加SE。打开ultralytics/nn/modules.py找到Bottleneck类classBottleneck(nn.Module):def__init__(self,c1,c2,shortcutTrue,g1,k(3,3),e0.5):super().__init__()c_int(c2*e)# hidden channelsself.cv1Conv(c1,c_,k[0],1)self.cv2Conv(c_,c2,k[1],1,gg)self.addshortcutandc1c2# 这里踩过坑SE要加在cv2之后shortcut之前self.seSEModule(c2,reduction16)# 新增defforward(self,x):returnxself.se(self.cv2(self.cv1(x)))ifself.addelseself.se(self.cv2(self.cv1(x)))注意看我把SE加在了cv2输出之后、shortcut相加之前。别这样写self.se(self.cv2(self.cv1(x))) x——虽然结果一样但语义上SE应该作用在特征提取分支上而不是残差连接上。姿势二在C2f的输出端加SE轻量方案如果模型已经很大了不想在每个Bottleneck里加SE参数量会翻倍可以在C2f模块整体输出后加一个SE。找到C2f类的forwardclassC2f(nn.Module):defforward(self,x):ylist(self.cv1(x).chunk(2,1))y.extend([m(y[-1])forminself.m])# 这里别这样写直接return self.cv2(torch.cat(y, 1))outself.cv2(torch.cat(y,1))returnself.se(out)ifhasattr(self,se)elseout这种方式参数量极小但效果不如姿势一。适合模型已经接近边缘设备极限的场景。姿势三在Neck的每个卷积后加SE暴力方案YOLOv8的NeckHead部分用了SPPF和多个卷积。如果你追求极致精度可以在Neck的每个Conv模块后加SE。但说实话我一般不推荐——参数量涨得厉害而且Neck部分的特征已经比较高层了SE带来的收益有限。定义SEModule的代码细节在ultralytics/nn/modules.py顶部添加classSEModule(nn.Module):def__init__(self,channels,reduction16):super().__init__()# 这里踩过坑reduction不能小于1否则中间层通道数为0self.avg_poolnn.AdaptiveAvgPool2d(1)self.fcnn.Sequential(nn.Linear(channels,channels//reduction,biasFalse),nn.ReLU(inplaceTrue),nn.Linear(channels//reduction,channels,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)注意biasFalse——全连接层不加偏置因为SE模块的核心是学习通道间的缩放关系偏置会引入不必要的偏移。另外inplaceTrue能省点显存但如果你用torch.jit导出模型建议改成inplaceFalse否则可能报错。修改配置文件在ultralytics/cfg/models/v8/下新建yolov8n-se.yaml# 只改backbone部分其他保持原样backbone:-[-1,1,Conv,[64,3,2]]# 0-P1/2-[-1,1,Conv,[128,3,2]]# 1-P2/4-[-1,3,C2f,[128,True]]# 2-[-1,1,Conv,[256,3,2]]# 3-P3/8-[-1,6,C2f,[256,True]]# 4-[-1,1,Conv,[512,3,2]]# 5-P4/16-[-1,6,C2f,[512,True]]# 6-[-1,1,Conv,[1024,3,2]]# 7-P5/32-[-1,3,C2f,[1024,True]]# 8-[-1,1,SPPF,[1024,5]]# 9等等这里有个问题配置文件里没法直接指定每个Bottleneck加SE。所以我们需要在代码层面做修改。更优雅的方式是写一个C2f_SE类继承C2f并重写__init__classC2f_SE(C2f):def__init__(self,c1,c2,n1,shortcutFalse,g1,e0.5):super().__init__(c1,c2,n,shortcut,g,e)# 替换原有的Bottleneck为带SE的版本self.mnn.ModuleList([Bottleneck_SE(self.c,self.c,shortcut,g,k(3,3),e1.0)for_inrange(n)])然后在配置文件中把C2f替换成C2f_SE。但这样改起来比较麻烦我一般直接在parse_model函数里做手脚——检测到配置中的C2f就自动替换成带SE的版本。训练时的坑与调参学习率要调低加了SE模块后模型对学习率更敏感。原来YOLOv8n用lr0.01加了SE后建议降到0.008。我试过0.01直接训loss震荡得厉害最后mAP反而低了0.02。压缩比r不是越小越好有人觉得r4比r16好因为中间层神经元更多。实际测试下来r8在COCO上比r16高0.3个点但参数量多了20%。对于边缘设备r16是性价比最高的选择。别这样写reduction2——中间层通道数直接减半参数量翻倍收益却微乎其微。位置选择有讲究我在YOLOv8n上做了消融实验只在backbone加SEmAP提升0.8%只在neck加SEmAP提升0.3%backboneneck都加mAP提升1.1%但参数量涨了15%结论backbone的低层特征更需要通道注意力neck部分加SE性价比不高。部署时的注意事项ONNX导出要小心SE模块里的AdaptiveAvgPool2d在ONNX导出时可能会被拆成多个op导致推理速度变慢。建议用torch.onnx.export时加上opset_version12或者手动把AdaptiveAvgPool2d替换成AvgPool2d前提是你知道输入尺寸。TensorRT加速SE模块的两个全连接层在TensorRT里会被优化成矩阵乘速度几乎没损失。但注意Sigmoid在FP16模式下可能精度不够建议用nn.Hardsigmoid替代——精度几乎一样推理更快。个人经验总结SE模块是我在YOLOv8上用得最多的注意力机制没有之一。它不像CBAM那样需要两个分支也不像ECA那样需要一维卷积简单粗暴但有效。如果你在调参时发现模型精度卡在某个点不动了加个SE往往能再往上跳0.5-1个点。但记住SE不是万能的。对于小目标检测任务SE的效果不如CACoordinate Attention因为SE只关注通道维度忽略了空间位置信息。另外如果你的数据集本身通道间相关性就很强比如遥感图像SE的收益会大打折扣。最后说一句别在YOLOv8s以上的模型里加SE——参数量涨得太多不如直接换大模型。YOLOv8n和YOLOv8m才是SE的最佳搭档。