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

第 8 章 · YOLOv10——NMS-Free 与效率优化的工程实践

开场:不只是去掉 NMS

上一章我们了解了 Ultralytics 这个”主程序”。YOLOv10 是在 Ultralytics 基础上的一次重要升级——但它的贡献远不止”去掉 NMS”这一件事。YOLOv10 是一次系统性的效率优化工程,几乎在检测器的每一个环节都做了”减法”,同时保持甚至提升了精度。

如果说 Ultralytics/YOLOv8 是一辆成熟的量产车,YOLOv10 就像一次”轻量化改装”——每个零件都审视一遍,该减的减该换的换,最终整车更轻更快但跑得一样远。


YOLOv10 论文的核心主张

YOLOv10 论文的标题是 “Real-Time End-to-End Object Detection”。核心论点是:YOLO 系列在后处理(NMS)和模型架构两方面都还有效率提升的空间。因此 YOLOv10 同时在两个方向做了改进。

方向一:去掉 NMS(后处理优化)
  → 双分配策略(one2many + one2one)

方向二:提升模型效率(架构优化)
  → 一系列轻量化设计:大核卷积、PSA、SCDown、C2fCIB

双分配策略详解

问题回顾

第 6 章讨论了 NMS 的五大问题。YOLOv10 选择从模型内部解决这个问题——让模型在训练时就学会”不产生重复框”。

训练时:两个头同时工作

输入图像
    │
    ▼
  Backbone + Neck → 多尺度特征
    │
    ├──→ one2many 头 (TAL 分配)
    │      每个 GT 分配 ~13 个正样本
    │      提供丰富的梯度 → backbone 学好特征
    │
    └──→ one2one 头 (匈牙利匹配)
           每个 GT 只分配 1 个正样本
           学会"去重" → 推理时直接用这个头

匈牙利匹配的成本矩阵

one2one 头使用匈牙利匹配来保证一对一分配:

# 简化版:one2one 头的匈牙利匹配
def hungarian_assign(pred_boxes, pred_scores, gt_boxes, gt_labels):
    n_pred = len(pred_boxes)
    n_gt = len(gt_boxes)

    # 构建成本矩阵 [n_pred, n_gt]
    cost_cls = -pred_scores[:, gt_labels]          # 分类成本
    cost_box = 1 - compute_iou(pred_boxes, gt_boxes)  # 定位成本
    cost_matrix = cost_cls + 5.0 * cost_box        # 加权组合

    # 匈牙利算法:找到总成本最小的一对一匹配
    pred_indices, gt_indices = linear_sum_assignment(cost_matrix)

    return pred_indices, gt_indices

推理时:只用 one2one 头

# 推理时的流程
def predict(image):
    features = backbone(image)
    features = neck(features)

    # 只用 one2one 头的输出
    cls_scores, bboxes = one2one_head(features)

    # 按分数过滤低质量预测
    mask = cls_scores.max(dim=-1).values > conf_threshold
    return bboxes[mask], cls_scores[mask]

    # 注意:没有 NMS 步骤!

双分配的效果

在 COCO 上,YOLOv10 和 YOLOv8 的精度基本持平(差距在 0.3 mAP 以内),但推理延迟更低且更稳定。

YOLOv8s:  模型推理 6.1ms + NMS ~2ms = 总计 ~8.1ms(NMS 时间不确定)
YOLOv10s: 模型推理 6.5ms + 后处理 0ms = 总计 ~6.5ms(完全确定)

效率驱动的架构设计

YOLOv10 不只是去掉了 NMS。它还对 YOLOv8 的架构做了一系列”减脂增肌”的改动。

轻量化分类头

YOLOv10 发现:分类头和回归头在计算量上严重不对称。分类头的参数和计算量占比远超回归头,但分类任务本身比回归简单得多。

YOLOv8 的分类头: 2 × (3×3 Conv, BN, SiLU) → 大量计算
YOLOv10 的改进:  用更少的通道 + 深度可分离卷积替代

效果: 分类头的计算量减半,精度几乎不变

C2fCIB:紧凑反转 Bottleneck

YOLOv10 把 C2f 中的标准 Bottleneck 替换为 CIB(Compact Inverted Bottleneck):

