首页 > 其他分享 >目标检测之AP,MAP计算

目标检测之AP,MAP计算

时间:2023-01-05 22:11:08浏览次数:37  
标签:MAP gt 预测 检测 TP AP score tp 类别

前言

基本上所有的中文文章都会告诉你什么是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值,可以看之后部分的解释。

image

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

相关文章