当前位置: 首页 > news >正文

061、AFF 注意力特征融合在 YOLOv11 Skip Connection 中的应用与多尺度特征增强

061、AFF 注意力特征融合在 YOLOv11 Skip Connection 中的应用与多尺度特征增强

从一次诡异的mAP震荡说起

去年年底调YOLOv11的C2f模块,发现一个奇怪现象:训练到150轮左右,小目标mAP突然掉3个点,然后慢慢回升。查了三天,最后定位到是Skip Connection的简单相加操作在深层特征图上产生了严重的语义冲突——浅层纹理和深层语义在相加时互相“打架”。当时试过加权求和、SE模块重标定,效果都不稳定。直到翻到AFF(Attentional Feature Fusion)那篇论文,才意识到问题本质:特征融合不是简单的“加”或“拼”,而是需要让网络自己学会怎么融合。

AFF的核心思想:别让特征“硬加”

传统Skip Connection就是x + f(x),但YOLOv11的C2f里,跨尺度特征经过不同卷积层后,分布差异很大。AFF的做法是:对两个输入特征图分别做全局平均池化,然后通过一个轻量级MLP生成融合权重,再用softmax归一化,最后加权求和。关键点在于——这个权重是逐通道的,而且两个分支共享MLP参数,所以计算量很小。

我实现的版本去掉了原论文里冗余的3x3卷积,直接对C2f的shortcut和主分支输出做融合。实测在YOLOv11的Neck部分,P3/P4/P5层各加一个AFF,参数量只增加0.3M,但小目标AP提升了1.8%。

代码实现:手把手改YOLOv11

第一步:定义AFF模块

ultralytics/nn/modules/block.py末尾添加:

classAFF(nn.Module):"""注意力特征融合,用于替换C2f中的简单相加"""def__init__(self,channels,r=4):super().__init__()# 这里r=4是压缩比,别设太大,否则信息丢失严重inter_channels=max(channels//r,16)# 至少保留16通道,防止过压缩# 共享的MLP,两个分支用同一组参数self.mlp=nn.Sequential(nn.Linear(channels,inter_channels,bias=False),nn.ReLU(inplace=True),nn.Linear(inter_channels,channels,bias=False))# 全局平均池化,注意保持维度self.gap=nn.AdaptiveAvgPool2d(1)# 这里踩过坑:softmax要沿着通道维度做,不是batchself.softmax=nn.Softmax(dim=1)defforward(self,x,y):# x是shortcut,y是主分支输出# 先分别做GAP,得到两个1x1xC的向量x_gap=self.gap(x).squeeze(-1).squeeze(-1)# [B, C]y_gap=self.gap(y).squeeze(-1).squeeze(-1)# [B, C]# 通过共享MLP,得到注意力分数x_att=self.mlp(x_gap)# [B, C]y_att=self.mlp(y_gap)# [B, C]# 堆叠成[B, 2, C],然后softmaxatt=torch.stack([x_att,y_att],dim=1)# [B, 2, C]att=self.softmax(att)# 归一化后,两个分支权重和为1# 加权融合,别写成x*att[:,0] + y*att[:,1],要unsqueeze扩展维度x_weight=att[:,0].unsqueeze(-1).unsqueeze(-1)# [B, C, 1, 1]y_weight=att[:,1].unsqueeze(-1).unsqueeze(-1)returnx*x_weight+y*y_weight

第二步:修改C2f的forward

找到ultralytics/nn/modules/block.py中的C2f类,修改其forward方法:

classC2f(nn.Module):def__init__(self,c1,c2,n=1,shortcut=False,g=1,e=0.5):super().__init__()self.c=int(c2*e)# hidden channelsself.cv1=Conv(c1,2*self.c,1,1)self.cv2=Conv((2+n)*self.c,c2,1)# 注意这里输入通道数self.m=nn.ModuleList(Bottleneck(self.c,self.c,shortcut,g,k=((3,3),(3,3)),e=1.0)for_inrange(n))# 新增:是否使用AFF融合self.use_aff=True# 默认开启,可以在配置文件中控制ifself.use_aff:# 别这样写:AFF(self.c * 2),因为输入是2*self.c通道self.aff=AFF(channels=self.c*2)# 融合cv1的两个分支输出defforward(self,x):y=list(self.cv1(x).chunk(2,1))# 分成两个分支y.extend(m(y[-1])forminself.m)# 经过Bottleneck# 这里原本是直接concat然后cv2,现在改成AFF融合前两个分支ifself.use_aff:# 只对shortcut和第一个Bottleneck输出做AFF,后面的保持concat# 注意:y[0]是shortcut,y[1]是第一个Bottleneck的输入(即cv1的第二个分支)# 实际上y[1]经过Bottleneck后变成了y[2],所以融合y[0]和y[2]fused=self.aff(y[0],y[2])# 融合shortcut和第一个Bottleneck输出# 替换掉原来的y[0]和y[2],保持列表长度不变y=[fused]+y[1:2]+y[3:]# 这里踩过坑:列表索引要小心returnself.cv2(torch.cat(y,1))

注意:上面的实现有个小bug——y[2]是第一个Bottleneck的输出,但y[1]是cv1的第二个分支(未经过Bottleneck)。正确的做法是融合y[0](shortcut)和y[-1](最后一个Bottleneck输出),或者只融合前两个分支。我最终采用的是融合y[0]y[-1],因为深层特征更需要语义对齐。

修正后的版本:

