mAP(轉(zhuǎn)載)
Pascal VOC中mAP的計(jì)算(轉(zhuǎn)自簡書)
其實(shí)我們訓(xùn)練部分是我們自己提出各種改進(jìn)的算法,得到一個(gè)我們自己的結(jié)果,但是驗(yàn)證部分基本都是差不多的,或者一律使用官方的驗(yàn)證方法,為了正確評估我們模型的結(jié)果,我們還是得弄清楚驗(yàn)證部分的代碼需要我們提供的是什么,它又計(jì)算出了什么,請時(shí)刻記住,這是我們的終極目標(biāo)。
這里就整理一下Pascal VOC這個(gè)數(shù)據(jù)集中mAP的計(jì)算,最后也會總結(jié)一下,到底怎樣使用這份代碼。Pascal VOC是早期目標(biāo)檢測(e.g. Fast/er RCNN, SSD)常用的標(biāo)準(zhǔn)數(shù)據(jù)集。
1、什么是mAP?
既然要看懂計(jì)算mAP的代碼,那肯定得先知道什么是mAP?不能違背人的認(rèn)知規(guī)律,當(dāng)然已經(jīng)在目標(biāo)檢測這方向已經(jīng)比較熟的大佬就可以跳過這部分。
這方面有很多人都寫過了,我建議可以看一下這一篇博客:
深度學(xué)習(xí)筆記(八):目標(biāo)檢測性能評價(jià)指標(biāo)(mAP、IOU..)
這里也給出自己看完這些博客還得注意的地方,算是給像我這樣的小白一些解惑:

來自維基百科的PR解釋
1、上圖來自維基百科中對于precision和recall的形象解釋,precision按字面理解,準(zhǔn)確率就是你預(yù)測出來為正類當(dāng)中確實(shí)有多少正類的比例(這里一開始我也有個(gè)疑問,為什么我們預(yù)測出來的都算是正類?其實(shí)仔細(xì)想一下也不難理解,就像目標(biāo)檢測,我們選出來的框自然是我們認(rèn)為是前景目標(biāo)的,我們怎么會去框一個(gè)背景呢,注意這里是我們認(rèn)為我們框的,實(shí)際完全有可能框到背景);recall按字面理解,召回率就是實(shí)際有這么多正類,你選出來了多少的一個(gè)比率,不理解的可以多看幾遍上面的圖幫助理解,這會影響到下面PR曲線繪制的理解。
2、一條P-R曲線對應(yīng)一個(gè)閾值,那這個(gè)閾值又是什么意思呢?例如VOC里面,這個(gè)閾值就代表和Ground Truth bbox的IoU大于這個(gè)閾值才會被認(rèn)為是正類【如下面代碼中的ovthresh】,但還得通過一定的準(zhǔn)則才能歸入TP(true positive),到這里我還會有兩個(gè)問題:1.PR曲線到底是怎么畫出來的?2.為什么R增大P會有減小的趨勢?,下面結(jié)合代碼部分我還會詳細(xì)講解,并且給出我實(shí)踐中畫出來的PR曲線。
3、通常人們都會說:P-R曲線圍起來的面積就是AP值,但是我想說的是,理解的時(shí)候可以這樣直觀認(rèn)識一下,具體計(jì)算的時(shí)候還是看代碼的實(shí)現(xiàn)方式,當(dāng)然實(shí)際計(jì)算都不能非常準(zhǔn)確到剛好是P-R曲線下的面積。
2、如何畫PR曲線和計(jì)算AP?
那我們?nèi)绾蝸懋婸R曲線和計(jì)算AP?PR曲線無非就是很多個(gè)(recall, precision)對連接起來的,一開始我還不知道這些點(diǎn)對是哪里來的?其實(shí)這就回到我們最原初的那個(gè)問題了,我們的輸入是什么?我們的輸入是我們遍歷完所有測試圖片對某一類(例如'car')給出的預(yù)測值(包括圖片索引,框的置信度分?jǐn)?shù)和四個(gè)坐標(biāo),這個(gè)預(yù)測值一般都是經(jīng)過NMS處理之后的,保證每張圖片里面不會有過多的框,因?yàn)榉治龃a過后,過多的框是會降低AP的),例如下面這張圖就是我跑CenterNet在Pascal VOC數(shù)據(jù)集上產(chǎn)生的20個(gè)類的結(jié)果:

