第 11 章 · YOLO-Master——MoE 遇见实时检测
开场:给检测器装上”专家团队”
想象你经营一家翻译公司。初创时只有一个全能翻译——所有语种、所有领域都靠这一个人。业务增长后,你发现全能翻译在每个方向上都不够专精。于是你组建了一个专家团队:日语专家、法语专家、医学翻译专家、法律翻译专家……每来一个新项目,前台(路由器)先判断项目类型,然后分配给最合适的专家。
这就是 YOLO-Master 的核心思想——用 Mixture of Experts (MoE) 替换检测器中”一个网络处理所有目标”的设计,让不同的专家子网络专注处理不同类型的目标。
YOLO-Master 是腾讯优图实验室的工作,入选 CVPR 2026,也是犀牛鸟赛道”目标检测”方向的核心项目。它在 GitHub 上约 500 star——star 数不多是因为它是一篇学术论文的配套代码,而不是像 Ultralytics 那样面向大众的工具。但它的学术价值和创新性是犀牛鸟评委最关注的。
MoE 基础回顾
什么是 MoE
MoE(Mixture of Experts)是一种条件计算(Conditional Computation)技术。核心思想是:不是所有输入都需要经过网络的所有参数。通过一个路由器(Router),每个输入只激活一小部分参数(专家),从而在增加模型总参数的同时控制计算量。
标准网络(Dense):
输入 → [所有参数] → 输出
每个输入都用全部参数计算
MoE 网络(Sparse):
输入 → [路由器] → 选择 Top-K 专家 → [被选中的专家] → 加权输出
每个输入只用部分参数计算
MoE 的数学表达
# MoE 的前向传播
def moe_forward(x, experts, router, top_k=2):
# x: 输入特征
# experts: N 个专家网络
# router: 路由网络
# Step 1: 路由器计算每个专家的权重
gate_logits = router(x) # [batch, num_experts]
gate_probs = softmax(gate_logits) # 归一化
# Step 2: 选择 Top-K 个专家
topk_probs, topk_indices = gate_probs.topk(top_k)
topk_probs = topk_probs / topk_probs.sum() # 重新归一化
# Step 3: 只计算被选中的专家
output = 0
for i, (prob, idx) in enumerate(zip(topk_probs, topk_indices)):
output += prob * experts[idx](x) # 加权求和
return output
MoE 的核心挑战
负载均衡问题:如果路由器总是把输入分配给同一两个”明星专家”,其他专家就变成了摆设——这叫”专家坍塌”(Expert Collapse)。就像一个翻译公司里所有项目都交给同一个人,其他翻译坐冷板凳。
路由不稳定问题:训练初期,路由器还不知道每个专家擅长什么,分配可能很随机。这种随机性会导致专家无法稳定地学习特定能力——因为它们收到的训练样本在不断变化。
YOLO-Master 的 ES_MoE 演进
YOLO-Master 提出了 ES_MoE(Efficient Sparse MoE)系列,从 v1 到 v4 逐步解决了 MoE 在检测中的核心问题。
ES_MoE v1:基础版
最初的想法很直接——把 YOLOv8 的 C2f 模块中的 Bottleneck 替换为 MoE Bottleneck:
class MoE_Bottleneck_v1(nn.Module):
"""v1: 直接把 Bottleneck 中的 FFN 换成 MoE"""
def __init__(self, c, num_experts=4, top_k=2):
super().__init__()
self.cv1 = Conv(c, c, 3, 1) # 第一个卷积不变
self.experts = nn.ModuleList([
Conv(c, c, 3, 1) for _ in range(num_experts)
])
self.router = nn.Linear(c, num_experts) # 路由器
self.top_k = top_k
def forward(self, x):
# 路由决策
# 对特征图做全局平均池化,得到通道描述符
gate_input = x.mean(dim=[2, 3]) # [B, C]
gate_logits = self.router(gate_input) # [B, num_experts]
topk_probs, topk_idx = gate_logits.softmax(-1).topk(self.top_k)
# 只计算被选中的专家
out = sum(
topk_probs[:, i].view(-1, 1, 1, 1) * self.experts[topk_idx[:, i]](x)
for i in range(self.top_k)
)
return x + out # 残差连接
v1 的问题:路由器只看全局平均池化后的通道信息,对空间位置不敏感。而且没有负载均衡机制,容易出现专家坍塌。
ES_MoE v2:加入负载均衡
v2 引入了辅助损失(Auxiliary Loss)来强制负载均衡:
# 负载均衡损失
def load_balance_loss(gate_probs, num_experts):
"""鼓励每个专家被选中的频率接近均匀分布"""
# gate_probs: [batch, num_experts]
# 每个专家被选中的频率
expert_freq = gate_probs.mean(dim=0) # [num_experts]
# 目标:每个专家的频率 = 1/num_experts
target = torch.ones_like(expert_freq) / num_experts
# 辅助损失 = 实际频率和均匀频率的 KL 散度
aux_loss = (expert_freq * expert_freq.log() - expert_freq * target.log()).sum()
return aux_loss
ES_MoE v3:共享专家
v3 引入了”共享专家”(Shared Expert)——一个始终被激活的专家,处理所有类型的输入共有的基础特征:
class MoE_Bottleneck_v3(nn.Module):
"""v3: 共享专家 + 路由专家"""
def __init__(self, c, num_experts=4, num_shared=1, top_k=2):
super().__init__()
# 共享专家:始终激活,处理通用特征
self.shared_experts = nn.ModuleList([
Conv(c, c, 3, 1) for _ in range(num_shared)
])
# 路由专家:按需激活,处理特定类型的目标
self.routed_experts = nn.ModuleList([
Conv(c, c, 3, 1) for _ in range(num_experts)
])
self.router = nn.Linear(c, num_experts)
self.top_k = top_k
def forward(self, x):
# 共享专家的输出(始终计算)
shared_out = sum(exp(x) for exp in self.shared_experts)
# 路由专家的输出(按需计算)
gate_input = x.mean(dim=[2, 3])
gate_logits = self.router(gate_input)
topk_probs, topk_idx = gate_logits.softmax(-1).topk(self.top_k)
routed_out = sum(
topk_probs[:, i].view(-1, 1, 1, 1) * self.routed_experts[topk_idx[:, i]](x)
for i in range(self.top_k)
)
return x + shared_out + routed_out
为什么需要共享专家? 不同类型的目标(人、车、动物)虽然外观差异很大,但它们共享一些基础视觉特征(边缘、纹理、颜色分布)。共享专家负责提取这些通用特征,路由专家在此基础上提取类别特定的特征。这种设计减少了路由专家之间的冗余学习。
ES_MoE v4:Z-Loss 路由稳定化
v4 引入了 Z-Loss 来稳定路由器的训练:
# Z-Loss: 防止路由器 logits 过大导致不稳定
def z_loss(gate_logits):
"""惩罚路由器输出的绝对值过大"""
# gate_logits: [batch, num_experts]
# 如果 logits 的绝对值很大,softmax 会趋近于 one-hot
# 导致梯度消失 + 路由震荡
return (gate_logits.logsumexp(dim=-1) ** 2).mean()
Z-Loss 的直觉:想象路由器是一个”判断难度”的打分系统。如果它的打分总是极端的(”这个目标 100% 应该给专家 A,0% 给专家 B”),那它的判断就没有”容错空间”——稍微换一个相似但不同的目标,路由可能就完全改变了。Z-Loss 鼓励路由器给出更”温和”的打分,让路由决策更稳定。
YOLO-Master 的完整架构
模型结构
输入图像 (640×640×3)
│
▼
┌───────────────────────┐
│ Backbone │
│ (CSPDarknet) │
│ 部分 stage 的 C2f │
│ 被替换为 MoE_C2f │
└──┬───┬───┬────────────┘
│ │ │
P3 P4 P5
│ │ │
▼ ▼ ▼
┌───────────────────────┐
│ Neck (PAN) │
│ 可选:MoE 也可以 │
│ 放在 Neck 中 │
└──┬───┬───┬────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────────┐
│ Head (解耦头) │
│ 分类 + 回归 │
│ 使用 TAL + NMS │
└───────────────────────┘
MoE 放在哪里
YOLO-Master 论文中探索了 MoE 模块在不同位置的效果:
位置选择的消融实验:
只在 Backbone 的 Stage 3-4: 效果最稳定
只在 Neck: 效果中等
Backbone + Neck 全部替换: 参数量太大,增益有限
只在 Head: 效果较差(Head 已经很轻量)
最终的设计选择是在 Backbone 的后半部分替换——这些 stage 的特征已经有了足够的语义信息,路由器可以做出有意义的决策。如果太早替换(如 Stage 1),特征还是低级的边缘和纹理,路由器没有足够信息判断”这是什么类型的目标”。
路由器设计
class ChannelRouter(nn.Module):
"""基于通道统计的路由器"""
def __init__(self, in_channels, num_experts):
super().__init__()
self.fc = nn.Sequential(
nn.AdaptiveAvgPool2d(1), # 全局平均池化
nn.Flatten(),
nn.Linear(in_channels, num_experts), # 输出专家权重
)
def forward(self, x):
return self.fc(x)
这个路由器非常轻量——只有一个全局池化和一个线性层,计算开销可以忽略不计。它把整个特征图压缩成一个通道描述符,然后基于这个描述符决定使用哪些专家。
训练策略
损失函数
YOLO-Master 的总损失由检测损失和 MoE 辅助损失组成:
Total Loss = L_detect + α * L_balance + β * L_z
L_detect: 标准的 YOLOv8 检测损失
= λ_cls * BCE + λ_box * CIoU + λ_dfl * DFL
L_balance: 负载均衡辅助损失
鼓励每个专家被均匀使用
L_z: Z-Loss
防止路由 logits 过大
α, β: 辅助损失的权重(通常 0.01-0.1)
太大会干扰检测任务本身
太小则无法有效约束路由
训练 Tips
1. 预训练阶段:
先用标准 YOLOv8 训练一个 baseline
→ 再替换模块为 MoE 版本并微调
→ 比从零训练 MoE 模型收敛更快
2. 专家数量:
4-8 个专家是常见选择
太多(>16)容易出现专家坍塌
太少(<4)MoE 的优势不明显
3. Top-K 选择:
Top-2 是最常用的设置
Top-1 太稀疏,训练信号不够
Top-3+ 计算量增加太多
4. 辅助损失权重:
建议从 0.01 开始,观察专家利用率
如果某些专家利用率 < 5%,适当增大权重
性能分析
COCO 结果
| 模型 | 参数量 | FLOPs | COCO mAP | 延迟 |
|---|---|---|---|---|
| YOLOv8s | 11.2M | 28.6G | 44.9 | 6.16ms |
| YOLO-Master-S | ~13M | ~30G | ~46.5 | ~7ms |
| YOLOv8m | 25.9M | 78.9G | 50.2 | 9.50ms |
| YOLO-Master-M | ~28M | ~82G | ~51.8 | ~10ms |
几个关键观察:
- YOLO-Master 在参数量和 FLOPs 只增加约 10-15% 的情况下,mAP 提升了 1.5-2.0。这是 MoE 的核心价值——用少量额外计算换取显著的精度提升。
- 延迟增加主要来自路由器的计算和专家选择的开销。在 GPU 上这个开销很小(因为专家的计算可以并行),但在 CPU 或边缘设备上可能更明显。
MoE 在不同目标类型上的提升
YOLO-Master 的一个有趣发现是:MoE 对”困难”目标的提升更大。
简单目标(大目标、无遮挡): mAP 提升 ~0.5-1.0
中等目标(中等大小、轻微遮挡): mAP 提升 ~1.5-2.0
困难目标(小目标、严重遮挡): mAP 提升 ~2.0-3.0
这符合 MoE 的设计直觉——简单目标用任何专家都能检测好,MoE 的优势体现不出来。困难目标需要”专业”处理,不同专家对不同类型的困难目标各有擅长,MoE 的路由能把每个困难目标分配给最合适的专家。
代码导航
YOLO-Master 的代码基于 Ultralytics,关键文件:
YOLO-Master/
├── ultralytics/ # Fork 自 Ultralytics
│ ├── nn/
│ │ ├── modules/
│ │ │ ├── block.py # 新增 MoE_C2f, ES_MoE 等模块
│ │ │ └── moe.py # MoE 核心实现(路由器、专家、负载均衡)
│ │ └── tasks.py # 注册 MoE 模块
│ ├── utils/
│ │ └── loss.py # 添加辅助损失(balance loss + z-loss)
│ └── cfg/
│ └── models/
│ └── yolo-master-s.yaml # MoE 模型配置
配置文件示例
# yolo-master-s.yaml(简化版)
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0: Stem
- [-1, 1, Conv, [128, 3, 2]] # 1
- [-1, 3, C2f, [128]] # 2: 前面用标准 C2f
- [-1, 1, Conv, [256, 3, 2]] # 3
- [-1, 6, C2f, [256]] # 4: 标准 C2f
- [-1, 1, Conv, [512, 3, 2]] # 5
- [-1, 6, MoE_C2f, [512, 4, 2]] # 6: 替换为 MoE_C2f(4专家,Top-2)
- [-1, 1, Conv, [1024, 3, 2]] # 7
- [-1, 3, MoE_C2f, [1024, 4, 2]] # 8: 替换为 MoE_C2f
- [-1, 1, SPPF, [1024, 5]] # 9
head:
# ... Neck 和 Head 和 YOLOv8 相同 ...
犀牛鸟的贡献方向
理解了 YOLO-Master 的完整架构后,以下是几个可能的贡献方向:
方向一:改进路由器
当前路由器只看全局平均池化后的通道信息——丢失了空间位置信息。可以尝试:
- 加入空间注意力:让路由器看"图像的哪个区域更需要专家"
- 多尺度路由:不同尺度用不同的路由策略
- 可学习的温度参数:让路由器的"确定性"随训练自适应调整
方向二:专家特化分析与增强
当前没有机制保证不同专家真的学到了不同的特征。可以尝试:
- 可视化分析:看每个专家被哪些类型的目标激活
- 正交性约束:鼓励不同专家的权重彼此正交
- 课程学习:先让专家自由学习,再逐步增加特化压力
方向三:MoE 与 NMS-Free 的结合
YOLO-Master 目前仍使用 NMS。可以探索用 MoE 的路由信息辅助去重。
方向四:高效 MoE
减少 MoE 的推理开销:
- 专家共享权重:不同专家共享部分参数,只有少量参数不同
- 动态专家数量:简单目标用 1 个专家,困难目标用 3 个
- 蒸馏:把 MoE 模型蒸馏成 dense 模型用于部署
初学者常见误区
误区一:”MoE 就是把模型变大了,所以效果更好”
→ 正确理解:MoE 的关键不是”更大”,而是”更聪明地分配计算”。一个 4 专家 Top-2 的 MoE 模型,每次推理只用 2 个专家的参数——实际计算量只比原模型多 ~50%(而不是 4 倍)。如果单纯把模型变大(如把 YOLOv8s 的通道数翻倍),参数量增加到 44M 但精度提升远不如 MoE 的 13M。MoE 的优势在于”条件计算”——根据输入选择最合适的参数子集。
误区二:”专家越多越好”
→ 正确理解:专家数量有一个”甜蜜点”——通常是 4-8 个。超过 8 个,路由器的决策变得更困难,负载均衡更难维持,而且内存占用线性增加。在 YOLO-Master 的场景中,COCO 有 80 个类别但可以粗略分为几个大类(人、车、动物、家具等),4-8 个专家足以覆盖主要的视觉模式。
误区三:”路由器能完美地把目标分配给最合适的专家”
→ 正确理解:路由器的决策是基于整个特征图的全局统计量(全局平均池化),而不是针对每个目标单独路由。这意味着在一张包含多种目标的图像中,所有目标使用相同的专家组合。这是 YOLO-Master 当前设计的一个局限——更精细的”token 级路由”(每个位置独立路由)是一个潜在的改进方向。
读完本章你能做什么
读完这一章,你应该能够:
- 用自己的话解释 MoE 的核心思想(条件计算、路由器、专家)
- 描述 ES_MoE v1-v4 的演进路线和每一版解决的问题
- 说出 YOLO-Master 在 YOLOv8 上的具体改动(哪些模块被替换)
- 理解负载均衡和 Z-Loss 的作用
- 列举至少 2 个犀牛鸟可能的贡献方向
到此为止,我们已经深入了解了赛道内 5 个核心项目。下一章我们把它们放在一起做一个全方位的对比。
导读目录:README.md 上一章:第 10 章 · mmdetection 下一章:第 12 章 · 跨项目对比分析