zoukankan      html  css  js  c++  java
  • mAP(转载)

    Pascal VOC中mAP的计算(转自简书)

    其实我们训练部分是我们自己提出各种改进的算法,得到一个我们自己的结果,但是验证部分基本都是差不多的,或者一律使用官方的验证方法,为了正确评估我们模型的结果,我们还是得弄清楚验证部分的代码需要我们提供的是什么,它又计算出了什么,请时刻记住,这是我们的终极目标
    这里就整理一下Pascal VOC这个数据集中mAP的计算,最后也会总结一下,到底怎样使用这份代码。Pascal VOC是早期目标检测(e.g. Fast/er RCNN, SSD)常用的标准数据集。

    1、什么是mAP?

    既然要看懂计算mAP的代码,那肯定得先知道什么是mAP?不能违背人的认知规律,当然已经在目标检测这方向已经比较熟的大佬就可以跳过这部分。
    这方面有很多人都写过了,我建议可以看一下这一篇博客:
    深度学习笔记(八):目标检测性能评价指标(mAP、IOU..)
    这里也给出自己看完这些博客还得注意的地方,算是给像我这样的小白一些解惑:

    来自维基百科的PR解释

    1、上图来自维基百科中对于precision和recall的形象解释,precision按字面理解,准确率就是你预测出来为正类当中确实有多少正类的比例(这里一开始我也有个疑问,为什么我们预测出来的都算是正类?其实仔细想一下也不难理解,就像目标检测,我们选出来的框自然是我们认为是前景目标的,我们怎么会去框一个背景呢,注意这里是我们认为我们框的,实际完全有可能框到背景);recall按字面理解,召回率就是实际有这么多正类,你选出来了多少的一个比率,不理解的可以多看几遍上面的图帮助理解,这会影响到下面PR曲线绘制的理解。
    2、一条P-R曲线对应一个阈值,那这个阈值又是什么意思呢?例如VOC里面,这个阈值就代表和Ground Truth bbox的IoU大于这个阈值才会被认为是正类【如下面代码中的ovthresh】,但还得通过一定的准则才能归入TP(true positive),到这里我还会有两个问题:1.PR曲线到底是怎么画出来的?2.为什么R增大P会有减小的趋势?,下面结合代码部分我还会详细讲解,并且给出我实践中画出来的PR曲线。
    3、通常人们都会说:P-R曲线围起来的面积就是AP值,但是我想说的是,理解的时候可以这样直观认识一下,具体计算的时候还是看代码的实现方式,当然实际计算都不能非常准确到刚好是P-R曲线下的面积。

    2、如何画PR曲线和计算AP?

    那我们如何来画PR曲线和计算AP?PR曲线无非就是很多个(recall, precision)对连接起来的,一开始我还不知道这些点对是哪里来的?其实这就回到我们最原初的那个问题了,我们的输入是什么?我们的输入是我们遍历完所有测试图片对某一类(例如'car')给出的预测值(包括图片索引,框的置信度分数和四个坐标,这个预测值一般都是经过NMS处理之后的,保证每张图片里面不会有过多的框,因为分析代码过后,过多的框是会降低AP的),例如下面这张图就是我跑CenterNet在Pascal VOC数据集上产生的20个类的结果:

    eval结果

    每一个文件里面都是长这样的:

    飞机类结果文件具体内容

    下面具体分析

    上面的这些结果文件就是对应【下面代码的104行的detfile = detpath.format(classname)】,因为我们当作他们都是positive,所以这里的每一个预测出来的框都会分配一个TP或者FP(或者根据官方规则,对于difficult=1的object是不参与AP计算的),所以上面一个文件中有这么多行,就会有这么长的TP和FP列表,每输入一个预测框就又可以计算一个(recall, precision)对,那么就可以画出非常细密的点,当然随着输入框的增多,recall肯定是上升的,因为实际的positive数量是固定的,随着输入框的增多,肯定是覆盖到这些真值的,根据recall的定义就会接近1;而此时在还没有正确预测中下一个时,也就是上面维基图中TP不变的情况下,增加了红半球FP的数量,那precision肯定会下降,只有预测中下一个真值的时候会突增一下,所以说,P-R曲线并不是递减的。下面就是我实践过程画出来的P-R曲线,仔细看会有局部的上升趋势(这里感觉理解起来不够形象的,最后还有一个我理解过程中参考别人博客的一个例子):

    P-R曲线

    下面来看具体代码

    至此,我们已经完成了所有预备的知识,需要来分析一下代码是怎么实现的?我在代码必要部分标上了中文注释:
    下面这个就是计算Pascal VOC中mAP的python版本的代码,摘自于Ross Girshick大神的Faster RCNN中的代码,后来非常多人都用到这份代码,比如:

    ## https://github.com/rbgirshick/py-faster-rcnn/blob/master/lib/datasets/voc_eval.py#L190
        def voc_ap(rec, prec, use_07_metric=False):
            """ ap = voc_ap(rec, prec, [use_07_metric])
            Compute VOC AP given precision and recall.
            If use_07_metric is true, uses the
            VOC 07 11 point method (default:False).
            """
            if use_07_metric:  #VOC在2010之后换了评价方法,所以决定是否用07年的
                # 11 point metric
                ap = 0.
                for t in np.arange(0., 1.1, 0.1):  #  07年的采用11个点平分recall来计算
                    if np.sum(rec >= t) == 0:
                        p = 0
                    else:
                        p = np.max(prec[rec >= t])  # 取一个recall阈值之后最大的precision
                    ap = ap + p / 11.  # 将11个precision加和平均
            else:  # 这里是用2010年后的方法,取所有不同的recall对应的点处的精度值做平均,不再是固定的11个点
                # correct AP calculation
                # first append sentinel values at the end
                mrec = np.concatenate(([0.], rec, [1.]))  #recall和precision前后分别加了一个值,因为recall最后是1,所以
                mpre = np.concatenate(([0.], prec, [0.])) # 右边加了1,precision加的是0
    
                # compute the precision envelope
                for i in range(mpre.size - 1, 0, -1):
                    mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])  #从后往前,排除之前局部增加的precison情况
    
                # to calculate area under PR curve, look for points
                # where X axis (recall) changes value
                i = np.where(mrec[1:] != mrec[:-1])[0]  # 这里巧妙的错位,返回刚好TP的位置,
                                                                                              # 可以看后面辅助的例子
    
                # and sum (Delta recall) * prec   用recall的间隔对精度作加权平均
                ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
            return ap
        # 计算每个类别对应的AP,mAP是所有类别AP的平均值
        def voc_eval(detpath,
                     annopath,
                     imagesetfile,
                     classname,
                     cachedir,
                     ovthresh=0.5,
                     use_07_metric=False):
            """rec, prec, ap = voc_eval(detpath,
                                        annopath,
                                        imagesetfile,
                                        classname,
                                        [ovthresh],
                                        [use_07_metric])
            Top level function that does the PASCAL VOC evaluation.
            detpath: Path to detections
                detpath.format(classname) should produce the detection results file.
            annopath: Path to annotations
                annopath.format(imagename) should be the xml annotations file.
            imagesetfile: Text file containing the list of images, one image per line.
            classname: Category name (duh)
            cachedir: Directory for caching the annotations
            [ovthresh]: Overlap threshold (default = 0.5)
            [use_07_metric]: Whether to use VOC07's 11 point AP computation
                (default False)
            """
            # assumes detections are in detpath.format(classname)
            # assumes annotations are in annopath.format(imagename)
            # assumes imagesetfile is a text file with each line an image name
            # cachedir caches the annotations in a pickle file
    
            # first load gt
            if not os.path.isdir(cachedir):
                os.mkdir(cachedir)
            cachefile = os.path.join(cachedir, 'annots.pkl')
            # read list of images
            with open(imagesetfile, 'r') as f:
                lines = f.readlines()
            imagenames = [x.strip() for x in lines]
    
            if not os.path.isfile(cachefile):
                # load annots 
                # 这里提取的是所有测试图片中的所有object gt信息, 07年的test真实标注是可获得的,12年就没有了
                recs = {}
                for i, imagename in enumerate(imagenames):
                    recs[imagename] = parse_rec(annopath.format(imagename))
                    if i % 100 == 0:
                        print 'Reading annotation for {:d}/{:d}'.format(
                            i + 1, len(imagenames))
                # save
                print 'Saving cached annotations to {:s}'.format(cachefile)
                with open(cachefile, 'w') as f:
                    cPickle.dump(recs, f)
            else:
                # load
                with open(cachefile, 'r') as f:
                    recs = cPickle.load(f)
    
            # extract gt objects for this class 从上面的recs提取我们要判断的那类标注信息
            class_recs = {}
            npos = 0
            for imagename in imagenames:
                R = [obj for obj in recs[imagename] if obj['name'] == classname]
                bbox = np.array([x['bbox'] for x in R])
                difficult = np.array([x['difficult'] for x in R]).astype(np.bool)
                det = [False] * len(R) # 该图片中该类别对应的所有bbox的是否已被匹配的标志位
                npos = npos + sum(~difficult) #累计所有图片中的该类别目标的总数,不算diffcult
                                                                             # 这里计算还是很巧妙的,npos=TP+FN
                class_recs[imagename] = {'bbox': bbox,
                                         'difficult': difficult,
                                         'det': det}
    
            # read dets
            detfile = detpath.format(classname)
            # 读取相应类别的检测结果文件,每一行对应一个检测目标
            with open(detfile, 'r') as f:
                lines = f.readlines()
    
            splitlines = [x.strip().split(' ') for x in lines]
            image_ids = [x[0] for x in splitlines]
            confidence = np.array([float(x[1]) for x in splitlines])
            BB = np.array([[float(z) for z in x[2:]] for x in splitlines])
    
            # sort by confidence 按置信度由大到小排序
            sorted_ind = np.argsort(-confidence)
            sorted_scores = np.sort(-confidence)
            BB = BB[sorted_ind, :]
            image_ids = [image_ids[x] for x in sorted_ind]
    
            # go down dets and mark TPs and FPs
            nd = len(image_ids) # 检测结果文件的行数
            tp = np.zeros(nd) # 用于标记每个检测结果是tp还是fp
            fp = np.zeros(nd)
            for d in range(nd):
               # 取出该条检测结果所属图片中的所有ground truth
                R = class_recs[image_ids[d]]
                bb = BB[d, :].astype(float)
                ovmax = -np.inf
                BBGT = R['bbox'].astype(float)
    
                if BBGT.size > 0:
                    # compute overlaps  计算与该图片中所有ground truth的最大重叠度
                    # intersection
                    ixmin = np.maximum(BBGT[:, 0], bb[0])
                    iymin = np.maximum(BBGT[:, 1], bb[1])
                    ixmax = np.minimum(BBGT[:, 2], bb[2])
                    iymax = np.minimum(BBGT[:, 3], bb[3])
                    iw = np.maximum(ixmax - ixmin + 1., 0.)
                    ih = np.maximum(iymax - iymin + 1., 0.)
                    inters = iw * ih
    
                    # union
                    uni = ((bb[2] - bb[0] + 1.) * (bb[3] - bb[1] + 1.) +
                           (BBGT[:, 2] - BBGT[:, 0] + 1.) *
                           (BBGT[:, 3] - BBGT[:, 1] + 1.) - inters)
    
                    overlaps = inters / uni
                    ovmax = np.max(overlaps)
                    jmax = np.argmax(overlaps)
                # 这里就是具体的分配TP和FP的规则了
                if ovmax > ovthresh:  # 如果最大的重叠度大于一定的阈值
                    if not R['difficult'][jmax]: # 如果最大重叠度对应的ground truth为difficult就忽略,
                                                                       # 因为上面npos就没算
                        if not R['det'][jmax]: # 如果对应的最大重叠度的ground truth以前没被匹配过则匹配成功,即tp
                            tp[d] = 1.
                            R['det'][jmax] = 1
                        else:  # 若之前有置信度更高的检测结果匹配过这个ground truth,则此次检测结果为fp
                            fp[d] = 1.
                else:
                    # 该图片中没有对应类别的目标ground truth或者与所有ground truth重叠度都小于阈值
                    fp[d] = 1.
    
            # compute precision recall
            fp = np.cumsum(fp) # 累加函数np.cumsum([1, 2, 3, 4]) -> [1, 3, 6, 10]
            tp = np.cumsum(tp)
            rec = tp / float(npos)
            # avoid divide by zero in case the first detection matches a difficult
            # ground truth
            prec = tp / np.maximum(tp + fp, np.finfo(np.float64).eps)
            ap = voc_ap(rec, prec, use_07_metric)
    
            return rec, prec, ap
    

    辅助理解的例子

    下面的例子来自有另一篇博客:COCO目标检测测评指标,里面的数据是这样的:这幅图里面计算AP的方法是2010年之后的那种,在这个例子中,一共有20个测试,但是只有6个正的测试样本,所以这里就分成了6个间隔,而不是11,如果我们把每一种颜色(其实这颜色块就是根据recall不同的值分的)看成是一个阶段,2010年之后的方法取的precison是后面阶段和本阶段中最大的(对应表格里面的那句话【Max Precision for Any Recall r' >= r】,注意是大于等于不是大于)

    例子

    下面是我理解2010年之后的评价方法时做的图示,看一下就会很快理解,也能发现代码中错位的巧妙,数据来自上面那幅图:

    2010年以后评价方法代码辅助理解示例

    总结

    所以我们回到开头的终极目标,当我们明白代码之后,就只要学会调用就好了,那对于Pascal VOC这个数据集来说:

    • 我们需要提供的:那20个预测结果文件,如第2大点的那幅图里面的看到的,和annotation文件(XML文件);
    • 计算出来的是:当然是mAP啦!

    目标检测中的AP,mAP(转自知乎)

    AP (Average precision)是主流的目标检测模型的评价指标。再介绍AP之前,我们先来回顾一下需要用到的几个概念precision,recall以及IoU。

    IoU(Intersection over union

    交并比IoU衡量的是两个区域的重叠程度,是两个区域重叠部分面积占二者总面积(重叠部分只计算一次)的比例。如下图,两个矩形框的IoU是交叉面积(中间图片红色部分)与合并面积(右图红色部分)面积之比。

    Iou的定义

    在目标检测任务中,如果我们模型输出的矩形框与我们人工标注的矩形框的IoU值大于某个阈值时(通常为0.5)即认为我们的模型输出了正确的

    精准率与召回率(Precision & Recall)

    Precision 和 Recall最早是信息检索中的概念,用来评价一个信息检索系统的优劣。Precision 就是检索出来的条目中(比如:文档、网页等)有多大比例是我们需要的,Recall就是所有我们需要的网页的条目有多大比例被检索出来了。用到目标检测领域,假设我们有一组图片,里面有若干待检测的目标,Precision就代表我们模型检测出来的目标有多打比例是真正的目标物体,Recall就代表所有真实的目标有多大比例被我们的模型检测出来了。

    我们如何来计算Precision和Recall的值呢。

    目标检测TP,FP, TN, FN含义
    图释TP, FP, TN,FN

    首先我们根据上图的规则计算出TP,FP,TN的值,则Precision,Recall可表示为

    [公式]

    [公式]

    PR曲线

    我们当然希望检测的结果P越高越好,R也越高越好,但事实上这两者在某些情况下是矛盾的。比如极端情况下,我们只检测出了一个结果,且是准确的,那么Precision就是100%,但是Recall就很低;而如果我们把所有结果都返回,那么必然Recall必然很大,但是Precision很低。

    因此在不同的场合中需要自己判断希望P比较高还是R比较高。如果是做实验研究,可以绘制Precision-Recall曲线来帮助分析。

    这里我们举一个简单的例子,假设我们的数据集中共有五个待检测的物体,我们的模型给出了10个候选框,我们按照模型给出的置信度由高到低对候选框进行排序。


    表格第二列表示该候选框是否预测正确(即是否存在某个待检测的物体与该候选框的iou值大于0.5)第三列和第四列表示以该行所在候选框置信度为阈值时,Precision和Recall的值。我们以表格的第三行为例进行计算:

    [公式] [公式] [公式]

    [公式]

    [公式]

    由上表以Recall值为横轴,Precision值为纵轴,我们就可以得到PR曲线。我们会发现,Precision与Recall的值呈现负相关,在局部区域会上下波动。

    PR曲线

    AP(Average Precision)

    顾名思义AP就是平均精准度,简单来说就是对PR曲线上的Precision值求均值。对于pr曲线来说,我们使用积分来进行计算。

    [公式]

    在实际应用中,我们并不直接对该PR曲线进行计算,而是对PR曲线进行平滑处理。即对PR曲线上的每个点,Precision的值取该点右侧最大的Precision的值。

    PR曲线的平滑处理

    用公式来描述就是 [公式] 。用该公式进行平滑后再用上述公式计算AP的值。

    Interplolated AP(Pascal Voc 2008 的AP计算方式)

    Pascal VOC 2008中设置IoU的阈值为0.5,如果一个目标被重复检测,则置信度最高的为正样本,另一个为负样本。在平滑处理的PR曲线上,取横轴0-1的10等分点(包括断点共11个点)的Precision的值,计算其平均值为最终AP的值。

    [公式]

    Pascal Voc 2008 AP计算方式

    在我们的例子里

    [公式]

    Area under curve

    上述方法有两个缺陷,第一个是使用11个采样点在精度方面会有损失。第二个是,在比较两个AP值较小的模型时,很难体现出两者的差别。所以这种方法在2009年的Pascalvoc之后便不再采用了。在Pascal voc 2010之后,便开始采用这种精度更高的方式。绘制出平滑后的PR曲线后,用积分的方式计算平滑曲线下方的面积作为最终的AP值。

    [公式]

    Pascal voc 2010-2012 AP 计算方式

    COCO mAP

    最新的目标检测相关论文都使用coco数据集来展示自己模型的效果。对于coco数据集来说,使用的也是Interplolated AP的计算方式。与Voc 2008不同的是,为了提高精度,在PR曲线上采样了100个点进行计算。而且Iou的阈值从固定的0.5调整为在 0.5 - 0.95 的区间上每隔0.5计算一次AP的值,取所有结果的平均值作为最终的结果。

    比如我们看一下YOLOv3的作者在论文中展示的在coco数据集上的实验结果

    我们发现除了AP,还有 [公式] , [公式] 等值,这些事代表什么意思呢?

    [公式]:IoU阈值为0.5时的AP测量值

    [公式]:IoU阈值为0.75时的测量值

    [公式] : 像素面积小于 [公式] 的目标框的AP测量值

    [公式] : 像素面积在[公式]- [公式]之间目标框的测量值

    [公式] : 像素面积大于 [公式] 的目标框的AP测量值

    注:通常来说AP是在单个类别下的,mAP是AP值在所有类别下的均值。在这里,在coco的语境下AP便是mAP,这里的AP已经计算了所有类别下的平均值,这里的AP便是mAP。

    参考文献[mAP (mean Average Precision) for Object Detection​

    mAP (mean Average Precision) for Object Detection

    Coco API

  • 相关阅读:
    对于es6新增的async 和 await的使用和理解
    如何实现文本的竖向排版 && js 打印指定区域
    鼠标透过蒙版点击触发下一层事件,上一层点击事件失效
    小程序的弊端以及Taro优点
    服务器端渲染和客户端渲染区别
    使用mobx 示例
    C语言
    C语言
    C语言
    [转]linux tar 解压命令总结
  • 原文地址:https://www.cnblogs.com/yxl-2018/p/12419500.html
Copyright © 2011-2022 走看看