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

第 6 章 · NMS 与端到端检测——去掉最后一个后处理瓶颈

开场:照片去重的烦恼

你出去旅游拍了 500 张照片,回来整理时发现:同一个景点从同一个角度拍了 5-6 张几乎一样的照片。你需要从中挑出最好的一张,删掉其余的。这个”去重”过程就是目标检测中 NMS 的日常类比。

检测模型在 8400 个位置做预测后,同一个目标可能被多个位置检测到——产生了一堆重叠的框。NMS(Non-Maximum Suppression,非极大值抑制)就是负责”从重叠的框中只保留最好的那个”的后处理步骤。

听起来很简单,但这个简单的步骤却是目标检测流水线中最大的”不优雅”之处——它不可微、有超参数、延迟不确定、在边缘场景下表现脆弱。消除 NMS,实现真正的端到端检测,是检测领域过去几年最活跃的研究方向之一。


NMS 到底在做什么?

标准 NMS 算法

输入:N 个检测框,每个有 (x1, y1, x2, y2, score, class)
参数:IoU 阈值 T(通常 0.45-0.7)
输出:过滤后的 M 个框(M << N)

算法:
1. 按分数(score)从高到低排序
2. 取出分数最高的框 B_best,加入最终结果
3. 计算 B_best 和所有剩余框的 IoU
4. 删掉 IoU > T 的框(它们被认为检测的是同一个目标)
5. 重复 2-4,直到没有剩余框

用代码来看:

def nms(boxes, scores, iou_threshold=0.45):
    """标准 NMS 实现"""
    # 按分数从高到低排序
    order = scores.argsort(descending=True)
    keep = []  # 保留的框索引

    while order.numel() > 0:
        i = order[0]           # 取出当前最高分的框
        keep.append(i)

        if order.numel() == 1:
            break

        # 计算最高分框和所有剩余框的 IoU
        ious = compute_iou(boxes[i], boxes[order[1:]])

        # 只保留 IoU < 阈值的框(不同目标的框)
        mask = ious < iou_threshold       # ← 关键:IoU 超过阈值的被删掉
        order = order[1:][mask]

    return keep

一个具体的例子

假设图中有一只猫,模型产生了 5 个框:

框 A: score=0.95, IoU(A,GT)=0.88     ← 最好的框
框 B: score=0.87, IoU(B,GT)=0.82     ← 重叠框
框 C: score=0.72, IoU(C,GT)=0.75     ← 重叠框
框 D: score=0.60, IoU(D,GT)=0.30     ← 另一个位置的低分框
框 E: score=0.45, IoU(E,GT)=0.91     ← 高 IoU 但低分

NMS 过程(阈值 0.5):
  Step 1: 取 A (0.95)。A 和 B 的 IoU=0.78 > 0.5 → 删 B
                       A 和 C 的 IoU=0.65 > 0.5 → 删 C
                       A 和 D 的 IoU=0.20 < 0.5 → 保留 D
                       A 和 E 的 IoU=0.85 > 0.5 → 删 E
  Step 2: 取 D (0.60)。无剩余框。

  结果: 保留 [A, D]

NMS 的五大问题

问题一:IoU 阈值是超参数

IoU 阈值需要手动设定,而且对检测质量影响很大。阈值太高(如 0.7):相邻的不同目标可能被误杀(两个紧挨着的人的框 IoU 可能超过 0.7)。阈值太低(如 0.3):同一目标的重复框保留下来太多。没有一个”对所有场景都好”的阈值。

问题二:不可微

NMS 涉及排序、比较、删除等离散操作,无法传播梯度。这意味着NMS 无法和模型一起端到端训练。模型学到的是”怎么产生好的框”,但它无法学到”怎么避免产生重复的框”——因为 NMS 把这个任务从训练中隔离了。

问题三:推理延迟不确定

NMS 的时间复杂度取决于保留的框数——目标越多,NMS 越慢。在密集场景(如一群鸟、拥挤人群)下,NMS 的耗时可能超过模型推理本身。这对实时系统很不友好——你无法保证每一帧的延迟都在限定范围内。

稀疏场景(5 个目标):  NMS 耗时 ~0.5ms
中等场景(50 个目标): NMS 耗时 ~5ms
密集场景(500 个目标): NMS 耗时 ~50ms   ← 可能超过模型推理时间

问题四:密集遮挡时的误杀