eval結(jié)果
每一個(gè)文件里面都是長這樣的:

飛機(jī)類結(jié)果文件具體內(nèi)容
下面具體分析
上面的這些結(jié)果文件就是對應(yīng)【下面代碼的104行的detfile = detpath.format(classname)】,因?yàn)槲覀儺?dāng)作他們都是positive,所以這里的每一個(gè)預(yù)測出來的框都會分配一個(gè)TP或者FP(或者根據(jù)官方規(guī)則,對于difficult=1的object是不參與AP計(jì)算的),所以上面一個(gè)文件中有這么多行,就會有這么長的TP和FP列表,每輸入一個(gè)預(yù)測框就又可以計(jì)算一個(gè)(recall, precision)對,那么就可以畫出非常細(xì)密的點(diǎn),當(dāng)然隨著輸入框的增多,recall肯定是上升的,因?yàn)閷?shí)際的positive數(shù)量是固定的,隨著輸入框的增多,肯定是覆蓋到這些真值的,根據(jù)recall的定義就會接近1;而此時(shí)在還沒有正確預(yù)測中下一個(gè)時(shí),也就是上面維基圖中TP不變的情況下,增加了紅半球FP的數(shù)量,那precision肯定會下降,只有預(yù)測中下一個(gè)真值的時(shí)候會突增一下,所以說,P-R曲線并不是遞減的。下面就是我實(shí)踐過程畫出來的P-R曲線,仔細(xì)看會有局部的上升趨勢(這里感覺理解起來不夠形象的,最后還有一個(gè)我理解過程中參考別人博客的一個(gè)例子):

P-R曲線
下面來看具體代碼
至此,我們已經(jīng)完成了所有預(yù)備的知識,需要來分析一下代碼是怎么實(shí)現(xiàn)的?我在代碼必要部分標(biāo)上了中文注釋:
下面這個(gè)就是計(jì)算Pascal VOC中mAP的python版本的代碼,摘自于Ross Girshick大神的Faster RCNN中的代碼,后來非常多人都用到這份代碼,比如:
- facebook research的Detectron框架
- amdegroot用PyTorch復(fù)現(xiàn)的ssd.pytorch
- CenterNet(Objects as Points)官方代碼中的VOC部分
## 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之后換了評價(jià)方法,所以決定是否用07年的
# 11 point metric
ap = 0.
for t in np.arange(0., 1.1, 0.1): # 07年的采用11個(gè)點(diǎn)平分recall來計(jì)算
if np.sum(rec >= t) == 0:
p = 0
else:
p = np.max(prec[rec >= t]) # 取一個(gè)recall閾值之后最大的precision
ap = ap + p / 11. # 將11個(gè)precision加和平均
else: # 這里是用2010年后的方法,取所有不同的recall對應(yīng)的點(diǎn)處的精度值做平均,不再是固定的11個(gè)點(diǎn)
# correct AP calculation
# first append sentinel values at the end
mrec = np.concatenate(([0.], rec, [1.])) #recall和precision前后分別加了一個(gè)值,因?yàn)閞ecall最后是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] # 這里巧妙的錯(cuò)位,返回剛好TP的位置,
# 可以看后面輔助的例子
# and sum (\Delta recall) * prec 用recall的間隔對精度作加權(quán)平均
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
return ap
# 計(jì)算每個(gè)類別對應(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真實(shí)標(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) #累計(jì)所有圖片中的該類別目標(biāo)的總數(shù),不算diffcult
# 這里計(jì)算還是很巧妙的,npos=TP+FN
class_recs[imagename] = {'bbox': bbox,
'difficult': difficult,
'det': det}
# read dets
detfile = detpath.format(classname)
# 讀取相應(yīng)類別的檢測結(jié)果文件,每一行對應(yīng)一個(gè)檢測目標(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)記每個(gè)檢測結(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 計(jì)算與該圖片中所有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就忽略,
# 因?yàn)樯厦鎛pos就沒算
if not R['det'][jmax]: # 如果對應(yīng)的最大重疊度的ground truth以前沒被匹配過則匹配成功,即tp
tp[d] = 1.
R['det'][jmax] = 1
else: # 若之前有置信度更高的檢測結(jié)果匹配過這個(gè)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ù)是這樣的:這幅圖里面計(jì)算AP的方法是2010年之后的那種,在這個(gè)例子中,一共有20個(gè)測試,但是只有6個(gè)正的測試樣本,所以這里就分成了6個(gè)間隔,而不是11,如果我們把每一種顏色(其實(shí)這顏色塊就是根據(jù)recall不同的值分的)看成是一個(gè)階段,2010年之后的方法取的precison是后面階段和本階段中最大的(對應(yīng)表格里面的那句話【Max Precision for Any Recall r' >= r】,注意是大于等于不是大于)

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

