news 2026/10/13 4:19:06

目标检测评估中Miss Rate vs FPPI曲线绘制与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
目标检测评估中Miss Rate vs FPPI曲线绘制与避坑指南

简介:面向目标检测、行人检测研究者的FPPI曲线绘制工具,可快速生成miss rate versus false positives per image(漏检率对每张图虚警数)曲线,直观反映检测器在漏检与虚警之间的权衡表现。压缩包内共10840个文件:10682个txt文件多为检测结果或标注数据,137个vbb文件为行人检测中常用的标注格式,7个m脚本实现核心计算与绘图逻辑,1个mat文件存放中间数据;总大小约161.4MB,另含少量系统隐藏文件,不影响正常使用。该资源已有1178人学习下载,适合计算机视觉方向的学生、研究者与算法工程师在实验验证或论文配图时直接套用。脚本封装了从数据解析、统计miss rate到绘制曲线的完整流程,配合示例数据可快速验证效果,也能移植到其他检测结果中,帮助深入理解FPPI、MR等评估指标的计算细节。

1. 为什么目标检测评估要画 miss rate vs FPPI:一张曲线暴露的真实部署问题

做目标检测评估时,我见过太多「mAP 很高、一上视频就翻车」的模型。mAP 会把几十张图里偶发的假阳性摊薄掉,让你看不清真实表现。miss rate versus false positives per image(FPPI)是另一套语言:FPPI 统计平均每张图冒出几个假阳性,miss rate 统计真值框里有多少没被检测到。这两个数组合起来,正是监控、自动驾驶、行人检测这类「宁漏勿乱报」场景最关心的落地指标。

一帧多报一个人,平台就多一次人工复核;一个行人漏检,可能直接引发事故。这个维度不看平均精度,而看误报密度与漏检率的权衡,miss rate vs FPPI 曲线就是把这个权衡完整画出来的工具。下面从匹配口径讲起,把计算逻辑拆到每一步,再给一套拿来就能改的绘图代码,连同绘制中的常见坑一起列清楚,适合正在做检测评估、想对比多个模型部署表现的开发者。

2. 匹配逻辑与统计口径:从检测框和真值框算出 FPPI 与 miss rate

画曲线之前,先把两个指标的定义对齐。很多人拿 PR 曲线的思路套 FPPI,结果分母用错,画出来的图自己都解释不了。

2.1 定义先对齐:FPPI 的分母是图像数,miss rate 的分母是真值框数

FPPI 的完整写法是 False Positives Per Image,分子是评估集上所有假阳性检测框的总数,分母是图像总数。比如测试集有 2000 张图,模型一共输出了 800 个假阳性框,FPPI 就是 0.4,意思是平均每张图会误报 0.4 次。这个数字直接对应部署成本:FPPI = 0.4 意味着每处理 10 帧画面,系统会向审核队列里塞 4 个假报警。

miss rate 的分子是「漏检的真值框数量」,分母是「真值框总数」。它和 recall 是互补关系:miss rate = 1 - recall。一个检测框只要和某个真值框匹配成功,这个真值就被视为「检出」,没被任何检测框匹配上的真值就计入漏检。这里的关键是「匹配」的规则,放在 2.2 展开。

用一张表对比三种评估口径的区别,方便你按任务选型:

指标组合横轴纵轴分母关键典型用途
PR 曲线PrecisionRecall检测框总数、真值框总数通用目标检测、类别较均衡
ROC 曲线FPRTPR负样本数、正样本数分类任务
FPPI vs Miss RateFPPIMiss Rate图像帧数、真值框总数视频监控、行人检测

ROC 和 PR 的分母都跟「样本数」绑定,类别不平衡时曲线会整体偏移,看着漂亮但读不出部署含义。FPPI 用图像数做分母,跟画面里有没有物体、有几个物体都无关,它回答的是「每帧误报率」这个运维问题。这也是为什么行人检测、视频监控这类连续帧任务普遍选择 miss rate vs FPPI 而不是 mAP。

这里还要强调:miss rate vs FPPI 曲线的横纵坐标都是「率」,不依赖检测框绝对数量,所以不同模型、不同分辨率输入之间可以直接对比。但对比时测试集必须完全一致,否则 FPPI 的分母(图像数)和分子(FP 数)都变了,曲线位置没有任何可比性。