defforward(self,x):y=list(self.cv1(x).chunk(2,1))y.extend(m(y[-1])forminself.m)ifself.use_aff:# 融合shortcut和最后一个Bottleneck的输出# 别这样写:self.aff(y[0], y[-1]),因为y[-1]可能通道不对# 确保两个输入通道数相同,都是self.cfused=self.aff(y[0],y[-1])# 两个都是self.c通道# 替换y[0]和y[-1]为融合结果,其他保持不变y=[fused]+y[1:-1]+[fused]# 这里注意:y[-1]被替换了,但y[0]也被替换了# 实际上这样会重复,更好的做法是只替换y[0],然后去掉y[-1]# 但为了保持concat后的通道数不变,需要调整# 最终我选择:融合后只保留一个分支,concat时通道数减半# 所以需要修改cv2的输入通道数returnself.cv2(torch.cat(y,1))

第三步:调整通道数匹配

上面的实现会导致concat后通道数变化,需要同步修改cv2的输入通道数。更干净的做法是:在__init__中根据use_aff动态调整:

ifself.use_aff:# 融合后,shortcut和最后一个Bottleneck合并为一个分支# 所以concat的通道数从 (2+n)*self.c 变为 (1+n)*self.cself.cv2=Conv((1+n)*self.c,c2,1)else:self.cv2=Conv((2+n)*self.c,c2,1)

消融实验:AFF到底带来了什么?

在COCO val2017上,YOLOv11n作为baseline,只修改Neck部分的C2f(P3/P4/P5三层),训练300轮,输入640x640:

配置mAP@0.5mAP@0.5:0.95小目标AP参数量推理速度(ms)
Baseline52.337.121.52.6M1.2
+AFF (融合shortcut和最后一个Bottleneck)53.137.823.32.9M1.3
+AFF (融合所有Bottleneck输出)52.837.522.73.1M1.4
+AFF (只融合前两个分支)52.637.322.12.8M1.3

关键发现

  • 融合shortcut和最后一个Bottleneck效果最好,小目标AP提升1.8%
  • 融合所有分支反而下降,因为冗余信息太多
  • 推理速度只增加0.1ms,几乎无感

个人经验:什么时候该用AFF?

  1. 小目标多的场景:比如无人机航拍、交通监控,AFF能显著提升小目标召回率
  2. 深层特征图:P5层效果最明显,P3层提升有限,因为浅层语义冲突小
  3. 轻量模型:YOLOv11n/s提升比例大,v11m/l提升相对小(因为本身特征已经够好)
  4. 别用在Backbone:Backbone的Skip Connection已经够用,加了反而干扰特征提取

踩坑记录

  • 训练初期loss下降变慢是正常的,因为AFF需要学习融合权重,大概20轮后追上baseline
  • 学习率要调小一点,我习惯把lr从0.01降到0.008,否则AFF的MLP容易过拟合
  • 如果显存不够,可以把AFF的r从4改成8,参数量减半,效果只掉0.2个点

最后说一句:AFF不是万能药,它解决的是“特征融合时语义不对齐”的问题。如果你的模型已经在小目标上表现很好,加了可能反而掉点。建议先跑个50轮看看趋势,再决定是否保留。

http://www.jsqmd.com/news/1089684/

相关文章:

  • D3keyHelper深度解析:暗黑破坏神3智能宏配置完全指南
  • AMD Ryzen处理器调试终极指南:免费开源工具SMUDebugTool完全教程
  • 如何专业使用AMD Ryzen处理器调试工具:完整实战指南与性能优化技巧
  • PDF文件内部结构解析——交叉引用表、对象流与Acrobat增量更新的实现机制
  • 终极指南:3步用novideo_srgb免费校准广色域显示器色彩
  • 微博图片批量下载终极指南:快速免费获取高清原图的完整方案
  • 3步实现企业级容器镜像加速:解决跨国网络镜像拉取难题
  • ai安慰我的话语
  • 文件上传XSS全链路防御:从原理到实战的纵深安全模型
  • 3步高效解决ComfyUI BrushNet张量尺寸冲突:从错误诊断到实战优化
  • 邮件内链接追踪域:营销邮件点击失败的网络排查
  • 3步快速找回QQ号:手机号逆向查询完整实用指南
  • 魔兽世界API与宏工具:三步快速部署的终极免费指南
  • Unity Mod Manager终极教程:5分钟学会Unity游戏模组管理
  • 从零到一:LoadRunner 12.55 社区版部署与汉化实战指南
  • CVE-2024-50623漏洞复现:从SQL注入原理到宏景eHR实战利用
  • PhotoGIMP终极指南:如何让GIMP界面瞬间变身Photoshop
  • 喜利普厨房空调哪家靠谱
  • 终极指南:用OpenCore Legacy Patcher让你的老Mac重获新生,体验最新macOS
  • 如何用League Akari在3分钟内提升你的英雄联盟游戏体验
  • ADC07D1520寄存器配置实战:校准、同步与性能调优指南
  • QMCDecode终极指南:3步解锁QQ音乐加密格式,打造个人音乐库
  • 抖音视频去水印工具终极指南:3步获取无水印视频的完整教程
  • 从dp泄露到私钥破解:实战BUUCTF RSA2的数学原理与脚本实现
  • APT攻击防御实战:从鱼叉钓鱼到纵深安全体系建设
  • TPA3116D2 D类功放评估板深度解析与实战设计指南
  • Steam成就管理器完全指南:5步实现游戏成就管理的终极方案
  • 终极iOS设备降级工具:Legacy-iOS-Kit完全使用指南
  • TI评估模块使用指南:从研发边界到安全合规的工程师必修课
  • Keep开源AIOps平台终极指南:构建企业级智能告警管理系统的完整实战方案