当两个不同目标严重重叠时(比如一个人抱着另一个人),它们的框 IoU 可能很高。NMS 会把其中一个删掉——这是真实的漏检,不是重复框。

问题五:串行计算

标准 NMS 是一个贪心算法,必须串行执行(每一步依赖上一步的结果)。在 GPU 上不好并行化。虽然有 batched NMS 等优化,但本质上仍然是串行逻辑。


NMS 的改进方案

在完全去掉 NMS 之前,学术界尝试了多种 NMS 的改进。

Soft-NMS

Soft-NMS 不直接删掉重叠框,而是根据 IoU 按比例降低它们的分数。IoU 越高,分数衰减越多。这样在密集场景下,被遮挡目标的框不会被完全删掉,只是分数变低——如果它本身分数够高,仍然能保留。

# Soft-NMS: 不删除,只降分
for remaining_box in remaining:
    iou = compute_iou(best_box, remaining_box)
    # 高斯衰减:IoU 越高,分数衰减越多
    remaining_box.score *= exp(-iou^2 / sigma)

DIoU-NMS

标准 NMS 用 IoU 判断两个框是否”指向同一个目标”。DIoU-NMS 改为同时考虑 IoU 和两个框中心点的距离——如果两个框虽然 IoU 高但中心点差很远,可能是不同的目标,不该删掉。

问题依然存在

这些改进缓解了 NMS 的一些问题,但没有从根本上解决:NMS 仍然是一个不可微的后处理步骤,仍然有超参数,仍然无法和模型端到端训练。


方案一:YOLOv10 的 NMS-Free——双头策略

核心思路

YOLOv10 的方案在上一章已经介绍了正样本分配的部分。从 NMS 的视角来看,它的核心思路是:

让模型在训练时就学会”不产生重复框”,这样推理时就不需要 NMS 来去重。

具体做法:用两个检测头(one2many 和 one2one)同时训练。one2many 用 TAL 分配,提供丰富的梯度信号让 backbone 学好特征。one2one 用匈牙利匹配,强制每个目标只有一个预测,学会”自己去重”。

训练时的损失函数:
  Total Loss = Loss_one2many (TAL) + Loss_one2one (Hungarian)
               ↓                       ↓
         教模型"检测"              教模型"去重"

推理时:
  只用 one2one 分支的输出 → 天然无重复 → 不需要 NMS

实际效果

YOLOv10 在 COCO 上的精度和 YOLOv8 相当,但推理延迟更低(省掉了 NMS 的时间)且完全确定(不受目标数量影响)。在目标数量很多的场景下优势更明显。

局限性

双分配策略的训练成本更高(额外的 one2one 分支和匈牙利匹配计算),而且 one2one 分支在极密集场景下的表现仍有待验证。


方案二:RT-DETR 的天然 NMS-Free——Set Prediction

集合预测范式

DETR 系列从一个完全不同的角度解决了这个问题。DETR 不是”先产生大量框再去重”,而是直接”产生恰好数量的框”。

传统 YOLO 思路:
  8400 个位置都预测 → 大量重复框 → NMS 去重 → 最终结果

DETR 思路:
  300 个 learnable query → 每个 query 负责"找"一个目标 → 匈牙利匹配 → 最终结果
  300 个 query 已经比目标数量多很多,无需 NMS

这里的关键概念是 query:DETR 有一组可学习的向量(比如 300 个),每个向量通过 Transformer Decoder 的 cross-attention 去”查询”图像特征,输出一个检测结果。训练时用匈牙利匹配保证一对一分配,模型自然学会”每个 query 只负责一个目标”。

RT-DETR 的具体实现

# RT-DETR Decoder 简化版
class RTDETRDecoder(nn.Module):
    def forward(self, memory, query_embed):
        # memory: Encoder 输出的图像特征
        # query_embed: 300 个查询向量

        # IoU-aware Query Selection: 从 memory 中选 Top-300 个质量最好的特征
        # 比 learnable query 更好——有了具体的图像信息做初始化

        for layer in self.decoder_layers:  # 6 层 Decoder
            # Self-Attention: query 之间互相通信
            #   → "你已经检测了那个人,我就不重复检测了"
            query = layer.self_attn(query)

            # Cross-Attention: query 和图像特征交互
            #   → "根据图像特征,更新我的检测结果"
            query = layer.cross_attn(query, memory)

        # 输出 300 个预测,每个包含类别 + 框
        return self.class_head(query), self.bbox_head(query)