2.2 贪心匹配:为什么必须按置信度降序、一个真值框只能匹配一次

从原始检测结果到 TP/FP/FN,中间最关键的一步是把检测框和真值框配对。常见做法是按置信度降序做贪心匹配:先把所有检测框按 score 从高到低排序,逐个拿出来,计算它和当前图像所有「未匹配」真值框的 IoU,选出 IoU 最大的那个;如果最大 IoU 达到阈值(比如 0.5),这个检测框计为 TP,对应的真值框标记为已匹配;否则计为 FP。

两个规则必须写死。第一,同一个真值框只能被一个检测框匹配,匹配过就要从候选池里移除。否则一个行人框被模型重复输出 3 次,3 个检测框都算 TP,recall 虚高,miss rate 被严重拉低。第二,必须按置信度降序处理。高置信度的检测优先占位,如果反着来,一个低分框先把真值占掉,后续高分框因为真值已匹配只能算 FP,相当于人为制造假阳性,曲线整体往右上漂移。

我一般还会在匹配代码里对「低分框匹配、高分框落空」的情况做一次审计:把匹配到的 (det_score, gt_idx) 对打出来,如果出现大量低分框匹配成功而高分框落空的情况,先怀疑排序逻辑而不是模型本身。这个检查在调试阶段能省很多时间。

匹配完成之后,每个检测框要么是 TP 要么是 FP,每个真值框要么「被检出」要么「漏检」。注意 TP 的数量在数值上等于「被检出」的真值框数量,因为在贪心匹配下每个 TP 唯一对应一个真值框。这一点是后面逐阈值统计能成立的前提。

2.3 遍历置信度阈值:把匹配结果变成一条单调曲线

单看某一阈值下的 FPPI 和 miss rate 只是两个孤立数字,曲线需要遍历所有可能阈值。做法是:把检测框按分数降序排列后,从最高分开始,逐个把检测框纳入「预测为正」集合。每纳入一个框,就重新计算一次累计 TP 和累计 FP,再除以对应分母得到当前点。

用一个例子说明。假设测试集 3 张图,共 2 个真值框;模型输出 3 个检测框,分数分别为 0.9、0.5、0.2。按降序匹配后,框 A(0.9)匹配到真值 1,框 B(0.5)匹配到真值 2,框 C(0.2)是 FP。

阈值纳入的检测累计 TP累计 FPFNFPPIMiss Rate
0.9只有框 A101050%
0.5框 A、B20000%
0.2框 A、B、C2100.330%

曲线从左上角(低 FPPI、高 miss rate)走到右下角(高 FPPI、低 miss rate)。因为累计 TP 和累计 FP 都单调不减,FPPI 单调不减、miss rate 单调不增,曲线本身一定是单调的。如果你画出来的曲线有来回折返,基本可以断定匹配或排序逻辑出了问题,这个判断方法在排查时非常有用。

实际操作中还有一个细节:多个检测框分数完全相同的情况。比如一批框的 score 都是 0.5,按每个分数取一个阈值会产生重复点,曲线出现垂直平台。处理办法是只在分数值变化时记录一个点,同一分数下所有框先累加完再取样。这个细节在避坑章节会再展开。

3. 完整绘制代码:从检测结果文件到 loglog 曲线的一站式实现

这一章直接给可运行的代码。我用两个文本文件作为输入:检测结果和真值,格式都是逗号分隔,方便你自己适配。

3.1 输入格式约定:检测框、真值框先统一成同一套坐标

我常用的检测结果文件是 dets.txt,每行一条检测框:

image_0001,0.92,120.5,80.3,260.4,310.7 image_0001,0.61,400.1,150.2,520.8,280.5

字段依次是图像名、置信度分数、x1、y1、x2、y2,坐标用左上角 (x1,y1) 和右下角 (x2,y2) 的绝对像素值。真值文件 gts.txt 每行一个真值框,没有分数:

image_0001,100.0,60.0,280.0,330.0

如果你的检测结果输出的是 xywh(中心点 x、中心点 y、宽、高),读取后先转换:

def xywh_to_xyxy(box): cx, cy, w, h = box return [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2]

这一步看似简单,但坐标系混乱导致的 IoU 全零事故我见过不止一次,后面避坑章节会细说。读取代码用标准 csv 模块就能稳定处理浮点和空行:

