犀牛鸟 2026 研究笔记 estelledc.github.io

第 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

几个关键观察:

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 级路由”(每个位置独立路由)是一个潜在的改进方向。


读完本章你能做什么

读完这一章,你应该能够:

到此为止,我们已经深入了解了赛道内 5 个核心项目。下一章我们把它们放在一起做一个全方位的对比。


导读目录:README.md 上一章:第 10 章 · mmdetection 下一章:第 12 章 · 跨项目对比分析