mAP, AUOCR, AUPR怎么计算、怎么用

在计算机视觉,特别是目标检测任务中,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.5IoU=0.75 为例详细计算,再推广到全部 10 个阈值。


🔹 情况 1:IoU 阈值 = 0.5

我们遍历排序后的 detections,判断是否为 TP(True Positive):

  • 初始化:所有 GT 未被匹配(GT1, GT2, GT3: available)
  • 设置 IoU_thres = 0.5

逐个处理:

  1. D1 (conf=0.95, IoU=0.4706 < 0.5)→ FP
  2. D2 (IoU=0.96 ≥ 0.5,且 GT2 可用)→ TP,标记 GT2 已用
  3. D3 (IoU=0.81 ≥ 0.5,GT3 可用)→ TP,标记 GT3 已用
  4. D4 (IoU=1.0 ≥ 0.5,但 GT1 仍可用!)→ TP,标记 GT1 已用。虽然 D1 也想匹配 GT1,但因 IoU<0.5 被拒,所以 GT1 仍空闲。
  5. 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):

  1. D1: IoU=0.47 < 0.75 → FP
  2. D2: IoU=0.96 ≥ 0.75 → TP(GT2)
  3. D3: IoU=0.81 ≥ 0.75 → TP(GT3)
  4. D4: IoU=1.0 ≥ 0.75 → TP(GT1)
  5. 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。排序后:

  1. D1: FP
  2. D2: TP(GT2)
  3. D3: IoU=0.81 < 0.85 → FP(GT3 未被匹配)
  4. D4: TP(GT1)
  5. 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}:

  1. 将样本按预测分数降序排序
  2. 遍历每个可能的阈值(通常取每个样本的分数作为阈值)
  3. 对每个阈值,计算对应的 (FPR, TPR)
  4. 使用梯形法则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,因此不受负样本规模影响。

计算方法

  1. 同样按预测分数降序排列样本
  2. 依次将每个样本视为"正",计算累计 TP、FP
  3. 得到一系列 (Recall, Precision) 点
  4. 插值处理 :为避免 precision 随 recall 增加而波动,通常采用 "非递增修正"(从右向左取最大 precision)
  5. 梯形法矩形法积分求面积

实践中常用 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 现在逐段计算:

  1. P0 → P1 : x:0→0, y:0→0.333 ; → 宽度 = 0 → 面积 = 0
  2. P1 → P2 : x:0→0 y:0.333→0.667; → 宽度 = 0 → 面积 = 0
  3. 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
  4. P3 → P4 : x:0.333→0.333, y:0.667→1.0 ;→ 宽度 = 0 → 面积 = 0
  5. 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
  6. 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 可能代价很高(如误诊健康人为病人)→ 此时应看 AUPRPrecision@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)=max⁡P(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

  1. For r=1.000r = 1.000r=1.000 :
    • 可选 precision: [0.300, 0.333, 0.375, 0.429]
    • max = 0.429
  2. 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)
  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)

计算各段面积:

  1. 0.000 → 0.333 : 宽度 = 0.333,高度 = 1.000;面积 = 0.333 × 1.000 = 0.333
  2. 0.333 → 0.667 : 宽度 = 0.334,高度 = 0.667;面积 ≈ 0.334 × 0.667 ≈ 0.223
  3. 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 计算全流程回顾

  1. 按预测分数降序排列样本
  2. 逐个累加 TP/FP,计算每个点的 Precision 和 Recall
  3. 提取 unique recall levels
  4. 对每个 recall,取其及更高 recall 中的最大 precision(非递增修正)
  5. 用修正后的 (Recall, Precision) 点,通过插值积分计算 AUPR

AUPR = 44/63 ≈ 0.698

AUPR 特别适合评估不平衡数据中的模型性能 ;它直接反映业务关心的 precision-recall 权衡 ;在目标检测中,AP(Average Precision)就是 AUPR ;当正样本稀疏时,AUPR 比 AUROC 更具信息量

相关推荐
Godspeed Zhao2 小时前
从零开始学AI5——数学应知应会0
人工智能
腾讯云大数据2 小时前
【数据湖仓】腾讯云发布面向AI的数据湖方案:TCLake+EMR打造AI-Ready数据底座
人工智能·云计算·腾讯云
橘子师兄2 小时前
C++AI大模型接入SDK—API接入大模型思路
开发语言·数据结构·c++·人工智能
DS随心转APP2 小时前
豆包输出word指令
人工智能·ai·chatgpt·deepseek·ds随心转
java1234_小锋2 小时前
【AI大模型面试题】在训练超大规模语言模型(如千亿参数级别)时,除了显存限制,最主要的训练挑战是什么?
人工智能·语言模型·自然语言处理
戴西软件2 小时前
戴西软件发布3DViz设计与仿真数据轻量化平台
大数据·人工智能·安全·机器学习·汽车
码农三叔2 小时前
(4-1)机械传动系统与关节设计:关节驱动方式对比
人工智能·架构·机器人·人形机器人
小汤圆不甜不要钱2 小时前
「Datawhale」RAG技术全栈指南 Task 3
人工智能·深度学习·机器学习·rag
AskHarries2 小时前
在 Qoder CLI 集成墨刀 MCP(modao-proto)完整指南
人工智能·ai编程