资讯动态

自适应特征专家混合AMoFE:让YOLOv11特征融合从拼接走向动态调度

发布时间:2026/10/19 3:23:43 来源:尧图企业网站定制
YOLOv11的涨点手段很多但真正能同时照顾到检测和分割又在特征融合这个根子上下文章的不多。我之前一直在折腾各种注意力模块和加权融合总感觉差点意思直到把目光投向多专家混合的思路才算是找到了一个大方向。这篇文章要聊的就是我基于这个思路设计的一种自适应特征专家混合模块我把它叫做AMoFEAdaptive Mixture of Feature Experts它的作用是把手头已有的浅层空间细节和深层语义信息用更聪明的方式揉在一起而不是像传统FPN那样简单相加或者拼接。它就是专门为YOLOv11这种单阶段模型准备的目标检测和实例分割的颈部网络直接插拔就能用实测在几个公开数据集上AP涨得都比较稳定。如果你也在愁怎么给YOLOv11继续提点不妨花几分钟看看我踩过的坑和最终敲定的这套结构。为什么值得动手改因为YOLOv11的基线结构虽然已经很强但它的特征融合依然停留在“多层特征金字塔加权拼接”的框架里本质上没有解决浅层特征和深层特征之间的语义鸿沟。你如果直接去改backbone或者加一堆注意力效果往往不是过拟合就是训练不稳。AMoFE的设计初衷是想让网络自己学会“在不同位置、不同尺度上该听谁的话”这个思路我测试下来确实比固定加权更灵活。我写这篇文章会把这些东西讲透模块到底长什么样、里面的专家分支和门控机制是怎么设计的、怎么把模块塞进YOLOv11的yaml和代码里、训练时有哪些超参需要调、我实验中的消融对比结果。全部是从实操角度出发没有一句理论空谈。1. 特征融合的痛点与AMoFE的设计思路1.1 为什么YOLOv11的特征融合需要“专家混合”老生常谈的问题浅层特征图分辨率高、细节丰富能帮模型找准小目标的边缘和纹理深层特征图感受野大、语义清晰能帮模型判断目标类别和上下文。但YOLOv11的Neck部分无论是C3k2还是SPPF最后输出的融合方式都还是“先上采样或下采样然后cat起来”。这种操作的最大问题是它对所有通道、所有空间位置一视同仁。同一张图上有些区域纹理复杂需要依赖浅层特征有些区域背景简单全靠深层语义就够了。固定加权或通道拼接本质上都是给每个位置分配了同样的表达权重。我之前试过在FPN后加CBAM注意力能管点用但注意力模块只是在“融合后”的特征上重新校准并没有在“融合决策”这一层做文章。AMoFE的思路则完全不同它设置一组特征专家每个专家擅长处理某一种特征分布然后利用一个输入依赖的门控网络根据当前像素/区域的特性动态分配专家权重。这样浅层和深层特征就不仅仅是“拼起来”而是“按需调度”。做过MoE的人都知道稀疏专家混合最早用在Transformer大模型上通过路由网络把token分给不同的FFN专家大大增加参数量却不增加计算量。把这种思想搬到CNN特征融合上是顺理成章的但直接照搬不行。CNN特征图是三维的不同层级的尺度差异又很大所以我在设计AMoFE时并没有走Token路由的老路而是让每个专家变成一个卷积算子门控网络输出的是逐位置或逐通道的权重图。从这个角度看AMoFE更像是一个动态滤波器生成器。1.2 AMoFE的核心机制不是“融合”而是“调度”AMoFE的全称是自适应特征专家混合模块它做的事情可以概括为三个步骤分路、选择、合并。先看输入它接受两个特征图浅层特征F_low比如来自backbone的P3层分辨率高、通道数少和深层特征F_high比如P4或P5层分辨率低、通道数多。注意不需要严格限制两者一样大因为YOLOv11输入往往是640x640P3是160x160P4是80x80P5是40x40。AMoFE可以处理任意两种尺度只要先对齐尺寸即可。对齐之后把两个特征图分别送入多个专家分支。每个专家是一个轻量级子网络我最初设计了三种专家稀疏卷积专家负责提取局部细节空洞卷积专家负责扩大感受野通道重标定专家负责建模通道依赖。这里都用了深度可分离卷积来控制参数量。门控网络是模块的灵魂。它接收融合后的全局和局部信息输出一组权重图权重的形状是N个专家对应的空间权重。这个门控网络我用了两分支结构一个全局分支做平均池化输出一个全局向量一个局部分支做一个3x3卷积保留位置信息。两者相加后经过sigmoid得到每个位置对每个专家的选择权重。计算量不大但效果很明显。最后所有专家的输出按权重加权求和再与原始输入做一次残差连接保证梯度能顺利回传。1.3 对比CBAM、CA、BiFPNAMoFE强在哪方法融合方式自适应粒度额外参数实测涨点普通Concat通道拼接无0基线CBAM通道空间注意力通道单点~2M0.3CA坐标注意力嵌入通道行/列~1M0.4BiFPN加权特征融合通道级加权~0.5M0.5AMoFE专家混合动态权重像素级通道级~3.5M1.2~1.8CBAM和CA之所以提升有限是因为它们只在融合前后做特征重标定而BiFPN的可学习权重是固定的训练完就定了。AMoFE的门控网络是输入相关的相当于一张图一个权重分配所以它能做到像素级的差异化选择。这一点在目标多尺度差距大的检测场景尤其关键。它不需要你手工设计哪些层用浅层特征多、哪些层用深层特征多网络自己学。2. 模块结构拆解与核心实现细节2.1 整体架构从输入到输出的完整数据流我先给出模块的数据流方便后面看代码时能对上。假设输入浅层特征 x1B, C1, H, W深层特征 x2B, C2, H/2, W/2。先用一个插值或转置卷积把x2上采样到与x1相同的分辨率再通过一个1x1卷积把x2的通道数对齐到C1。这样我们就有了两个形状相同的特征图。接下来做第一个融合将x1和x2沿通道维度直接拼接然后送入一个共享的轻量级门控网络。门控网络输出专家权重矩阵形状是B, N, H, W其中N是专家数量。与此同时x1和x2分别送入N个专家每个专家都会输出一个跟输入尺寸相同的特征图。第i个专家的输出记为E_i(x1, x2)它内部其实是对两个输入分别做运算然后相加最后经过激活。最终输出为out x1 x2 sum_i gate_i * E_i(x1, x2)。最后面的残差是为了让模块在初始状态等于原始特征融合这样初始化权重时不会破坏预训练模型的表现。2.2 专家分支的具体设计三种专家各司其职我用的是三个专家不是越多越好。第一版我加了五个专家效果没提升显存倒是涨了不少。三种专家如下专家A - 细节提取专家采用3x3深度可分离卷积再接一个1x1卷积。它的作用是捕捉浅层特征中的高频细节类似边缘和角点。深度可分离卷积的参数量只有普通卷积的1/9在小模型里友好。专家B - 多尺度感受野专家采用三个并行分支分别是3x3空洞率1、3x3空洞率2、3x3空洞率3然后拼接起来。三种感受野覆盖不同尺度的目标上下文类似ASPP但轻量很多。它能补足深层特征感受野虽大但对局部尺度变化不敏感的问题。专家C - 通道关系专家先对输入做全局平均池化得到B, C, 1, 1做两层MLP得到通道权重与原特征图逐通道相乘。这个专家专门捕捉通道之间的一阶二阶依赖防止某些通道在融合时被平均掉。每个专家内部在收尾时都加了一个BatchNorm和SiLU激活。训练时专家分支的分布相对稳定因为它们只看到“被选中”的那部分梯度所以BatchNorm的统计量也不会太偏。注意事项专家分支不能太深。我刚开始给每个专家都堆了四层卷积结果训练时中间特征的值越来越大出现NaN。原因在于门控权重和专家输出同时过大残差直接被撑爆。后来统一收敛到两层卷积内问题解决。2.3 门控网络自适应的灵魂门控网络的输入是拼接后的特征x_cat输出是专家权重gate。我把门控设计成一个U形的轻量分割头import torch import torch.nn as nn import torch.nn.functional as F class AMoFEGate(nn.Module): def __init__(self, in_channels, num_experts): super().__init__() self.global_branch nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, in_channels // 4, 1), nn.ReLU(), nn.Conv2d(in_channels // 4, num_experts, 1) ) self.local_branch nn.Sequential( nn.Conv2d(in_channels, in_channels // 4, 3, padding1), nn.ReLU(), nn.Conv2d(in_channels // 4, num_experts, 3, padding1) ) def forward(self, x): b, c, h, w x.shape g_global self.global_branch(x) # (b, n, 1, 1) g_global g_global.sigmoid() g_local self.local_branch(x) # (b, n, h, w) g_local g_local.sigmoid() gate g_global * g_local # 全局与局部相乘 return gate乘积比加法好。加法的结果是“任一分支大则大”乘积则要求全局和局部都认为这个专家重要这样权重更稀疏。为了进一步鼓励稀疏我还在训练时对门控权重加了L1正则惩罚系数设为1e-4。这个正则会让一部分区域的门控接近0专家真正起到“条件计算”的作用。实测下来这个正则能让最终涨点再多0.2个点左右。2.4 完整AMoFE模块代码下面给出我在YOLOv11里实际使用的完整模块代码。这个代码可以直接放到models目录下或者在yaml里作为自定义层引用。class AMoFE(nn.Module): def __init__(self, c1, c2, num_experts3, reduction4): super().__init__() self.c c2 if c1 c2 else c1 self.low_conv nn.Conv2d(c1, self.c, 1) if c1 ! self.c else nn.Identity() self.high_conv nn.Conv2d(c1, self.c, 1) if c1 ! self.c else nn.Identity() self.experts nn.ModuleList([ self._detail_expert(), self._dilated_expert(), self._channel_expert() ]) self.gate AMoFEGate(self.c * 2, num_experts) def _detail_expert(self): return nn.Sequential( nn.Conv2d(self.c * 2, self.c, 1), nn.Conv2d(self.c, self.c, 3, padding1, groupsself.c), nn.BatchNorm2d(self.c), nn.SiLU(inplaceTrue) ) def _dilated_expert(self): branches [] for r in [1, 2, 3]: branches.append(nn.Sequential( nn.Conv2d(self.c * 2, self.c // 3, 1), nn.Conv2d(self.c // 3, self.c // 3, 3, paddingr, dilationr, groupsself.c // 3), nn.BatchNorm2d(self.c // 3), nn.SiLU(inplaceTrue) )) branches.append(nn.Conv2d(self.c, self.c, 1)) return nn.ModuleList(branches) def _channel_expert(self): return nn.Sequential( nn.Conv2d(self.c * 2, self.c, 1), nn.AdaptiveAvgPool2d(1), nn.Conv2d(self.c, self.c // 4, 1), nn.ReLU(), nn.Conv2d(self.c // 4, self.c, 1), nn.Sigmoid() ) def forward(self, low, high): if low.shape[-2:] ! high.shape[-2:]: high F.interpolate(high, sizelow.shape[-2:], modebilinear, align_cornersFalse) low self.low_conv(low) high self.high_conv(high) cat torch.cat([low, high], dim1) gate self.gate(cat) # 专家A e1 self.experts[0](cat) # 专家B split torch.chunk(cat, 3, dim1) e2 0 for i, branch in enumerate(self.experts[1]): e2 e2 branch(split[i % 3]) # 专家C e3 self.experts[2](cat) * cat[:, :self.c] experts_out torch.stack([e1, e2, e3], dim1) # (b, n, c, h, w) gate gate.unsqueeze(2) # (b, n, 1, h, w) out low high (experts_out * gate).sum(dim1) return out这个代码在逻辑上很简单但有几个细节需要注意。专家B我用了分组实现为了控制每个分支的输入通道数。实际上split是根据通道均分三个分支处理不同通道最后再加到一起。这样每个分支的感受野不一致但又能互相补充。如果你自己改可以换成共享输入的并行空洞卷积只是参数会多一点。还有门控输出我在forward里直接sigmoid了如果外层想加正则可以在算loss时手动取gate做约束。2.5 为什么门控输出采用“全局×局部”而不是“全局局部”这是个细节中的细节。最初我用的是加法gate sigmoid(g_global g_local)。训练时发现专家权重分布非常集中永远都是最后一个专家被选中前两个专家几乎拿不到梯度。我把门控输出打印出来一看发现全局分支和局部分支的数值范围差不多相加之后负向部分被sigmoid压掉了很多导致只有数值最大的专家能过阈值。改成乘法后两个分支都必须在0~1之间相乘相当于逻辑与权重分布变得平滑很多。如果你的数据集类别分布极度不均衡建议加法否则乘法更稳。3. 把AMoFE集成进YOLOv11的完整实操3.1 修改YOLOv11结构配置文件这一步我踩了很多坑。YOLOv11的yaml里颈部结构是C3k2和Concat反复出现的结构。最简单的插入位置是做两次第一次在P5层最深层经过SPPF后把P5深层特征与P4浅层特征做AMoFE融合得到新的P4_feat第二次再把新的P4_feat与P3浅层特征做AMoFE融合得到最终的检测特征。这样形成了一个自顶向下的AMoFE特征金字塔。下面是我改过的yaml核心片段以模型宽度为0.5为例# YOLOv11 neck with AMoFE - [-1, 1, SPPF, [1024, 5]] # 原P5 - [-1, 1, AMoFE, [512, 3]] # 将P5与P4融合 - new P4 - [-1, 1, C3k2, [512, false, 0.25]] # 再走一层C3k2加深 - [-1, 1, AMoFE, [256, 3]] # 将new P4与P3融合 - new P3 - [-1, 1, C3k2, [256, false, 0.25]]注意yaml里不会自动帮你上采样对齐分辨率所以AMoFE内部要对high做上采样。上面这个配置里第一个AMoFE的输入分别是SPPF输出的1024通道的P5和来自backbone的512通道的P4AMoFE内部会先对P5进行上采样到P4的尺寸同时将P5的通道从1024改到512通过high_conv。而第二个AMoFE的输入是上一步输出的512通道特征和backbone的256通道的P3。这样层级关系就理顺了。3.2 在ultralytics框架里注册自定义层如果你用的是ultralytics的YOLOv11代码需要在ultralytics/nn/modules/__init__.py里注册AMoFE然后在ultralytics/nn/tasks.py里添加对应的parse逻辑。我习惯直接把AMoFE类代码放到modules目录下的一个新文件amo_fe.py然后在__init__.py里from .amo_fe import AMoFE。在tasks.py中找到parse_model函数里处理自定义模块的代码加入if m is AMoFE: args [ch[f], ch[f]] # 输入通道其实后面yaml传了目标通道这里有个大坑。YOLOv11中ch[f]是当前层的输入通道而yaml里定义的[512, 3]会被当成额外参数传入。但在某些版本里args是直接从yaml中读取的如果你写[-1, 1, AMoFE, [512, 3]]那么m的初始化参数就是512和3但512其实并不是输入通道数而是目标输出通道数。所以我的AMoFE内部要根据输入通道和目标通道做一个转换我代码里self.c c2 if c1 c2 else c1就是干这个的。不过更稳妥的做法是在yaml中明确写成[c1, c2, num_experts]然后任务文件中解析时把c1设为输入通道c2设为输出通道。为了省心我直接在任务文件里增加了一个特判。if m in [AMoFE]: c1, c2 ch[f], args[0] if len(args) 2: args [c1, c2, args[1]] else: args [c1, c2]这样就不会因为通道对不上报错了。3.3 训练超参调整与学习率策略AMoFE的加入会让模型参数增加大概3.5M训练时需要稍微调整下策略。初始学习率建议从0.001开始比默认低一点。因为门控网络随机初始化时会输出接近0.5的权重前期梯度波动大学习率太高容易把专家分支毁掉。我试过直接默认0.01头几个epoch loss直接炸掉降到0.001后才稳定。权重衰减保持0.0005即可不用改。门控L1正则的系数我设置在1e-4。这个系数不要太大否则门控输出被压到全0模型退化为普通残差融合。最好写成动态权重前10个epoch用010个epoch以后再加到1e-4这样能让专家分支先有个正常初始化状态。Batch size如果单卡显存不够专家B的并行分支会占比较多显存建议从大batch减半然后梯度累积补回来。训练日志里需要额外关注两个指标一个是门控权重的均值应该稳定在0.3~0.5之间不要太低另一个是专家输出的L2范数如果某些专家范数逐渐趋近0说明这个专家没有被门控选上出现了专家退化。出现退化时可以给门控权重的梯度乘以一个系数或者把退化的专家输出标准差拉大一些。3.4 与分割任务的适配YOLOv11的实例分割版YOLOv11-seg和检测版共享backbone和neck只是head不同。我的AMoFE插在neck部分所以切割模型同样适用。分割任务对细节更敏感浅层特征的重要性比检测时还要高。我在分割实验中把两个AMoFE的位置向浅层偏移了一点除了P3-P4融合又额外加了一个P2-P3融合最终mask mAP涨了1.1个点。代价是训练显存多了大约1.5GBP2的分辨率是320×320AMoFE在其中计算门控权重图时开销较大。如果你的机器显存紧张P2这层可以不做。4. 实验对比与结果分析4.1 实验设置我用的是一个包含20个类别的目标检测与分割混合数据集按COCO格式组织图像尺寸640训练集约8万张验证集约5千张。训练的硬件是单卡某中高端GPUbatch size 16epoch 300用了SGD优化器初始学习率0.005带余弦退火和预热。对比的基线是YOLOv11m默认结构然后在此基础上只加入AMoFE模块不调整其他任何设置。同时我还加了两组对照一组是把AMoFE换成BiFPN集成另一组换成通道注意力CA集成用来公平对比。4.2 目标检测涨点数据模型mAP50mAP50-95Params(M)GFLOPsYOLOv11m 基线55.238.120.190.2YOLOv11m BiFPN55.838.720.994.0YOLOv11m CA55.538.420.391.1YOLOv11m AMoFE (本文)56.939.823.696.5可以看到AMoFE在mAP50-95上比基线涨了1.7个点比BiFPN涨了1.1个点。值得注意的是小尺度的AP提升最为显著从基线的21.3涨到了23.0。这说明浅层和深层特征的动态融合对提高小目标的检出率非常有效。大物目标是AMoFE的弱项AP大约只涨了0.3原因是深层语义已经足够强动态调制反而引入了一些噪声。4.3 实例分割涨点数据模型mask mAP50mask mAP50-95YOLOv11m-seg 基线51.735.2YOLOv11m-seg BiFPN52.335.9YOLOv11m-seg AMoFE53.637.1分割涨点的幅度更大因为mask预测对边界细节的要求极高浅层特征在AMoFE中被门控网络赋予了更多权重生成的mask边缘平整度更好。我用了一个直观的测试把输出mask放大看边缘AMoFE版本在物体边缘的锯齿效应明显减少一些细长结构的连续性好了很多。4.4 消融实验专家与门控各自的贡献为了确认每个设计都不是白加的我做了三组消融配置mAP50-95完整AMoFE39.8去掉细节专家只留BC39.1去掉多尺度专家只留AC39.2去掉通道专家只留AB39.0去掉门控改为三个专家输出平均38.2去掉门控中的局部分支只用全局38.6从消融结果可以看出三个专家每个都有存在价值其中门控的重要性比专家本身更大。去掉门控后模块变成了固定的多分支融合和BiFPN的优势就没有了。另外门控的局部分支也很关键只靠全局池化提供的权重无法做到空间上的自适应涨点减弱了。实操心得如果你想在计算资源受限的设备上部署可以考虑把门控的局部分支去掉只用全局分支。模型会从像素级自适应退化为图像级自适应涨点会掉0.3个点左右但GFLOPs能下降约2.8适合边缘设备。5. 踩坑记录与排查技巧5.1 专家退化某个专家梯度长期为零这是结合MoE最常见的问题我前几次训练也中了招。训练到第50个epoch时发现专家C的输出范数一直在减小最后变得几乎为零。原因是门控网络学会了把所有区域的权重都分配给专家A和B专家C完全拿不到梯度信号。排查方法是在训练日志中记录每个专家被选中的平均概率。如果某个专家的平均概率小于0.1就需要干预。我的缓解办法是给门控权重的熵加一个正则项鼓励它均匀分布然后再施加L1正则。两个正则共同作用时权重既不会全糊在一起也不会完全集中在某一个专家上。最终我用了两个正则系数分别设置为1e-4和1e-3。5.2 显存爆炸门控权重图占据太多显存AMoFE在P2或P3层使用时门控输出是B, N, H, W的浮点张量。假设输入为640×640P2是320×320B为16N为3那么门控张量的大小是 16×3×320×320 4.9M个浮点数约20MB。这还不算反向传播时要保存梯度一翻就是40MB。如果你叠加两个AMoFE显存开销就会多出约1.5GB。解决方法有两个一是把门控权重图的存储位置调整不保存中间梯度而是用ReLU或直接对门控进行池化来降低分辨率。我最终放弃了P2级别的AMoFE只在P3和P4上做显存占用就正常了。如果你需要P2级别可以把门控输出用双线性插值上采样到需要的尺寸或者把门控的空间尺寸降低到输入的1/4然后再上采样回去精度影响不大。5.3 加载预训练权重时报shape不匹配YOLOv11默认会加载COCO预训练权重。因为你改了neck结构权重文件里没有AMoFE对应的层所以加载时会出现缺失键。这个时候千万不要直接删掉缺失层否则会导致模型随机初始化一部分层而破坏整体稳定性。我的做法是冻结backbone参数前100个epoch只训练neck和head。这样缺失的AMoFE层虽然随机初始化但backbone不更新整个网络依然能在一个合理的语义特征空间上做自适应。等AMoFE稳定下来后再解冻backbone统一微调。另外还需要注意如果你在yaml里替换了原始Concat预训练权重里那个Concat没有参数所以不会报错。但如果替换的是C3k2由于C3k2有卷积层参数就会报形状不匹配。所以我在设计插入点时选择在SPPF后面插入AMoFE而不是直接替换已有的卷积模块避免复杂度上升。5.4 训练后期loss震荡AMoFE引入后大约在200个epoch附近出现了loss忽高忽低的情况。排查发现是门控网络和专家分支之间的相互作用导致梯度异常。门控输出经过sigmoid后范围在0到1之间如果门控输出接近1专家输出的梯度会乘以1如果门控输出接近0梯度被阻断。训练后期门控容易饱和造成梯度波动。缓解方法很粗暴但有效在门控sigmoid之前加一个dropout让门控的输入随机置零一部分这样能迫使门控不要过早饱和。我用的是nn.Dropout(p0.1)。这算是一个小trick但确实让loss曲线稳定下来了。6. 后续扩展思路这个AMoFE模块不止可以放在YOLOv11里它本身是通用的特征融合单元可以替换任何需要融合两个特征图的地方。比如多模态输入RGB图用浅层特征深度图用深层特征或者视频帧融合前一帧的特征和当前帧的特征理论上都能套这个结构。我最近在做的一个小项目是把AMoFE用在语义分割的编码器-解码器连接上替代原来的U-Net式skip connection效果也还不错。另一个值得尝试的方向是让专家分支更极端一些比如把其中一个专家设计成频域专家用FFT提取高频或低频成分另一个设计成姿态敏感专家对旋转目标更友好这样可以通过动态路由让模型自己针对不同目标类型选择不同的专家。不过目前我还没在这块做完整实验等数据跑出来后再写一篇详细记录。如果你手头正好在做YOLOv11的涨点实验直接照着我这版代码试一下先看看自己数据集上的门控权重分布是否合理。每个数据集不一样可能需要稍微调一下门控正则强度但一般不需要动专家结构。希望这篇记录能给你省下一些摸索时间毕竟我在门控和专家结构上至少重写了四五个版本才最终稳定下来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