Pascal VOC中mAP的計算

其實我們訓(xùn)練部分是我們自己提出各種改進的算法,得到一個我們自己的結(jié)果,但是驗證部分基本都是差不多的宋列,或者一律使用官方的驗證方法,為了正確評估我們模型的結(jié)果危彩,我們還是得弄清楚驗證部分的代碼需要我們提供的是什么,它又計算出了什么泳桦,請時刻記住汤徽,這是我們的終極目標(biāo)

這里就整理一下Pascal VOC這個數(shù)據(jù)集中mAP的計算蓬痒,最后也會總結(jié)一下泻骤,到底怎樣使用這份代碼漆羔。Pascal VOC是早期目標(biāo)檢測(e.g. Fast/er RCNN, SSD)常用的標(biāo)準(zhǔn)數(shù)據(jù)集梧奢。


1、什么是mAP演痒?

既然要看懂計算mAP的代碼亲轨,那肯定得先知道什么是mAP?不能違背人的認(rèn)知規(guī)律,當(dāng)然已經(jīng)在目標(biāo)檢測這方向已經(jīng)比較熟的大佬就可以跳過這部分鸟顺。
這方面有很多人都寫過了惦蚊,我建議可以看一下這一篇博客:
深度學(xué)習(xí)筆記(八):目標(biāo)檢測性能評價指標(biāo)(mAP、IOU..)
這里也給出自己看完這些博客還得注意的地方讯嫂,算是給像我這樣的小白一些解惑:

來自維基百科的PR解釋

1蹦锋、上圖來自維基百科中對于precision和recall的形象解釋,precision按字面理解欧芽,準(zhǔn)確率就是你預(yù)測出來為正類當(dāng)中確實有多少正類的比例(這里一開始我也有個疑問莉掂,為什么我們預(yù)測出來的都算是正類?其實仔細(xì)想一下也不難理解千扔,就像目標(biāo)檢測憎妙,我們選出來的框自然是我們認(rèn)為是前景目標(biāo)的库正,我們怎么會去框一個背景呢,注意這里是我們認(rèn)為我們框的厘唾,實際完全有可能框到背景)褥符;recall按字面理解,召回率就是實際有這么多正類抚垃,你選出來了多少的一個比率喷楣,不理解的可以多看幾遍上面的圖幫助理解,這會影響到下面PR曲線繪制的理解讯柔。
2抡蛙、一條P-R曲線對應(yīng)一個閾值,那這個閾值又是什么意思呢魂迄?例如VOC里面粗截,這個閾值就代表和Ground Truth bbox的IoU大于這個閾值才會被認(rèn)為是正類【如下面代碼中的ovthresh】,但還得通過一定的準(zhǔn)則才能歸入TP(true positive)捣炬,到這里我還會有兩個問題:1.PR曲線到底是怎么畫出來的熊昌?2.為什么R增大P會有減小的趨勢?湿酸,下面結(jié)合代碼部分我還會詳細(xì)講解婿屹,并且給出我實踐中畫出來的PR曲線。
3推溃、通常人們都會說:P-R曲線圍起來的面積就是AP值昂利,但是我想說的是,理解的時候可以這樣直觀認(rèn)識一下铁坎,具體計算的時候還是看代碼的實現(xiàn)方式蜂奸,當(dāng)然實際計算都不能非常準(zhǔn)確到剛好是P-R曲線下的面積。

2硬萍、如何畫PR曲線和計算AP扩所?

那我們?nèi)绾蝸懋婸R曲線和計算AP?PR曲線無非就是很多個(recall, precision)對連接起來的朴乖,一開始我還不知道這些點對是哪里來的祖屏?其實這就回到我們最原初的那個問題了,我們的輸入是什么买羞?我們的輸入是我們遍歷完所有測試圖片對某一類(例如'car')給出的預(yù)測值(包括圖片索引袁勺,框的置信度分?jǐn)?shù)和四個坐標(biāo),這個預(yù)測值一般都是經(jīng)過NMS處理之后的畜普,保證每張圖片里面不會有過多的框期丰,因為分析代碼過后,過多的框是會降低AP的),例如下面這張圖就是我跑CenterNet在Pascal VOC數(shù)據(jù)集上產(chǎn)生的20個類的結(jié)果:

eval結(jié)果

每一個文件里面都是長這樣的:
飛機類結(jié)果文件具體內(nèi)容

下面具體分析

上面的這些結(jié)果文件就是對應(yīng)【下面代碼的104行的detfile = detpath.format(classname)】咐汞,因為我們當(dāng)作他們都是positive盖呼,所以這里的每一個預(yù)測出來的框都會分配一個TP或者FP(或者根據(jù)官方規(guī)則,對于difficult=1的object是不參與AP計算的)化撕,所以上面一個文件中有這么多行几晤,就會有這么長的TP和FP列表,每輸入一個預(yù)測框就又可以計算一個(recall, precision)對植阴,那么就可以畫出非常細(xì)密的點蟹瘾,當(dāng)然隨著輸入框的增多,recall肯定是上升的掠手,因為實際的positive數(shù)量是固定的憾朴,隨著輸入框的增多,肯定是覆蓋到這些真值的喷鸽,根據(jù)recall的定義就會接近1众雷;而此時在還沒有正確預(yù)測中下一個時,也就是上面維基圖中TP不變的情況下做祝,增加了紅半球FP的數(shù)量砾省,那precision肯定會下降,只有預(yù)測中下一個真值的時候會突增一下混槐,所以說编兄,P-R曲線并不是遞減的。下面就是我實踐過程畫出來的P-R曲線声登,仔細(xì)看會有局部的上升趨勢(這里感覺理解起來不夠形象的狠鸳,最后還有一個我理解過程中參考別人博客的一個例子):

