在计算机视觉,特别是目标检测任务中,mAP@0.5:0.95 (mean Average Precision from IoU threshold 0.5 to 0.95)是一个广泛使用的综合性能评估指标。它衡量的是模型在不同 IoU(Intersection over Union)阈值 下的平均精度(AP),再对所有类别取平均得到 mAP。
IoU(交并比)
-
定义:预测框与真实框的交集面积除以并集面积。
-
公式:IoU=AreaofOverlapAreaofUnionIoU=\frac{Area of Overlap}{Area of Union}IoU=AreaofUnionAreaofOverlap
-
用于判断一个预测是否为"正确检测":若 IoU ≥ 阈值(如 0.5),则视为正例(True Positive)。
Precision(精确率)与 Recall(召回率)
- Precision = TP / (TP + FP)
- Recall = TP / (TP + FN)
- 在目标检测中,通过改变置信度阈值,可以得到一系列 (Precision, Recall) 点,构成 PR 曲线。
Average Precision(AP)
- 对 PR 曲线下的面积进行积分或插值计算,得到单个类别的 AP。
- COCO 数据集采用 11 点插值法 或更精细的 所有点插值法(目前主流用后者)。
mAP@0.5**:0.95 的具体计算步骤**
-
设定多个 IoU 阈值 :从 0.5 到 0.95,步长为 0.05,共 10 个阈值:IoU thresholds={0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9,0.95}IoU~thresholds=\{0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9,0.95\}IoU thresholds={0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9,0.95}
-
对每个类别、每个 IoU 阈值计算 AP
- 对于给定类别和 IoU 阈值(如 0.5),根据模型输出的所有预测框(按置信度排序),计算 TP/FP,进而得到 PR 曲线,再计算 AP。
- 重复此过程,得到该类别在 10 个 IoU 阈值下的 10 个 AP 值。
-
对 10 个 AP 取平均,得到该类别的 AP@[0.5:0.95] APclass=110∑t=110AP(IoUt)AP_{class}=\frac{1}{10}∑_{t=1}^{10}AP(IoU_t)APclass=101∑t=110AP(IoUt)
-
对所有类别取平均,得到 mAP@[0.5:0.95] mAP@0.5:0.95=1C∑c=1CAPclasscmAP@0.5:0.95=\frac1C∑{c=1}^CAP{class_c}mAP@0.5:0.95=C1∑c=1CAPclassc 。其中 C 是类别总数。
mAP@0.5:0.95 综合考量了以下多个维度:
| 维度 | 说明 |
|---|---|
| 定位精度(Localization Accuracy) | 通过高 IoU 阈值(如 0.95)要求预测框与真实框高度重合,考验模型精确定位能力。 |
| 检测鲁棒性(Robustness across IoU) | 不仅看宽松阈值(0.5),还看严格阈值(0.95),避免模型只在宽松条件下表现好。 |
| 类别泛化能力 | 对所有类别分别计算后平均,反映模型在不同类别上的整体性能。 |
| 置信度排序质量 | AP 依赖 PR 曲线,而 PR 曲线由不同置信度阈值下的检测结果生成,因此也反映了模型对预测置信度的校准能力。 |
| 查全与查准的平衡 | AP 本身是 Precision-Recall 曲线下面积,综合考虑了漏检(低 recall)和误检(低 precision)。 |
- mAP@0.5:只用 IoU=0.5 计算,标准较宽松,常见于 PASCAL VOC。
- mAP@0.75:更严格,强调高精度定位。
- mAP@0.5:0.95:COCO 数据集官方指标,更全面、更具挑战性。
问题设定(简化场景)
通过一个具体、简化但完整的目标检测评估实例 ,来一步步推导 mAP@0.5:0.95 的计算过程。为便于理解,我们做如下设定:
- 任务:目标检测
- 类别数:1 类(例如"猫")→ 这样 mAP = AP(因为只有一类)
- 真实框(Ground Truth, GT):3 个
- 模型预测框(Predictions):5 个(每个有置信度分数)
真实框(GT):
| ID | 坐标 (x1, y1, x2, y2) |
|---|---|
| GT1 | (50, 50, 100, 100) |
| GT2 | (150, 150, 200, 200) |
| GT3 | (250, 250, 300, 300) |
预测框(Detections):
| ID | 坐标 | 置信度 |
|---|---|---|
| D1 | (55, 55, 95, 95) | 0.95 |
| D2 | (148, 148, 202, 202) | 0.90 |
| D3 | (255, 255, 295, 295) | 0.85 |
| D4 | (50, 50, 100, 100) | 0.60 |
| D5 | (400, 400, 450, 450) | 0.50 |
注意:D4 与 GT1 完全重合,但置信度较低;D5 是一个误检(无对应 GT)。
第一步:计算每个预测框与所有 GT 的 IoU
我们先计算每个 Detection 与每个 GT 的 IoU:
对于两个框 A 和 B:
-
交集面积:intersect=max(0,min(x2A,x2B)−max(x1A,x1B))×max(0,min(y2A,y2B)−max(y1A,y1B))intersect=max(0,min(x2_A,x2_B)−max(x1_A,x1_B))×max(0,min(y2_A,y2_B)−max(y1_A,y1_B))intersect=max(0,min(x2A,x2B)−max(x1A,x1B))×max(0,min(y2A,y2B)−max(y1A,y1B))
-
并集面积:union=areaA+areaB−intersectunion=areaA+areaB−intersectunion=areaA+areaB−intersect
-
IoU = intersect / union
手动计算关键 IoU(只保留最大匹配):
| Detection | 最佳匹配 GT | IoU 值 |
|---|---|---|
| D1 | GT1 | IoU ≈ (40×40)/(50×50 + 50×50 − 40×40) = 1600 / (2500 + 2500 − 1600) = 1600 / 3400 ≈ 0.4706 |
| D2 | GT2 | 框几乎完全重合 → IoU ≈ 0.96 |
| D3 | GT3 | 同理,IoU ≈ 0.81 |
| D4 | GT1 | 完全重合 → IoU = 1.0 |
| D5 | 无匹配 | ------(所有 IoU = 0) |
⚠️ 注意:虽然 D1 和 D4 都匹配 GT1,但一个 GT 只能被匹配一次(通常分配给置信度最高且 IoU ≥ threshold的 detection)。但在 AP 计算中,我们会按置信度排序后逐个处理,并标记 GT 是否已被"占用"。
第二步:按置信度降序排列预测框
| 排名 | Detection | 置信度 | 最佳 GT | IoU |
|---|---|---|---|---|
| 1 | D1 | 0.95 | GT1 | 0.4706 |
| 2 | D2 | 0.90 | GT2 | 0.96 |
| 3 | D3 | 0.85 | GT3 | 0.81 |
| 4 | D4 | 0.60 | GT1 | 1.0 |
| 5 | D5 | 0.50 | ------ | 0.0 |
注意:尽管 D4 的 IoU 更高,但置信度低,排在后面。
第三步:对每个 IoU 阈值(0.5, 0.55, ..., 0.95)分别计算 AP
我们以 IoU=0.5 和 IoU=0.75 为例详细计算,再推广到全部 10 个阈值。
🔹 情况 1:IoU 阈值 = 0.5
我们遍历排序后的 detections,判断是否为 TP(True Positive):
- 初始化:所有 GT 未被匹配(GT1, GT2, GT3: available)
- 设置 IoU_thres = 0.5
逐个处理:
- D1 (conf=0.95, IoU=0.4706 < 0.5)→ FP
- D2 (IoU=0.96 ≥ 0.5,且 GT2 可用)→ TP,标记 GT2 已用
- D3 (IoU=0.81 ≥ 0.5,GT3 可用)→ TP,标记 GT3 已用
- D4 (IoU=1.0 ≥ 0.5,但 GT1 仍可用!)→ TP,标记 GT1 已用。虽然 D1 也想匹配 GT1,但因 IoU<0.5 被拒,所以 GT1 仍空闲。
- D5 (IoU=0)→ FP
✅ 得到序列(按顺序):
- Predictions: [D1, D2, D3, D4, D5]
- Labels: [FP, TP, TP, TP, FP]
累计 TP 数:[0,1,2,3,3],总正样本数(GT 数)= 3,计算每个点的 Precision 和 Recall:
| k | Cumulative TP | FP | Precision = TP/(TP+FP) | Recall = TP / 3 |
|---|---|---|---|---|
| 1 | 0 | 1 | 0/1 = 0.00 | 0/3 = 0.00 |
| 2 | 1 | 1 | 1/2 = 0.50 | 1/3 ≈ 0.333 |
| 3 | 2 | 1 | 2/3 ≈ 0.667 | 2/3 ≈ 0.667 |
| 4 | 3 | 1 | 3/4 = 0.75 | 3/3 = 1.00 |
| 5 | 3 | 2 | 3/5 = 0.60 | 1.00 |
注意:Precision 在 recall 不增加时可能下降,但标准做法是进行 precision 的单调修正(non-increasing):从右往左取最大值。修正后 Precision(确保随 recall 增加不下降):
- Recall 点:[0.00, 0.333, 0.667, 1.00]
- 对应 Precision(取每个 recall 下的最大 precision):
- 在 recall=1.00 时,max precision from right = max(0.60, 0.75) = 0.75
- recall=0.667 → max(0.667, 0.75) = 0.75
- recall=0.333 → max(0.50, 0.75) = 0.75
- recall=0.00 → 保持 0(或忽略)
但更标准的做法是使用 所有 recall 点下的插值 precision ,COCO 使用 101 点插值(r = 0.00, 0.01, ..., 1.00) ,但为简化,我们采用 所有 unique recall 点下的最大 precision 并计算曲线下面积(梯形法)。Unique recall levels: 0, 1/3, 2/3, 1。对应 max precision at or after each recall:
- r=0 → p=0.75(因为之后有更高 precision)
- 实际标准方法:对每个 recall level r,取 max{p(r') | r' ≥ r}
所以:
- r=0 → max p = 0.75
- r=1/3 → max p = 0.75
- r=2/3 → max p = 0.75
- r=1 → p = 0.75
于是 PR 曲线是平的?这不太对。让我们重新按 COCO 官方方式:直接对 PR 曲线所有点进行积分(使用所有检测点) ,但更简单的是采用 11 点插值法(教学常用):
11-point interpolation:在 recall = 0.0, 0.1, 0.2, ..., 1.0 处,取该 recall 及之后的最大 precision。
但我们只有 recall 到 1.0,且最大 precision 在 recall≥0.333 后都是 0.75。所以 11 点中:
- recall ≤ 0.333 → 可取到 precision 0.75(因为存在 recall=0.333 时 p=0.5,但之后有更高,所以取 0.75)
- 实际上,在 11 点法中,每个 r_i = i/10,取 max{p | recall ≥ r_i}
查表:
- r ≥ 0.0 → max p = 0.75
- r ≥ 0.1 → max p = 0.75
- ...
- r ≥ 1.0 → p = 0.75
所以所有 11 点 precision 都是 0.75?这显然高估了。问题出在:D1 是 FP,拉低了早期 precision ,但因为我们只在有效 recall 点采样,而模型在 recall=0.333 时 precision=0.5,之后提升到 0.75。更准确的方式(现代标准):使用所有 positive predictions 的 recall 点,并计算 AUC(Area Under Curve) via interpolated PR 。但为教学清晰,我们采用 "所有检测点"的 precision-recall 点 + 单调修正 + 梯形积分。修正后的 precision 序列(从后往前取最大):
- 原始(按检测顺序)recall: [0, 0.333, 0.667, 1.0, 1.0]
- 原始 precision: [0, 0.5, 0.667, 0.75, 0.6]
从右往左修正 precision:
- 最后一个:0.6 → 但 recall 不变,所以保留最后一个有效点为 (1.0, 0.75)
- 倒数第二:0.75 → keep
- 倒数第三:0.667 → max(0.667, 0.75) = 0.75
- 倒数第四:0.5 → max(0.5, 0.75) = 0.75
- 第一个:0 → 可设为 0.75(但 recall=0 时通常不计入)
所以有效 PR 点为:
- (0.00, 1.0) ← 人为添加起点(recall=0, precision=1)
- (0.333, 0.75)
- (0.667, 0.75)
- (1.00, 0.75)
用梯形法计算 AUC:AP0.5=∑i=1n(ri−ri−1)⋅piAP0.5=∑{i=1}^n(r_i−r{i−1})⋅p_iAP0.5=∑i=1n(ri−ri−1)⋅pi 。取点:
- r0=0, p0=1(惯例)
- r1=0.333, p1=0.75
- r2=0.667, p2=0.75
- r3=1.00, p3=0.75
计算:
- (0.333−0) × 0.75 = 0.24975
- (0.667−0.333) × 0.75 = 0.2505
- (1.00−0.667) × 0.75 = 0.24975
总和 ≈ 0.75
✅ 所以 AP@0.5 ≈ 0.75
🔹 情况 2:IoU 阈值 = 0.75
现在 threshold 更严格。重新判断每个 detection 是否 TP(IoU ≥ 0.75):
- D1: IoU=0.47 < 0.75 → FP
- D2: IoU=0.96 ≥ 0.75 → TP(GT2)
- D3: IoU=0.81 ≥ 0.75 → TP(GT3)
- D4: IoU=1.0 ≥ 0.75 → TP(GT1)
- D5: IoU=0 → FP
似乎和 IoU=0.5 时结果一样!因为 D1 虽然 IoU=0.47,但即使在 0.5 也没被算 TP,而其他都满足 0.75。所以 AP@0.75 = 0.75?等等!不对:D3 的 IoU=0.81 ≥ 0.75 → OK。但假如 D3 的 IoU 是 0.70,那在 0.75 就不算 TP。但在本例中,D2、D3、D4 都满足 ≥0.75,所以结果相同。
✅ AP@0.75 = 0.75
🔹 情况 3:IoU 阈值 = 0.8
- D1: 0.47 → FP
- D2: 0.96 ≥ 0.8 → TP
- D3: 0.81 ≥ 0.8 → TP
- D4: 1.0 → TP
- D5: FP
✅ 依然 3 TP → AP@0.75 = 0.75
🔹 情况 4:IoU 阈值 = 0.85
- D3: IoU=0.81 < 0.85 → 不再是 TP!
- D2: 0.96 → TP
- D4: 1.0 → TP
- D1, D5: FP
现在只有 2 个 TP(D2, D4),漏检了 GT3。排序后:
- D1: FP
- D2: TP(GT2)
- D3: IoU=0.81 < 0.85 → FP(GT3 未被匹配)
- D4: TP(GT1)
- D5: FP
- TP 序列:[0,1,1,2,2]
- Recall = TP / 3 → [0, 0.333, 0.333, 0.667, 0.667]
- Precision = [0, 1/2=0.5, 1/3≈0.333, 2/4=0.5, 2/5=0.4]
修正 precision(从右往左取最大):
- recall=0.667 → max p = max(0.5, 0.4) = 0.5
- recall=0.333 → max(0.5, 0.333, 0.5) = 0.5
- recall=0 → 0.5
有效点:
- (0, 1) ← 起点
- (0.333, 0.5)
- (0.667, 0.5)
AUC = (0.333−0)×0.5 + (0.667−0.333)×0.5 = 0.1665 + 0.167 ≈ 0.3335
✅ AP@0.85 ≈ 0.333
🔹 情况 5:IoU = 0.9
- D2: 0.96 ≥ 0.9 → TP
- D3: 0.81 < 0.9 → FP
- D4: 1.0 → TP
- 其他 FP
✅ 同样只有 2 TP → AP ≈ 0.333
🔹 情况 6:IoU = 0.95
- D2: IoU=0.96 ≥ 0.95 → TP
- D4: 1.0 → TP
- D3: 0.81 < 0.95 → FP
还是 2 TP → AP ≈ 0.333
假设 D2 的 IoU 正好是 0.96,满足 0.95。
但如果 D2 的 IoU 是 0.94,则在 0.95 时只有 D4 是 TP → AP 更低。
第四步:汇总 10 个 IoU 阈值下的 AP
| IoU 阈值 | 是否满足 TP 条件(D1,D2,D3,D4) | TP 数 | AP(近似) |
|---|---|---|---|
| 0.50 | D2✓, D3✓, D4✓ | 3 | 0.75 |
| 0.55 | 同上(D3=0.81>0.55) | 3 | 0.75 |
| 0.60 | 同上 | 3 | 0.75 |
| 0.65 | 同上 | 3 | 0.75 |
| 0.70 | 同上 | 3 | 0.75 |
| 0.75 | 同上 | 3 | 0.75 |
| 0.80 | 同上(D3=0.81>0.80) | 3 | 0.75 |
| 0.85 | D3✗ | 2 | 0.333 |
| 0.90 | D3✗ | 2 | 0.333 |
| 0.95 | D3✗ | 2 | 0.333 |
注意:D3 的 IoU=0.81,所以在 0.85 及以上失效。
现在计算平均 AP:前 7 个(0.50~0.80):7 × 0.75 = 5.25,后 3 个(0.85~0.95):3 × 0.333 ≈ 0.999。总和 ≈ 5.25 + 0.999 = 6.249,平均 = 6.249 / 10 ≈ 0.625
第五步:得到最终 mAP@0.5**:0.95**
由于只有 1 个类别,mAP@0.5:0.95 = AP@[0.5:0.95] ≈ 0.625
python
import numpy as np
from collections import defaultdict
from typing import List, Tuple, Dict, Optional
def calculate_iou(box1: np.ndarray, box2: np.ndarray) -> float:
"""
计算两个框之间并集的交集(IoU)。
Box format: [x1, y1, x2, y2]
"""
x1_max = max(box1[0], box2[0])
y1_max = max(box1[1], box2[1])
x2_min = min(box1[2], box2[2])
y2_min = min(box1[3], box2[3])
if x2_min <= x1_max or y2_min <= y1_max:
return 0.0 # No intersection
intersection_area = (x2_min - x1_max) * (y2_min - y1_max)
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
union_area = area1 + area2 - intersection_area
return intersection_area / union_area
def calculate_ap_per_class(
pred_boxes: np.ndarray,
pred_scores: np.ndarray,
pred_labels: np.ndarray,
gt_boxes: np.ndarray,
gt_labels: np.ndarray,
class_id: int,
iou_threshold: float = 0.5
) -> float:
"""
计算给定IoU阈值下单个类别的平均精度(AP)。
pred_boxes: Shape (N_pred, 4) - 预测边界框
pred_scores: Shape (N_pred,) - 预测得分/置信度
pred_labels: Shape (N_pred,) - 预测的类标签
gt_boxes: Shape (N_gt, 4) - ground truth 边界框
gt_labels: Shape (N_gt,) - ground truth 类标签
class_id: 要评估的类ID
iou_threshold: 考虑匹配的IoU阈值
Returns:
指定类别和IoU阈值的AP。
"""
# 过滤特定类的预测和基本事实
pred_mask = pred_labels == class_id
class_pred_boxes = pred_boxes[pred_mask]
class_pred_scores = pred_scores[pred_mask]
gt_mask = gt_labels == class_id
class_gt_boxes = gt_boxes[gt_mask]
if len(class_gt_boxes) == 0:
# 该类别没有 ground truth,AP 未定义。通常情况下为 0 。
return 0.0
if len(class_pred_boxes) == 0:
# No predictions for this class
return 0.0
# 按置信度得分从高到低对预测结果进行排序
sorted_indices = np.argsort(-class_pred_scores)
class_pred_boxes = class_pred_boxes[sorted_indices]
class_pred_scores = class_pred_scores[sorted_indices]
# 创建一个数组,用于记录哪些真实框已被匹配成功
matched_gts = np.zeros(len(class_gt_boxes), dtype=bool)
# 初始化用于记录真阳性(TP)和假阳性(FP)的列表
tp = np.zeros(len(class_pred_boxes))
fp = np.zeros(len(class_pred_boxes))
for i, pred_box in enumerate(class_pred_boxes):
# 计算与同一类别的所有真实框的交并比(IoU)
ious = np.array([
calculate_iou(pred_box, gt_box) for gt_box in class_gt_boxes
])
# 找到尚未匹配的、与当前待匹配对象最匹配的真实框。
best_match_idx = -1
best_iou = -1
for j in range(len(class_gt_boxes)):
if not matched_gts[j] and ious[j] > best_iou:
best_iou = ious[j]
best_match_idx = j
if best_iou >= iou_threshold:
# 将真实情况标记为匹配状态
matched_gts[best_match_idx] = True
tp[i] = 1
else:
fp[i] = 1
# 计算累计的真阳性(TP)和假阳性(FP)值
cum_tp = np.cumsum(tp)
cum_fp = np.cumsum(fp)
# 计算召回率和准确率
recalls = cum_tp / len(class_gt_boxes) # 该类别的总 GT 数量
precisions = cum_tp / (cum_tp + cum_fp)
# 如果未进行任何预测,则处理除以零的情况
if len(precisions) == 0:
return 0.0
# 采用非递增精度插值(如在 COCO 评估中所采用的那样),从列表的末尾开始依次回溯,以确保精度不会递增。
for i in range(len(precisions) - 2, -1, -1):
precisions[i] = max(precisions[i], precisions[i + 1])
# 采用插值后的精度值进行整合,使用公式:AP = sum((r[i] - r[i-1]) * p_interp[i]),
# 在召回率为 0 的位置添加一个点,其精度值为 precisions[0],以开始整合过程
unique_recalls, unique_indices = np.unique(recalls, return_index=True)
interpolated_precisions = []
for ur in unique_recalls:
# 找出所有召回率大于或等于当前唯一召回率的最大精度值。
max_prec = np.max(precisions[recalls >= ur])
interpolated_precisions.append(max_prec)
interpolated_precisions = np.array(interpolated_precisions)
# 计算插值后的 PR 曲线下的面积。 注意:我们使用唯一的召回率及其对应的插值精度值
# 第一个召回率通常为 0,因此我们不会从 0 开始进行积分。 标准方法:累加 (delta_recall * 精度在该召回率下的值)
if len(unique_recalls) > 1:
ap = np.sum(np.diff(unique_recalls) * interpolated_precisions[:-1])
else:
# 如果只有一个召回级别(例如,召回率 = 0),那么平均精度通常为 0 。
ap = 0.0
return ap
def calculate_map_per_iou_threshold(
pred_boxes: np.ndarray,
pred_scores: np.ndarray,
pred_labels: np.ndarray,
gt_boxes: np.ndarray,
gt_labels: np.ndarray,
iou_threshold: float
) -> float:
"""
计算在特定的 IoU(交并比)阈值下所有类别的平均精度(mAP)。
Args:
pred_boxes: Shape (N_pred, 4)
pred_scores: Shape (N_pred,)
pred_labels: Shape (N_pred,)
gt_boxes: Shape (N_gt, 4)
gt_labels: Shape (N_gt,)
iou_threshold: IoU threshold for evaluation
Returns:
在给定的 IoU 阈值下的 mAP 值。
"""
unique_classes = np.unique(np.concatenate([pred_labels, gt_labels]))
aps = []
for class_id in unique_classes:
ap = calculate_ap_per_class(
pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels, class_id, iou_threshold
)
aps.append(ap)
# 计算在预测结果或真实数据中所包含的所有类别上的平均 AP 值
return np.mean(aps) if aps else 0.0
def calculate_map_range(
pred_boxes: np.ndarray,
pred_scores: np.ndarray,
pred_labels: np.ndarray,
gt_boxes: np.ndarray,
gt_labels: np.ndarray,
iou_thresholds: List[float] = [0.5 + i * 0.05 for i in range(10)] # 0.5 to 0.95 step 0.05
) -> float:
"""
通过将多个 IoU 阈值下的 mAP 值进行平均计算,来得出 mAP@0.5:0.95(或任何自定义的范围)的值。
Args:
pred_boxes, pred_scores, pred_labels, gt_boxes, gt_labels: As above.
iou_thresholds: List of IoU thresholds to evaluate.
Returns:
所有指定的 IoU 阈值下的平均 mAP 值。
"""
map_values = []
for iou_thresh in iou_thresholds:
map_val = calculate_map_per_iou_threshold(
pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels, iou_thresh
)
map_values.append(map_val)
return np.mean(map_values) if map_values else 0.0
def run_example():
"""
运行一个多类示例以测试该实现的正确性。
"""
print("--- Multi-Class Target Detection Example ---")
# 构造一些 ground truth boxes and labels Format: [x1, y1, x2, y2, class_id]
gt_data = [
[10, 10, 50, 50, 0], # Cat
[60, 60, 100, 100, 1], # Dog
[110, 110, 150, 150, 2], # Bird
[160, 160, 200, 200, 0], # Cat
]
gt_boxes = np.array([[item[0], item[1], item[2], item[3]] for item in gt_data])
gt_labels = np.array([item[4] for item in gt_data])
# 做出一些预测 Format: [x1, y1, x2, y2, class_id, confidence_score]
pred_data = [
[12, 12, 48, 48, 0, 0.9], # Correct Cat - High Confidence
[112, 112, 148, 148, 2, 0.85], # Correct Bird - High Confidence
[62, 62, 98, 98, 1, 0.8], # Correct Dog - Medium Confidence
[162, 162, 198, 198, 0, 0.75], # Correct Cat - Lower Confidence
[10, 10, 50, 50, 1, 0.6], # Wrong Class (Cat GT, Dog Pred)
[210, 210, 250, 250, 2, 0.55], # Wrong Location (FP Bird)
[60, 60, 100, 100, 0, 0.5], # Wrong Class (Dog GT, Cat Pred)
]
pred_boxes = np.array([[item[0], item[1], item[2], item[3]] for item in pred_data])
pred_labels = np.array([item[4] for item in pred_data])
pred_scores = np.array([item[5] for item in pred_data])
print("Ground Truth Boxes (x1, y1, x2, y2, class):")
print(gt_boxes)
print(gt_labels)
print("\nPrediction Boxes (x1, y1, x2, y2, class, conf):")
print(pred_boxes)
print(pred_labels)
print(pred_scores)
# --- Calculate metrics ---
iou_thresh = 0.5
ap_cat_0_5 = calculate_ap_per_class(
pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels, class_id=0, iou_threshold=iou_thresh
)
ap_dog_0_5 = calculate_ap_per_class(
pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels, class_id=1, iou_threshold=iou_thresh
)
ap_bird_0_5 = calculate_ap_per_class(
pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels, class_id=2, iou_threshold=iou_thresh
)
map_0_5 = calculate_map_per_iou_threshold(
pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels, iou_threshold=iou_thresh
)
map_05_to_095 = calculate_map_range(
pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels
)
print("\n--- Calculated Metrics ---")
print(f"AP for Class 'Cat' (@IoU=0.5): {ap_cat_0_5:.4f}")
print(f"AP for Class 'Dog' (@IoU=0.5): {ap_dog_0_5:.4f}")
print(f"AP for Class 'Bird' (@IoU=0.5): {ap_bird_0_5:.4f}")
print(f"mAP@0.5: {map_0_5:.4f}")
print(f"mAP@0.5:0.95: {map_05_to_095:.4f}")
if __name__ == "__main__":
run_example()
若不加入更多逻辑,直接将其映射到 sklearn 的格式会比较复杂。 sklearn 的评估接口(AP)要求输入的是二元标签(0 或 1)以及连续的分数。我们的任务需要根据交并比(IoU)将预测结果与真实结果进行匹配。 因此,sklearn 并不能直接用于 mAP 的计算。
在目标检测任务中,除了 mAP@0.5:0.95 这一核心指标外,还有多个维度的评估指标可用于分析模型在定位(Localization) 、分类(Classification) 和置信度校准(Confidence Calibration) 等方面的性能。
按功能维度分类的目标检测评估指标
1. 定位相关指标(Localization)
这些指标衡量预测框与真实框的空间对齐程度:
| 指标 | 公式 / 说明 | 特点 |
|---|---|---|
| IoU(Intersection over Union) | ||
| DIoU / CIoU / GIoU | IoU 的改进版本,引入中心点距离、长宽比等 | 更平滑、可导,常用于 loss,也可作评估 |
| Localization Error | 如 L1 或 L2 距离 between centers | 直观但不考虑尺度 |
| AR (Average Recall) @ different IoU | 在固定检测数量下(如每图100个预测),计算不同 IoU 阈值下的平均召回率 | COCO 提供 AR@0.5, AR@0.75, AR@[0.5:0.95] |
COCO 定位能力分析:通过比较 mAP@0.5 与 mAP@0.95,可判断模型定位精度------若两者差距大,说明模型只能在宽松 IoU 下工作。
2. 分类相关指标(Classification)
目标检测中的分类通常指"类别标签是否正确",前提是定位已达标(IoU ≥ threshold)。
| 指标 | 说明 |
|---|---|
| Per-class AP / mAP | 各类别的 AP,反映类别不平衡或难易程度 |
| Precision-Recall Curve (PR Curve) | 横轴 recall,纵轴 precision,曲线下面积即 AP |
| F1-score @ optimal confidence threshold | 平衡 precision 与 recall,常用于部署时选阈值 |
| Top-1 / Top-k Classification Accuracy (within TP) | 对所有 TP 预测,统计其类别是否正确(在多类检测中) |
分类错误通常表现为 FP(False Positive) ------ 即 IoU 足够但类别错。
3. 置信度与校准相关指标(Confidence Calibration)
模型输出的置信度(confidence score)应反映预测为真的概率。理想情况下:
"置信度 = 80%" → 实际正确率 ≈ 80%
常用指标:
| 指标 | 说明 |
|---|---|
| Reliability Diagram(可靠性图) | 将预测按置信度分桶(如 [0.0--0.1), ..., [0.9--1.0]),每桶计算实际准确率,画出"置信度 vs 准确率"曲线。理想为 y=x 对角线 |
| Expected Calibration Error (ECE) | 各桶加权平均的 |
| Maximum Calibration Error (MCE) | 最大偏差 |
这些指标在自动驾驶、医疗影像等高风险场景尤为重要。
其他高级分析工具(COCO-style)
COCO 官方评估还提供按不同维度分组的 mAP,用于深入诊断:
| 分组维度 | 说明 |
|---|---|
| Scale | APS(小目标 <32²)、APM(32²~96²)、APL(>96²) |
| Max Detections per Image | 如 mAP@1(每图最多1个预测)、mAP@10、mAP@100,反映模型在有限预算下的效率 |
| Number of GT instances | 分析模型在拥挤场景(多目标)下的表现 |
目标检测常用指标速查表
| 目标 | 推荐指标 |
|---|---|
| 整体性能 | mAP@0.5:0.95(COCO 标准) |
| 宽松定位 | mAP@0.5 |
| 严格定位 | mAP@0.75 或 mAP@0.9 |
| 小目标检测 | APS |
| 分类质量 | Per-class AP, PR 曲线 |
| 置信度校准 | Reliability Diagram, ECE |
| 检测效率 | AR@100, Inference FPS |
AUROC(Area Under the ROC Curve)和 AUPR(Area Under the Precision-Recall Curve)是评估二分类模型性能 的两个核心指标。它们从不同角度刻画模型在不同决策阈值下的判别能力,适用于不同数据分布和任务目标。
AUROC(Area Under the ROC Curve)
- ROC 曲线 (Receiver Operating Characteristic Curve):以 假正率(FPR) 为横轴,真正率(TPR,即 Recall) 为纵轴,描绘模型在所有可能分类阈值下的性能轨迹。
- AUROC:ROC 曲线下面积,取值范围为 [0,1] 。
- AUROC = 1:完美分类器
- AUROC = 0.5:等同于随机猜测
- AUROC < 0.5:模型表现比随机还差(可反转预测)
| 指标 | 公式 | 说明 |
|---|---|---|
| TPR(Sensitivity, Recall) | TPTP+FN\frac{TP}{TP+FN}TP+FNTP | 正样本中被正确识别的比例 |
| FPR(1 − Specificity) | FPFP+TN\frac{FP}{FP+TN}FP+TNFP | 负样本中被错误判为正的比例 |
ROC 曲线通过遍历所有可能的置信度阈值(如 0.0 → 1.0),计算每一点的 (FPR, TPR),连接成曲线。
计算方法(离散近似) : 假设模型对 N 个样本输出预测分数 sis_isi ,真实标签 yi∈{0,1}y_i∈\{0,1\}yi∈{0,1}:
- 将样本按预测分数降序排序
- 遍历每个可能的阈值(通常取每个样本的分数作为阈值)
- 对每个阈值,计算对应的 (FPR, TPR)
- 使用梯形法则 或Mann-Whitney U 统计量 计算 AUC:AUROC=∑i:yi=1rank(si)−n1(n1+1)2n1n0AUROC=\frac{∑_{i:yi=1}rank(s_i)−\frac{n_1(n_1+1)}2}{n_1n_0}AUROC=n1n0∑i:yi=1rank(si)−2n1(n1+1)
其中:
- n1 = 正样本数, n0 = 负样本数
- rank(si) 是正样本 i 在全体样本中的排序位置(从高到低)
✅ 这表明:AUROC 等价于"随机选一个正样本和一个负样本,模型给正样本打分更高的概率"。
考量的信息维度
| 维度 | 说明 |
|---|---|
| 判别能力(Discriminative Power) | 能否将正负样本分开(与阈值无关) |
| 对类别不平衡的鲁棒性? | ❌ 不鲁棒:FPR 分母含 TN,当负样本极多时,FP 变化对 FPR 影响微弱,AUROC 仍可能很高 |
| 是否依赖阈值? | 否,评估的是全阈值范围下的综合性能 |
适用场景 ,推荐使用当:
- 正负样本比例相对均衡(如 1:1 到 1:10)
- 关注整体判别能力,而非特定操作点
- 医学诊断、信用评分等需要权衡敏感性与特异性的领域
不推荐使用当:
- 正样本极度稀疏(如欺诈检测、目标检测、罕见病筛查)
- 负样本数量巨大且未明确定义(如目标检测中的背景区域)
AUPR(Area Under the Precision-Recall Curve)
- PR 曲线 (Precision-Recall Curve):以 Recall(TPR) 为横轴,Precision 为纵轴。
- AUPR :PR 曲线下面积,取值范围 [0,1] ,但上限受正样本比例影响。
核心公式
| 指标 | 公式 | 说明 |
|---|---|---|
| Precision | TPTP+FP\frac{TP}{TP+FP}TP+FPTP | 预测为正的样本中,真实为正的比例 |
| Recall(TPR) | TPTP+FN\frac{TP}{TP+FN}TP+FNTP | 同上 |
注意:PR 曲线不涉及 TN,因此不受负样本规模影响。
计算方法
- 同样按预测分数降序排列样本
- 依次将每个样本视为"正",计算累计 TP、FP
- 得到一系列 (Recall, Precision) 点
- 插值处理 :为避免 precision 随 recall 增加而波动,通常采用 "非递增修正"(从右向左取最大 precision)
- 用梯形法 或矩形法积分求面积
实践中常用
sklearn.metrics.average_precision_score,它实现的是 "插值 PR AUC",更稳定。
考量的信息维度
| 维度 | 说明 |
|---|---|
| 正样本预测质量 | 在不同召回水平下,预测的准确性如何 |
| 对类别不平衡的敏感性 | 高度敏感:当正样本少时,少量 FP 会大幅降低 precision,AUPR 下降明显 |
| 关注 FP 的代价 | 是,precision 直接反映误报成本 |
适用场景 ,强烈推荐用于:
- 正样本稀疏的任务,比如:
- 目标检测(前景 vs 背景)
- 异常检测
- 推荐系统(点击/购买行为稀疏)
- 生物信息学(致病基因预测)
- 关注高 precision 或高 recall 的实际部署场景
- 需要评估模型在有限资源下(如只处理 top-K 预测)的表现
不适合:
- 负样本难以定义或无限多(但此时 PR 仍是唯一合理选择)
- 仅关心整体排序能力而不关心误报(此时 AUROC 可能更合适)
AUROC vs AUPR:对比
| 特性 | AUROC | AUPR |
|---|---|---|
| 是否依赖 TN | 是 | 否 |
| 对类别不平衡的敏感性 | 低(可能虚高) | 高(反映真实困难) |
| 理想值 | 1.0 | 1.0(但上限 ≈ 正样本比例) |
| 随机模型期望值 | 0.5 | ≈ 正样本比例(如 1% 正样本 → AUPR≈0.01) |
| 目标检测适用性 | 不适用 | 核心指标(AP 即 AUPR) |
| 解释性 | "正样本得分高于负样本的概率" | "在不同召回率下,预测的准确率" |
💡 经典结论 (Davis & Goadrich, 2006):
"ROC 曲线变化平缓时,PR 曲线可能剧烈变化" ------ 在不平衡数据中,AUPR 更能揭示模型差异。
计算 AUROC 问题设定
假设我们有一个 AI 模型用于判断患者是否患有某种疾病(二分类):
- 正类(Positive):患病(label = 1)
- 负类(Negative):健康(label = 0)
数据集(共 6 个样本)
| 样本 ID | 真实标签yi | 模型预测分数 sis_isi(如 softmax 概率) |
|---|---|---|
| A | 1 | 0.9 |
| B | 1 | 0.8 |
| C | 0 | 0.7 |
| D | 1 | 0.6 |
| E | 0 | 0.5 |
| F | 0 | 0.4 |
正样本数 n1=3n_1 = 3n1=3(A, B, D)
负样本数 n0=3n_0 = 3n0=3(C, E, F)
这是一个平衡数据集,适合用 AUROC 分析。
步骤 1:按预测分数降序排列
我们将样本按 si 从高到低排序,便于遍历阈值:
| 排名 | 样本 | si | yi |
|---|---|---|---|
| 1 | A | 0.9 | 1 |
| 2 | B | 0.8 | 1 |
| 3 | C | 0.7 | 0 |
| 4 | D | 0.6 | 1 |
| 5 | E | 0.5 | 0 |
| 6 | F | 0.4 | 0 |
步骤 2:枚举所有可能的分类阈值
在二分类中,阈值 τ\tauτ 决定:若 si≥τ,则预测为 1;否则为 0。我们取所有可能的"有效"阈值,通常包括:
- 每个样本的分数
- 极端值(如 τ=+∞τ=+∞τ=+∞, τ=−∞\tau = -\inftyτ=−∞)
但更高效的做法是:在每两个相邻分数之间设置阈值,使得预测结果发生变化。我们考虑以下 7 个关键阈值(从高到低):
| 阈值τ | 预测规则 | 预测结果(按样本 A~F) |
|---|---|---|
| τ>0.9 | 全预测为 0 | [0,0,0,0,0,0] |
| 0.8<τ≤0.9 | 仅 A=1 | [1,0,0,0,0,0] |
| 0.7<τ≤0.8 | A,B=1 | [1,1,0,0,0,0] |
| 0.6<τ≤0.7 | A,B,C=1 | [1,1,1,0,0,0] |
| 0.5<τ≤0.6 | A,B,C,D=1 | [1,1,1,1,0,0] |
| 0.4<τ≤0.5 | A~E=1 | [1,1,1,1,1,0] |
| τ≤0.4 | 全预测为 1 | [1,1,1,1,1,1] |
注意:阈值选在分数之间,确保每次只增加一个正预测。
步骤 3:对每个阈值计算 TP, FP, TN, FN → 得到 (FPR, TPR)
我们逐行计算:
1. τ>0.9**→ 全预测 0**
- TP = 0, FP = 0
- FN = 3(所有正样本漏检), TN = 3
- TPR = 0 / 3 = 0.0
- FPR = 0 / 3 = 0.0
2. 0.8<τ≤0.9**→ 预测 [1,0,0,0,0,0]**
- TP = 1(A 正确)
- FP = 0(无负样本被误判)
- FN = 2(B,D 漏检)
- TN = 3
- TPR = 1/3 ≈ 0.333
- FPR = 0/3 = 0.0
3. 0.7<τ≤0.8**→ [1,1,0,0,0,0]**
- TP = 2(A,B)
- FP = 0
- TPR = 2/3 ≈ 0.667
- FPR = 0/3 = 0.0
4. 0.6<τ≤0.7**→ [1,1,1,0,0,0]**
- TP = 2(A,B;C 是负样本但被预测为1 → FP)
- FP = 1(C)
- TPR = 2/3 ≈ 0.667
- FPR = 1/3 ≈ 0.333
5. 0.5<τ≤0.6 → [1,1,1,1,0,0]
- TP = 3(A,B,D)
- FP = 1(C)
- TPR = 3/3 = 1.0
- FPR = 1/3 ≈ 0.333
6. 0.4<τ≤0.5 → [1,1,1,1,1,0]
- TP = 3
- FP = 2(C,E)
- TPR = 1.0
- FPR = 2/3 ≈ 0.667
7. τ≤0.4**→ 全预测 1**
- TP = 3
- FP = 3(C,E,F)
- TPR = 1.0
- FPR = 3/3 = 1.0
步骤 4:列出 ROC 曲线上的点(FPR, TPR)**
按阈值从高到低(即从保守到激进),得到 ROC 点序列:
| 点序 | FPR | TPR | 说明 |
|---|---|---|---|
| P0 | 0.0 | 0.0 | 全负 |
| P1 | 0.0 | 0.333 | 仅 A |
| P2 | 0.0 | 0.667 | A+B |
| P3 | 0.333 | 0.667 | A+B+C(C 是 FP) |
| P4 | 0.333 | 1.0 | A+B+D(全召回) |
| P5 | 0.667 | 1.0 | 多一个 FP(E) |
| P6 | 1.0 | 1.0 | 全正 |
ROC 曲线从 (0,0) 开始,到 (1,1) 结束。
步骤 5:计算 AUROC --- 方法 1(梯形积分法)
我们将 ROC 曲线视为由线段连接的折线,用梯形面积公式 累加:对于相邻两点 (xi,yi) 和 (xi+1,yi+1),梯形面积为:Area∗i=(x∗i+1−xi)⋅yi+yi+12\text{Area}*i = (x*{i+1} - x_i) \cdot \frac{y_i + y_{i+1}}{2}Area∗i=(x∗i+1−xi)⋅2yi+yi+1 现在逐段计算:
- P0 → P1 : x:0→0, y:0→0.333 ; → 宽度 = 0 → 面积 = 0
- P1 → P2 : x:0→0 y:0.333→0.667; → 宽度 = 0 → 面积 = 0
- P2 → P3 : x:0→0.333, y:0.667→0.667; → 面积 = (0.333 − 0) × (0.667 + 0.667)/2 = 0.333 × 0.667 ≈ 0.222
- P3 → P4 : x:0.333→0.333, y:0.667→1.0 ;→ 宽度 = 0 → 面积 = 0
- P4 → P5 : x:0.333→0.667, y:1.0→1.0; → 面积 = (0.334) × (1.0 + 1.0)/2 ≈ 0.334 × 1.0 = 0.334
- P5 → P6 : x:0.667→1.0, y:1.0→1.0; → 面积 = (0.333) × 1.0 = 0.333
总 AUROC ≈ 0 + 0 + 0.222 + 0 + 0.334 + 0.333 = 0.889
更精确计算:
- 第3段:(1/3) × (2/3) = 2/9 ≈ 0.2222
- 第5段:(1/3) × 1 = 1/3 ≈ 0.3333
- 第6段:(1/3) × 1 = 1/3 ≈ 0.3333
→ 总和 = 2/9 + 1/3 + 1/3 = 2/9 + 6/9 = 8/9 ≈ 0.888...
步骤 6:计算 AUROC --- 方法 2(排序概率解释)
AUROC = P(模型给正样本打分 > 负样本打分)
我们枚举所有 正样本-负样本对,共 n1×n0=3×3=9 对:
正样本:A(0.9), B(0.8), D(0.6)
负样本:C(0.7), E(0.5), F(0.4)
比较每一对:
| 正样本 | 负样本 | 正分数 > 负分数? |
|---|---|---|
| A (0.9) | C (0.7) | ✅ 是 |
| A | E (0.5) | ✅ |
| A | F (0.4) | ✅ |
| B (0.8) | C (0.7) | ✅ |
| B | E | ✅ |
| B | F | ✅ |
| D (0.6) | C (0.7) | ❌ 否(0.6 < 0.7) |
| D | E (0.5) | ✅ |
| D | F (0.4) | ✅ |
成功对数:8 对 ; 失败对数:1 对(D vs C)。因此:AUROC=89≈0.8889\text{AUROC} = \frac{8}{9} \approx 0.8889AUROC=98≈0.8889 与梯形法结果一致!
这就是 AUROC 的概率解释 :它衡量模型对正负样本的排序能力,与具体阈值无关。
步骤 7:绘制 ROC 曲线
- 从 (0,0) 垂直上升到 (0, 0.667)(因前两个预测都是 TP,无 FP)
- 然后水平右移到 (0.333, 0.667)(出现第一个 FP)
- 再垂直上升到 (0.333, 1.0)(召回最后一个正样本)
- 最后水平到 (1,1)
python
import numpy as np
from typing import List, Tuple, Union
import matplotlib.pyplot as plt # 用于绘制 ROC 曲线
def calculate_tpr_fpr_at_threshold(y_true: np.ndarray, y_scores: np.ndarray, threshold: float) -> Tuple[float, float]:
"""
在给定阈值下计算真正率(TPR)和假正率(FPR)。
y_true (np.ndarray): 真实标签,形状为 (N,),元素为 0 或 1。
y_scores (np.ndarray): 模型预测的分数,形状为 (N,)。
threshold (float): 用于分类的阈值。
返回:
Tuple[float, float]: (TPR, FPR)
"""
# 根据阈值生成预测标签
y_pred = (y_scores >= threshold).astype(int)
# 计算混淆矩阵的元素
tp = np.sum((y_true == 1) & (y_pred == 1))
fp = np.sum((y_true == 0) & (y_pred == 1))
tn = np.sum((y_true == 0) & (y_pred == 0))
fn = np.sum((y_true == 1) & (y_pred == 0))
# 计算 TPR (Recall) 和 FPR
# 防止除零错误
tpr = tp / (tp + fn) if (tp + fn) > 0 else 0.0
fpr = fp / (fp + tn) if (fp + tn) > 0 else 0.0
return tpr, fpr
def compute_roc_curve(y_true: np.ndarray, y_scores: np.ndarray) -> Tuple[List[float], List[float]]:
"""
计算 ROC 曲线上的点。
y_true (np.ndarray): 真实标签,形状为 (N,),元素为 0 或 1。
y_scores (np.ndarray): 模型预测的分数,形状为 (N,)。
返回:
Tuple[List[float], List[float]]: (fpr_list, tpr_list)
"""
# 获取所有唯一的预测分数,并按降序排列
unique_scores = np.unique(y_scores)[::-1]
# 初始化列表存储 FPR 和 TPR
fpr_list = []
tpr_list = []
# 遍历每个唯一的分数作为阈值
for thresh in unique_scores:
tpr, fpr = calculate_tpr_fpr_at_threshold(y_true, y_scores, thresh)
fpr_list.append(fpr)
tpr_list.append(tpr)
# ROC 曲线必须从 (0,0) 开始,到 (1,1) 结束
# 如果第一个点不是 (0,0),则添加
if not (len(fpr_list) > 0 and fpr_list[0] == 0.0 and tpr_list[0] == 0.0):
fpr_list.insert(0, 0.0)
tpr_list.insert(0, 0.0)
# 如果最后一个点不是 (1,1),则添加
if not (len(fpr_list) > 0 and fpr_list[-1] == 1.0 and tpr_list[-1] == 1.0):
fpr_list.append(1.0)
tpr_list.append(1.0)
return fpr_list, tpr_list
def calculate_auc_trapz(x_vals: List[float], y_vals: List[float]) -> float:
"""
使用梯形法则计算曲线下面积(AUC)。
x_vals (List[float]): X 轴坐标(如 FPR)。
y_vals (List[float]): Y 轴坐标(如 TPR)。
返回:
float: 计算得到的 AUC。
"""
auc = 0.0
for i in range(1, len(x_vals)):
base_width = x_vals[i] - x_vals[i - 1]
height_avg = (y_vals[i] + y_vals[i - 1]) / 2.0
auc += base_width * height_avg
return auc
def calculate_auroc(y_true: np.ndarray, y_scores: np.ndarray) -> float:
"""
计算 AUROC(Area Under the ROC Curve)。
y_true (np.ndarray): 真实标签,形状为 (N,),元素为 0 或 1。
y_scores (np.ndarray): 模型预测的分数,形状为 (N,)。
返回:
float: 计算得到的 AUROC 值。
"""
if len(y_true) != len(y_scores):
raise ValueError("y_true 和 y_scores 的长度必须相同")
# 检查标签是否为二分类
unique_labels = np.unique(y_true)
if not set(unique_labels).issubset({0, 1}):
raise ValueError("y_true 必须只包含 0 和 1")
# 检查是否有足够的正类和负类样本
pos_count = np.sum(y_true == 1)
neg_count = np.sum(y_true == 0)
if pos_count == 0 or neg_count == 0:
# 如果只有一类样本,AUROC 没有意义,通常返回 0.5(随机猜测)
print("警告:数据集中只有一类样本,AUROC 返回 0.5。")
return 0.5
# 计算 ROC 曲线上的点
fpr_list, tpr_list = compute_roc_curve(y_true, y_scores)
# 使用梯形法则计算 AUC
auroc = calculate_auc_trapz(fpr_list, tpr_list)
return auroc
# 图像级分类。我们假设有一个模型,对 10 张图像进行预测,输出"猫"的最大置信度分数,然后判断该图像是否包含猫。
def run_auroc_example():
"""
运行一个图像级二分类(猫存在性)的 AUROC 示例。
"""
# 模拟数据, 真实标签:1 表示图像中有猫,0 表示没有猫
y_true_images = np.array([1, 1, 0, 1, 0, 0, 1, 0, 1, 0])
# 预测分数:模型认为该图像包含猫的置信度
y_scores_images = np.array([0.9, 0.8, 0.3, 0.85, 0.2, 0.1, 0.95, 0.4, 0.7, 0.15])
print("图像真实标签 (有猫=1, 无猫=0):", y_true_images)
print("模型预测分数 (猫的置信度):", y_scores_images)
# --- 计算 AUROC ---
auroc_custom = calculate_auroc(y_true_images, y_scores_images)
print("\n--- 自定义实现计算结果 ---")
print(f"自定义 AUROC: {auroc_custom:.4f}")
# --- 绘制 ROC 曲线 ---
fpr_list, tpr_list = compute_roc_curve(y_true_images, y_scores_images)
plt.figure(figsize=(8, 6))
plt.plot(fpr_list, tpr_list, marker='o', label=f'ROC Curve (AUROC = {auroc_custom:.4f})')
plt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Random Classifier (AUROC = 0.5)')
plt.xlabel('假正率 (FPR)')
plt.ylabel('真正率 (TPR)')
plt.title('ROC 曲线')
plt.legend()
plt.grid(True)
plt.show()
# --- 与 sklearn 对比 ---
print("\n--- 与 sklearn 对比 ---")
try:
from sklearn.metrics import roc_auc_score, roc_curve
auroc_sklearn = roc_auc_score(y_true_images, y_scores_images)
fpr_sk, tpr_sk, _ = roc_curve(y_true_images, y_scores_images)
print(f"sklearn AUROC: {auroc_sklearn:.4f}")
print(f"自定义 AUROC: {auroc_custom:.4f}")
print(f"差异: {abs(auroc_sklearn - auroc_custom):.6f}")
# 检查 ROC 曲线点是否一致
print("\n自定义 FPR:", fpr_list)
print("sklearn FPR: ", fpr_sk)
print("自定义 TPR:", tpr_list)
print("sklearn TPR: ", tpr_sk)
# 由于浮点精度和实现细节,允许微小差异
curves_match = np.allclose(fpr_list, fpr_sk, atol=1e-7) and np.allclose(tpr_list, tpr_sk, atol=1e-7)
if curves_match:
print("\n✅ ROC 曲线点与 sklearn 完全一致!")
else:
print("\n⚠️ ROC 曲线点与 sklearn 存在微小差异(通常在可接受范围内)")
except ImportError:
print("sklearn 未安装,跳过对比。")
if __name__ == "__main__":
run_auroc_example()
关于 AUROC 分析与讨论
为什么 AUROC = 8/9?
- 模型几乎完美排序,只有一个错误:将负样本 C(0.7)排在了正样本 D(0.6)前面。
- 这反映了模型的主要缺陷:对 D 的置信度低估,或对 C 的置信度高估。
如果数据不平衡会怎样?假设负样本增加到 97 个(总 100 样本,正=3),而模型仍只错判 C 为高分:
- FPR = 1/97 ≈ 0.01,几乎不变
- AUROC 仍接近 0.99,虚高!
- 但实际应用中,FP=1 可能代价很高(如误诊健康人为病人)→ 此时应看 AUPR 或 Precision@Recall=1.0
AUROC 的优势与局限
| 优势 | 局限 |
|---|---|
| 与阈值无关,评估整体排序能力 | 对类别不平衡不敏感(可能误导) |
| 几何直观,易于可视化 | 无法反映实际业务中的误报成本 |
| 在平衡数据中非常有效 | 不适用于目标检测等 TN 无限场景 |
计算 AUPR 问题设定
假设我们开发一个 AI 模型,用于从大量人群中筛查一种罕见疾病:
- 正类(1):患病(非常稀少)
- 负类(0):健康(绝大多数)
数据集(共 10 个样本)
| 样本 ID | 真实标签yi | 模型预测分数si |
|---|---|---|
| P1 | 1 | 0.95 |
| P2 | 1 | 0.85 |
| N1 | 0 | 0.80 |
| N2 | 0 | 0.70 |
| N3 | 0 | 0.60 |
| N4 | 0 | 0.55 |
| P3 | 1 | 0.50 |
| N5 | 0 | 0.40 |
| N6 | 0 | 0.30 |
| N7 | 0 | 0.20 |
正样本数 n1=3n_1 = 3n1=3(P1, P2, P3)
负样本数 n0=7 → 正样本比例 = 3/10 = 30%(已算中度不平衡;若扩展到 1% 会更极端)
步骤 1:按预测分数降序排列
这是计算 PR 曲线的关键第一步:
| 排名 | 样本 | si | yi | 累计 TP | 累计 FP |
|---|---|---|---|---|---|
| 1 | P1 | 0.95 | 1 | 1 | 0 |
| 2 | P2 | 0.85 | 1 | 2 | 0 |
| 3 | N1 | 0.80 | 0 | 2 | 1 |
| 4 | N2 | 0.70 | 0 | 2 | 2 |
| 5 | N3 | 0.60 | 0 | 2 | 3 |
| 6 | N4 | 0.55 | 0 | 2 | 4 |
| 7 | P3 | 0.50 | 1 | 3 | 4 |
| 8 | N5 | 0.40 | 0 | 3 | 5 |
| 9 | N6 | 0.30 | 0 | 3 | 6 |
| 10 | N7 | 0.20 | 0 | 3 | 7 |
注意:P3 虽然真实为正,但模型对其信心较低(0.50),排在多个负样本之后。
步骤 2:对每个"有效阈值"计算 Precision 和 Recall
我们以每个样本的分数作为阈值上限 ,即:当 τ≤si 时,该样本被预测为正。由于我们按分数降序处理,每处理一个样本,就相当于降低一次阈值,增加一个预测。对每个位置 k(前 k 个样本被预测为正),计算:
- TP(k) = 前 k 个中真实为 1 的数量
- FP(k) = 前 k 个中真实为 0 的数量
- Precision(k) = TP(k) / (TP(k) + FP(k))
- Recall(k) = TP(k) / n1 = TP(k) / 3
当 TP+FP=0(即无预测)时,通常跳过或设 precision=1(但 recall=0)
现在逐行计算:
| k | 样本 | TP | FP | Precision = TP/(TP+FP) | Recall = TP/3 |
|---|---|---|---|---|---|
| 1 | P1 | 1 | 0 | 1/1 = 1.000 | 1/3 ≈ 0.333 |
| 2 | P2 | 2 | 0 | 2/2 = 1.000 | 2/3 ≈ 0.667 |
| 3 | N1 | 2 | 1 | 2/3 ≈ 0.667 | 2/3 ≈ 0.667 |
| 4 | N2 | 2 | 2 | 2/4 = 0.500 | 2/3 ≈ 0.667 |
| 5 | N3 | 2 | 3 | 2/5 = 0.400 | 2/3 ≈ 0.667 |
| 6 | N4 | 2 | 4 | 2/6 ≈ 0.333 | 2/3 ≈ 0.667 |
| 7 | P3 | 3 | 4 | 3/7 ≈ 0.429 | 3/3 = 1.000 |
| 8 | N5 | 3 | 5 | 3/8 = 0.375 | 1.000 |
| 9 | N6 | 3 | 6 | 3/9 ≈ 0.333 | 1.000 |
| 10 | N7 | 3 | 7 | 3/10 = 0.300 | 1.000 |
注意:在 k=7 时,虽然 recall 达到 1.0,但 precision 反而比 k=6 时上升 (因为新增的是 TP)。这说明 PR 曲线不是单调的!
步骤 3:对 PR 曲线进行"非递增修正"(标准做法)
由于 PR 曲线可能波动(如 k=6 → k=7 时 precision 从 0.333 升到 0.429),而理想情况下,随着 recall 增加,precision 应不增 (因为要召回更多正样本,必然引入更多 FP)。因此,COCO 和 sklearn 等采用 "从右向左取最大 precision" 的修正方法:
对每个 recall 水平 r,定义:P~(r)=maxP(r′)∣r′≥r\tilde{P}(r) = \max P(r') \mid r' \geq rP~(r)=maxP(r′)∣r′≥r
我们从最高 recall 向最低遍历,记录当前最大 precision:
| k | Recall | 原始 Precision | 修正后 PrecisionP*P* |
|---|---|---|---|
| 10 | 1.000 | 0.300 | 0.429 ← max from k=7~10 |
| 9 | 1.000 | 0.333 | 0.429 |
| 8 | 1.000 | 0.375 | 0.429 |
| 7 | 1.000 | 0.429 | 0.429 |
| 6 | 0.667 | 0.333 | 0.429 ← 因为后面有更高 |
| 5 | 0.667 | 0.400 | 0.429 |
| 4 | 0.667 | 0.500 | 0.429 ❌ → 等等!这里有问题! |
关键纠正 :实际上,修正应在 unique recall levels 上进行,而不是每个 k。观察 recall 值,只有三个 unique levels:
- r=0.333(TP=1)
- r=0.667 (TP=2)
- r=1.000 (TP=3)
对每个 unique recall,取该 recall 及更高 recall 中的最大 precision:
- For r=1.000r = 1.000r=1.000 :
- 可选 precision: [0.300, 0.333, 0.375, 0.429]
- max = 0.429
- For r=0.667r = 0.667r=0.667 :
- 包括所有 recall ≥ 0.667 → 即 TP≥2 的所有点
- precision 值: [0.667, 0.500, 0.400, 0.333, 0.429, 0.375, 0.333, 0.300]
- max = 0.667(来自 k=3)
- For r=0.333r = 0.333r=0.333 :
- recall ≥ 0.333 → 所有点
- max precision = 1.000(来自 k=1 或 k=2)
所以修正后的 PR 点为:
| Recall | 修正后 Precision |
|---|---|
| 0.000 | 1.000 ← 人为添加起点(惯例) |
| 0.333 | 1.000 |
| 0.667 | 0.667 |
| 1.000 | 0.429 |
为什么 recall=0 时 precision=1?
这是 PR 曲线的标准起点,表示"不预测任何样本"时,没有 FP,可视为 precision=1(尽管无意义,但便于积分)。
步骤 4:计算 AUPR(使用修正后的 PR 点)
我们使用矩形法(sklearn 默认) 或插值法 。这里采用 "插值 PR AUC" ,即:AUPR=∑i=1m(ri−ri−1)⋅piAUPR=∑{i=1}^m(r_i−r{i−1})⋅p_iAUPR=∑i=1m(ri−ri−1)⋅pi 。其中 pi 是在 recall 区间 [ri−1,ri] 上的最大 precision(即修正后的值)。取点序列(含起点):
- (r0,p0)=(0.000,1.000)
- (r1,p1)=(0.333,1.000)
- (r2,p2)=(0.667,0.667)
- (r3,p3)=(1.000,0.429)
计算各段面积:
- 0.000 → 0.333 : 宽度 = 0.333,高度 = 1.000;面积 = 0.333 × 1.000 = 0.333
- 0.333 → 0.667 : 宽度 = 0.334,高度 = 0.667;面积 ≈ 0.334 × 0.667 ≈ 0.223
- 0.667 → 1.000 : 宽度 = 0.333,高度 = 0.429;面积 ≈ 0.333 × 0.429 ≈ 0.143
总 AUPR ≈ 0.333 + 0.223 + 0.143 = 0.699
更精确计算(用分数):
- 1/3 × 1 = 1/3 ≈ 0.3333
- 1/3 × 2/3 = 2/9 ≈ 0.2222
- 1/3 × 3/7 = 1/7 ≈ 0.1429
→ 总和 = 1/3 + 2/9 + 1/7 = (21 + 14 + 9)/63 = 44/63 ≈ 0.6984
python
import numpy as np
from typing import List, Tuple
import matplotlib.pyplot as plt # 用于绘制 PR 曲线
def calculate_precision_recall_at_threshold(y_true: np.ndarray, y_scores: np.ndarray, threshold: float) -> Tuple[float, float]:
"""
在给定阈值下计算精确率(Precision)和召回率(Recall)。
y_true (np.ndarray): 真实标签,形状为 (N,),元素为 0 或 1。
y_scores (np.ndarray): 模型预测的分数,形状为 (N,)。
threshold (float): 用于分类的阈值。
返回:
Tuple[float, float]: (Precision, Recall)
"""
# 根据阈值生成预测标签
y_pred = (y_scores >= threshold).astype(int)
# 计算混淆矩阵的元素
tp = np.sum((y_true == 1) & (y_pred == 1))
fp = np.sum((y_true == 0) & (y_pred == 1))
# tn = np.sum((y_true == 0) & (y_pred == 0)) # PR 曲线不需要 TN
fn = np.sum((y_true == 1) & (y_pred == 0))
# 计算 Precision 和 Recall
# 防止除零错误
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 # 如果没有预测为正,则 precision 为 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 # 如果没有真实正样本,则 recall 为 0
return precision, recall
def compute_pr_curve(y_true: np.ndarray, y_scores: np.ndarray) -> Tuple[List[float], List[float]]:
"""
计算 PR 曲线上的点。
y_true (np.ndarray): 真实标签,形状为 (N,),元素为 0 或 1。
y_scores (np.ndarray): 模型预测的分数,形状为 (N,)。
返回:
Tuple[List[float], List[float]]: (recall_list, precision_list)
"""
# 获取所有唯一的预测分数,并按降序排列
unique_scores = np.unique(y_scores)[::-1]
# 初始化列表存储 Recall 和 Precision
recall_list = []
precision_list = []
# 遍历每个唯一的分数作为阈值
for thresh in unique_scores:
precision, recall = calculate_precision_recall_at_threshold(y_true, y_scores, thresh)
recall_list.append(recall)
precision_list.append(precision)
# 为了正确计算 AUC,需要进行非递增修正 (interpolation),从右往左扫描,确保 precision 不随 recall 增加而减少
corrected_precision_list = precision_list[:]
for i in range(len(corrected_precision_list) - 2, -1, -1):
corrected_precision_list[i] = max(corrected_precision_list[i], corrected_precision_list[i + 1])
# sklearn 方式:对 unique_recall_levels 进行插值
unique_recalls, indices = np.unique(recall_list, return_index=True)
interpolated_precisions = []
for ur in unique_recalls:
# 对于每个 unique recall level,找到所有 recall >= ur 的最大 precision
max_prec = np.max(np.array(precision_list)[np.array(recall_list) >= ur])
interpolated_precisions.append(max_prec)
# 添加起始点 (0, max_prec_of_all)
if len(interpolated_precisions) > 0:
max_prec_overall = interpolated_precisions[0] # 最大 precision 通常是第一个
unique_recalls = np.insert(unique_recalls, 0, 0.0)
interpolated_precisions = np.insert(interpolated_precisions, 0, max_prec_overall)
return unique_recalls.tolist(), interpolated_precisions
def calculate_auc_trapz(x_vals: List[float], y_vals: List[float]) -> float:
"""
使用梯形法则计算曲线下面积(AUC)。
x_vals (List[float]): X 轴坐标(如 Recall)。
y_vals (List[float]): Y 轴坐标(如 Precision)。
返回:
float: 计算得到的 AUC。
"""
auc = 0.0
for i in range(1, len(x_vals)):
base_width = x_vals[i] - x_vals[i - 1]
height_avg = (y_vals[i] + y_vals[i - 1]) / 2.0
auc += base_width * height_avg
return auc
def calculate_aupr(y_true: np.ndarray, y_scores: np.ndarray) -> float:
"""
计算 AUPR(Area Under the Precision-Recall Curve)。采用与 sklearn.metrics.average_precision_score 类似的插值方法。
y_true (np.ndarray): 真实标签,形状为 (N,),元素为 0 或 1。
y_scores (np.ndarray): 模型预测的分数,形状为 (N,)。
返回:
float: 计算得到的 AUPR 值。
"""
if len(y_true) != len(y_scores):
raise ValueError("y_true 和 y_scores 的长度必须相同")
# 检查标签是否为二分类
unique_labels = np.unique(y_true)
if not set(unique_labels).issubset({0, 1}):
raise ValueError("y_true 必须只包含 0 和 1")
# 检查是否有正类样本
pos_count = np.sum(y_true == 1)
if pos_count == 0:
# 如果没有正样本,AUPR 没有意义,通常返回 0
print("警告:数据集中没有正类样本,AUPR 返回 0。")
return 0.0
neg_count = np.sum(y_true == 0)
if neg_count == 0:
# 如果全是正样本,模型应该完美预测,AUPR 为 1
print("警告:数据集中没有负类样本,AUPR 返回 1。")
return 1.0
# 计算经过插值的 PR 曲线上的点
recall_list, interpolated_precision_list = compute_pr_curve(y_true, y_scores)
# 使用梯形法则计算 AUC,sklearn 的 average_precision_score 使用公式: sum((r[i] - r[i-1]) * p_interp[i]),这与梯形法则略有不同,它是矩形面积之和。但 sklearn 内部实现是基于排序对的数学等价形式。
aupr = 0.0
for i in range(1, len(recall_list)):
delta_recall = recall_list[i] - recall_list[i-1]
aupr += delta_recall * interpolated_precision_list[i]
# AP = sum((r[i] - r[i-1]) * p_interp[i]) where p_interp[i] = max(p[j] for j where r[j] >= r[i])
recall_array = np.array(recall_list)
precision_array = np.array(interpolated_precision_list)
diff_recall = np.diff(recall_array) # [r[1]-r[0], r[2]-r[1], ...]
precision_for_integration = precision_array[1:] # [p[1], p[2], ...]
aupr = np.sum(diff_recall * precision_for_integration)
return aupr
def run_aupr_example():
"""
运行一个二分类 AUPR 示例,并与 sklearn 对比。
"""
# 使用我们在之前的对话中讨论过的不平衡数据集
# 10 个样本,3 个正样本 (P1, P2, P3),7 个负样本 (N1-N7)
y_true = np.array([1, 1, 0, 0, 0, 0, 1, 0, 0, 0]) # P1, P2, N1, N2, N3, N4, P3, N5, N6, N7
y_scores = np.array([0.95, 0.85, 0.80, 0.70, 0.60, 0.55, 0.50, 0.40, 0.30, 0.20])
print("真实标签 (y_true):", y_true)
print("预测分数 (y_scores):", y_scores)
# --- 计算 AUPR ---
aupr_custom = calculate_aupr(y_true, y_scores)
print("\n--- 自定义实现计算结果 ---")
print(f"自定义 AUPR: {aupr_custom:.4f}")
# --- 与 sklearn 对比 ---
print("\n--- 与 sklearn 对比 ---")
try:
from sklearn.metrics import average_precision_score, precision_recall_curve
aupr_sklearn = average_precision_score(y_true, y_scores)
precision_sk, recall_sk, _ = precision_recall_curve(y_true, y_scores)
print(f"sklearn AUPR (AP): {aupr_sklearn:.4f}")
print(f"自定义 AUPR: {aupr_custom:.4f}")
print(f"差异: {abs(aupr_sklearn - aupr_custom):.6f}")
# 检查 PR 曲线点是否一致
# sklearn 的 PR 曲线是倒序的 (recall 从 high->low),并且包含了起始点
print("\n自定义 Recall (unique, interp):", np.array(compute_pr_curve(y_true, y_scores)[0]))
print("sklearn Recall: ", recall_sk)
print("自定义 Precision (interp):", np.array(compute_pr_curve(y_true, y_scores)[1]))
print("sklearn Precision: ", precision_sk)
# sklearn 的 recall 和 precision 数组长度可能比我们处理后的长(因为它保留了所有点,然后插值)
if abs(aupr_sklearn - aupr_custom) < 1e-6:
print("\n✅ AUPR 计算结果与 sklearn 完全一致!")
else:
print("\n⚠️ AUPR 计算结果与 sklearn 存在微小差异,请检查实现细节。")
# --- 绘制 PR 曲线 --- 获取插值后的曲线点用于绘图
recall_interp, prec_interp = compute_pr_curve(y_true, y_scores)
# 为了绘图,我们还需要原始的 PR 点
unique_scores_rev = np.unique(y_scores)[::-1]
orig_precisions = []
orig_recalls = []
for thresh in unique_scores_rev:
p, r = calculate_precision_recall_at_threshold(y_true, y_scores, thresh)
orig_precisions.append(p)
orig_recalls.append(r)
plt.figure(figsize=(10, 6))
plt.subplot(1, 2, 1)
plt.plot(orig_recalls, orig_precisions, marker='o', linestyle='--', label='原始 PR 点', alpha=0.6)
plt.plot(recall_interp, prec_interp, marker='x', label='插值 PR 曲线', linewidth=2)
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title(f'Precision-Recall Curve\n(AUPR = {aupr_custom:.4f})')
plt.legend()
plt.grid(True)
plt.subplot(1, 2, 2)
# 绘制 sklearn 的 PR 曲线作为对比
plt.plot(recall_sk, precision_sk, marker='.', label='sklearn PR Curve', alpha=0.8)
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title(f'sklearn PR Curve\n(AP = {aupr_sklearn:.4f})')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
except ImportError:
print("sklearn 未安装,跳过对比。")
if __name__ == "__main__":
run_aupr_example()
对比:如果用原始未修正的 PR 点会怎样?
若直接用原始点做梯形积分,会低估性能(因 precision 波动下降)。而修正后的 AUPR 更能反映模型在最佳操作点下的潜力 ,这也是 COCO 和 sklearn 的标准。 sklearn.metrics.average_precision_score 默认使用这种插值方法,结果 ≈ 0.698。
与随机模型和理想模型对比
- 随机模型期望 AUPR ≈ 正样本比例 = 3/10 = 0.3
- 理想模型 AUPR = 1.0(所有正样本排在最前)
- 我们的模型 AUPR ≈ 0.698 → 明显优于随机,但有提升空间(主要因 P3 排太靠后)
AUPR 如何反映模型缺陷?
- 模型成功将 P1、P2 排在最前(precision=1.0)
- 但在召回第 3 个正样本(P3)前,误报了 4 个负样本(N1~N4)
- 导致在 recall=1.0 时,precision 仅为 3/7 ≈ 0.429
- AUPR 敏锐捕捉到了这一"高召回代价"
如果这是癌症筛查系统,意味着:"为了不错过任何一个患者(recall=100%),每检测出 3 个病人,就有 4 个健康人被误诊!"→ AUPR 低提醒我们:需要提高对 P3 类样本的判别能力。
与 AUROC 对比(同一数据集)
快速计算 AUROC(用排序对方法):
- 正样本:P1(0.95), P2(0.85), P3(0.50)
- 负样本:N1(0.80), N2(0.70), ..., N7(0.20)
正-负对共 3×7=21 对。哪些对失败(正分数 ≤ 负分数)?
- P3(0.50) vs N1(0.80), N2(0.70), N3(0.60), N4(0.55) → 4 对失败
- 其他对均成功(P1、P2 分数高于所有负样本)
→ 成功对 = 21 − 4 = 17
→ AUROC = 17/21 ≈ 0.810
虽然 AUROC=0.81 看似不错,但 AUPR=0.70 更真实反映了在高召回下的低精度问题。
AUPR 计算全流程回顾
- 按预测分数降序排列样本
- 逐个累加 TP/FP,计算每个点的 Precision 和 Recall
- 提取 unique recall levels
- 对每个 recall,取其及更高 recall 中的最大 precision(非递增修正)
- 用修正后的 (Recall, Precision) 点,通过插值积分计算 AUPR
AUPR = 44/63 ≈ 0.698
AUPR 特别适合评估不平衡数据中的模型性能 ;它直接反映业务关心的 precision-recall 权衡 ;在目标检测中,AP(Average Precision)就是 AUPR ;当正样本稀疏时,AUPR 比 AUROC 更具信息量