前言
基本上所有的中文文章都会告诉你什么是mAP,什么是Precision、Recall、TP、FP、FN,但就是不讲清楚到底该怎么计算,应该先算什么再算什么,在项目中应该怎么自己写对应的代码。图省事看了网上那么多中文答案/文章,都不如去看一篇英文GitHub代码(github.com/rafaelpadilla/Object-Detection-Metrics,强烈推荐阅读README和lib/Evaluator.py中的GetPascalVOCMetrics函数,讲计算过程讲的很清楚)。
概念
网上介绍mAP含义的文章很多,解释的很好,写得也很清楚了,但有一点不足就是看完之后不知道该怎么写出对应代码。本文就不再重复介绍各个名词的详细概念,而是选择用一些例子来简单说明。
名词解释
相对来说比较准确的解释,但不容易理解,通俗解释可以看「举例说明」部分。
TP (True Positive):一个正确的检测,检测的IOU ≥ threshold。即预测的边界框(bounding box)中分类正确且边界框坐标正确的数量。
FP (False Positive):一个错误的检测,检测的IOU < threshold。即预测的边界框中分类错误或者边界框坐标不达标的数量,即预测出的所有边界框中除去预测正确的边界框,剩下的边界框的数量。
FN (False Negative):一个没有被检测出来的ground truth。所有没有预测到的边界框的数量,即正确的边界框(ground truth)中除去被预测正确的边界框,剩下的边界框的数量。
Precision (准确率 / 精确率):「Precision is the ability of a model to identify only the relevant objects」,准确率是模型只找到相关目标的能力,等于TP/(TP+FP)。即模型给出的所有预测结果中命中真实目标的比例。
Recall (召回率):「Recall is the ability of a model to find all the relevant cases (all ground truth bounding boxes)」,召回率是模型找到所有相关目标的能力,等于TP/(TP+FN)。即模型给出的预测结果最多能覆盖多少真实目标。
一般来说,对于多分类目标检测的任务,会分别计算每个类别的TP、FP、FN数量,进一步计算每个类别的Precision、Recall。
score、confidence:每个预测边界框的分数/置信度,不同论文中的表达方式不同。即你的模型除了需要给出每个预测边界框的分类结果和边界坐标,还要给出这个边界框包含目标的可能性,score/confidence高代表模型认为其包含目标的可能性大,或者说模型优先选择score/confidence高的边界框。
Precision x Recall curve (PR曲线):所有precision-recall点相连形成的曲线(一般会为每个类别的预测框建立各自的PR曲线)。至于为什么同一个recall值下会有多个precision值,可以看之后部分的解释。
AP (average Precision):平均精度,在不同recall下的最高precision的均值(一般会对各类别分别计算各自的AP)。
mAP (mean AP):平均精度的均值,各类别的AP的均值。
举例说明
一般来说对模型进行训练、验证和测试都会有对应的数据集,为了提高计算效率,训练/测试时不会一个个的将图片输入到模型中,而是将多个(如32个)图片组成一个batch输入到模型中。对于这种情况,计算TP、FP、FN时就应该以batch为单位进行计算。
假设对于每个图片,模型都会预测出几百个不同类别的预测框(如有的图片会预测出200个,有的会预测出150个),每个预测框包含[预测的分类、边界坐标和预测框score]。接下来将解释各个名词在这个例子中分别代表什么。
TP:只考虑预测框的分类和边界坐标,对于一张图片中的某一类别的全部预测框,如果一个预测框与某一个gt (ground truth,真实值) 的IOU大于阈值,即认为这个边界框预测正确,计作一个TP。但是在实际预测中,经常会出现多个预测框与同一个gt的IOU都大于阈值,这时通常只将这些预测框中score最大的算作TP,其它算作FP。
FP:对于一张图片中某一类别的所有预测到的边界框,除TP之外的记为FP (不过在实际计算时,因为每个预测框不是TP就是FP,所以一般都只设置一个TP标志位,如果当前预测框预测正确,那么这个标志位设置为1代表TP,否则设置为0代表FP)。
FN:对于一张图片中某一类别的所有gt边界框,除TP之外的记为FN。
(2020.3.31 更新,新增Precision、Recall以及如何计算PR曲线的方法)
如一个图片中有100个预测到的边界框,其中包含A类别30个、B类别30个、C类别40个。如果包含A类别的gt有4个,且有2个被预测的边界框找到,那么A类别的TP+2,FP+28,FN+2;同样,如果这个图片中没有B类别和C类别的目标,那么B类别的TP+0,FP+30,FN+0,C类别的TP+0,FP+40,FN+0;如果图片中还包含D类别1个gt,那么此时应该对D类别的TP+0,FP+0,FN+1。对待测试数据集中所有batch中的所有图片按上述方法计算TP、FP、FN并累加,最终可以得到A, B, C, D, E...等所有类别在当前数据集的所有图片中的TP、FP和FN。
但上述情况太过简单,实际计算时会遇到许多复杂的情况,比如说两个或者多个预测框对于同一个物体的gt的IOU都大于阈值,再比如同一个预测框对于两个相临的物体的gt的IOU都大于阈值等等。所以对于每张图片,应该按预测框的score从大到小依次与gt进行匹配,如果IOU大于阈值,则记做TP,否则记做FP,直到所有预测框或者所有gt都匹配完。
上面只是介绍了TP、FP的计算方法,使用上述方法进行累加TP和FP,最终只会得到一组Precision-Recall的值,而不是像PR曲线上有许多组PR的值,那么PR曲线又是怎么得到的呢。
首先让我们来看看Precision和Recall在这个例子中的计算方式。
Precision:根据上述方法,对于某个类别A,我们先计算每张图片中A类别TP和FP的数量并进行累加,即可得到类别A在整个数据集中TP和FP的数量,计算TP/(TP+FP)即可得到类别A的Precision (计算Precision的时候只需要用到TP和FP),但是会发现Precision的数值是受模型预测出的边界框的数量(上述计算式的分母部分)影响的,如果我们控制模型输出预测框的数量,就可以得到不同的Precision,所以我们可以设置不同的score阈值,最终得到不同数量的TP和FP。
Recall:对于某个类别A,按上述方法进行累加TP的数量,计算TP/(n_gt)即可得到Recall,其中n_gt表示类别A在所有图片中gt的数量之和。同理,如果控制模型输出的预测框的数量,就会改变TP的数量,也就会改变Recall的值。
综上,想要得到PR曲线,可以通过改变score的阈值来控制模型输出的预测框数量,从而得到不同的TP、FP、FN。不过在实际操作中,并不需要手动来设置score的阈值,因为每个预测框都有一个score,我们只需要将其按从小到大进行排序,然后每次选择最后一个score作为阈值即可,这样如果类别A在所有图片中的预测框数量之和有100个,就可以计算100组类别A的Precision-Recall值。
Precision-Recall曲线具体计算方式如下(文字解释,下一节有伪代码解释):
因为计算类别A的PR曲线需要根据所有图片中的预测框score进行排序(以数据集为单位计算PR曲线),但TP、FP却是需要在每张图片中单独计算(以图片为单位计算TP、FP),所以我们需要建立一个数组记录每个图片中所有预测框的TP和score。
假设现在数据集中一共有5张图片,①第1张图片中有2个A类别的gt,有三个A类别的预测框,score分别为(0.3, 0.5, 0.9),按照上述计算TP的方法(按score从大到小的顺序匹配)发现score为0.3和0.9的与gt相匹配,则将这两个记为TP。建立用于计算PR曲线的数组metric和记录A类别gt总数的变量ngt,向数组中加入(0.3, 1), (0.5, 0), (0.9, 1)三组数据(每组数据的第一个代表预测框的score,第二个代表这个预测框是否是TP),并将n_gt累加3;使用相同的方法计算接下来的几张图片,②第2张图片中没有A类别的物体(gt数量为0),则n_gt+=0,但有一个关于A类别的预测框,score为0.45,则向metric中加入(0.45, 0);③第3张图片中有1个A类别的物体,但没有预测框,则n_gt+=1;④第4张图片中有3个A类别的物体,有5个预测框,其中有3个与gt相匹配,最终n_gt+=3,metric中加入(0.85, 1), (0.8, 1), (0.7, 1), (0.35, 0), (0.1, 0);⑤第5张图片中没有A类别的物体,也没有A类别的预测框。
最终将得到的A类别的n_gt=6,metric按score值从大到小进行排序为(0.9, 1), (0.85, 1), (0.8, 1), (0.7, 1), (0.5, 0), (0.45, 0), (0.35, 0), (0.3, 1), (0.1, 0) 共9个预测框,也就是可以得到9组PR值,先计算所有的预测框(score阈值设置为0),得到Precision为5/9、Recall为5/6,然后依次去掉metric中最后一组数据,可以得到(5/8, 5/6), (4/7, 4/6), (4/6, 4/6), (4/5, 4/6), (4/4, 4/6), (3/3, 3/6), (2/2, 2/6), (1/1, 1/6) 共9组PR值。
观察上述PR值的规律会发现,随着score的阈值的增大,预测框总数会一直减小,而TP却会有时保持不变,有时变小,所以会出现同一Recall对应多组Precision的情况。最终在计算AP时,我们只需要取每个Recall值对应的最大Precision进行计算即可。因为A类别一共有6个gt,所以Recall的值应该是从1/6~6/6共6个,也就是要取6组PR值计算平均Precision,因为这个例子中没有出现Recall=6/6的情况,所以R=6/6时的Precision算作0,即类别A的AP=(1/1 + 2/2 + 3/3 + 4/4+ 5/8 + 0) / 6 = 0.7708。按照同样的方式计算其他类别的AP,最后求所有AP的均值,即可得到mAP。
计算过程的伪代码
对于一个需要计算mAP的数据集(例如在训练模型时希望知道模型每训练几轮之后在验证集中的表现,即计算其在验证集中的mAP),通常需要先对数据集进行处理,让其以batch的形式输入到模型中得到结果。而计算AP需要以整个数据集为单位,所以在对整个数据集进行计算之前,先对每个类别分别建立tps、scores、n_gts数组(将上述的metric数组拆分成score和tp两个数组,方便计算),用于保存模型在每个图片上的预测框的表现。(伪代码可以只看注释部分)
tps = [[] for _ in class_list]
socres = [[] for _ in class_list]
n_gts = [0 for _ in class_list]
# 对于每个batch
for batch, batch_gt in Dataloader:
batch_prediction = model.predict(batch)
# 对于其中的每个图片,取其中预测的边界框
for (pred, gt) in zip(batch_prediction, batch_gt):
# 对于每个分类c
for c in class_list:
# 如果当前图片中包含当前类别的gt/预测的边界框,则进行计算,否则判断是否存在下一个类别
if (pred hasclass c) or (gt hasclass c):
pred_c, gt_c = pred[c], gt[c]
current_tp = [0] * len(pred_c)
current_score = [0] * len(pred_c)
# 按score值对预测的边界框进行递减排序,然后从大到小依次进行下一步计算
pred_c = sorted pred_c by pred_c[i]['score']
# 如果当前预测框与某个gt相匹配(IOU>=threshold),那么current_tp[i] = 1
for i in pred_c:
...
if pred_c[i]['box'] matches gt_c[j]['box']:
current_tp[i] = 1
current_score[i] = pred_c[i]['score']
# 计算完成后将结果添加到tps、socres、n_gts数组中
tps[c] += current_tp
socres[c] += current_score
n_gts[c] += len(gt_c)
# 最后即可得到每个类别的tps, socres, n_gts用于下一步计算
通过上面的计算过程,对于一个数据集,最终会得到三个数组,tps记录这个数据集中每个分类的所有预测框(所有图片中的)中哪个有相匹配的gt、哪个没有;scores记录tps中每个预测框对应的score值,用于对TP进行排序;n_gts记录这个数据集中每个分类共含有多少个gt。
计算PR曲线、AP都需要得到不同recall下precision的值,由于recall=TP/(all gt nums),所以只要得到不同的TP即可得到不同的recall值,而想要增加或减少TP的方式,只能是让用于计算TP的总预测框数量变多/变少,即动态的调整score的阈值,让更多/更少的预测框参与计算。
由于已经得到了全部score下每个预测框是否为TP的结果,所以只需要设置不同的score,取不同的子序列用于计算即可。然而手动设置不同score阈值对于不同的模型并不通用,所以通常采用对TP数组求累加和的方式,对每一个score都进行一次计算。
具体操作如下
- 计算完成每个分类在所有batch中的tps和n_gts后,再次根据scores中的值进行排序
- 将TP按照score值从小到大依次累加,再除以总的元素个数,即可得到在每个score阈值下的Precision值
- 同理,如果对TP的累加结果除以每个分类的n_gts的值,即可得到在每个score阈值下的Recall值
通过这样的计算得到的Recall数组一定是越来越大的(因为分母是固定的,但分子却越来越大),而Precision由于分母是递增的,元素可能增大也可能变小。
例如数据集中A类别共有4个gt(来自不同图片),一共有来自不同图片的5个预测框,按照score值从大到小排序后,tps = [0, 0, 1, 1, 0, 0, 1, 0, 1, 0],那么对应的Recall=[0, 0, 1/4, 2/4, 2/4, 2/4, 3/4, 3/4, 4/4, 4/4],对应的Precision=[0/1, 0/2, 1/3, 2/4, 2/5, 2/6, 3/7, 3/8, 4/9, 4/10]。
有了Precision和Recall数组后,即可画出对应的PR曲线(曲线图片之后再补)。
如果按照VOC07的AP计算标准,对于0.0-1.0的11个Recall阈值,分别取每个阈值之上的最大precision进行计算平均值。那么类别A的AP = (1/3+1/3 + 1/3 + 2/4 + 2/4 + 2/4 + 3/7 + 3/7 +4/9 + 4/9 + 4/9) / 11 = 0.4264。按相同的方法求出其它类别的AP,再对所有AP求均值,即可得到模型在当前数据集上的mAP。
Python代码
下面的代码是我的项目中用来计算TP、PR值、AP的代码,因为网络模型是用Pytorch实现的,所以在calculate_tp函数中输入的都是torch.Tensor。在我的项目中,验证集中每个batch过后都要针对其中的每个图片计算一次tps,即使用一次calculate_tp函数,并更新总的gt_nums和tps, scores数组。最后当所有图片都预测并计算完tps后,将所有gt_nums、tps和scores放入calculate_pr函数中计算PR曲线上的点,然后放入voc_ap函数中计算AP。
def calculate_tp(pred_boxes, pred_scores, gt_boxes, gt_difficult, iou_thresh = 0.5):
"""
calculate tp/fp for all predicted bboxes for one class of one image.
对于匹配到同一gt的不同bboxes,让score最高tp = 1,其它的tp = 0
Args:
pred_boxes: Tensor[N, 4], 某张图片中某类别的全部预测框的坐标 (x0, y0, x1, y1)
pred_scores: Tensor[N, 1], 某张图片中某类别的全部预测框的score
gt_boxes: Tensor[M, 4], 某张图片中某类别的全部gt的坐标 (x0, y0, x1, y1)
gt_difficult: Tensor[M, 1], 某张图片中某类别的gt中是否为difficult目标的值
iou_thresh: iou 阈值
Returns:
gt_num: 某张图片中某类别的gt数量
tp_list: 记录某张图片中某类别的预测框是否为tp的情况
confidence_score: 记录某张图片中某类别的预测框的score值 (与tp_list相对应)
"""
if gt_boxes.numel() == 0:
return 0, [], []
# 若无对应的boxes,则 tp 为空
if pred_boxes.numel() == 0:
return len(gt_boxes), [], []
# 否则计算所有预测框与gt之间的iou
ious = pred_boxes.new_zeros((len(gt_boxes), len(pred_boxes)))
for i in range(len(gt_boxes)):
gb = gt_boxes[i]
area_pb = (pred_boxes[:, 2] - pred_boxes[:, 0]) * (pred_boxes[:, 3] - pred_boxes[:, 1])
area_gb = (gb[2] - gb[0]) * (gb[3] - gb[1])
xx1 = pred_boxes[:, 0].clamp(min = gb[0].item()) # [N-1,]
yy1 = pred_boxes[:, 1].clamp(min = gb[1].item())
xx2 = pred_boxes[:, 2].clamp(max = gb[2].item())
yy2 = pred_boxes[:, 3].clamp(max = gb[3].item())
inter = (xx2 - xx1).clamp(min = 0) * (yy2 - yy1).clamp(min = 0) # [N-1,]
ious[i] = inter / (area_pb + area_gb - inter)
# 每个预测框的最大iou所对应的gt记为其匹配的gt
max_ious, max_ious_idx = ious.max(dim = 0)
not_difficult_gt_mask = gt_difficult == 0
gt_num = not_difficult_gt_mask.sum().item()
if gt_num == 0:
return 0, [], []
# 保留 max_iou 中属于 非difficult 目标的预测框,即应该去掉与 difficult gt 相匹配的预测框,不参与p-r计算
# 如果去掉与 difficult gt 对应的iou分数后,候选框的最大iou依然没有发生改变,则可认为此候选框不与difficult gt相匹配,应该保留
not_difficult_pb_mask = (ious[not_difficult_gt_mask].max(dim = 0)[0] == max_ious)
max_ious, max_ious_idx = max_ious[not_difficult_pb_mask], max_ious_idx[not_difficult_pb_mask]
if max_ious_idx.numel() == 0:
return gt_num, [], []
confidence_score = pred_scores.view(-1)[not_difficult_pb_mask]
tp_list = torch.zeros_like(max_ious)
for i in max_ious_idx[max_ious > iou_thresh].unique():
gt_mask = (max_ious > iou_thresh) * (max_ious_idx == i)
idx = (confidence_score * gt_mask.float()).argmax()
tp_list[idx] = 1
return gt_num, tp_list.tolist(), confidence_score.tolist()
def calculate_pr(gt_num, tp_list, confidence_score):
"""
calculate all p-r pairs among different score_thresh for one class, using `tp_list` and `confidence_score`.
Args:
gt_num (Integer): 某张图片中某类别的gt数量
tp_list (List): 记录某张图片中某类别的预测框是否为tp的情况
confidence_score (List): 记录某张图片中某类别的预测框的score值 (与tp_list相对应)
Returns:
recall
precision
"""
if gt_num == 0:
return [0], [0]
if isinstance(tp_list, (tuple, list)):
tp_list = np.array(tp_list)
if isinstance(confidence_score, (tuple, list)):
confidence_score = np.array(confidence_score)
assert len(tp_list) == len(confidence_score), "len(tp_list) and len(confidence_score) should be same"
if len(tp_list) == 0:
return [0], [0]
sort_mask = np.argsort(-confidence_score)
tp_list = tp_list[sort_mask]
recall = np.cumsum(tp_list) / gt_num
precision = np.cumsum(tp_list) / (np.arange(len(tp_list)) + 1)
return recall.tolist(), precision.tolist()
def voc_ap(rec, prec, use_07_metric = False):
"""Compute VOC AP given precision and recall. If use_07_metric is true, uses
the VOC 07 11-point method (default:False).
"""
if isinstance(rec, (tuple, list)):
rec = np.array(rec)
if isinstance(prec, (tuple, list)):
prec = np.array(prec)
if use_07_metric:
# 11 point metric
ap = 0.
for t in np.arange(0., 1.1, 0.1):
if np.sum(rec >= t) == 0:
p = 0
else:
p = np.max(prec[rec >= t])
ap = ap + p / 11.
else:
# correct AP calculation
# first append sentinel values at the end
mrec = np.concatenate(([0.], rec, [1.]))
mpre = np.concatenate(([0.], prec, [0.]))
# compute the precision envelope
for i in range(mpre.size - 1, 0, -1):
mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
# to calculate area under PR curve, look for points
# where X axis (recall) changes value
i = np.where(mrec[1:] != mrec[:-1])[0]
# and sum (\Delta recall) * prec
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
return ap
标签:MAP,gt,预测,检测,TP,AP,score,tp,类别
From: https://www.cnblogs.com/sxq-blog/p/17028977.html