目錄
前言
為了瞭解模型的泛化能力,即判斷模型的好壞,我們需要用某個指標來衡量,有了評價指標,就可以對比不同模型的優劣,並透過這個指標來進一步調參最佳化模型。對於分類和迴歸兩類監督模型,分別有各自的評判標準。
不同的問題和不同的資料集都會有不同的模型評價指標,比如分類問題,資料集類別平衡的情況下可以使用準確率作為評價指標,但是現實中的資料集幾乎都是類別不平衡的,所以一般都是採用 AP
作為分類的評價指標,分別計算每個類別的 AP
,再計算mAP
。
一,精確率、召回率與F1
1.1,準確率
準確率(精度) – Accuracy,預測正確的結果佔總樣本的百分比,定義如下:
錯誤率和精度雖然常用,但是並不能滿足所有任務需求。以西瓜問題為例,假設瓜農拉來一車西瓜,我們用訓練好的模型對西瓜進行判別,現如精度只能衡量有多少比例的西瓜被我們判斷類別正確(兩類:好瓜、壞瓜)。但是若我們更加關心的是“挑出的西瓜中有多少比例是好瓜”,或者”所有好瓜中有多少比例被挑出來“,那麼精度和錯誤率這個指標顯然是不夠用的。
雖然準確率可以判斷總的正確率,但是在樣本不平衡的情況下,並不能作為很好的指標來衡量結果。舉個簡單的例子,比如在一個總樣本中,正樣本佔 90%,負樣本佔 10%,樣本是嚴重不平衡的。對於這種情況,我們只需要將全部樣本預測為正樣本即可得到 90% 的高準確率,但實際上我們並沒有很用心的分類,只是隨便無腦一分而已。這就說明了:由於樣本不平衡的問題,導致了得到的高準確率結果含有很大的水分。即如果樣本不平衡,準確率就會失效。
1.2,精確率、召回率
精確率(查準率)P
、召回率(查全率)R
的計算涉及到混淆矩陣的定義,混淆矩陣表格如下:
名稱 | 定義 |
---|---|
True Positive (真正例, TP ) |
將正類預測為正類數 |
True Negative (真負例, TN ) |
將負類預測為負類數 |
False Positive (假正例, FP ) |
將負類預測為正類數 → 誤報 (Type I error) |
False Negative (假負例子, FN ) |
將正類預測為負類數 → 漏報 (Type II error) |
查準率與查全率計算公式:
- 查準率(精確率)\(P = TP/(TP+FP)\)
- 查全率(召回率)\(R = TP/(TP+FN)\)
精準率和準確率看上去有些類似,但是完全不同的兩個概念。精準率代表對正樣本結果中的預測準確程度,而準確率則代表整體的預測準確程度,既包括正樣本,也包括負樣本。
精確率描述了模型有多準,即在預測為正例的結果中,有多少是真正例;召回率則描述了模型有多全,即在為真的樣本中,有多少被我們的模型預測為正例。精確率和召回率的區別在於分母不同,一個分母是預測為正的樣本數,另一個是原來樣本中所有的正樣本數。
1.3,F1 分數
如果想要找到 \(P\) 和 \(R\) 二者之間的一個平衡點,我們就需要一個新的指標:\(F1\) 分數。\(F1\) 分數同時考慮了查準率和查全率,讓二者同時達到最高,取一個平衡。\(F1\) 計算公式如下:
這裡的 \(F1\) 計算是針對二分類模型,多分類任務的 \(F1\) 的計算請看下面。
\(F1\) 度量的一般形式:\(F_{\beta}\),能讓我們表達出對查準率/查全率的偏見,\(F_{\beta}\) 計算公式如下:
其中 \(\beta >1\) 對查全率有更大影響,\(\beta < 1\) 對查準率有更大影響。
不同的計算機視覺問題,對兩類錯誤有不同的偏好,常常在某一類錯誤不多於一定閾值的情況下,努力減少另一類錯誤。在目標檢測中,mAP(mean Average Precision)作為一個統一的指標將這兩種錯誤兼顧考慮。
很多時候我們會有多個混淆矩陣,例如進行多次訓練/測試,每次都能得到一個混淆矩陣;或者是在多個資料集上進行訓練/測試,希望估計演算法的”全域性“效能;又或者是執行多分類任務,每兩兩類別的組合都對應一個混淆矩陣;....總而來說,我們希望能在 \(n\) 個二分類混淆矩陣上綜合考慮查準率和查全率。
一種直接的做法是先在各混淆矩陣上分別計算出查準率和查全率,記為 \((P_1,R_1),(P_2,R_2),...,(P_n,R_n)\) 然後取平均,這樣得到的是”宏查準率(Macro-P
)“、”宏查準率(Macro-R
)“及對應的”宏 \(F1\)(Macro-F1
)“:
另一種做法是將各混淆矩陣對應元素進行平均,得到 \(TP、FP、TN、FN\) 的平均值,再基於這些平均值計算出”微查準率“(Micro-P
)、”微查全率“(Micro-R
)和”微 \(F1\)“(Mairo-F1
)
1.4,PR 曲線
精準率和召回率的關係可以用一個 P-R
圖來展示,以查準率 P
為縱軸、查全率 R
為橫軸作圖,就得到了查準率-查全率曲線,簡稱 P-R 曲線,PR
曲線下的面積定義為 AP
:
1.4.1,如何理解 P-R 曲線
可以從排序型模型或者分類模型理解。以邏輯迴歸舉例,邏輯迴歸的輸出是一個 0
到 1
之間的機率數字,因此,如果我們想要根據這個機率判斷使用者好壞的話,我們就必須定義一個閾值 。通常來講,邏輯迴歸的機率越大說明越接近 1
,也就可以說他是壞使用者的可能性更大。比如,我們定義了閾值為 0.5
,即機率小於 0.5
的我們都認為是好使用者,而大於 0.5
都認為是壞使用者。因此,對於閾值為 0.5
的情況下,我們可以得到相應的一對查準率和查全率。
但問題是:這個閾值是我們隨便定義的,我們並不知道這個閾值是否符合我們的要求。 因此,為了找到一個最合適的閾值滿足我們的要求,我們就必須遍歷 0
到 1
之間所有的閾值,而每個閾值下都對應著一對查準率和查全率,從而我們就得到了 PR
曲線。
最後如何找到最好的閾值點呢? 首先,需要說明的是我們對於這兩個指標的要求:我們希望查準率和查全率同時都非常高。 但實際上這兩個指標是一對矛盾體,無法做到雙高。圖中明顯看到,如果其中一個非常高,另一個肯定會非常低。選取合適的閾值點要根據實際需求,比如我們想要高的查全率,那麼我們就會犧牲一些查準率,在保證查全率最高的情況下,查準率也不那麼低。。
1.5,ROC 曲線與 AUC 面積
PR
曲線是以Recall
為橫軸,Precision
為縱軸;而ROC
曲線則是以FPR
為橫軸,TPR
為縱軸。P-R 曲線越靠近右上角效能越好**。PR
曲線的兩個指標都聚焦於正例PR
曲線展示的是Precision vs Recall
的曲線,ROC
曲線展示的是FPR
(x 軸:False positive rate) vsTPR
(True positive rate, TPR)曲線。
二,AP 與 mAP
2.1,AP 與 mAP 指標理解
AP
衡量的是訓練好的模型在每個類別上的好壞,mAP
衡量的是模型在所有類別上的好壞,得到 AP
後 mAP
的計算就變得很簡單了,就是取所有 AP
的平均值。AP
的計算公式比較複雜(所以單獨作一章節內容),詳細內容參考下文。
mAP
這個術語有不同的定義。此度量指標通常用於資訊檢索、影像分類和目標檢測領域。然而這兩個領域計算 mAP
的方式卻不相同。這裡我們只談論目標檢測中的 mAP
計算方法。
mAP
常作為目標檢測演算法的評價指標,具體來說就是,對於每張圖片檢測模型會輸出多個預測框(遠超真實框的個數),我們使用 IoU
(Intersection Over Union,交併比)來標記預測框是否預測準確。標記完成後,隨著預測框的增多,查全率 R
總會上升,在不同查全率 R
水平下對準確率 P
做平均,即得到 AP,最後再對所有類別按其所佔比例做平均,即得到 mAP
指標。
2.2,近似計算AP
知道了AP
的定義,下一步就是理解AP
計算的實現,理論上可以透過積分來計算AP
,公式如下:
但通常情況下都是使用近似或者插值的方法來計算 \(AP\)。
- 近似計算 \(AP\) (
approximated average precision
),這種計算方式是approximated
形式的; - 很顯然位於一條豎直線上的點對計算 \(AP\) 沒有貢獻;
- 這裡 \(N\) 為資料總量,\(k\) 為每個樣本點的索引, \(Δr(k)=r(k)−r(k−1)\)。
近似計算 AP
和繪製 PR
曲線程式碼如下:
import numpy as np
import matplotlib.pyplot as plt
class_names = ["car", "pedestrians", "bicycle"]
def draw_PR_curve(predict_scores, eval_labels, name, cls_idx=1):
"""calculate AP and draw PR curve, there are 3 types
Parameters:
@all_scores: single test dataset predict scores array, (-1, 3)
@all_labels: single test dataset predict label array, (-1, 3)
@cls_idx: the serial number of the AP to be calculated, example: 0,1,2,3...
"""
# print('sklearn Macro-F1-Score:', f1_score(predict_scores, eval_labels, average='macro'))
global class_names
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(15, 10))
# Rank the predicted scores from large to small, extract their corresponding index(index number), and generate an array
idx = predict_scores[:, cls_idx].argsort()[::-1]
eval_labels_descend = eval_labels[idx]
pos_gt_num = np.sum(eval_labels == cls_idx) # number of all gt
predict_results = np.ones_like(eval_labels)
tp_arr = np.logical_and(predict_results == cls_idx, eval_labels_descend == cls_idx) # ndarray
fp_arr = np.logical_and(predict_results == cls_idx, eval_labels_descend != cls_idx)
tp_cum = np.cumsum(tp_arr).astype(float) # ndarray, Cumulative sum of array elements.
fp_cum = np.cumsum(fp_arr).astype(float)
precision_arr = tp_cum / (tp_cum + fp_cum) # ndarray
recall_arr = tp_cum / pos_gt_num
ap = 0.0
prev_recall = 0
for p, r in zip(precision_arr, recall_arr):
ap += p * (r - prev_recall)
# pdb.set_trace()
prev_recall = r
print("------%s, ap: %f-----" % (name, ap))
fig_label = '[%s, %s] ap=%f' % (name, class_names[cls_idx], ap)
ax.plot(recall_arr, precision_arr, label=fig_label)
ax.legend(loc="lower left")
ax.set_title("PR curve about class: %s" % (class_names[cls_idx]))
ax.set(xticks=np.arange(0., 1, 0.05), yticks=np.arange(0., 1, 0.05))
ax.set(xlabel="recall", ylabel="precision", xlim=[0, 1], ylim=[0, 1])
fig.savefig("./pr-curve-%s.png" % class_names[cls_idx])
plt.close(fig)
2.3,插值計算 AP
插值計算(Interpolated average precision
) \(AP\) 的公式的演變過程這裡不做討論,詳情可以參考這篇文章,我這裡的公式和圖也是參考此文章的。11
點插值計算方式計算 \(AP\) 公式如下:
- 這是通常意義上的
11
points_Interpolated
形式的AP
,選取固定的 \({0,0.1,0.2,…,1.0}\)11
個閾值,這個在 PASCAL2007 中使用 - 這裡因為參與計算的只有
11
個點,所以 \(K=11\),稱為 11 points_Interpolated,\(k\) 為閾值索引 - \(P_{interp}(k)\) 取第 \(k\) 個閾值所對應的樣本點之後的樣本中的最大值,只不過這裡的閾值被限定在了 \({0,0.1,0.2,…,1.0}\) 範圍內。
從曲線上看,真實 AP< approximated AP < Interpolated AP
,11-points Interpolated AP
可能大也可能小,當資料量很多的時候會接近於 Interpolated AP
,與 Interpolated AP
不同,前面的公式中計算 AP
時都是對 PR
曲線的面積估計,PASCAL 的論文裡給出的公式就更加簡單粗暴了,直接計算11
個閾值處的 precision
的平均值。PASCAL
論文給出的 11
點計算 AP
的公式如下。
1, 在給定 recal
和 precision
的條件下計算 AP
:
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
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
2,給定目標檢測結果檔案和測試集標籤檔案 xml
等計算 AP
:
def parse_rec(filename):
""" Parse a PASCAL VOC xml file
Return : list, element is dict.
"""
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_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 result file
detpath.format(classname) should produce the detection results file.
annopath: Path to annotations file
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
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)
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]
confidence = np.array([float(x[1]) for x in splitlines])
BB = np.array([[float(z) for z in x[2:]] for x in splitlines])
nd = len(image_ids)
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):
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
# 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:
if not R['difficult'][jmax]:
if not R['det'][jmax]:
tp[d] = 1.
R['det'][jmax] = 1
else:
fp[d] = 1.
else:
fp[d] = 1.
# compute precision recall
fp = np.cumsum(fp)
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
2.4,mAP 計算方法
因為 \(mAP\) 值的計算是對資料集中所有類別的 \(AP\) 值求平均,所以我們要計算 \(mAP\),首先得知道某一類別的 \(AP\) 值怎麼求。不同資料集的某類別的 \(AP\) 計算方法大同小異,主要分為三種:
(1)在 VOC2007
,只需要選取當 \(Recall >= 0, 0.1, 0.2, ..., 1\) 共 11
個點時的 Precision
最大值,然後 \(AP\) 就是這 11
個 Precision
的平均值,\(mAP\) 就是所有類別 \(AP\) 值的平均。VOC
資料集中計算 \(AP\) 的程式碼(用的是插值計算方法,程式碼出自py-faster-rcnn倉庫)
(2)在 VOC2010
及以後,需要針對每一個不同的 Recall
值(包括 0 和 1),選取其大於等於這些 Recall
值時的 Precision
最大值,然後計算 PR
曲線下面積作為 \(AP\) 值,\(mAP\) 就是所有類別 \(AP\) 值的平均。
(3)COCO
資料集,設定多個 IOU
閾值(0.5-0.95
, 0.05
為步長),在每一個 IOU
閾值下都有某一類別的 AP
值,然後求不同 IOU
閾值下的 AP
平均,就是所求的最終的某類別的 AP
值。
三,目標檢測度量標準彙總
評價指標 | 定義及理解 |
---|---|
mAP |
mean Average Precision, 即各類別 AP 的平均值 |
AP |
PR 曲線下面積,後文會詳細講解 |
PR 曲線 |
Precision-Recall 曲線 |
Precision |
\(TP / (TP + FP)\) |
Recall |
\(TP / (TP + FN)\) |
TP |
IoU>0.5 的檢測框數量(同一 Ground Truth 只計算一次,閾值取 0.5 ) |
FP |
IoU<=0.5 的檢測框,或者是檢測到同一個 GT 的多餘檢測框的數量 |
FN |
沒有檢測到的 GT 的數量 |