import csv def load_detections(path): dets = [] with open(path, "r") as f: reader = csv.reader(f) for row in reader: if len(row) < 6: continue # 跳过空行或残缺行 dets.append({ "image_id": row[0], "score": float(row[1]), "bbox": [float(row[2]), float(row[3]), float(row[4]), float(row[5])] }) return dets def load_ground_truth(path): gt = {} with open(path, "r") as f: reader = csv.reader(f) for row in reader: if len(row) < 5: continue img_id = row[0] bbox = [float(row[1]), float(row[2]), float(row[3]), float(row[4])] gt.setdefault(img_id, []).append(bbox) return gt

这里用 dict[image_id] -> list of bbox 组织真值,方便匹配时按图检索。用 setdefault 而不是 defaultdict,避免误读一个不存在的图像名时悄悄创建一个空表,这样至少能在匹配阶段暴露出图像名不一致的问题。

3.2 核心计算函数:IoU、贪心匹配与逐阈值统计

先定义 IoU 和贪心匹配。IoU 计算要防御边界坐标反了的情况(比如 x2 < x1),这时交集宽高算出负值,直接归零:

def compute_iou(bbox_a, bbox_b): x1 = max(bbox_a[0], bbox_b[0]) y1 = max(bbox_a[1], bbox_b[1]) x2 = min(bbox_a[2], bbox_b[2]) y2 = min(bbox_a[3], bbox_b[3]) w = max(0.0, x2 - x1) h = max(0.0, y2 - y1) inter_area = w * h area_a = (bbox_a[2] - bbox_a[0]) * (bbox_a[3] - bbox_a[1]) area_b = (bbox_b[2] - bbox_b[0]) * (bbox_b[3] - bbox_b[1]) union = area_a + area_b - inter_area return inter_area / union if union > 0 else 0.0

area_a 和 area_b 如果算出负数,说明坐标本身就有问题:可能是坐标格式混用,也可能是归一化方式不一致。建议在这里打一条日志,把异常框的宽高打印出来,而不是让程序静默跑完。贪心匹配函数返回每个检测框的 TP/FP 标记和分数,供后续统计使用:

def greedy_match(detections, ground_truth, iou_threshold=0.5): dets = sorted(detections, key=lambda d: d["score"], reverse=True) matched_gt = set() # 记录 (image_id, gt_index) flags = [] # 1 表示 TP, 0 表示 FP for det in dets: img_id = det["image_id"] best_iou = 0.0 best_idx = -1 for i, gt_bbox in enumerate(ground_truth.get(img_id, [])): if (img_id, i) in matched_gt: continue iou = compute_iou(det["bbox"], gt_bbox) if iou > best_iou: best_iou = iou best_idx = i if best_iou >= iou_threshold: flags.append(1) matched_gt.add((img_id, best_idx)) else: flags.append(0) scores = [d["score"] for d in dets] return scores, flags

注意两个细节:候选真值框限定在同一图像内,ground_truth.get(img_id, []) 天然保证了这一点,跨图匹配没有意义;matched_gt 用 (image_id, gt_index) 做键,因为不同图像里 gt_index 相同是正常情况,只存 index 会串图。排序必须在函数内部做,调用方传什么顺序都不能信任。

逐阈值统计的逻辑是:scores 与 flags 已经按分数降序排列,遍历到第 i 个位置时,「分数 >= 当前分数」的检测框恰好就是前 i 个。累计 TP 和 FP 后除以对应分母就得到曲线点:

def compute_fppi_miss_rate(scores, flags, num_images, num_gt_boxes): fppi_list = [] miss_rate_list = [] fp_cum = 0 tp_cum = 0 for i in range(len(scores)): fp_cum += 1 - flags[i] tp_cum += flags[i] # 如果下一个分数与当前相同,先不记录,等这一整段同分框累加完 if i < len(scores) - 1 and scores[i + 1] == scores[i]: continue fppi = fp_cum / num_images miss_rate = (num_gt_boxes - tp_cum) / num_gt_boxes fppi_list.append(fppi) miss_rate_list.append(miss_rate) return fppi_list, miss_rate_list

threshold 在这里体现为「累计到当前分数位置」,所以不需要显式比较分数。这样处理天然覆盖了所有可能的阈值点,也避免了 for threshold in thresholds 这种需要手动指定步长的写法。num_gt_boxes 是所有图像非忽略真值框的总数,统计时别忘了把 ignore 框排除,细节见避坑章节。