2010年以后評價(jià)方法代碼輔助理解示例
總結(jié)
所以我們回到開頭的終極目標(biāo),當(dāng)我們明白代碼之后,就只要學(xué)會調(diào)用就好了,那對于Pascal VOC這個(gè)數(shù)據(jù)集來說:
- 我們需要提供的:那20個(gè)預(yù)測結(jié)果文件,如第2大點(diǎn)的那幅圖里面的看到的,和annotation文件(XML文件);
- 計(jì)算出來的是:當(dāng)然是mAP啦!
目標(biāo)檢測中的AP,mAP(轉(zhuǎn)自知乎)
AP (Average precision)是主流的目標(biāo)檢測模型的評價(jià)指標(biāo)。再介紹AP之前,我們先來回顧一下需要用到的幾個(gè)概念precision,recall以及IoU。
IoU(Intersection over union)
交并比IoU衡量的是兩個(gè)區(qū)域的重疊程度,是兩個(gè)區(qū)域重疊部分面積占二者總面積(重疊部分只計(jì)算一次)的比例。如下圖,兩個(gè)矩形框的IoU是交叉面積(中間圖片紅色部分)與合并面積(右圖紅色部分)面積之比。

在目標(biāo)檢測任務(wù)中,如果我們模型輸出的矩形框與我們?nèi)斯?biāo)注的矩形框的IoU值大于某個(gè)閾值時(shí)(通常為0.5)即認(rèn)為我們的模型輸出了正確的
精準(zhǔn)率與召回率(Precision & Recall)
Precision 和 Recall最早是信息檢索中的概念,用來評價(jià)一個(gè)信息檢索系統(tǒng)的優(yōu)劣。Precision 就是檢索出來的條目中(比如:文檔、網(wǎng)頁等)有多大比例是我們需要的,Recall就是所有我們需要的網(wǎng)頁的條目有多大比例被檢索出來了。用到目標(biāo)檢測領(lǐng)域,假設(shè)我們有一組圖片,里面有若干待檢測的目標(biāo),Precision就代表我們模型檢測出來的目標(biāo)有多打比例是真正的目標(biāo)物體,Recall就代表所有真實(shí)的目標(biāo)有多大比例被我們的模型檢測出來了。
我們?nèi)绾蝸碛?jì)算Precision和Recall的值呢。
首先我們根據(jù)上圖的規(guī)則計(jì)算出TP,F(xiàn)P,TN的值,則Precision,Recall可表示為
PR曲線
我們當(dāng)然希望檢測的結(jié)果P越高越好,R也越高越好,但事實(shí)上這兩者在某些情況下是矛盾的。比如極端情況下,我們只檢測出了一個(gè)結(jié)果,且是準(zhǔn)確的,那么Precision就是100%,但是Recall就很低;而如果我們把所有結(jié)果都返回,那么必然Recall必然很大,但是Precision很低。
因此在不同的場合中需要自己判斷希望P比較高還是R比較高。如果是做實(shí)驗(yàn)研究,可以繪制Precision-Recall曲線來幫助分析。
這里我們舉一個(gè)簡單的例子,假設(shè)我們的數(shù)據(jù)集中共有五個(gè)待檢測的物體,我們的模型給出了10個(gè)候選框,我們按照模型給出的置信度由高到低對候選框進(jìn)行排序。

