目标檢測模型評估名額mAP及代碼注釋
最近剛看完Faster R-CNN的源碼,對于mAP這個目标檢測的衡量名額之前也大緻的了解一下,但是它與準确度(Accuray)、精度(Precision)、召回率(recall)等的關系是怎麼樣的?這些都還沒有了解,是以剛好順着Faster R-CNN的源碼,把這個問題搞清楚一些。
一、目标檢測問題
每個模型在
“驗證/測試”資料集上來評估性能,性能衡量使用各種統計量如
準确度(accuracy),
精度(precision),
召回率(recall)等。選擇的統計量通常針對特定應用場景和用例。 對于每個應用場景,選擇一個能夠客觀比較模型的度量名額非常重要。
大多數時候,這些名額很容易了解和計算。例如,在二進制分類中,精确度和召回率是一個一個簡單直覺的統計量。然而,目标檢測是一個非常不同且有趣的問題。即使你的目标檢測器在圖檔中檢測到貓,但如果你無法定位,它也沒有用處。由于你要預測的是圖像中各個物體是否出現及其位置,如何計算mAP将非常有趣。
在目标檢測問題中,給定一個圖像,找到它所包含的物體,找到他們的位置并對它們進行分類。目标檢測模型通常是在一組特定的類集合上進行訓練的,是以模型隻會定位和分類圖像中的那些類。另外,對象的位置通常采用矩形邊界框表示。是以,目标檢測涉及圖像中物體的定位和分類。
二、評估目标檢測模型
1.為什麼是mAP?
目标檢測問題中的每個圖檔都可能包含一些不同類别的物體。如前所述,需要評估模型的物體分類和定位性能。是以,用于圖像分類問題的标準名額precision不能直接應用于此。是以我們需要一種新的評估方式,即mAP(Mean Average Precision),來衡量模型的性能。
2.Ground Truth的定義
對于任何算法,評估名額需要知道ground truth(真實标簽)資料。 我們隻知道訓練、驗證和測試資料集的ground truth。對于目标檢測問題,ground truth包括圖像中物體的類别以及該圖像中每個物體的真實邊界框。
訓練時,模型需要的輸入是原始的圖像,以及Ground Truth的坐标值和類别。
在計算mAP之前我們再來複習以下比較重要的概念。
3.IOU的概念
IOU的概念應該比較簡單,就是衡量監測框和标簽框的重合程度。
2.TP TN FP FN的概念
TP(True Positives)意思是“被分為正樣本,并且分對了”,
TN(True Negatives)意思是“被分為負樣本,而且分對了”,
FP(False Positives)意思是“被分為正樣本,但是分錯了”,
FN(False Negatives)意思是“被分為負樣本,但是分錯了”。
按下圖來解釋,左半矩形是正樣本,右半矩形是負樣本。一個2分類器,在圖上畫了個圓,分類器認為圓内是正樣本,圓外是負樣本。那麼左半圓分類器認為是正樣本,同時它确實是正樣本,那麼就是“被分為正樣本,并且分對了”即TP,左半矩形扣除左半圓的部分就是分類器認為它是負樣本,但是它本身卻是正樣本,就是“被分為負樣本,但是分錯了”即FN。右半圓分類器認為它是正樣本,但是本身卻是負樣本,那麼就是“被分為正樣本,但是分錯了”即FP。右半矩形扣除右半圓的部分就是分類器認為它是負樣本,同時它本身确實是負樣本,那麼就是“被分為負樣本,而且分對了”即TN
3.Precision(精度)和Recall(召回率)的概念
有了上面TP TN FP FN的概念,這個Precision和Recall的概念一張圖就能說明。
翻譯成中文就是“
分類器認為是正類并且确實是正類的部分占所有分類器認為是正類的比例”,衡量的是一個分類器分出來的正類的确是正類的機率。兩種極端情況就是,如果精度是100%,就代表所有分類器分出來的正類确實都是正類。如果精度是0%,就代表分類器分出來的正類沒一個是正類。
光是精度還不能衡量分類器的好壞程度,比如50個正樣本和50個負樣本,我的分類器把49個正樣本和50個負樣本都分為負樣本,剩下一個正樣本分為正樣本,這樣我的精度也是100%,但是傻子也知道這個分類器很垃圾。 翻譯成中文就是“分類器認為是正類并且确實是正類的部分占所有确實是正類的比例**”,衡量的是一個分類能把所有的正類都找出來的能力。兩種極端情況,如果召回率是100%,就代表所有的正類都被分類器分為正類。如果召回率是0%,就代表沒一個正類被分為正類。
4.舉例計算mAP
在對多标簽圖像分類時,首先用訓練好的模型得到所有測試樣本的confidence score,每一類(如car)的confidence score都儲存到一個檔案中(如test_car.txt)。假設該檔案包含20個測試樣本(即對應圖1中的矩形),每個id, confidence score, ground truth label如下:
接下來按照confidence score從大到小排序:
上面雖然有分數,但是我們沒有判定每個id是屬于哪個類别。這裡有兩種方法。
- 設定門檻值,比如score>=0.50
- 或者每一類的前五必為該類。
這裡我們選用前五這種方法來計算:
前五如下:
意思就是說,前五被認為應該全是Positive,但是隻有gt_label=1時才是正真的Positive。
true positives就是指第4和第2張圖檔共2張照片,false positives就是指第13,19,6張圖檔共3張照片。
前五之後如下:
就是說,按照這個标準來講,前五之後的所有都被認為是Negative,那麼其中,false negatives是指第9,16,7,20張圖檔共4張照片,true negatives是指第1,18,5,15,10,17,12,14,8,11,3張圖檔共11張照片。
1).計算Precision和Recall的值:
,則Precision=2/5=40%,意思是對于car這一類别,我們標明了5個樣本,其中正确的有2個,即準确率為40%。
,則Recall=2/(2+4)=30%,意思是在所有測試樣本中,共有6個car,但是因為我們隻召回了2個,是以召回率為30%。
2).做PR圖
實際多類别分類任務中,通常不滿足隻通過top-5來衡量一個模型的好壞,而是需要知道從top-1到top-N(N是所有測試樣本個數,本文中為20)對應的precision和recall。顯然随着我們標明的樣本越來也多,recall一定會越來越高,而precision整體上會呈下降趨勢。把recall當成橫坐标,precision當成縱坐标,即可得到常用的precision-recall曲線。例子中precision-recall曲線如下:
圖中一共有20個點,也就是使用每個點作為界限點。
3).VOC計算方法
PASCAL VOC CHALLENGE的計算方法:
- 07年的方法:首先設定一組門檻值,[0, 0.1, 0.2, …, 1]。然後對于recall大于每一個門檻值(比如recall>0.3),都會得到一個對應的最大precision。這樣,就計算出了11個precision。AP即為這11個precision的平均值。這種方法英文叫做11-point interpolated average precision。計算曲線的下面積 則為AP。
- 10年之後的方法:新的計算方法假設這N個樣本中有M個正例,那麼會得到M個recall值(1/M, 2/M, ..., M/M),對于每個recall值r,可以計算出對應(r' > r)的最大precision,然後對這M個precision值取平均即得到最後的AP值。計算方法如下:
對于上面的例子中。一共有20個測試,但是隻有6個正的測試樣本(以下是第二種計算mAP的方法)。
上表中的最後一欄就是car這類的AP。而mAP就是10個種類的AP求平均值。
從表中gt_label可以看出正例是6個,其他是負例。Recall分别為1/6,2/6,3/6,4/6,5/6,6/6。對于每個recall值,都對應着很多種top取法,是以每個recall值對應的諸多取法中(包括等于此recall的取法)有一個最大的precision,把每種recall對應最大的precision求和取平均即AP。
比如2/6的recall,查找上表,能得到recall2/6值的種類:從第2個開始到第5個,而到上表第6個,因為對應的是正例,是以就不是recall為2/6的範圍了(因為前面已經有2個正例,如果再加一個正例,recall值就是3/6了),這幾個取法對應最大的precision是2/2。同理,recall 4/6的取法就是第四個正例開始(4/7)到第5個正例前(4/10)之間的範圍,對應最大的pricision是4/7。
相應的Precision-Recall曲線(這條曲線是單調遞減的)如下:
AP衡量的是學出來的模型在每個類别上的好壞,mAP衡量的是學出的模型在所有類别上的好壞,得到AP後mAP的計算就變得很簡單了,就是取所有AP的平均值。
5.目标檢測計算mAP
檢測出來的bbox包含score和bbox,按照score降序排序,是以每添加一個樣本,就代表門檻值降低一點(真實情況下score降低,iou不一定降低)。這樣就是可以有很多種門檻值,每個門檻值情況下計算一個prec和recall。
具體過程如下:
- 使用區域選擇算法得到候選區域
- 對候選區域,計算每一個候選區域和标定框(groud truth)的iou
- 設定一個iou門檻值,大于這個的标為正樣本,小于的标為負樣本,由此得到一個類似于分類時的測試集。
- 将給定的測試集(正負樣本),通過分類器,算出每一個圖檔是正樣本的score
- 設定一個score門檻值,大于等于此值的視作正樣本,小于的作為負樣本
- 根據上一步的結果可以算出準确率和召回率
- 調節score門檻值,算出召回率從0到1時的準确率,得到一條曲線計算曲線的下面積 則為AP(這是07年方法,10年的方法參考上面),這條曲線就是對每個類的單獨計算出來的。通過計算所有類的AP就可以計算mAP了。
三、python版本的VOC計算方式
下面摘自前段時間閱讀Faster R-CNN源碼中,計算mAP的部分代碼。
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import xml.etree.ElementTree as ET
import os
import pickle
import numpy as np
def parse_rec(filename):
"""
Parse a PASCAL VOC xml file
讀取XML檔案中的各種資訊
"""
tree = ET.parse(filename)
objects = []
for obj in tree.findall('object'):
obj_struct = {}
obj_struct['name'] = obj.find('name').text
obj_struct['pose'] = obj.find('pose').text
obj_struct['truncated'] = int(obj.find('truncated').text)
obj_struct['difficult'] = int(obj.find('difficult').text)
bbox = obj.find('bndbox')
obj_struct['bbox'] = [int(bbox.find('xmin').text),
int(bbox.find('ymin').text),
int(bbox.find('xmax').text),
int(bbox.find('ymax').text)]
objects.append(obj_struct)
return objects
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:
# 11 point metric
# 2010年以前按recall等間隔取11個不同點處的精度值做平均(0., 0.1, 0.2, …, 0.9, 1.0)
ap = 0.
for t in np.arange(0., 1.1, 0.1):
if np.sum(rec >= t) == 0:
p = 0
else:
# 取最大值等價于2010以後先計算包絡線的操作,保證precise非減
p = np.max(prec[rec >= t])
ap = ap + p / 11.
else:
# 2010年以後取所有不同的recall對應的點處的精度值做平均
# 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
# 計算包絡線,從後往前取最大保證precise非減
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
# 找出所有檢測結果中recall不同的點
i = np.where(mrec[1:] != mrec[:-1])[0]
# and sum (Delta recall) * prec
# 用recall的間隔對精度作權重平均
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
return 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, '%s_annots.pkl' % imagesetfile)
# 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 annotations
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, 'wb') as f:
pickle.dump(recs, f)
else:
# load
with open(cachefile, 'rb') as f:
try:
recs = pickle.load(f)
except:
recs = pickle.load(f, encoding='bytes')
# extract gt objects for this class
# 提取所有測試圖檔中目前類别所對應的所有ground_truth
class_recs = {}
npos = 0
# 周遊所有測試圖檔
for imagename in imagenames:
# 找出所有目前類别對應的object
R = [obj for obj in recs[imagename] if obj['name'] == classname]
# 該圖檔中該類别對應的所有bbox
bbox = np.array([x['bbox'] for x in R])
difficult = np.array([x['difficult'] for x in R]).astype(np.bool)
# 該圖檔中該類别對應的所有bbox的是否已被比對的标志位
det = [False] * len(R)
# 累計所有圖檔中的該類别目标的總數,不算diffcult
npos = npos + sum(~difficult)
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]
# 讀取圖像的ID
confidence = np.array([float(x[1]) for x in splitlines])
# 讀取測試圖像的置信度
BB = np.array([[float(z) for z in x[2:]] for x in splitlines])
# 讀取特使圖像的Predict BBox
nd = len(image_ids) #該類别檢測結果的總數(所有檢測出的bbox的數目)
# 用于标記每個檢測結果是TP(True positive)還是FP
tp = np.zeros(nd)
fp = np.zeros(nd)
if BB.shape[0] > 0:
# 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
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:
# 計算與該圖檔中所有ground truth的最大重疊度
# compute overlaps
# 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)
# 如果最大的重疊度大于一定的門檻值
if ovmax > ovthresh:
# 如果最大重疊度對應的ground truth為difficult就忽略
if not R['difficult'][jmax]:
# 如果對應的最大重疊度的ground truth以前沒被比對過則比對成功,即TP
if not R['det'][jmax]:
tp[d] = 1.
R['det'][jmax] = 1
# 若之前有置信度更高的檢測結果比對過這個ground truth,則此次檢測結果為FP
else:
fp[d] = 1.
# 該圖檔中沒有對應類别的目标ground truth或者與所有ground truth重疊度都小于門檻值
else:
fp[d] = 1.
# 按置信度取不同數量檢測結果時的累計fp和tp
# compute precision recall
# np.cumsum([1, 2, 3, 4]) -> [1, 3, 6, 10]
fp = np.cumsum(fp)
tp = np.cumsum(tp)
# 召回率為占所有真實目标數量的比例,非減的,注意npos本身就排除了difficult,是以npos=tp+fn
rec = tp / float(npos)
# 精度為取的所有檢測結果中tp的比例
# 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目标檢測測評名額www.jianshu.com
小小将:目标檢測模型的評估名額mAP詳解(附代碼)zhuanlan.zhihu.com
目标檢測中的mAP是什麼含義?www.zhihu.com
CSDN-專業IT技術社群-登入blog.csdn.net