ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

目标检测后处理核心:NMS原理、缺陷与改进变体全解析

目标检测后处理核心:NMS原理、缺陷与改进变体全解析 干目标检测的人十有八九都在后处理里跟 NMS 打过交道。但说实话很多人对它的理解停留在“把重合的框去掉”这个层面真问到 NMS 是怎么抑制的、多类别怎么做、它到底有哪些坑、改进版本之间有什么区别能讲清楚的人并不多。我早先用 YOLO 系列做项目时也被 NMS 坑过好几次——明明训练时 mAP 看着还行一上密集场景就各种漏检后来仔细把 NMS 的原理和各个变体梳理了一遍才发现很多问题是后处理本身带来的。这篇就把 NMS 从原理到改进完整地总结一遍希望对正在调检测模型的朋友有帮助。1. NMS 到底在干什么从贪心抑制说起1.1 检测框为什么会重叠在聊 NMS 之前得先理解检测框为什么会大量重叠。一个训练好的检测器在推理时会一次性输出几千甚至上万个候选框。以 YOLO 为例输入一张 640×640 的图特征图有三个尺度每个尺度上每个格子预测 3 个锚框总共输出的候选框数量是 25200 个。这些候选框里绝大部分置信度很低是背景框但即使是同一个目标也可能有多个锚框同时命中它而且这些框的中心点微有偏移、尺寸微有差异。换句话说同一目标产生多个高质量候选框本质上是检测器设计造成的——多尺度特征图、多锚框机制、滑动窗口式的密集预测这些设计保证了召回率代价就是输出高度冗余。NMS 就是用来消除这种冗余的。它做的事情很简单从一堆重叠的框里挑出每个目标“最可信”的那个框把其它多余的框干掉。1.2 IoU评判重叠程度的标准NMS 的核心判据是 IoUIntersection over Union也就是两个框的交集面积除以并集面积。这个指标大家都很熟了但有一点值得强调IoU 对框的重叠程度非常敏感两个框哪怕中心点完全一样只要尺寸差一点IoU 就可能掉得很快反过来两个框尺寸相同只要中心偏移稍微大一点IoU 也会显著下降。举个例子两个完全重合的框 IoU1.0两个完全不挨着的框 IoU0而一个 10×10 的框和一个 12×12 的框中心重合时 IoU 100 / (100 144 - 100) ≈ 0.694。这个数值落在常见阈值 0.5 之上所以会被判定为“冗余”。但注意如果这个框再大一点变成 16×16IoU 就只有 100 / (100 256 - 100) ≈ 0.39小于 0.5就不会被抑制了。这个细节很关键——同一个目标因为候选框尺寸差异较大就可能被 NMS 当成两个目标导致重复检测。1.3 标准 NMS 的流程和代码标准 NMS 的流程可以用一句话概括按置信度从高到低排序依次选取当前最高分的框然后删除所有与它 IoU 大于阈值且得分较低的框直到遍历完所有框。用伪代码表示会更清楚输入候选框集合 B对应得分 S阈值 Nt 输出保留框集合 D 1. 按得分对 B 降序排序 2. 选择得分最高的框 b_m加入 D 3. 计算 b_m 与 B 中其余框的 IoU 4. 删除与 b_m 的 IoU Nt 的框 5. 重复 2-4直到 B 为空这个流程写成 Python 也就二三十行核心部分大致如下def nms(boxes, scores, iou_threshold): x1 boxes[:, 0] y1 boxes[:, 1] x2 boxes[:, 2] y2 boxes[:, 3] areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) inter w * h iou inter / (areas[i] areas[order[1:]] - inter) idx np.where(iou iou_threshold)[0] order order[idx 1] return keep很多人写 NMS 时会忽略一个细节如果两个框 IoU 恰好等于阈值应该保留还是抑制不同实现里有和的区别。我在实际对比过 torchvision 的 nms 实现和手写版本后发现这类边界情况虽然出现频率不高但在大规模数据集上确实会导致少量指标波动。这个取舍没有标准答案但至少你要知道自己用的哪个版本否则复现实验时可能对不上数。1.4 NMS 里的“极大值”指的是什么“非极大值抑制”这个名字里的“极大值”指的是当前候选框集合里得分最高的那个框。抑制的基本逻辑是得分最高的框已经是最可信的了其它跟它高度重叠的框大概率是在重复描述同一个目标因此得分低的那些应该被剔除。这个逻辑在大多数场景下成立但它隐含了一个假设得分高 定位准。这个假设恰恰是 NMS 一系列缺陷的根源。分类得分衡量的是“这个框里有没有目标、是什么目标”而不是“这个框边界贴合目标有多准”。一个框可能分类得分很高但定位偏移严重另一个框分类得分稍低但边界非常精准。标准 NMS 只看得分不看定位精度于是经常把好框误杀留下一个烂框。这个问题后面讲 Softer-NMS 时会再展开。2. 多类别 NMS一个容易被忽视的细节2.1 多类别 NMS 是分开做还是统一做很多初学者会问检测器输出多个类别NMS 是在所有类别上统一做还是在每个类别上单独做答案很明确多类别 NMS 是在每个类别上独立进行的。也就是说最终输出的检测结果里类别 A 的框只会跟类别 A 的框做抑制不会去抑制类别 B 的框。为什么这么设计根本原因是同一物理目标在某个置信度阈值下只会被预测成一个类别而不同类别的框重叠往往意味着它们是不同目标只是恰好挨得近。比如一张图里有一只猫和一条狗它们可能距离很近框有重合但 NMS 不应该因为它们重合就删掉其中一个。如果在所有类别上统一做 NMS高得分的猫框会直接把狗框干掉召回率会掉得非常厉害。2.2 实现上的两种做法多类别 NMS 的工程实现有两种常见做法。第一种是循环每个类别分别调用单类别 NMS。这种写法直观但缺点是 Python 循环在类别多时效率不高。Coco 数据集有 80 个类别每类都过一次 NMS 函数如果函数内部还有 Python 级别的开销整体耗时会成为瓶颈。第二种做法更高效把所有类别的框合并到一起按类别加一个偏移量然后对所有框一次性按“类别偏移后的坐标”排序和计算。具体来说可以给每个类别的框的坐标加上一个很大的偏移比如 4096 × 类别索引让不同类别的框在空间上天然分开这样一次 NMS 就等价于所有类别分别做 NMS。torchvision 和大部分深度学习框架用的是这个思路。下面是这种 batched 实现的大致写法def batched_nms(boxes, scores, labels, iou_threshold): max_coordinate boxes.max() offset labels.to(boxes.dtype) * (max_coordinate * 2 1) boxes_offset boxes.clone() boxes_offset[:, 0] offset boxes_offset[:, 1] offset boxes_offset[:, 2] offset boxes_offset[:, 3] offset return nms(boxes_offset, scores, iou_threshold)这种实现方式有个细节需要注意偏移量要足够大保证不同类别的框经过偏移后绝不重叠。max_coordinate * 2 1是一个常用的经验值因为一张图里任意两个框的最大坐标差不会超过最大坐标值的两倍再加上 1 就完全错开了。2.3 多类别场景下的两个坑多类别 NMS 看起来简单实际操作中有两个坑很容易踩。第一个坑是背景类。有些检测器尤其是用 Softmax 做分类头的会输出一个背景类但 NMS 时背景类不应该参与。如果你在实现里直接对所有类别做 NMS背景类的高分框会把前景框全干掉因为背景框通常覆盖面积特别大。解决方法是过滤掉背景类再对剩下的类别做多类别 NMS。第二个坑是类别置信度阈值截断。很多框架在 NMS 之前会先做一次低分过滤比如 conf_thres0.25但不同类别的最优阈值往往不一样。比如“斑马线”这类类别因为视觉特征明确低置信度也可能很准而“人”这类大姿态变化的类别低置信度框通常不可信。如果所有类别共用同一个阈值就会在部分类别上出现漏检。这个问题在类别不平衡的数据集上尤其明显我见过有人直接在每类置信度上乘以一个类别权重再做 NMS效果比统一阈值好不少。3. NMS 的四大缺陷被“非极大值”掩盖的问题3.1 缺陷一阈值敏感一步错步步错NMS 对 IoU 阈值极其敏感。阈值设得高比如 0.7抑制力度弱保留的框多容易产生重复检测阈值设得低比如 0.3抑制力度强删除的框多又容易漏检。最理想的情况是阈值恰好等于“同一目标两个框的 IoU 下限”但这个值在真实数据里根本无法预先知道。我之前跑过一个行人检测的实验同一个模型NMS 阈值从 0.4 调到 0.6mAP 能波动 2~3 个点。这在已经训练好的模型上是非常大的变化因为模型本身的精度提升往往也就一两个点。更麻烦的是不同数据集的最优 NMS 阈值不一样密集场景需要更高阈值防止误杀稀疏场景需要更低阈值防止重复。一个固定阈值很难通吃所有场景。3.2 缺陷二密集场景下的误杀这是 NMS 最著名的缺陷论文里叫“the misalignment between classification score and localization quality”也好叫“suppression failure in crowded scenes”也好本质都是一个问题两个不同目标靠得很近时它们的 IoU 可能极高NMS 会把得分较低的框直接干掉哪怕它是另一个目标的检测框。举一个最经典的例子一群人站在一起左后方一个人被右前方的人挡住了一半身体这两个人的检测框有很大面积重合。假设左后方那个人的检测框分类得分是 0.8右前方那个人的是 0.9标准 NMS 在 IoU 0.5 时会毫不犹豫删掉 0.8 那个框于是漏检了一个人。这个现象在行人检测、车辆拥堵场景、密集人群计数里尤其严重。CrowdHuman 数据集上标准 NMS 的漏检率明显高于改进方法原因就在于此。3.3 缺陷三分类得分不等于定位精度前面已经提到标准 NMS 用分类得分作为“谁更可信”的标准但定位精度和分类得分并没有直接关系。我遇到过一个很典型的 case检测戴口罩的人时同一个人的两个框一个框把嘴巴和鼻子完整包住但分类得分只有 0.75另一个框只框住了额头区域分类得分却高达 0.92。标准 NMS 会把 0.92 的框留下来0.75 的框删掉结果输出一个不完整的检测框。现代检测器普遍存在这个问题。像 FCOS、ATSS 这类 anchor-free 方法分类分支和回归分支是平行的两个头它们的输出分布并没有强对齐。有些工作比如 IoU-aware会额外预测一个 IoU 分支然后用分类得分乘以 IoU 预测值作为 NMS 的排序依据就是为了缓解这个问题。3.4 缺陷四硬抑制缺乏全局最优意识标准 NMS 每一步都是“当下最优”的选择取当前最高分框删除与它冲突的框。但局部最优不能保证全局最优。考虑一个复杂场景A 框得分 0.9和 B 框得分 0.8的 IoU 是 0.6B 框和 C 框得分 0.95的 IoU 是 0.6但 A 和 C 的 IoU 只有 0.2。标准 NMS 先选中 A因为 A 最高分的前一步 A 可能不是最高分——注意这里要按得分排实际上顺序是先按得分排序C 0.95 最高先选 C然后删除与 C IoU 0.5 的所有框B 被删接着选 A。最终结果是保留了 C 和 A删掉了 B。但如果人类来判断保留 A 和 B 才是更好的组合因为 B 和 A 虽然重叠但保留两个框覆盖了两个目标。这说明标准 NMS 本质上是一个贪心算法它只在每一步做局部最优决策无法回溯、无法修正。当框之间的重叠关系比较复杂时贪心策略很容易做出不太合理的决定。很多改进 NMS 的工作比如基于图模型的、基于学习的都是朝着“考虑全局”的方向走。4. NMS 改进思路地图沿着四条路线演进4.1 路线一从硬抑制到软抑制标准 NMS 把与高分框重叠的框直接丢弃这是“硬抑制”。软抑制的思路是不要一棍子打死而是根据重叠程度降低这些框的得分。重叠越狠降分越猛重叠较少降分也少。这样一个得分原本 0.8 的框因为重叠被降到了 0.3只要置信度阈值是 0.25它还是能留下来如果被降到了 0.1就直接被过滤掉了。这个思路的直接影响是密集场景下被主框“连累”的次优框有更大机会存活下来从而提升召回率。Soft-NMS 是这条路线最经典的代表。软抑制的代价也显而易见如果没有精确的置信度阈值配合会产生更多的假阳性框。所以实际使用 Soft-NMS 时通常需要把最终的置信度阈值稍微调高一点或者使用类别感知的阈值。4.2 路线二从 IoU 到距离度量标准 NMS 用 IoU 度量两个框的重叠程度但 IoU 有一个明显短板当两个框没有交集时IoU0无法区分“完全不相干”和“略有重叠”。而且 IoU 对框的尺寸变化不敏感两个尺寸差异很大的框即使中心点完全重合IoU 也可能不高。改进方向是引入更丰富的几何信息其中最成功的是距离度量。DIoU-NMS 在 IoU 的基础上引入两个框中心点的归一化距离把“重叠程度”扩展成“重叠 接近程度”。这样做的好处是对于密集场景中两个中心点很近的框即使 IoU 不大但只要距离够近也会被抑制从而减少对同一目标的重复检测。这类改进对 anchor 的分布比较敏感。如果你的检测器本身输出的框中心偏移很大距离度量可能反而会误抑制。我实测下来DIoU-NMS 在行人检测上的收益比较明显但在一般物体检测比如 COCO 上提升有限。4.3 路线三从固定阈值到自适应/学习式决策标准 NMS 的阈值是人工设置的超参数难以自适应不同场景。学习式 NMS 的思路是不再依赖人工设定阈值而是让网络自己学习“哪些框应该被抑制”。这一类方法里有代表性的包括Relation-NMS用 attention 机制建模框与框之间的关系让网络决定哪些框应该被删除。Learnable NMS把 NMS 建模成可微的网络层在训练时端到端优化。Matrix NMS用矩阵运算并行化地计算框与框之间的抑制关系代替循环。学习式方法的优势是上限高因为它能根据数据自动学习抑制策略不受人工规则的局限。但劣势也突出实现复杂度高、训练不稳定、推理时可能还要额外的前向传播工程上落地成本较大。4.4 路线四从固定框到不确定性感知这条路线主要解决“得分高但定位差”的问题。代表方法是 Softer-NMS它让检测器额外预测每个框的定位不确定性方差然后在 NMS 阶段对于置信度接近的多个框用方差加权的方式融合它们的坐标得到一个更精准的最终框。换句话说这不是“删掉哪些框”的问题而是“如何综合多个框的信息”。标准 NMS 留下的框直接来自某个候选框Softer-NMS 留下的框是多个候选框的加权平均结果。这个思路在边界框回归精度要求高的场景如自动驾驶的 3D 检测里效果突出。4.5 各方法核心对比一览方法核心思想改进侧重实现难度适用场景标准 NMS贪心 硬抑制基准低通用Soft-NMS重叠降分召回率低密集场景DIoU-NMS中心距离惩罚重复抑制低行人/车辆密集Softer-NMS方差加权融合定位精度中高精度框回归Relation-NMS关系建模全局决策高复杂遮挡Matrix NMS并行抑制推理速度中视频/实时场景5. 主流 NMS 变体的代码级解读5.1 Soft-NMS降分而不是删除Soft-NMS 的改动很小核心就是在 NMS 循环里对被抑制的框不直接删除而是乘以一个权重函数。权重函数有两种常见形式线性函数和高斯函数。线性函数def soft_nms_linear(boxes, scores, iou_threshold, sigma0.5, score_threshold0.001): x1, y1, x2, y2 boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1) iou inter / (areas[i] areas[order[1:]] - inter) weight np.where(iou iou_threshold, 1.0 - iou, 1.0) scores[order[1:]] * weight # 重新排序 new_order order[1:][scores[order[1:]] score_threshold] new_order new_order[np.argsort(scores[new_order])[::-1]] order np.concatenate(([i], new_order)) return keep高斯函数形式更常用权重是weight np.exp(-(iou * iou) / sigma)。从实际效果看sigma0.5是论文里的默认值但具体数据上需要调。线性函数的优势是更直观但边界处不光滑高斯函数更平滑但多了一个 sigma 超参。我自己实测下来的经验Soft-NMS 对 mAP 的提升通常在 0.5~1.5 个点之间但在密集场景的提升更明显可以达到 2~3 个点。它几乎不增加推理耗时因为只是在 NMS 循环里多了几次乘法和比较。所以如果你的部署环境允许改后处理Soft-NMS 是一个非常值得先试的方案。注意Soft-NMS 在低置信度框本身就很多的数据上会显著增加假阳性。建议配合提升 score_threshold或者在验证集上重新搜索阈值。5.2 DIoU-NMS把中心距离加进惩罚DIoU-NMS 的改进思路来自 DIoU loss 的几何直觉。它的惩罚项是 IoU 减去中心点距离的归一化值公式为penalty iou - (distance_center^2) / (diagonal_length^2)其中distance_center是两个框中心点的欧氏距离diagonal_length是包含两个框的最小外接矩形的对角线长度。这个值落在 [-1, 1] 区间DIoU-NMS 在penalty 阈值时抑制这个框。实现时需要注意计算 penalty 需要额外算中心点和最小外接框比纯 IoU 多一点计算量但整体开销仍然很小几乎可以忽略。def diou_nms(boxes, scores, iou_threshold): x1, y1, x2, y2 boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1) iou inter / (areas[i] areas[order[1:]] - inter) # 中心距离惩罚 center_x1 (x1[i] x2[i]) / 2 center_y1 (y1[i] y2[i]) / 2 center_x2 (x1[order[1:]] x2[order[1:]]) / 2 center_y2 (y1[order[1:]] y2[order[1:]]) / 2 dc (center_x1 - center_x2) ** 2 (center_y1 - center_y2) ** 2 c_x1 np.minimum(x1[i], x1[order[1:]]) c_y1 np.minimum(y1[i], y1[order[1:]]) c_x2 np.maximum(x2[i], x2[order[1:]]) c_y2 np.maximum(y2[i], y2[order[1:]]) c_diag (c_x2 - c_x1) ** 2 (c_y2 - c_y1) ** 2 diou iou - dc / c_diag idx np.where(diou iou_threshold)[0] order order[idx 1] return keepDIoU-NMS 有一个比标准 NMS 更“激进”的特性它不只抑制那些 IoU 很高但中心偏远的框还会抑制那些 IoU 不高但中心距离极近的框。这意味着它对“中心漂移”的候选框更宽容但同时也可能误伤一些中心靠得很近的不同目标。因此 DIoU-NMS 的阈值通常要比标准 NMS 稍微放宽一点比如从 0.5 放宽到 0.6否则密集场景会变得过于激进。5.3 Softer-NMS让框“软合并”Softer-NMS 不做抑制决策的修改而是改变“保留框的产生方式”。它有两个核心点第一检测器在预测边界框的同时还要预测每个框的定位方差。这个方差表示模型对边界坐标的置信程度方差越大表示模型越不确定。第二在 NMS 阶段对于若干被判定为“冗余”的框不再直接删掉而是用它们的坐标和方差做加权平均产生一个新的坐标。权重的计算方式是方差的倒数方差越小的框权重越大。加权平均的公式大致如下def softer_nms_fusion(boxes, variances): # boxes: [N, 4] # variances: [N, 4] 每个坐标的方差 weights 1.0 / variances fused_box (boxes * weights).sum(axis0) / weights.sum(axis0) return fused_box这个逻辑很像在多个“带噪声的测量值”之间做最优融合。方差小的框被认为是更准确的测量因此贡献更大方差大的框被认为是噪声更大的测量因此贡献小。Softer-NMS 对定位精度的提升在那些边界框回归头本身预测就很不稳定的模型上最明显。但如果回归头已经非常精准Soft-NMS 能带来的提升就非常有限因为多个框的加权平均值并不会显著好于单个最优框。这是使用 Softer-NMS 前需要判断的一点。5.4 Matrix NMS让抑制并行化Matrix NMS 是为了解决 NMS 在视频流和超大分辨率场景下的耗时问题。它的核心思想是把 NMS 的循环抑制过程转变成一次矩阵运算。具体做法是构造一个 N×N 的矩阵M[i][j] 表示第 i 个框和第 j 个框的 IoU然后通过一系列矩阵操作包括排序、累乘、更新一次性算出每个框最终被保留的概率。这样就把原来需要串行循环的抑制过程变成了可以批量并行执行的矩阵计算。Matrix NMS 在深度学习框架里有明显优势因为它可以完全摆脱 Python 循环用 GPU 矩阵运算直接完成。在大规模检测框比如一万个框以上时速度能比标准 NMS 快一个数量级。不过需要注意Matrix NMS 的抑制结果和标准 NMS 并不完全等价因为它对“抑制传播”的建模方式不同。如果项目对精度要求严苛建议先在验证集上对比一下差异。6. 工程落地实操选型、调参与加速经验6.1 评估指标决定 NMS 变体的选择选 NMS 变体时先看你的评估指标是什么。如果评测用的是 mAP0.5那么 NMS 的影响可能不大因为 0.5 的 IoU 门槛对定位精度要求不高标准 NMS 只要阈值适中基本都能选出一个能用的框。但如果评测用的是 mAP0.75COCO 的主指标定位精度就很重要了此时 Softer-NMS 这类能提升框质量的方案就更值得考虑。如果评测指标是 ARAverage Recall也就是只看召回率那么 Soft-NMS 和 DIoU-NMS 是更好的选择因为它们在保留更多候选框方面更友好。实际项目里AR 和 mAP 往往是此消彼长的你需要根据业务场景做取舍漏检更严重就牺牲一点精确率换召回误检更严重就反过来。6.2 NMS 在推理管线中的位置很多人忽略一个问题NMS 是放在类别过滤之前还是之后常规做法是先做低分过滤conf_thres再做 NMS最后再做类别过滤如果要过滤特定类别的话。低分过滤放在 NMS 之前可以显著减少参与 NMS 的框数量降低计算量。举个具体例子在 YOLOv5 的部署流程里模型输出 25200 个框直接全量做 NMS耗时约 10ms如果先过滤掉置信度低于 0.25 的框通常只剩几百个框NMS 耗时降到 1ms。这个优化对于 CPU 部署尤其重要。但它有一个副作用如果低分过滤阈值设得过高一些真实的低置信度目标会被直接截断即便 NMS 本来可能保留它们。所以阈值的选择需要在速度和召回率之间做权衡。6.3 向量化实现和性能实测手写 Python NMS 的最大问题是慢核心瓶颈是for循环和每次迭代都要重新排序。在候选框只有几百个时还好但候选框一旦上千Python 循环就会拖慢整个推理链路。优化手段有两个方向。第一是向量化把循环内的 IoU 计算整体替换成矩阵运算避免逐框计算。第二是减少循环次数提前按得分排序然后只在得分变化剧烈的地方截断因为低得分的框在高阈值下通常不影响 NMS 结果。在相同硬件单核 CPU下做一个简单的对比纯 Python 循环 NMS 处理 1000 个框需要约 12ms向量化 NMS 处理同样数量只需要约 2ms而 batched 多类别 NMS 配合提前过滤可以从 12ms 压到 1.5ms 左右。这个差距在实时视频流场景下是致命的。6.4 工程上常见的四个调参问题第一个问题是 NMS 阈值该取多少。COCO 上官方基线常用 0.5但这只是因为历史原因PASCAL VOC era 用 0.5不代表各个数据集都最优。实际调参建议在验证集上画一条 “NMS 阈值 vs mAP” 的曲线一般来说是一条单峰曲线峰值出现在 0.45~0.6 之间。我跑过的多数检测器在 0.55 附近会比 0.5 好一些但这不是绝对规律。第二个问题是多类别阈值是否要按类分开。在训练数据类别不平衡时对所有类别用同一个 conf_thres 是不合理的。一个可行的做法是在验证集上按类统计 Precision-Recall 曲线找到每个类的最优工作点然后设置类别相关的 conf_thres 或 NMS 阈值。这个方法实现起来不难但对部分稀有类别的召回提升很明显。第三个问题是 TTATest-Time Augmentation后的 NMS 怎么做。多尺度测试之后同一个目标会有多个来自不同尺度的重复框此时 NMS 之前需要对所有尺度的框进行坐标统一归一化再合在一起做一次 NMS。这里的陷阱是不同尺度产生的框IoU 差异可能很大直接合在一起做 NMS 容易出现同一目标保留两个框的情况。我实测下来TTA 后用 NMS 阈值 0.4 而不是 0.5能有效减少这种重复。第四个问题是训练时要不要加 NMS。很多人训练检测器时完全不管 NMS只在推理时使用。但这样会导致训练和推理的不一致训练时模型看到的是没有经过 NMS 的密集预测输出推理时却要做 NMS。这种不一致在极端情况下会放大 NMS 带来的误杀。建议是在训练后期比如最后 10 个 epoch在验证集上逐步调整 NMS 超参让训练结束时的模型已经适应推理时的后处理方式。6.5 一个可落地的选型建议如果只让我推荐一个通用方案我会选 Soft-NMS DIoU-NMS 的组合用 DIoU 的几何信息做抑制决策用 Soft 机制保留密集场景的召回。具体公式可以写成penalty iou - (dc / c_diag) weight exp(-penalty * penalty / sigma)不过这一类自定义 NMS 在工程化时需要考虑部署框架的支持。有些部署框架如 TensorRT里没有现成的自定义 NMS 算子你需要自己写 plugin 或者在预处理阶段用 Python/shell 脚本来实现。如果是纯 python 的推理 pipeline比如用 PyTorch 做服务端推理那改起来相对容易如果是 C 部署就要评估手写 NMS 的成本。我个人的经验是先用标准 NMS 跑一套 baseline记录 mAP、AR、推理耗时三项指标然后依次替换 Soft-NMS、DIoU-NMS、Softer-NMS每替换一个都在验证集上看这三项指标的变化。如果某一项指标提升但另一项明显下降就要回到业务需求判断优先级而不是盲目追求 mAP 单一数字。这样选出来的方案才是真正贴合项目需求的方案而不是看起来“最新最先进”的方案。
返回列表