1. 置信度到底是个什么东西
1.1 从一个真实翻车现场说起
去年帮一个做工地安全帽检测的朋友调模型,他跑过来跟我说:“模型训练完了,mAP看着挺高,但一上线全是误报,安全帽没戴的工人没检测出来几个,反而把塔吊上的灯泡全框成了安全帽。”我让他把推理脚本发过来一看,问题出在一行代码上——他把置信度阈值设成了0.05。这个值意味着模型只要觉得某个框里有5%的概率是目标,就把它输出出来。结果就是满屏的框,真正有用的信息全被淹没了。
这个案例几乎每天都在发生。很多人训练完YOLO,看到验证集指标不错,就以为万事大吉,结果一部署就发现效果完全不是那么回事。置信度(Confidence)这个看似简单的概念,实际上是连接模型训练和实际部署之间最关键的一道阀门。你把阀门开太大,漏检;开太小,误检。找到那个刚刚好的位置,才是真正让模型落地的功夫。
这篇文章我会把YOLO里的置信度从头到尾拆一遍——它是什么、怎么算出来的、训练时怎么参与损失、推理时怎么用、不同版本之间有什么区别、实际部署时怎么调。不管你是刚跑通第一个YOLO demo的新手,还是已经在做边缘部署的老手,应该都能从里面找到一些之前没注意到的细节。
1.2 置信度的数学定义与直觉理解
先给一个严谨但不绕弯的定义。在YOLO系列中,置信度是模型对“这个预测框内是否存在目标”以及“这个框定位有多准”的联合估计。用公式表达就是:
Confidence = P(Object) × IoU(pred, truth)
这里P(Object)表示网格单元内存在目标的概率,IoU(pred, truth)表示预测框和真实框之间的交并比。注意,这是一个乘积关系,不是两个独立输出。很多初学者会误以为置信度就是“分类概率”,其实不是——分类概率是另一个独立的输出(在YOLOv3及以后版本中),置信度只管“有没有东西”和“框得准不准”。
用一个生活化的类比:你让一个实习生在一张照片里找猫。置信度就是他举手说“我找到了”时,你对他这句话的信任程度。如果他平时很靠谱,说找到了基本就是真找到了(高P(Object)),而且他指的位置也很准(高IoU),那这个置信度就高。如果他平时就爱瞎指,或者虽然找到了但框得歪歪扭扭,那置信度就低。
在YOLOv1中,置信度直接就是上面那个乘积,没有独立的分类概率输出,类别概率是乘在置信度上的。从YOLOv2开始,分类概率被分离出来,置信度只负责“objectness”这一件事。到了YOLOv3及以后,每个预测框的输出变成了三个部分:边界框坐标(4个值)、置信度(1个值)、类别概率(N个值,N为类别数)。最终判断一个检测结果是否有效,需要同时看置信度和类别概率。
1.3 置信度与分类概率的区别和联系
这是最容易混淆的地方,我单独拎出来说清楚。
假设你在做一个交通标志检测,类别有“限速”“禁止通行”“停车”三种。模型在某个位置输出了一个预测框,它的输出可能是这样的:
- 边界框坐标:x=0.45, y=0.32, w=0.12, h=0.08
- 置信度:0.87
- 类别概率:[0.92, 0.05, 0.03]
这里的0.87表示模型认为这个框里“有东西”且“框得还行”的综合信心。而类别概率[0.92, 0.05, 0.03]表示,在“有东西”的前提下,这个东西是“限速”的概率是0.92。最终这个检测结果的得分是:
最终得分 = 置信度 × 类别概率 = 0.87 × 0.92 = 0.8004
这个最终得分才是我们做NMS(非极大值抑制)和阈值过滤时用的值。很多部署脚本里写的conf_thres,实际上过滤的是这个乘积,而不是原始的置信度。这一点在YOLOv5的代码里体现得很明显——non_max_suppression函数里会先把置信度和类别概率相乘,得到最终的x[:, 4] * x[:, 5:],然后再做阈值过滤。
注意:不同版本的YOLO在推理后处理时对置信度的使用方式略有差异。YOLOv5/v8的官方代码中,
conf_thres过滤的是置信度乘以类别概率后的值。如果你自己写后处理,一定要搞清楚你过滤的到底是哪个量,否则调参时会完全找不着北。
2. 置信度在训练中是怎么参与计算的
2.1 正负样本的置信度标签分配
训练的时候,模型输出的置信度需要有一个“标准答案”来对比,这个标准答案就是置信度标签。YOLO的做法是:对于每个预测框,如果它负责预测一个真实目标(即它是正样本),那么它的置信度标签就是预测框与真实框的IoU;如果它不负责预测任何目标(即它是负样本),那么它的置信度标签就是0。
这里有一个关键问题:哪些预测框算正样本?这个问题的答案在YOLO的不同版本中经历了很大的演变。
YOLOv1的做法比较粗暴:每个网格单元预测2个框,选择与真实框IoU最大的那个作为正样本,另一个作为负样本。这导致正负样本极度不平衡,而且一个网格只能预测一个目标,对密集场景很不友好。
YOLOv2引入了Anchor机制,每个网格预测多个Anchor框,仍然是选IoU最大的作为正样本。YOLOv3进一步改进,通过设定IoU阈值来分配正负样本——IoU大于阈值的作为正样本,小于另一个阈值的作为负样本,中间的忽略不计。
到了YOLOv5/v8,样本分配策略变成了跨网格分配。具体来说,一个真实目标不仅会分配给它中心点所在的网格,还会分配给周围几个网格中Anchor匹配度较高的预测框。这样做的好处是增加了正样本的数量,缓解了正负样本不平衡的问题,让模型在训练时能获得更多的梯度信号。
2.2 置信度损失函数的设计与演变
置信度的损失函数设计是YOLO系列演进中最能体现工程智慧的部分之一。
YOLOv1用的是平方和误差(SSE),简单直接,但有个大问题:正负样本的损失权重是一样的。由于负样本数量远远多于正样本(一张图里大部分区域都是背景),模型会倾向于把所有框的置信度都预测成0,这样负样本的损失很小,但正样本的损失就爆炸了。YOLOv1的解决办法是给正样本的置信度损失加了一个权重系数λ_coord=5,给负样本的置信度损失加了λ_noobj=0.5。这种手工调权重的做法虽然有效,但不够优雅。
YOLOv3开始引入二元交叉熵(BCE)损失来处理置信度。BCE的公式是:
L = -[y·log(p) + (1-y)·log(1-p)]
其中y是标签(正样本为IoU值,负样本为0),p是预测置信度。BCE的好处是梯度形式更合理,当预测值偏离标签越远时,梯度越大,收敛越快。
YOLOv5/v8在BCE的基础上又引入了Focal Loss的思想(虽然官方没有明确叫Focal Loss,但实现上用了类似的调制因子)。核心思路是:对于那些已经预测得很准的样本(无论正负),降低它们的损失权重,让模型把注意力集中在那些难分的样本上。具体实现上,YOLOv5的置信度损失是:
# 简化示意 bce_loss = F.binary_cross_entropy_with_logits(pred_conf, true_conf, reduction='none') # 正样本权重 pos_weight = true_conf # IoU值越大,权重越大 # 负样本权重 neg_weight = 1.0 - true_conf loss = (pos_weight * bce_loss).sum() / num_pos + (neg_weight * bce_loss).sum() / num_neg这种设计让模型在训练时更关注那些“模棱两可”的预测框,而不是已经确定是背景的区域。
2.3 正负样本不平衡的实际影响
正负样本不平衡是目标检测里的经典问题,在置信度上的体现尤为明显。我做过一个统计:在一张640×640的输入图像中,YOLOv5的三个检测头总共会输出25200个预测框(80×80×3 + 40×40×3 + 20×20×3)。如果图中有10个目标,那么正样本大约只有几十个(考虑跨网格分配),负样本有25000多个。比例大约是1:500。
这种极端不平衡会导致两个问题:
第一,模型容易“偷懒”。因为负样本占了绝大多数,模型只要把所有框的置信度都预测成接近0,就能让总损失很小。但这样正样本的置信度也会被压低,导致推理时大量漏检。
第二,梯度被负样本主导。即使每个负样本的损失很小,25000个加起来也会盖过几十个正样本的梯度。模型参数更新会被负样本“带偏”。
解决这个问题的常见手段包括:Focal Loss(降低易分样本的权重)、OHEM(在线难例挖掘,只选损失最大的负样本参与训练)、正样本加权(给正样本的损失乘一个大于1的系数)。YOLOv5/v8采用的是第一种和第三种的结合,实际效果比较稳。
实操心得:如果你在训练自己的数据集时发现模型收敛后置信度普遍偏低(比如正样本的预测置信度只有0.3-0.4),大概率是正负样本不平衡导致的。可以尝试在损失函数里给正样本的置信度损失加一个权重系数(比如2.0-5.0),或者检查一下你的Anchor设置是否合理——Anchor和真实框的匹配度太低也会导致正样本置信度上不去。
3. 推理阶段置信度的完整处理流程
3.1 从模型输出到最终检测结果的每一步
推理阶段,置信度的处理流程可以拆成四步:模型前向输出 → 置信度过滤 → NMS → 最终输出。每一步都有细节,我逐个说。
第一步:模型前向输出。YOLO模型的原始输出是一个张量,形状通常是[batch, num_anchors, 5+num_classes]。其中5代表4个坐标加1个置信度,num_classes是类别概率。注意,这个置信度是经过Sigmoid激活后的值,范围在0到1之间。
第二步:置信度过滤。这是第一道阀门。对于每个预测框,计算最终得分 = 置信度 × 类别概率的最大值。如果这个得分低于conf_thres,就直接丢弃。这一步通常能过滤掉95%以上的预测框,把计算量降下来。
第三步:NMS(非极大值抑制)。经过置信度过滤后,剩下的框可能还有几百个,其中很多是同一个目标的重复检测。NMS的作用就是把这些重复的框去掉,只保留得分最高的那个。NMS的IoU阈值(iou_thres)通常设在0.45到0.5之间。
第四步:最终输出。经过NMS后,剩下的框就是最终的检测结果。每个框包含坐标、置信度、类别标签和类别概率。
这里有一个容易被忽略的细节:置信度过滤和NMS的顺序。有些实现是先做NMS再做置信度过滤,有些是反过来。YOLOv5的官方实现是先做置信度过滤再做NMS,这样做的好处是减少NMS的计算量。但如果你先做NMS,可能会因为低置信度的框参与了IoU计算,导致一些高分框被错误抑制。所以推荐先过滤再NMS。
3.2 conf_thres和iou_thres的配合关系
conf_thres和iou_thres是两个需要配合调整的参数,单独调一个往往达不到最优效果。
conf_thres控制的是“什么样的框有资格进入NMS”。设得太高,漏检;设得太低,NMS的输入框太多,计算慢,而且容易把一些低质量的框保留下来。
iou_thres控制的是“什么样的框算重复”。设得太高(比如0.7),同一个目标的多个框可能都被保留,导致重复检测;设得太低(比如0.3),相邻的两个不同目标可能被错误合并,导致漏检。
我一般会这样配合调整:先把iou_thres固定在0.45(这是YOLOv5的默认值,对大多数场景都适用),然后单独调conf_thres。等conf_thres找到一个比较合适的值后,再微调iou_thres。如果发现同一个目标经常出现多个框,就把iou_thres调低一点;如果发现相邻目标经常被合并,就把iou_thres调高一点。
下面这个表格是我在不同场景下常用的参数组合,供参考:
| 场景类型 | conf_thres | iou_thres | 说明 |
|---|---|---|---|
| 通用检测 | 0.25 | 0.45 | YOLOv5默认值,适合大多数场景 |
| 高精度优先 | 0.5-0.6 | 0.5 | 宁可漏检不可误检,如安防报警 |
| 高召回优先 | 0.1-0.15 | 0.4 | 宁可误检不可漏检,如医疗筛查 |
| 密集小目标 | 0.2 | 0.3-0.35 | 降低IoU阈值避免相邻目标被合并 |
| 单目标检测 | 0.3 | 0.5 | 场景简单,可以适当提高阈值 |
3.3 不同YOLO版本的置信度后处理差异
YOLOv5和YOLOv8在置信度后处理上有一个重要区别:YOLOv8取消了objectness分支。
在YOLOv5中,每个预测框的输出是[tx, ty, tw, th, obj, cls1, cls2, ...],其中obj就是置信度。而在YOLOv8中,输出变成了[tx, ty, tw, th, cls1, cls2, ...],没有独立的置信度分支了。YOLOv8的做法是直接用类别概率的最大值作为置信度。这个改动的原因是:作者认为objectness和类别概率在信息上有冗余,去掉objectness可以简化模型结构,同时减少计算量。
实际影响是:YOLOv8的conf_thres过滤的是类别概率的最大值,而不是“objectness × 类别概率”。这意味着在相同阈值下,YOLOv8的输出框数量通常会比YOLOv5多一些。如果你从YOLOv5迁移到YOLOv8,可能需要把conf_thres稍微调高一点(比如从0.25调到0.3)才能达到类似的过滤效果。
YOLOv6和YOLOv7在这一点上更接近YOLOv5,保留了objectness分支。YOLOv9和YOLOv10则各有各的设计,YOLOv10因为采用了无NMS的设计,置信度的处理方式又有不同。但不管哪个版本,核心逻辑是一样的:置信度是模型对“这个框里有目标”的信心,最终过滤时通常要结合类别概率一起考虑。
4. 置信度调参的实战方法论
4.1 用PR曲线找到最优置信度阈值
调conf_thres最科学的方法是看PR曲线(Precision-Recall Curve)。PR曲线展示了在不同置信度阈值下,模型的精确率(Precision)和召回率(Recall)的变化关系。
具体操作步骤:
- 在验证集上跑推理,把所有预测框的置信度和对应的TP/FP标签保存下来。
- 按置信度从高到低排序,逐个作为阈值,计算每个阈值下的Precision和Recall。
- 以Recall为横轴、Precision为纵轴画曲线。
- 找到曲线上的“拐点”——即Precision开始明显下降之前对应的Recall值,那个点对应的置信度就是比较优的阈值。
实际操作中,你不需要自己写代码画PR曲线。YOLOv5/v8的训练脚本在验证阶段会自动生成PR曲线图,保存在runs/val/exp目录下。你打开PR_curve.png,找到曲线拐点,然后看对应的置信度值就行了。
但PR曲线给的是一个“全局最优”的阈值,实际部署时还要考虑业务需求。比如在安防场景,你更看重Precision(减少误报),那就选曲线左侧的点,对应较高的置信度阈值;在医疗筛查场景,你更看重Recall(减少漏检),那就选曲线右侧的点,对应较低的置信度阈值。
4.2 分场景、分类别的置信度策略
一个经常被忽略的事实是:不同类别的检测难度不同,最优置信度阈值也不同。用一个全局阈值来处理所有类别,往往会导致“好检的类别过检、难检的类别漏检”。
举个例子:在一个包含“人”“车”“交通灯”的数据集里,“人”和“车”的特征明显,模型很容易学到高置信度;“交通灯”目标小、特征弱,模型输出的置信度普遍偏低。如果你把全局阈值设成0.5,交通灯可能全被过滤掉了;如果设成0.2,人和车的误检又会增多。
解决办法是分类别设置置信度阈值。在YOLOv5/v8的推理代码中,可以这样实现:
# 假设有3个类别,分别设置不同的阈值 class_conf_thres = [0.5, 0.5, 0.2] # 人、车、交通灯 # 在NMS之前,对每个类别单独过滤 for i in range(num_classes): class_mask = (pred_classes == i) & (pred_scores > class_conf_thres[i]) # 保留满足条件的框这种策略在实际项目中非常实用。我做过一个工业质检的项目,检测“划痕”“凹坑”“脏污”三种缺陷,划痕的特征最明显,阈值设0.6就够了;脏污的特征最弱,阈值要降到0.15才能保证召回。分类别调阈值后,整体F1分数比用全局阈值高了将近8个百分点。
4.3 置信度校准:让输出概率更可信
模型输出的置信度并不总是“校准”的。所谓校准,是指模型输出的置信度应该等于实际正确的概率。比如模型输出0.8的置信度,那么在所有输出0.8置信度的样本中,应该有大约80%是正确的。但实际中,YOLO的置信度往往偏高——输出0.8的样本可能只有60%是正确的。
置信度校准的方法有几种:
温度缩放(Temperature Scaling)是最简单的一种。在模型输出置信度之前,除以一个温度参数T(T>1会让置信度更平滑,T<1会让置信度更极端)。T通过在验证集上优化NLL(负对数似然)来学习。
Platt Scaling是另一种方法,用一个Logistic回归把原始置信度映射到校准后的置信度。
Isotonic Regression是非参数方法,适合数据量较大的情况。
在实际部署中,如果你需要把置信度作为“可信度”展示给用户(比如“这个检测结果有85%的把握”),那校准就很重要。如果只是用来做阈值过滤,校准的收益就没那么大——因为阈值本身也是可以调的,校准相当于把阈值调整的工作转移到了校准参数上。
实操心得:YOLOv5/v8的置信度普遍偏高,尤其是在训练数据量不大(少于5000张)的情况下。如果你发现模型输出一堆0.9以上的置信度但实际误检很多,不要惊讶,这是正常现象。解决办法要么是增加训练数据,要么是做置信度校准,要么就是简单粗暴地把
conf_thres调高。
5. 置信度相关的常见问题与排查
5.1 训练时置信度不收敛怎么办
这是训练阶段最常见的问题。表现是:训练了很多轮,置信度损失下降不明显,或者正样本的预测置信度始终很低(比如一直在0.1-0.3之间徘徊)。
排查思路按优先级排列:
第一,检查数据标注。这是最常见的原因。用LabelImg或类似工具标注时,如果框的位置不准(比如框比目标大很多或小很多),模型学到的IoU就会很低,置信度标签自然就低。打开几张训练图,把标注框画出来看看,是不是每个框都紧贴目标边缘。
第二,检查Anchor设置。如果你的数据集里目标尺寸和默认Anchor差异很大(比如默认Anchor是针对COCO的,而你的目标是细长条的工业零件),正样本匹配数量会很少,置信度学不上去。用K-means在训练集上重新聚类Anchor。
第三,检查学习率。学习率太大,置信度会在0附近震荡;学习率太小,收敛太慢。YOLOv5/v8的默认学习率(0.01)对大多数场景都适用,但如果你的数据集很小(少于1000张),可以适当降低到0.001。
第四,检查损失函数权重。如果置信度损失在总损失中占比太小,模型会优先优化分类和定位损失,忽略置信度。可以在损失函数里给置信度损失加一个权重系数。
5.2 推理时误检率高、置信度虚高
误检率高通常有两种表现:一是背景被误检为目标,二是目标被误检为错误的类别。置信度虚高指的是这些误检的框往往还有不低的置信度(0.5以上)。
原因和解决办法:
训练数据中负样本不足。如果训练集里全是目标图片,没有纯背景图片,模型没见过“没有目标”的场景,就会把背景也当成目标。解决办法是在训练集里加入10%-20%的纯背景图片。
类别不平衡。如果某个类别的样本特别多,模型会倾向于把这个类别预测到所有框上。解决办法是对少样本类别做过采样,或者在损失函数里给少样本类别更高的权重。
过拟合。如果模型在训练集上表现很好但验证集上误检多,可能是过拟合了。解决办法是增加数据增强、加Dropout、减小模型规模。
置信度校准问题。如前所述,YOLO的置信度天然偏高。如果误检框的置信度集中在0.3-0.5之间,可以尝试把conf_thres调到0.5以上。
5.3 边缘部署时置信度阈值怎么定
边缘部署(比如Jetson、RK3588、Atlas等设备)和服务器部署有一个重要区别:计算资源有限,后处理时间不能太长。这意味着你不能把conf_thres设得太低,否则NMS的输入框太多,后处理时间会爆炸。
我的经验是:在边缘设备上,conf_thres不要低于0.2。如果低于0.2,NMS的输入框可能超过1000个,在Jetson Nano这种设备上后处理时间可能超过50ms,严重影响帧率。
另外,边缘部署时建议开启类别无关的NMS(class-agnostic NMS),即所有类别一起做NMS,而不是每个类别单独做。这样可以减少NMS的调用次数,提升速度。但要注意,如果不同类别的目标经常重叠(比如人和人手里的手机),类别无关的NMS可能会导致漏检。
下面这个表格总结了不同边缘设备上的推荐参数:
| 设备 | 推荐conf_thres | 推荐iou_thres | 说明 |
|---|---|---|---|
| Jetson Nano | 0.3 | 0.45 | 算力有限,阈值不宜过低 |
| Jetson Xavier NX | 0.25 | 0.45 | 算力中等,可用默认值 |
| RK3588 | 0.25 | 0.45 | NPU加速,后处理是瓶颈 |
| Atlas 200 | 0.3 | 0.5 | 内存有限,减少框数量 |
| 树莓派+NPU | 0.35 | 0.45 | 算力最弱,阈值要高 |
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 正样本置信度低 | 标注不准/Anchor不匹配 | 可视化标注框和Anchor | 重新标注/重聚类Anchor |
| 误检多且置信度高 | 负样本不足/过拟合 | 检查训练集构成 | 加背景图/加数据增强 |
| 漏检多 | conf_thres太高 | 看PR曲线 | 降低conf_thres |
| 重复框多 | iou_thres太高 | 看NMS输出 | 降低iou_thres |
| 相邻目标被合并 | iou_thres太低 | 看NMS输出 | 提高iou_thres |
| 边缘设备帧率低 | conf_thres太低 | 统计NMS输入框数量 | 提高conf_thres |
| 置信度不收敛 | 学习率/损失权重问题 | 看损失曲线 | 调学习率/加损失权重 |
6. 置信度在不同任务中的特殊考量
6.1 实例分割中的置信度
YOLOv8的实例分割任务中,置信度的含义和检测任务基本一致,但多了一个掩码置信度的概念。每个检测框除了有一个检测置信度外,还有一个掩码质量分数,表示预测的掩码和真实掩码的IoU。
在实际使用中,掩码置信度通常和检测置信度一起决定最终输出。YOLOv8的分割输出中,每个实例的最终得分是检测置信度乘以掩码质量分数。这意味着即使检测置信度很高,如果掩码质量差,最终得分也会被拉低。
做实例分割时,conf_thres的调整逻辑和检测类似,但要注意掩码的后处理比检测框更耗时。在边缘设备上做实例分割,conf_thres要比纯检测设得更高(建议0.35以上),否则掩码后处理会成为瓶颈。
6.2 姿态估计中的置信度
YOLOv8的姿态估计任务中,置信度分为两个层级:人体检测置信度和关键点置信度。人体检测置信度和普通检测一样,关键点置信度表示每个关键点(如鼻子、肩膀、膝盖)的预测可信度。
关键点置信度的阈值通常设得比检测置信度低(比如0.3),因为关键点本身比较难预测,设太高会导致很多关键点被丢弃,骨架画不完整。在实际应用中,我一般会把关键点置信度阈值设在0.2-0.3之间,然后通过骨架的几何约束(比如膝盖应该在髋关节下方)来过滤明显错误的关键点。
6.3 多模态任务中的置信度
YOLO的多模态扩展(如图文多模态目标检测)中,置信度的含义会变得更复杂。除了视觉置信度外,还可能有一个文本对齐置信度,表示检测到的目标和文本描述的匹配程度。
这类任务的置信度调参需要同时考虑两个模态的贡献。我的经验是:先单独调视觉置信度阈值,让检测结果在视觉上合理;然后再调文本对齐阈值,过滤掉和文本描述不匹配的检测。两个阈值不要同时调,否则很难定位问题。
7. 几个容易被忽略的置信度细节
7.1 置信度与NMS的交互
NMS的输入是经过置信度过滤的框,但NMS本身也会影响最终输出的置信度分布。具体来说,如果一个高置信度的框抑制了一个低置信度的框,那个低置信度的框就消失了。但如果两个框的IoU刚好在阈值附近,可能会出现“该抑制的没抑制,不该抑制的抑制了”的情况。
一个实用的技巧是:在做NMS之前,先把置信度低于某个值(比如0.1)的框全部丢掉,不管conf_thres设的是多少。这样可以减少NMS的计算量,同时避免低质量框干扰NMS的IoU计算。
7.2 批量推理时的置信度处理
批量推理(batch inference)时,不同图片的置信度分布可能差异很大。如果用一个全局conf_thres,可能会出现某张图输出几百个框、另一张图输出几个框的情况。
解决办法是按图片动态调整阈值。比如先计算每张图所有预测框的置信度中位数,然后以中位数的某个倍数作为该图的阈值。这种方法在视频流处理中特别有用,因为相邻帧的置信度分布通常比较接近。
7.3 置信度阈值的自动化搜索
手动调conf_thres很耗时,可以用网格搜索来自动化。基本思路是:在验证集上,让conf_thres从0.05到0.95以0.05为步长遍历,计算每个阈值下的F1分数,选F1最高的那个。
best_f1 = 0 best_conf = 0.25 for conf in np.arange(0.05, 0.95, 0.05): # 在验证集上跑推理,用conf作为阈值 results = model(val_images, conf=conf) # 计算F1 f1 = compute_f1(results, val_labels) if f1 > best_f1: best_f1 = f1 best_conf = conf print(f"Best conf_thres: {best_conf}, F1: {best_f1}")这个搜索过程在验证集不大(比如几百张图)的情况下,几分钟就能跑完。找到最优阈值后,再在测试集上验证一下,确保没有过拟合到验证集。
注意:自动化搜索找到的阈值是针对特定数据集的。如果你的部署场景和训练集分布差异很大(比如训练集是白天图片,部署场景有夜间图片),搜索到的阈值可能不适用。这种情况下,最好在部署场景中采集一些图片,人工标注后作为“部署验证集”,在这个集上重新搜索阈值。
7.4 置信度与模型量化的关系
模型量化(比如FP32转INT8)会影响置信度的输出分布。量化后的模型,置信度往往会变得更“保守”——高置信度的值会降低,低置信度的值会升高,整体分布更集中。
如果你在量化后发现模型漏检增多,不要急着怀疑量化本身有问题,先试试把conf_thres降低0.05-0.1。很多时候,量化后的模型只是需要重新调一下阈值,精度损失并没有想象中那么大。
我在RK3588上部署YOLOv5s的经历就很典型:FP32模型上conf_thres=0.25效果很好,转成INT8后同样的阈值下漏检明显增多。把阈值降到0.18后,检测效果基本恢复到了FP32的水平,而推理速度提升了将近3倍。
7.5 置信度输出的可视化技巧
调试置信度问题时,把置信度分布画出来往往比看数字更直观。我通常会用两种图:
第一种是置信度直方图。把所有预测框的置信度画成直方图,看看分布是单峰还是双峰。如果是双峰(比如一堆在0.1附近,一堆在0.8附近),说明模型对正负样本的区分度很好;如果是单峰且集中在中间(比如0.3-0.5),说明模型对很多样本都“拿不准”,可能需要更多训练数据或更好的Anchor。
第二种是置信度- IoU散点图。横轴是预测框和真实框的IoU,纵轴是置信度。理想情况下,IoU高的框置信度也高,散点应该呈正相关。如果散点很分散,说明置信度和定位质量的相关性不强,可能需要调整损失函数里定位损失和置信度损失的比例。
这两种图用Matplotlib几行代码就能画出来,但对调参的帮助非常大。我每次遇到置信度相关的问题,第一步就是画这两张图,往往看一眼就能定位到问题方向。