# 标准 Bottleneck(YOLOv8):
#   x → 1×1 Conv(降通道)→ 3×3 Conv(特征提取)→ 残差连接

# CIB(YOLOv10):
#   x → 3×3 Depthwise Conv → 1×1 Pointwise Conv
#     → 3×3 Depthwise Conv → 1×1 Pointwise Conv → 残差连接
#
#   用深度可分离卷积替代标准卷积,大幅减少参数量

class CIB(nn.Module):
    def __init__(self, c1, c2):
        super().__init__()
        self.dw1 = nn.Conv2d(c1, c1, 3, padding=1, groups=c1)  # Depthwise
        self.pw1 = nn.Conv2d(c1, c2, 1)                         # Pointwise
        self.dw2 = nn.Conv2d(c2, c2, 3, padding=1, groups=c2)  # Depthwise
        self.pw2 = nn.Conv2d(c2, c2, 1)                         # Pointwise

    def forward(self, x):
        return x + self.pw2(self.dw2(self.pw1(self.dw1(x))))

PSA:部分自注意力

YOLOv10 在 Stage 4(最后一个 stage,特征图最小)引入了 PSA(Partial Self-Attention)——只在一部分通道上做自注意力,另一部分直接通过。

class PSA(nn.Module):
    """部分自注意力:只对一半通道做 MHSA"""
    def __init__(self, c, num_heads=4):
        super().__init__()
        self.cv1 = Conv(c, c)
        self.attn = nn.MultiheadAttention(c // 2, num_heads)  # 只用一半通道
        self.ffn = nn.Sequential(
            nn.Linear(c // 2, c * 2),
            nn.GELU(),
            nn.Linear(c * 2, c // 2),
        )
        self.cv2 = Conv(c, c)

    def forward(self, x):
        x = self.cv1(x)
        a, b = x.chunk(2, dim=1)    # 分成两半
        b = self.attn(b, b, b)      # 只对一半做注意力
        b = b + self.ffn(b)         # FFN
        return self.cv2(torch.cat([a, b], dim=1))  # 合并

为什么只在最后一个 stage?因为 Stage 4 的特征图最小(20×20=400 个位置),自注意力的 O(N²) 复杂度可以接受。如果在 Stage 2(80×80=6400 个位置)做自注意力,计算量会爆炸。

SCDown:空间-通道解耦下采样

传统的下采样用 3×3 stride=2 的卷积,同时改变空间分辨率和通道数。YOLOv10 把这两个操作分开:

class SCDown(nn.Module):
    """先降通道(1×1),再降分辨率(DW 3×3 stride=2)"""
    def __init__(self, c1, c2):
        super().__init__()
        self.pw = Conv(c1, c2, k=1)  # 先改通道(便宜)
        self.dw = Conv(c2, c2, k=3, s=2, g=c2)  # 再改分辨率(用 DW 更便宜)

    def forward(self, x):
        return self.dw(self.pw(x))

大核卷积

在某些 stage 中,YOLOv10 把 3×3 深度可分离卷积替换为 7×7 或更大的核——更大的感受野,但由于是 Depthwise 的,参数量增加很少。


完整架构对比

把 YOLOv10 和 YOLOv8 的架构差异汇总:

组件 YOLOv8 YOLOv10 变化原因
Bottleneck 标准 Conv CIB(深度可分离) 减少参数和 FLOPs
分类头 2×Conv (等通道) 轻量化(减通道) 分类任务更简单
下采样 3×3 stride=2 Conv SCDown(通道+空间解耦) 减少冗余计算
Stage 4 纯卷积 PSA(部分自注意力) 增加全局建模能力
后处理 NMS 无(双分配) 去掉延迟不确定性
大核 7×7 DW Conv 更大感受野,低开销

性能数据

模型 参数量 FLOPs COCO mAP 延迟 (T4 TRT FP16)
YOLOv8n 3.2M 8.7G 37.3 6.16ms
YOLOv10n 2.3M 6.7G 38.5 1.84ms
YOLOv8s 11.2M 28.6G 44.9 7.07ms
YOLOv10s 7.2M 21.6G 46.3 2.49ms
YOLOv8m 25.9M 78.9G 50.2 9.50ms
YOLOv10m 15.4M 59.1G 51.1 4.74ms

几个值得注意的数据点:


代码导航

YOLOv10 的代码建立在 Ultralytics 之上,关键修改集中在几个位置:

THU-MIG/yolov10/
├── ultralytics/
│   ├── nn/
│   │   ├── modules/
│   │   │   └── block.py      # 新增 CIB, PSA, SCDown 等模块
│   │   └── tasks.py           # 注册新模块 + one2one 头
│   ├── utils/
│   │   └── loss.py            # 添加 one2one 分支的损失计算
│   └── cfg/
│       └── models/v10/
│           └── yolov10s.yaml  # 新的配置文件

配置文件示例

# yolov10s.yaml 的关键差异(相比 yolov8s.yaml)
backbone:
  - [-1, 1, Conv, [64, 3, 2]]        # 0: stem
  - [-1, 1, Conv, [128, 3, 2]]       # 1
  - [-1, 3, C2fCIB, [128]]           # 2: C2f → C2fCIB(使用 CIB)
  # ...
  - [-1, 1, SCDown, [512, 3, 2]]     # 7: 下采样用 SCDown
  - [-1, 2, C2fCIB, [512]]           # 8
  - [-1, 1, SPPF, [512, 5]]          # 9
  - [-1, 1, PSA, [512]]              # 10: 最后加 PSA

head:
  # ... Neck 部分和 YOLOv8 类似 ...
  # 最后有两个头:
  - [one2many_head]                   # 训练时用
  - [one2one_head]                    # 推理时用

对 YOLO-Master 的启示

YOLOv10 的工程实践对理解 YOLO-Master 有几个重要启示:

启示一:模块级替换的范式

YOLOv10 的方法论是”在 YOLOv8 的架构上,逐模块替换为更高效的版本”。YOLO-Master 做的事情本质上是一样的——只不过替换的内容不是轻量化模块,而是 MoE 模块。

YOLOv10 的替换:  C2f → C2fCIB(更高效)
YOLO-Master 的替换: C2f → MoE_C2f(更智能)

启示二:NMS-Free 不是必须的

YOLOv10 去掉了 NMS,YOLO-Master 没有。这说明在犀牛鸟的评分体系中,NMS-Free 不是一个硬性要求——你可以把精力放在 MoE 的改进上,而不是同时解决 NMS 的问题。

启示三:效率的多维思考

YOLOv10 从后处理、基础模块、注意力机制、下采样等多个维度优化效率。这提醒我们:MoE 虽然可以提升精度,但也增加了参数量和路由开销。如何在”MoE 带来的精度提升”和”MoE 引入的效率成本”之间取得平衡,是 YOLO-Master 贡献的关键。


初学者常见误区

误区一:”YOLOv10 在所有方面都比 YOLOv8 好”

→ 正确理解:YOLOv10 在效率指标上确实全面领先 YOLOv8(更少参数、更低延迟、同等甚至更高的 mAP)。但 YOLOv10 目前的社区支持和文档没有 YOLOv8 成熟(star 数约 11.3K vs 58.5K),工业落地案例也更少。做工程项目时,技术指标不是唯一考量——生态成熟度同样重要。

误区二:”双分配策略让训练变慢两倍”

→ 正确理解:one2one 头的额外开销并不大。匈牙利匹配在 CPU 上执行,典型场景下(COCO 平均 7 个目标/图)只需要微秒级时间。而且 one2one 头的梯度不传给 backbone(只传给 one2one 头自身的参数),所以反向传播的成本增加有限。实测训练时间增加约 10-15%,远不到两倍。

误区三:”PSA 和 Transformer 差不多,所以 YOLOv10 也是 Transformer 检测器”

→ 正确理解:PSA 只在最后一个 stage 的一半通道上使用,占整个模型计算量的很小比例(< 5%)。YOLOv10 仍然是一个以卷积为主的检测器,PSA 只是一个”点缀”——用最小的代价引入全局信息。这和 RT-DETR 那种整个 Encoder/Decoder 都基于 Transformer 的架构有本质区别。


读完本章你能做什么

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


导读目录:README.md 上一章:第 7 章 · Ultralytics (YOLOv8) 下一章:第 9 章 · RT-DETR