3.3 绘图与保存:log 坐标、阶梯曲线与参考线

曲线画在 loglog 坐标下,因为 FPPI 通常跨越几个数量级,线性坐标会把低 FPPI 区域压扁。用 matplotlib 的 step 方式能更好体现「阈值步进」的语义:

import matplotlib.pyplot as plt def plot_fppi_miss_rate(fppi_list, miss_rate_list, label="model"): plt.figure(figsize=(7, 5)) plt.plot( fppi_list, miss_rate_list, drawstyle="steps-post", # 阶梯状,阈值步进的真实语义 linewidth=1.5, label=label ) plt.xscale("log") plt.yscale("log") plt.xlabel("False positives per image") plt.ylabel("Miss rate") plt.xlim(1e-4, 10) plt.ylim(1e-4, 1) plt.grid(True, which="both", linestyle="--", alpha=0.5) plt.legend() plt.savefig("fppi_miss_rate.png", dpi=150)

drawstyle 用 steps-post,表示在阈值点之后保持水平,直到下一个点再跳变,对应「阈值从左往右移动、更多低分框被纳入」的过程。xlim 和 ylim 要覆盖你关心的 FPPI 区间,监控场景通常从 1e-4 开始看,因为低误报区域的形状才是部署价值所在。

三个附注:一是数据集中若完全没有真值框,分母为零,计算会直接崩,脚本里要加保护;二是如果测试集只有几十张图,FPPI 的最小分辨率是 1/num_images,把 x 轴画到小于这个值的范围没有意义;三是绘图前我通常先打印 fppi_list[0] 和 miss_rate_list[-1] 两个端点,确认没有 NaN 或无穷大。调用一气呵成:

dets = load_detections("dets.txt") gt = load_ground_truth("gts.txt") scores, flags = greedy_match(dets, gt, iou_threshold=0.5) num_images = len(set(gt.keys()) | set(d["image_id"] for d in dets)) num_gt = sum(len(v) for v in gt.values()) x, y = compute_fppi_miss_rate(scores, flags, num_images, num_gt) plot_fppi_miss_rate(x, y)

注意:num_images 不要只从检测结果取。如果某张图模型一个框都没输出,它不会出现在 dets 里,用检测框集合数当分母会把 FPPI 算高。稳妥做法是用真值文件里的图像集合,再和检测文件的图像集合取并集,兼顾「有真值无检测」和「有检测无真值」两种情况。

4. 参数怎么设、曲线怎么读:阈值、采样与多模型对比

代码能跑通之后,真正的差别在参数和读图方法。这一章讲我在实际使用中怎么定 IoU 阈值、怎么控制曲线点密度、以及多模型对比时怎么让图不糊成一团。

4.1 IoU 阈值选 0.5 还是更严:任务决定标准

匹配阶段那个 0.5 的默认值不是随便拍的。通用检测评估里 0.5 用得最广,因为它允许检测框和真值框有一定偏移,更多反映「找没找到」而不是「框得准不准」。但部署场景往往要更严格:行人检测里遮挡严重,框偏半个身位可能就导致后续跟踪失败,所以不少评估协议把 IoU 阈值提到 0.7 或按标注规范来定。

我在对比两个模型时,会同时画 0.5 和 0.7 两套曲线。如果两套曲线的相对位置不一致,比如模型 A 在 0.5 下更好、在 0.7 下更差,说明 A 的检测框定位精度不够,只是「蒙对了位置」。这类信息从 mAP 单值里很难看出来,但 FPPI 曲线在高低两个 IoU 标准下会给出非常直观的交叉。

匹配阈值和评估阈值必须一致。有些开发者匹配用 0.5、评估却用 0.7,得到的 TP 集合前后矛盾,曲线上的 miss rate 含义混乱。我一般把 IoU 阈值作为函数参数全局传一遍,避免两处各写一个常数,改的时候只改一处。

4.2 曲线点太密太抖怎么办:阈值采样与阶梯步长

按 3.2 的代码,每个分数段对应一个曲线点。当测试集检测框上万时,曲线会有上千个点,loglog 坐标下低 FPPI 区间的点密集得像噪声,打印成 PDF 后线条臃肿。常见做法是对 FPPI 轴重采样:在 log 空间均匀取参考点,对 miss rate 做插值。

先保证原始点单调,再做插值。因为 FPPI 单调不减、miss rate 单调不增,np.interp 可以直接用:

import numpy as np def resample_curve(fppi, miss_rate, num_points=50, fppi_min=1e-4, fppi_max=1.0): log_fppi = np.log10(fppi) ref_log = np.linspace(np.log10(fppi_min), np.log10(fppi_max), num_points) # left/right 参数处理参考点落在数据范围之外的场景 mr_interp = np.interp(ref_log, log_fppi, miss_rate, left=1.0, right=miss_rate[-1]) return 10 ** ref_log, mr_interp

插值要在 logFPPI 空间做,而不是在原始 FPPI 上直接线性插值。因为 loglog 图的视觉均匀性来自对数坐标,log 空间插值才能保证重采样点在图上的间距均匀。插值后的曲线不再有台阶语义,绘图时用普通 line 而不是 steps-post,并配合少量 marker 标记原始阈值位置,信息量更真实。

重采样后,原始曲线在哪些区间点很密、哪些区间只有两三个点,这个信息被掩盖了。我习惯在最终图上叠加少量原始散点(空心圆),让读者能判断低 FPPI 段的形状是否可靠。如果某段只有两个原始点,曲线形状再平滑也只是插值出来的,不能过度解读。

4.3 多模型放一张图:marker、颜色与图例的对比习惯

多个模型对比是这条曲线最常见的用法。放一张图时,只靠颜色区分在黑白打印下完全失效,我一般给每条线配不同的 marker 和线型组合:模型 A 用圆形 marker + 实线,模型 B 用方块 marker + 虚线,每个模型的 marker 每隔 10 到 15 个点画一个,避免糊成一条。

FPPI=1 的位置加一条竖向参考线很有用,它代表「平均每张图一个误报」的工况。很多评估报告会直接报这个工况下的 miss rate,作为单值对比。配合横向参考线(miss rate = 0.5)可以圈出「半数目标被漏检」的误报容忍上限,两个参考线交叉出来的区域就是你模型应该努力靠近的角落。

图例命名我习惯带上 IoU 阈值和置信度筛选条件,比如「模型A (IoU=0.5)」,而不是裸写模型名。曲线数据本身不依赖置信度筛选,但训练时如果做了 score filter(比如只保留 score > 0.01 的输出),要在图注说明,否则别人复现会得到不同的起点。

多模型曲线交叉时的读法:低 FPPI 区域曲线更低者更优,因为实际部署中误报预算有限,优先看 10^-2 到 10^0 这个区间的相对位置。如果一个模型在低 FPPI 区间高、高 FPPI 区间低,说明它适合用在审核人力充足但漏检代价极高的场景;反之则适合误报敏感的监控告警场景。曲线形状本身就是一种场景适配判断工具。

5. 绘制 FPPI 曲线的 5 个避坑点:匹配、格式与统计口径翻车实录

这一章全部是实际绘制中踩过的坑,每条按「现象 → 原因 → 解决」写,可以直接当 checklist 用。

5.1 一个真值被多个检测框重复匹配,miss rate 虚低

  • 现象:miss rate 在低 FPPI 区就低到离谱,阈值很高、几乎没纳入几个检测框时,漏检却明显偏少,曲线整体贴着左下方。
  • 原因:贪心匹配没维护「已匹配真值」集合。一个目标被模型输出 3 个重叠框,3 个框分别匹配到同一个真值、各计一次 TP,recall 被放大 3 倍,miss rate = 1 - recall 就虚低。
  • 解决:匹配循环里用集合记录 (image_id, gt_index),处理每个检测前先跳过已匹配项。再加一层防御:在统计循环里加断言assert tp_cum <= num_gt_boxes,一旦累计 TP 超过真值框总数,必然存在重复匹配,脚本立刻报错而不是输出一张漂亮的假曲线。

5.2 置信度乱序导致曲线回退,出现锯齿

  • 现象:曲线中段出现明显回退,miss rate 本来在降突然往上跳一块;或者 FPPI 先增后减,这在数学上不可能发生。
  • 原因:读取检测结果后没有按分数降序排序,逐阈值累加时「后面纳入的检测框分数反而更高」,累计逻辑被打乱,曲线自然乱套。
  • 解决:所有统计前先执行一次 dets.sort(key=lambda d: d["score"], reverse=True)。如果数据来自多个文件拼接,排序必须在拼接之后,而不是每个文件各自排序后合并。文件内有序不代表全局有序,这个坑在分块导出检测结果时特别常见。

5.3 坐标格式混用:x1y1x2y2 与 xywh 的 IoU 全零

  • 现象:FPPI 曲线从头到尾没有下降,miss rate 恒等于 1,检查发现所有检测框与真值框的 IoU 都是 0。
  • 原因:检测模型输出的是 (cx, cy, w, h),真值标注却是 (x1, y1, x2, y2),IoU 计算把中心点坐标当成左上角、宽度当成右下角,框的位置整体错位,交集为空。
  • 解决:读取后立即统一坐标格式,用 3.1 的 xywh_to_xyxy 做转换,并打印前 5 个框目检:x1 < x2、y1 < y2、宽高为正。我还会额外算一个「检测框面积与真值面积的中位数比例」,如果数量级差很多(比如检测框面积全是 1 像素),基本可以断定归一化方式不一致。这个检查在混合分辨率数据集上尤其重要。

5.4 忽略区域没排除,FPPI 被假阳性刷高

  • 现象:模型在人群密集区域输出大量检测框,但这些区域按标注规范属于「不需要评估」的忽略区域,结果 FPPI 虚高,模型被错误判死。
  • 原因:真值文件里带了 ignore 标记,统计时没把 ignore 框剔除。更隐蔽的是「匹配到 ignore 的检测框」——模型其实准确检出了目标,只是目标被标为忽略,这类检测计成 FP 会严重冤枉模型。
  • 解决:读真值时把 ignore 框单独存放,统计分母只计非 ignore 框;匹配时先检查检测框是否与 ignore 框 IoU 超阈值,命中则跳过,既不参与 TP 也不参与 FP。注意逻辑顺序:先检查 ignore,再匹配普通 GT,避免命中 ignore 的框又跑去匹配普通真值。
def load_gt_with_ignore(path): gt, ignore_gt = {}, {} with open(path, "r") as f: for row in csv.reader(f): img_id = row[0] bbox = [float(row[1]), float(row[2]), float(row[3]), float(row[4])] is_ignore = len(row) > 5 and int(row[5]) == 1 target = ignore_gt if is_ignore else gt target.setdefault(img_id, []).append(bbox) return gt, ignore_gt

注意:匹配到 ignore 框的检测怎么处理,不同评估协议不一样。有些基准要求计 FP,有些要求直接跳过。动手前先查清楚你在对齐的协议怎么规定,不要凭习惯定。

5.5 相同分数重复采样与 score 边界二义性

  • 现象:曲线出现长段垂直平台,重采样插值时在平台区产生异常跳变,log-average miss rate 两次跑结果不一致。
  • 原因:大批检测框分数相同,每个分数都采一个点产生重复;同时「score >= 阈值」和「score > 阈值」两种取法在相同分数上结果不同,同一个脚本里两种逻辑混用,曲线重现性极差。
  • 解决:阈值点只在分数值变化时记录,实现方式见 3.2:遍历时若下一个分数与当前相同,先继续累加,直到分数变化才取点。取等号规则全局固定为「>=」,并在代码注释里写明。这样无论检测框分数如何分布,输出曲线都是确定性的。

6. 进阶:用 log-average miss rate 做量化对比,把整条曲线压成一个单值

曲线适合观察,但调参和模型选型时需要单值。行人检测评估里最常用的单值化方法是 log-average miss rate(log 平均漏检率):在 FPPI 从 10^-2 到 10^0 的对数区间内均匀取参考点,插值出对应的 miss rate,再取对数平均。取这个区间是因为实际部署能容忍的误报密度大致在这个范围,低于 1% 的工况很少硬性要求,高于每图 1 个误报基本不可用。计算代码很短:

def log_average_miss_rate(fppi, miss_rate, fppi_min=1e-2, fppi_max=1.0): # 在 logFPPI 空间均匀取 100 个参考点,插值 miss rate log_fppi = np.log10(fppi) ref_log = np.linspace(np.log10(fppi_min), np.log10(fppi_max), 100) mr_interp = np.interp(ref_log, log_fppi, miss_rate, left=1.0, right=miss_rate[-1]) log_mr = np.log10(np.clip(mr_interp, 1e-5, 1.0)) return 10 ** np.mean(log_mr)

np.interp 要求自变量单调递增,这正是前面反复强调的曲线单调性;如果传入的 fppi 有抖动,这里会得到不可信结果。clip 下限取 1e-5,是为了防止 miss rate 恰好为 0 时 log 取负无穷。

我早期踩过一次:直接用算术平均算 miss rate,结果一个低 FPPI 段表现极好、高 FPPI 段崩盘的模型,和一个全程平稳的模型得分相近。换成 log 平均后,低 FPPI 段(误报稀少的高压区)的差异被正确放大,两个模型的排序立刻清晰了。这个单值指标要和曲线一起呈现,报告里写「log-average miss rate:0.32」时,最好附上曲线让读者看到形状。

如果两条曲线几乎重合,先别怀疑是玄学,优先检查是不是检测结果文件读重了,或者两个模型共用了一套输出。调参时我还会把两次实验的曲线在 log 空间逐点相减画成第二条小图,哪一段变好、哪一段变坏一目了然,比盯着两条纠缠的曲线高效得多。希望这套绘制流程帮你一次跑通,少走我当时调试匹配逻辑走到半夜的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 4:18:53

仿QQ即时通讯项目实战:Java网络编程核心与Socket实现

很多人刚开始学Java的时候&#xff0c;耳边经常会出现一个建议&#xff1a;写一个仿QQ的即时通讯项目试试。这个题目听起来像个教学小玩具&#xff0c;但真做起来&#xff0c;你会发现自己要把Java的集合框架、多线程、IO流、Socket编程、Swing界面开发甚至数据库操作全部过一遍…

作者头像 李华
网站建设 2026/10/13 4:18:48

Gemini生成Word文档:三种路线从复制粘贴到API自动化

经常有人在群里问&#xff1a;Gemini 到底能不能生成 Word 文档&#xff1f;问这个问题的人&#xff0c;十有八九不是想了解某个冷门功能&#xff0c;而是已经和 Gemini 聊出了一篇方案、一份周报、一个产品说明&#xff0c;接下来却卡在"怎么把它变成能交差、能打印、能发…

作者头像 李华
网站建设 2026/10/13 4:18:18

MySQL迁移达梦数据库:SQL语法差异与改造实践清单

做数据库迁移这事&#xff0c;真正让人头疼的从来不是数据搬不动&#xff0c;而是搬过去之后应用跑不起来。MySQL 迁到达梦&#xff08;后面统一叫 DM&#xff09;就是典型场景&#xff1a;表面看都是关系型数据库&#xff0c;写个 SELECT、INSERT 没啥区别&#xff0c;但真把手…

作者头像 李华
网站建设 2026/10/13 4:18:15

递归求单链表结点个数:从代码实现到内存与栈安全的工程实践

1. 这不是一道“刷题”题&#xff0c;而是一把打开数据结构思维的钥匙“递归求解单链表中的结点个数”——看到这个标题&#xff0c;很多人第一反应是&#xff1a;哦&#xff0c;这是《数据结构与算法》课后习题第3章第2题&#xff0c;用递归写个count函数就完事了。但在我带过…

作者头像 李华
网站建设 2026/10/13 4:18:01

MySQL跳过身份验证重置root密码:原理、实操与安全收尾

1. 什么情况下才需要这招&#xff1a;先判断问题真伪先说一个我印象特别深的场景。某天快下班的时候&#xff0c;一个同事跑过来说测试库连不上了&#xff0c;报错是Access denied for user rootlocalhost。他第一反应是密码被人改了&#xff0c;准备直接停库重装。我当时拦住了…

作者头像 李华
网站建设 2026/10/13 4:17:35

Databricks性能优化实战:从架构到Delta Lake的全面指南

如果你做过数据平台、数仓&#xff0c;或者只是把 Spark 当作日常作业引擎用过一阵子&#xff0c;那“Databricks”这个名字大概率绕不开。我接触这个平台也是从一次事故开始的&#xff1a;线上数仓作业莫名其妙从十分钟涨到三小时&#xff0c;老板盯着看板&#xff0c;我盯着一…

作者头像 李华