第 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 |
几个值得注意的数据点:
- YOLOv10n 的参数量只有 YOLOv8n 的 72%,但 mAP 反而高了 1.2
- YOLOv10s 的延迟只有 YOLOv8s 的 35%(这包含了 NMS 省下的时间 + 架构优化省下的时间)
- FLOPs 的减少幅度(~23%)和延迟的减少幅度(~50-70%)不成正比——因为省掉 NMS 减少了大量不体现在 FLOPs 中的计算
代码导航
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 的架构有本质区别。
读完本章你能做什么
读完这一章,你应该能够:
- 说出 YOLOv10 相比 YOLOv8 的 5 项主要架构改进
- 解释双分配策略的训练和推理流程
- 理解 CIB、PSA、SCDown 各自解决什么问题
- 对比 YOLOv10 和 YOLOv8 的性能数据
- 将 YOLOv10 的”模块级替换”思路映射到 YOLO-Master 的 MoE 替换
导读目录:README.md 上一章:第 7 章 · Ultralytics (YOLOv8) 下一章:第 9 章 · RT-DETR