表格第二列表示該候選框是否預(yù)測正確(即是否存在某個(gè)待檢測的物體與該候選框的iou值大于0.5)第三列和第四列表示以該行所在候選框置信度為閾值時(shí),Precision和Recall的值。我們以表格的第三行為例進(jìn)行計(jì)算:
由上表以Recall值為橫軸,Precision值為縱軸,我們就可以得到PR曲線。我們會發(fā)現(xiàn),Precision與Recall的值呈現(xiàn)負(fù)相關(guān),在局部區(qū)域會上下波動。
AP(Average Precision)
顧名思義AP就是平均精準(zhǔn)度,簡單來說就是對PR曲線上的Precision值求均值。對于pr曲線來說,我們使用積分來進(jìn)行計(jì)算。
在實(shí)際應(yīng)用中,我們并不直接對該P(yáng)R曲線進(jìn)行計(jì)算,而是對PR曲線進(jìn)行平滑處理。即對PR曲線上的每個(gè)點(diǎn),Precision的值取該點(diǎn)右側(cè)最大的Precision的值。
用公式來描述就是 。用該公式進(jìn)行平滑后再用上述公式計(jì)算AP的值。
Interplolated AP(Pascal Voc 2008 的AP計(jì)算方式)
Pascal VOC 2008中設(shè)置IoU的閾值為0.5,如果一個(gè)目標(biāo)被重復(fù)檢測,則置信度最高的為正樣本,另一個(gè)為負(fù)樣本。在平滑處理的PR曲線上,取橫軸0-1的10等分點(diǎn)(包括斷點(diǎn)共11個(gè)點(diǎn))的Precision的值,計(jì)算其平均值為最終AP的值。
在我們的例子里
Area under curve
上述方法有兩個(gè)缺陷,第一個(gè)是使用11個(gè)采樣點(diǎn)在精度方面會有損失。第二個(gè)是,在比較兩個(gè)AP值較小的模型時(shí),很難體現(xiàn)出兩者的差別。所以這種方法在2009年的Pascalvoc之后便不再采用了。在Pascal voc 2010之后,便開始采用這種精度更高的方式。繪制出平滑后的PR曲線后,用積分的方式計(jì)算平滑曲線下方的面積作為最終的AP值。
COCO mAP
最新的目標(biāo)檢測相關(guān)論文都使用coco數(shù)據(jù)集來展示自己模型的效果。對于coco數(shù)據(jù)集來說,使用的也是Interplolated AP的計(jì)算方式。與Voc 2008不同的是,為了提高精度,在PR曲線上采樣了100個(gè)點(diǎn)進(jìn)行計(jì)算。而且Iou的閾值從固定的0.5調(diào)整為在 0.5 - 0.95 的區(qū)間上每隔0.5計(jì)算一次AP的值,取所有結(jié)果的平均值作為最終的結(jié)果。
比如我們看一下YOLOv3的作者在論文中展示的在coco數(shù)據(jù)集上的實(shí)驗(yàn)結(jié)果
我們發(fā)現(xiàn)除了AP,還有 ,
等值,這些事代表什么意思呢?
:IoU閾值為0.5時(shí)的AP測量值
:IoU閾值為0.75時(shí)的測量值
: 像素面積小于
的目標(biāo)框的AP測量值
: 像素面積在
-
之間目標(biāo)框的測量值
: 像素面積大于
的目標(biāo)框的AP測量值
注:通常來說AP是在單個(gè)類別下的,mAP是AP值在所有類別下的均值。在這里,在coco的語境下AP便是mAP,這里的AP已經(jīng)計(jì)算了所有類別下的平均值,這里的AP便是mAP。
浙公網(wǎng)安備 33010602011771號