P-R曲線

下面來看具體代碼

至此,我們已經(jīng)完成了所有預(yù)備的知識悯嗓,需要來分析一下代碼是怎么實現(xiàn)的件舵?我在代碼必要部分標(biāo)上了中文注釋:
下面這個就是計算Pascal VOC中mAP的python版本的代碼,摘自于Ross Girshick大神的Faster RCNN中的代碼绅作,后來非常多人都用到這份代碼芦圾,比如:

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對應(yīng)的點處的精度值做平均,不再是固定的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的間隔對精度作加權(quán)平均
        ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
    return ap
# 計算每個類別對應(yīng)的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真實標(biāo)注是可獲得的萎津,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提取我們要判斷的那類標(biāo)注信息
    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) # 該圖片中該類別對應(yīng)的所有bbox的是否已被匹配的標(biāo)志位
        npos = npos + sum(~difficult) #累計所有圖片中的該類別目標(biāo)的總數(shù)卸伞,不算diffcult
                                                                     # 這里計算還是很巧妙的,npos=TP+FN
        class_recs[imagename] = {'bbox': bbox,
                                 'difficult': difficult,
                                 'det': det}

    # read dets
    detfile = detpath.format(classname)
    # 讀取相應(yīng)類別的檢測結(jié)果文件锉屈,每一行對應(yīng)一個檢測目標(biāo)
    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) # 檢測結(jié)果文件的行數(shù)
    tp = np.zeros(nd) # 用于標(biāo)記每個檢測結(jié)果是tp還是fp
    fp = np.zeros(nd)
    for d in range(nd):
       # 取出該條檢測結(jié)果所屬圖片中的所有g(shù)round 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  計算與該圖片中所有g(shù)round 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的規(guī)則了
        if ovmax > ovthresh:  # 如果最大的重疊度大于一定的閾值
            if not R['difficult'][jmax]: # 如果最大重疊度對應(yīng)的ground truth為difficult就忽略荤傲,
                                                               # 因為上面npos就沒算
                if not R['det'][jmax]: # 如果對應(yīng)的最大重疊度的ground truth以前沒被匹配過則匹配成功,即tp
                    tp[d] = 1.
                    R['det'][jmax] = 1
                else:  # 若之前有置信度更高的檢測結(jié)果匹配過這個ground truth颈渊,則此次檢測結(jié)果為fp
                    fp[d] = 1.
        else:
            # 該圖片中沒有對應(yīng)類別的目標(biāo)ground truth或者與所有g(shù)round truth重疊度都小于閾值
            fp[d] = 1.

    # compute precision recall
    fp = np.cumsum(fp) # 累加函數(shù)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目標(biāo)檢測測評指標(biāo)遂黍,里面的數(shù)據(jù)是這樣的:這幅圖里面計算AP的方法是2010年之后的那種,在這個例子中俊嗽,一共有20個測試雾家,但是只有6個正的測試樣本,所以這里就分成了6個間隔绍豁,而不是11芯咧,如果我們把每一種顏色(其實這顏色塊就是根據(jù)recall不同的值分的)看成是一個階段,2010年之后的方法取的precison是后面階段和本階段中最大的(對應(yīng)表格里面的那句話【Max Precision for Any Recall r' >= r】竹揍,注意是大于等于不是大于)

例子

下面是我理解2010年之后的評價方法時做的圖示唬党,看一下就會很快理解,也能發(fā)現(xiàn)代碼中錯位的巧妙鬼佣,數(shù)據(jù)來自上面那幅圖:
2010年以后評價方法代碼輔助理解示例

總結(jié)

所以我們回到開頭的終極目標(biāo)驶拱,當(dāng)我們明白代碼之后,就只要學(xué)會調(diào)用就好了晶衷,那對于Pascal VOC這個數(shù)據(jù)集來說:

  • 我們需要提供的:那20個預(yù)測結(jié)果文件蓝纲,如第2大點的那幅圖里面的看到的,和annotation文件(XML文件)晌纫;
  • 計算出來的是:當(dāng)然是mAP啦税迷!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锹漱,隨后出現(xiàn)的幾起案子箭养,更是在濱河造成了極大的恐慌,老刑警劉巖哥牍,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毕泌,死亡現(xiàn)場離奇詭異,居然都是意外死亡嗅辣,警方通過查閱死者的電腦和手機撼泛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澡谭,“玉大人愿题,你說我怎么就攤上這事。” “怎么了潘酗?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵杆兵,是天一觀的道長。 經(jīng)常有香客問我仔夺,道長拧咳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任囚灼,我火速辦了婚禮骆膝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灶体。我一直安慰自己阅签,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布蝎抽。 她就那樣靜靜地躺著政钟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪樟结。 梳的紋絲不亂的頭發(fā)上养交,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音瓢宦,去河邊找鬼碎连。 笑死,一個胖子當(dāng)著我的面吹牛驮履,可吹牛的內(nèi)容都是我干的鱼辙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼玫镐,長吁一口氣:“原來是場噩夢啊……” “哼倒戏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起恐似,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤杜跷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后矫夷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葛闷,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年口四,在試婚紗的時候發(fā)現(xiàn)自己被綠了孵运。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秦陋。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡蔓彩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赤嚼,我是刑警寧澤旷赖,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站更卒,受9級特大地震影響等孵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹂空,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一俯萌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧上枕,春花似錦咐熙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锈玉,卻和暖如春爪飘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拉背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工师崎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人椅棺。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓抡诞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親土陪。 傳聞我的和親對象是個殘疾皇子昼汗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容