PRC(Precision-Recall Curve)
之前在做分类时候接触过 ROC,今天来介绍一个用于目标检测的评估标准曲线 PRC。我们知道在判定
我们先简单复习一下 ROC 曲线
TPR(True Positive Rate)
所有正例样本中预测为正例样本也就是召回率(recall)
FPR(False Positive Rate)
所有负例样本中,预测为正例样本的
那么我们知道 AUC 指标就是 ROC 曲线与 x 轴和 y 轴围成区域的面积就是 AUC 的指标。AUC 指标越接近 1 说明分类器越好,AUC 越接近 0.5 说明分类器越不好。
其实 AP(Average precision) 就是计算 PRC 曲线下的面积
| 目标 | 预测边界框 | 置信度 | 边界框和GT的IoU |
|---|---|---|---|
| car1 | bbox1 | 0.9 | 0.8 |
| car2 | bbox2 | 0.7 | 0.2 |
| car2 | bbox3 | 0.6 | 0.9 |
| car3 | bbox4 | 0.1 | 0.3 |
| car3 | bbox5 | 0.8 | 0.9 |
| car4 | bbox6 | 0.8 | 0.7 |
| car5 | bbox7 | 0 | 0 |
在图像中有 5 目标,模型预测结果为 6 个边界框,最后一个 bbox7 是不存在的。其实边界框一共识别出了 4 条 cars。其中第 1 个边界框(bbox1)检测出 car1 而随后 bbox2 和 bbox3 同时预测出 car2。这就是上面这张表的基本含义。随后置信度也就是 bbox 的置信度的得分,然后还有就是 bbox 与 GT 的 IoU 的值
设定 IoU 阈值
首先要考虑给出一个 IoU 的阈值,这里 IoU 阈值暂时设定为 0.5 ,然后在表格添加一列,在这一列中用数值 1 和 0 分别表示边界框与真实框之间计算的 IoU 大于 0.5 和小于 0.5。
| 目标 | 预测边界框 | 置信度 | 边界框和GT的IoU | IoU > 0.5 |
|---|---|---|---|---|
| car1 | bbox1 | 0.9 | 0.8 | 1 |
| car2 | bbox2 | 0.7 | 0.2 | 0 |
| car2 | bbox3 | 0.6 | 0.9 | 1 |
| car3 | bbox4 | 0.1 | 0.3 | 0 |
| car3 | bbox5 | 0.8 | 0.9 | 1 |
| car4 | bbox6 | 0.8 | 0.7 | 1 |
| car5 | bbox7 | 0 | 0 | 0 |
根据置信度得分排序
接下来就是根据置信度对预测框进行排序,因为接下里会利用不同置信度取值来不同精准率(precision)和召回率(recall)
| 排序 | 目标 | 预测边界框 | 置信度 | 边界框和GT的IoU | IoU > 0.5 |
|---|---|---|---|---|---|
| 1 | car1 | bbox1 | 0.9 | 0.8 | 1 |
| 2 | car3 | bbox5 | 0.8 | 0.9 | 1 |
| 3 | car4 | bbox6 | 0.8 | 0.7 | 1 |
| 4 | car2 | bbox2 | 0.7 | 0.2 | 0 |
| 5 | car2 | bbox3 | 0.6 | 0.9 | 1 |
| 6 | car3 | bbox4 | 0.1 | 0.3 | 0 |
| 7 | car5 | bbox7 | 0 | 0 | 0 |
计算 precision 和 recall
我们之前介绍如何判断边界框为 FN 的条件是,groun truth 没有个被任何预测框所覆盖,这里预测框首先是有效的边界框,首先我们按照排序从小到大依次来选取置信度值做置信度的阈值,如果边界框的置信度大于阈值其目标预测值为 1 否则为 0。
| 排序 | 目标 | 预测边界框 | 置信度 | 预测(选取阈值后) | IoU > 0.5 | |
|---|---|---|---|---|---|---|
| 1 | car1 | bbox1 | 0.9 | 1 | 1 | TP |
| 2 | car3 | bbox5 | 0.8 | 0 | 1 | FN |
| 3 | car4 | bbox6 | 0.8 | 0 | 1 | FN |
| 4 | car2 | bbox2 | 0.7 | 0 | 0 | FN |
| 5 | car2 | bbox3 | 0.6 | 0 | 1 | FN |
| 6 | car3 | bbox4 | 0.1 | 0 | 0 | FN |
| 7 | car5 | bbox7 | 0 | 0 | 0 | FN |
我们可以简单回顾一下 FN 定义,groun truth 没有个被任何 TP 预测的边界框所覆盖,注意这里是指 TP 预测边界框,这里因为预测为 0 表示预测背景了,所以记做 FN
如果选取排在第一位 bbox 的置信度的 0.9 作为置信度阈值的话,任何大于 0.9 就是预测(选取阈值后) 而任何小于 0.9 的 bbox 的预测(选取阈值后) 都设置为 0。当一个目标被多个 bbox 所覆盖,这个目标也只能计算一次 FN,因为图像上有 5 辆 cars ,且其中一只被正确预测,所以 FN 取值为 4 而不是 6。
接下来我们再来一起计算一个取不同置信度作为自信度阈值来计算其对应的精准度和
| 排序 | 目标 | 预测边界框 | 置信度 | 预测(选取阈值后) | IoU > 0.5 | |
|---|---|---|---|---|---|---|
| 1 | car1 | bbox1 | 0.9 | 1 | 1 | TP |
| 2 | car3 | bbox5 | 0.8 | 1 | 1 | TP |
| 3 | car4 | bbox6 | 0.8 | 1 | 1 | TP |
| 4 | car2 | bbox2 | 0.7 | 1 | 0 | FP |
| 5 | car2 | bbox3 | 0.6 | 0 | 1 | FN |
| 6 | car3 | bbox4 | 0.1 | 0 | 0 | FN |
| 7 | car5 | bbox7 | 0 | 0 | 0 | FN |
上面是取排序为 4 置信度来做置信度的阈值,那么其实很好理解,值得说一下就是 bbox2 置信度满足,不过因为 IoU 并不满足大于阈值 0.5 所以处理为 FP
| 排序 | 目标 | 置信度 | IoU > 0.5 | precision | recall |
|---|---|---|---|---|---|
| 1 | bbox1 | 0.9 | 1 | 1 | 0.2 |
| 2 | bbox1 | 0.8 | 1 | 1 | 0.4 |
| 3 | bbox6 | 0.8 | 1 | 1 | 0.6 |
| 4 | bbox2 | 0.7 | 0 | 0.75 | 0.6 |
| 5 | bbox3 | 0.6 | 1 | 0.8 | 0.8 |
| 6 | bbox4 | 0.1 | 0 | 0.66 | 0.8 |
这样一来会发现在取不同置信度阈值前提下,会得到不同 precision 和 recall 的组合,然后用这些值来进行绘制一条曲线。然后通过计算这条曲线下方与坐标轴围成的面积,就是衡量目标检测模型的一个指标 AP,不同比赛会对 AP 计算给出不同计算方式。
mAP
上面已经介绍过了目标检测模型检测能力的衡量指标,也介绍什么是 AP 以及如何计算 AP。接下来就来看一看什么是 mAP,其实比较简单,上面 AP 是针对每一个类别进行计算,m 是均值意思,那么也就是将所有类别的 AP 都加起来,然后在除以类别数,就是想要的 mAP。
在许多场景,我们会用 AP 作为 mAP 的简称,因为 IoU 阈值对于 AP 计算比较重要,所以通常在介绍 mAP 都会给出 IoU 阈值的取值是多少。
解读 COCO 评估
- 首先来看第一行,第一行表示从 IoU=0.5 到 IoU=0.95 依次以 0.05 为间隔进行计算 mAP 然后对计算得到 mAP 进行求均值
- 其他两个是分别在 AP=0.5 和 AP=0.75 处来计算 mAP,这里要补充一下为什么分别列出这两个 IoU=0.75 和 IoU=0.5 处的 AP 值呢? 这是因为这两个值具有一定代表含义,IoU=0.5 是比较经典,而 IoU=0.75 是一个比较严格位置来看模型表现,因为 IoU 值越大说明
- 接下来再去看 AP Across Scale 这是看在不同面积下 AP 值的大小
- Average Recall(AR) 中指标表示在每张图像如果检测框为 100 的情况下各个类别平均检出率是多少,当一张图像在每一个图像检出 10 检测框的平均检出能力是多少,然后就是如果图像只给出一个检出框情况下,检出率又是多少
代码实现
def mean_average_precision(pred_boxes,true_boxes,iou_threshold=0.5,box_format="corners",num_classes=80):
#
average_precisions = []
epsilon = 1e-6
for c in range(num_classes):
detections = []
ground_truths = []
for detection in pred_boxes:
if detection[1] == c:
detections.append(detection)
for true_box in true_boxes:
if true_box[1] == c:
ground_truths.append(true_box)
- pred_boxes: 模型对训练数据集中所有样本预测出边界框
[[train_idx, class_pred, prob_score,x1,y1,x2,y2],[].[]]train_idx 就是图像索引,预测框来自哪一个图像,class_pred 预测边界框给出类别,prob_score 边界框置信度得分 - true_boxes: 训练数据集中所有的真实边界框
- iou_threshold: IoU 的阈值
- box_format: 用于指定计算预测边界框和真实框之间 IoU 方式,是角点形式,也就是左上角和右下角确定边界框
- num_classes: 类别数
pred_boxes (list): [[train_idx, class_pred, prob_score,x1,y1,x2,y2],[].[]]
- 在 mAP 其中 mean 表示是按类别的求均值,所以就先计算每个类别的 AP 值,求和后再除以类别数来算计 mAP,所以遍历所有类别,将预测为该类别的预测边界框放入到 detections 中,将属于该类别的真实框放入到 ground_truths 中。
for true_box in true_boxes:
if true_box[1] == c:
ground_truths.append(true_box)
amount_bboxes = Counter([gt[0] for gt in ground_truths])
这里 Counter 用于样本图像中包含边界框个数,计算每张图像中出现的目标数量,也就是真实边界框数量
关于 Counter 用法如下,
gt_truths = [[1,2],[1,3],[2,1],[3,1],[3,2]]
Counter([gt[0] for gt in gt_truths])
#Counter({1: 2, 2: 1, 3: 2})
amount_bboxes = Counter([gt[0] for gt in ground_truths])
for key, val in amount_bboxes.items():
amount_bboxes[key] = torch.zeros(val)
上面代码是将键值转换为 Tensor 类型,
amount_boxes = {0:torch.tensor([0,0,0]),1: torch.tensor([0,0,0]}
gt_truths = [[1,2],[1,3],[2,1],[3,1],[3,2]]
amount_gt_truths = Counter([gt[0] for gt in gt_truths])
for key, val in amount_gt_truths.items():
amount_gt_truths[key] = torch.zeros(val)
amount_gt_truths
Counter({1: tensor([0., 0.]), 2: tensor([0.]), 3: tensor([0., 0.])})
通常会有多个预测边界框会与真实编辑框存在面积交集,不过通常只会保留一个复合条件且最优的预测编辑框为 TP,而其他按 FP 处理。之所以需要这样数据结构,主要是为了记录当前图像中真实边界框是否已经存在其 TP 的预测边界框
detections.sort(key=lambda x:x[2],revser=True)
TP = torch.zeros((len(detections)))
FP = torch.zeros((len(detections)))
total_true_bboxes = len(ground_truths)
上面代码是按置信度对预测边界框进行排序,排序后的预测框集,生成一个[0,0,0...], 在 TP 和 FP 列表中每个位置都对应一个预测边界框。随后对每个预测边界框进行判断其属于 TP 或者还是 FP,就将该列表其所在位置 0 更新为 1
total_true_bboxes = len(ground_truths)
# 遍历所有的预测边界框
for detection_idx, detection in enumerate(detections):
ground_truth_img = [
bbox for bbox in ground_truths if bbox[0] == detection[0]
]
筛选该预测边界框出现图像中所有的真实边界框
num_gts = len(ground_truth_img)
#
best_iou = 0
#遍历该预测框出现图像中的所有的真实边界框
for idx, gt in enumerate(ground_truth_img):
#计算每一个真实边界框框与该预测的边界框的 IoU
iou = intersection_over_union(
torch.tensor(detection[3:]),
torch.tensor(gt[3:]),
box_format=box_format)
#找到与该预测边界框 IoU 最大的真实框
if iou > best_iou:
best_iou = iou
best_gt_idx = idx
下面代码逻辑,首先判断找到与预测边界框计算 IoU 值最大的真实边界框,如果这个 IoU 值大于事先设定好的 IoU 阈值,那么就继续判断与该预测边界框相交的真实边界框是否被占用,如果没有被占用,那么就将说明该预测边界框是该真实边界框的 TP 所以将该预测边界框在 TP 列表中对应位置更新为 1,否则该预测边界框计入到 FP 同时更新 FP 列表中对应位置,如果其计算出 IoU 值不满足大于阈值 IoU 直接计入到 FP 中
if best_iou > iou_threshold:
#判断
if amount_bboxes[detection[0]][best_gt_idx] == 0:
TP[detection_idx] = 1
amount_bboxes[detection[0]][best_gt_idx] = 1
else:
FP[detection_idx] = 1
else:
FP[detection_idx] = 1
TP_cumsum = torch.cumsum(TP,dim=0)
a = torch.zeros((5))
a[0::2] = 1 #tensor([1., 0., 1., 0., 1.])
torch.cumsum(a,dim=0)#tensor([1., 1., 2., 2., 3.])
TP_cumsum = torch.cumsum(TP,dim=0)
FP_cumsum = torch.cumsum(FP,dim=0)
recalls = TP_cumsum/(total_true_bboxes + epsilon)
precisions = torch.divide(TP_cumsum,(TP_cumsum + FP_cumsum + epsilon))
# 这是因为 AP 是计算一个 y 轴是 precision 而 x 轴是 recall
# 对 precisions 和 recalls 分别在第一个位置添加 1 和 0 这因为这个曲线是始于(0,1) 坐标
precisions = torch.cat((torch.tensor([1]),precisions))
recalls = torch.cat((torch.tensor([0]),recalls))
#计算曲线下积分面积也就是该类别的AP值,梯形法则
average_precisions.append(torch.trapz(precisions,recalls))
#计算 mAP
return sum(average_precisions)/len(average_precisions)
完整代码
def mean_average_precision(pred_boxes,true_boxes,iou_threshold=0.5,box_format="corners",num_classes=80):
#AP
average_precisions = []
# 主要为了数值稳定性,例如避免分母为 0
epsilon = 1e-6
# 遍历所有类别,分别计算每一个类别的 AP
for c in range(num_classes):
#预测的边界框
detections = []
#GT边界框
ground_truths = []
for detection in pred_boxes:
if detection[1] == c:
detections.append(detection)
for true_box in true_boxes:
if true_box[1] == c:
ground_truths.append(true_box)
amount_bboxes = Counter([gt[0] for gt in ground_truths])
#对于每一个GT的边界框
for key, val in amount_bboxes.items():
amount_bboxes[key] = torch.zeros(val)
# 按置信度对预测边界框进行排序
detections.sort(key=lambda x:x[2],revser=True)
#元素为预测边界框数量的初始为 0 的列表
# 排序后的预测框集,生成一个[0,0,0...], 在 TP 和 FP 列表中每个位置都对应一个预测边界框
# 随后对每个预测边界框进行判断其属于 TP 或者还是 FP,就将该列表其所在位置 0 更新为 1
TP = torch.zeros((len(detections)))
FP = torch.zeros((len(detections)))
# 真实边界框数量
total_true_bboxes = len(ground_truths)
# 遍历所有的预测边界框
for detection_idx, detection in enumerate(detections):
#筛选该预测边界框出现图像中所有的真实边界框
ground_truth_img = [
bbox for bbox in ground_truths if bbox[0] == detection[0]
]
#在该图像中出现目标的数量
num_gts = len(ground_truth_img)
#
best_iou = 0
#遍历该预测框出现图像中的所有的真实边界框
for idx, gt in enumerate(ground_truth_img):
#计算每一个真实边界框框与该预测的边界框的 IoU
iou = intersection_over_union(
torch.tensor(detection[3:]),
torch.tensor(gt[3:]),
box_format=box_format)
#找到与该预测边界框 IoU 最大的真实框
if iou > best_iou:
best_iou = iou
best_gt_idx = idx
# 下面代码逻辑,首先判断找到与预测边界框计算 IoU 值最大的真实边界框,
# 如果这个 IoU 值大于事先设定好的 IoU 阈值,那么就继续判断与该预测边界框相交的
# 真实边界框是否被占用,如果没有被占用,那么就将说明该预测边界框是该真实边界框的
# TP 所以将该预测边界框在 TP 列表中对应位置更新为 1,否则该预测边界框计入到 FP
# 同时更新 FP 列表中对应位置,如果其计算出 IoU 值不满足大于阈值 IoU 直接计入到
# FP 中
if best_iou > iou_threshold:
#判断
if amount_bboxes[detection[0]][best_gt_idx] == 0:
TP[detection_idx] = 1
amount_bboxes[detection[0]][best_gt_idx] = 1
else:
FP[detection_idx] = 1
else:
FP[detection_idx] = 1
TP_cumsum = torch.cumsum(TP,dim=0)
FP_cumsum = torch.cumsum(FP,dim=0)
recalls = TP_cumsum/(total_true_bboxes + epsilon)
precisions = torch.divide(TP_cumsum,(TP_cumsum + FP_cumsum + epsilon))
# 这是因为 AP 是计算一个 y 轴是 precision 而 x 轴是 recall
# 对 precisions 和 recalls 分别在第一个位置添加 1 和 0 这因为这个曲线是始于(0,1) 坐标
precisions = torch.cat((torch.tensor([1]),precisions))
recalls = torch.cat((torch.tensor([0]),recalls))
#计算曲线下积分面积也就是该类别的AP值,梯形法则
average_precisions.append(torch.trapz(precisions,recalls))
#计算 mAP
return sum(average_precisions)/len(average_precisions)
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。