关键洞察:Decoder 中的 Self-Attention 让 query 之间可以”沟通”——如果 query_5 已经在检测目标 A,query_12 通过 self-attention 会”知道”这件事,从而避免重复检测目标 A。这是 DETR 天然 NMS-Free 的根本原因——去重逻辑被内化到了模型的注意力机制中

DETR 方案的 Trade-off

优势:

劣势:


YOLO-Master 的 NMS 现状与未来可能

当前状态

YOLO-Master 目前仍然使用 NMS 后处理——和 YOLOv8 一样。这是因为 YOLO-Master 的核心创新在 MoE 自适应计算,而非后处理。

MoE 为 NMS 替代提供的可能性

YOLO-Master 的 MoE 架构为”自适应去重”提供了一个有趣的可能方向:

设想:MoE 辅助去重
  路由器在做"判断目标难度"时,已经在感知空间布局
  → 理论上可以让某些专家专门负责"去重"
  → 或者让路由信息指导 NMS 的阈值选择
  → 或者在 MoE 后面接一个轻量的 one2one 头

这还是一个开放的研究方向——把 MoE 的路由信息用于去重,可能比纯匈牙利匹配的方案更高效(因为不需要 O(N^3) 的匹配)。这是犀牛鸟项目中一个高难度但高价值的潜在贡献方向。


NMS-Free 方案的全景对比

维度 标准 NMS Soft-NMS YOLOv10 双分配 DETR 集合预测
方法类别 后处理 后处理 模型内置 模型内置
可微性 部分
超参数 IoU 阈值 IoU 阈值 + sigma
延迟确定性 不确定 不确定 确定 确定
密集场景表现 可能误杀 更好 待验证 良好
训练复杂度 增加 增加(收敛慢)
推理开销 额外时间 额外时间 无额外 无额外

端到端检测的意义

“去掉 NMS”不仅仅是一个工程优化——它代表了目标检测范式的一次根本性转变。

从”检测 + 后处理”到”端到端检测”,意味着:

模型的训练和推理在同一个计算图中完成,所有步骤都是可微的,可以端到端优化。这就像自动驾驶从”分模块开发(感知→规划→控制各自独立优化)”到”端到端学习(一个模型从像素到方向盘)”的转变——虽然更难训练,但上限更高。

对于犀牛鸟参赛者,理解 NMS 和端到端检测的 trade-off,能帮你更好地定位 YOLO-Master 的改进方向——是在 MoE 框架内实现去 NMS?还是把 NMS 优化留给 Ultralytics 的通用层?这取决于你选择的贡献路线。


初学者常见误区

误区一:”NMS 是一个过时的技术,应该尽快抛弃”

→ 正确理解:NMS 虽然有诸多问题,但在大多数实际场景中仍然工作得很好。YOLOv8 + NMS 在 COCO 上的 mAP 与 YOLOv10(无 NMS)相当。NMS 的问题主要在延迟不确定性和密集遮挡场景——如果你的应用不涉及这些场景,NMS 完全够用。工程中”能用且简单”往往比”优雅但复杂”更有价值。

误区二:”端到端 = 更高精度”

→ 正确理解:端到端检测的优势不在精度,而在架构简洁性和推理延迟确定性。在 COCO 标准 benchmark 上,NMS-free 方案的精度和 NMS 方案基本持平(甚至略低)。端到端的真正价值在于:可以和下游任务(跟踪、分割)无缝衔接、推理延迟完全确定、不需要调 NMS 超参数。

误区三:”DETR 的 Self-Attention 完美解决了去重问题”

→ 正确理解:DETR 需要 6 层 Decoder 才能有效去重,前几层仍然会有重复预测。而且 Self-Attention 的复杂度和 query 数量的平方成正比——query 数量太少会漏检,太多会增加计算量。当前 DETR 系列通常使用 300 个 query,这对 COCO(平均 7 个目标/图)来说绰绰有余,但对极密集场景(如人群计数、卫星图像)可能不够。


读完本章你能做什么

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

到此为止,我们已经建立了目标检测的完整基础知识。接下来进入 Part 3——逐个深入赛道内的 5 个核心项目。让我们从工业标杆 Ultralytics 开始。


导读目录:README.md 上一章:第 5 章 · 正样本分配 下一章:第 7 章 · Ultralytics (YOLOv8)