SSD演算法評估:AP, mAP和Precision-Recall曲線
前言
對於目標檢測演算法來說,最終模型的評價至關重要。本文將針對SSD檢測框架,簡要敘述如何在模型的測試階段,針對標註好的測試集,得到mAP,每一類的AP,以及畫出P-R曲線。這裡博主不再贅述mAP的概念和計算公式,只說怎麼修改程式碼。
模型測試
SSD演算法的模型測試主要有兩種方式,一種是訓練中每間隔一定的迭代次數進行測試,一種是在模型訓練結束後,針對某個caffemodel進行測試。第一種很簡單,可以直接在solver.prototxt中指定test_interval
等引數即可。
第二種也很容易,只需要準備好如下檔案即可:train.prototxt,test.prototxt,solver.prototxt,test.sh,訓練好的caffemodel以及標註的測試集lmdb。
博主選擇在其他地方單獨開了一個資料夾,因此需要修改以上檔案裡面的路徑資訊,為了直觀,我都是用絕對路徑。首先是指令碼檔案test.sh,這個很好寫,類似下面就行:
/home/mx/paper-use/caffe/build/tools/caffe train \
--solver='solver.prototxt' \
--weights='KITTI_SSD_300x300_ft_iter_61000.caffemodel' \
--gpu 0 2>&1 | tee test_result.log
然後是solver.prototxt檔案,大致不變,需要指定訓練和測試的網路檔案,然後把最大迭代次數max_iter
test_iter
設定為測試圖片數量除以測試的batchsize,這樣就可以直接進入測試階段。
模型測試中會用到train.prototxt和test.prototxt檔案,其中需要修改的地方只是所有的路徑,列舉如下:
# train.prototxt
data_param {
source: "/home/mx/paper-use/caffe/examples/KITTI/KITTI_trainval_lmdb"
batch_size: 1 # 根據顯示卡調整,越大越好
backend: LMDB
}
label_map_file: "/home/mx/paper-use/caffe/data/KITTI/labelmap_kitti.prototxt"
# test.prototxt
data_param {
source: "/home/mx/paper-use/caffe/examples/KITTI/KITTI_test_lmdb"
batch_size: 1
backend: LMDB
}
annotated_data_param {
batch_sampler {
}
label_map_file: "/home/mx/paper-use/caffe/data/KITTI/labelmap_kitti.prototxt"
}
save_output_param {
output_directory: "/home/mx/paper-use/test-kitti-model/main"
output_name_prefix: "comp4_det_test_"
output_format: "VOC"
label_map_file: "/home/mx/paper-use/caffe/data/KITTI/labelmap_kitti.prototxt"
name_size_file: "/home/mx/paper-use/caffe/data/KITTI/test_name_size.txt"
num_test_image: 899 # 測試圖片的數量
}
name_size_file: "/home/mx/paper-use/caffe/data/KITTI/test_name_size.txt"
最後直接執行./test.sh
命令來執行測試過程,列印的重要資訊如下所示:
I0127 20:25:05.363581 9889 solver.cpp:332] Iteration 0, loss = 1.2211
I0127 20:25:05.363626 9889 solver.cpp:433] Iteration 0, Testing net (#0)
I0127 20:25:05.376278 9889 net.cpp:693] Ignoring source layer mbox_loss
I0127 20:25:16.671056 9889 solver.cpp:553] Test net output #0: detection_eval = 0.774722
I0127 20:25:16.671093 9889 solver.cpp:337] Optimization Done.
I0127 20:25:16.671098 9889 caffe.cpp:254] Optimization Done.
detection_eval就是mAP,表明本次訓練的KITTI模型,其mAP是77.4%。
輸出詳細的AP資訊
剛才簡單回顧了SSD模型的測試過程,可知模型的測試階段,solver.cpp一般只會列印mAP資訊,但是我們還想知道每一類的AP資訊,而且如果能畫出P-R曲線就更好了,這都有利於分析模型好壞。
事實上,在仔細看過solver.cpp的原始碼後,可發現如下語句:
...
if (param_.show_per_class_result()) {
LOG(INFO) << "class" << label << ": " << APs[label];
}
...
什麼意思?這就是說,只要在solver.prototxt中加入如下命令,同時令ap_version: "11point"
,就能在終端中列印每一類的AP資訊。
show_per_class_result: true
- 1
於是做了實驗,發現果然列印了每一類的檢測精度:
I0127 20:43:58.062511 13937 solver.cpp:332] Iteration 0, loss = 1.22286
I0127 20:43:58.062556 13937 solver.cpp:433] Iteration 0, Testing net (#0)
I0127 20:43:58.074950 13937 net.cpp:693] Ignoring source layer mbox_loss
I0127 20:44:09.344396 13937 solver.cpp:540] class1: 0.722297
I0127 20:44:09.344655 13937 solver.cpp:540] class2: 0.878479
I0127 20:44:09.344856 13937 solver.cpp:540] class3: 0.723391
I0127 20:44:09.344866 13937 solver.cpp:553] Test net output #0: detection_eval = 0.774722
I0127 20:44:09.344885 13937 solver.cpp:337] Optimization Done.
I0127 20:44:09.344889 13937 caffe.cpp:254] Optimization Done.
car的精度最高,達到了87.8%,而person和cyclist則要低一些,說明我們可以在後兩類的提升上想想辦法。
進一步看,SSD原始碼中涉及到mAP計算的部分在bbox_util.cpp中,可以發現一個ComputeAP
函式,貼上如下:
void ComputeAP(const vector<pair<float, int> >& tp, const int num_pos,
const vector<pair<float, int> >& fp, const string ap_version,
vector<float>* prec, vector<float>* rec, float* ap) {
const float eps = 1e-6;
CHECK_EQ(tp.size(), fp.size()) << "tp must have same size as fp.";
const int num = tp.size();
// Make sure that tp and fp have complement value.
for (int i = 0; i < num; ++i) {
CHECK_LE(fabs(tp[i].first - fp[i].first), eps);
CHECK_EQ(tp[i].second, 1 - fp[i].second);
}
prec->clear();
rec->clear();
*ap = 0;
if (tp.size() == 0 || num_pos == 0) {
return;
}
// Compute cumsum of tp.
vector<int> tp_cumsum;
CumSum(tp, &tp_cumsum);
CHECK_EQ(tp_cumsum.size(), num);
// Compute cumsum of fp.
vector<int> fp_cumsum;
CumSum(fp, &fp_cumsum);
CHECK_EQ(fp_cumsum.size(), num);
// Compute precision.
for (int i = 0; i < num; ++i) {
prec->push_back(static_cast<float>(tp_cumsum[i]) /
(tp_cumsum[i] + fp_cumsum[i]));
}
// Compute recall.
for (int i = 0; i < num; ++i) {
CHECK_LE(tp_cumsum[i], num_pos);
rec->push_back(static_cast<float>(tp_cumsum[i]) / num_pos);
}
if (ap_version == "11point") {
// VOC2007 style for computing AP.
vector<float> max_precs(11, 0.);
int start_idx = num - 1;
for (int j = 10; j >= 0; --j) {
for (int i = start_idx; i >= 0 ; --i) {
if ((*rec)[i] < j / 10.) {
start_idx = i;
if (j > 0) {
max_precs[j-1] = max_precs[j];
}
break;
} else {
if (max_precs[j] < (*prec)[i]) {
max_precs[j] = (*prec)[i];
}
}
}
}
for (int j = 10; j >= 0; --j) {
*ap += max_precs[j] / 11;
}
} else if (ap_version == "MaxIntegral") {
// VOC2012 or ILSVRC style for computing AP.
float cur_rec = rec->back();
float cur_prec = prec->back();
for (int i = num - 2; i >= 0; --i) {
cur_prec = std::max<float>((*prec)[i], cur_prec);
if (fabs(cur_rec - (*rec)[i]) > eps) {
*ap += cur_prec * fabs(cur_rec - (*rec)[i]);
}
cur_rec = (*rec)[i];
}
*ap += cur_rec * cur_prec;
} else if (ap_version == "Integral") {
// Natural integral.
float prev_rec = 0.;
for (int i = 0; i < num; ++i) {
if (fabs((*rec)[i] - prev_rec) > eps) {
*ap += (*prec)[i] * fabs((*rec)[i] - prev_rec);
}
prev_rec = (*rec)[i];
}
} else {
LOG(FATAL) << "Unknown ap_version: " << ap_version;
}
}
從函式可知,SSD計算AP的方法有三種:一種是VOC2007的11point方法,一種是VOC2012的最大值積分法,最後是普通積分方法,可以通過超引數ap_version
來控制,預設是”Integral”。其中區別可以參考這篇文章:ap、mAP多標籤影象分類任務的評價方法
。根據高等數學原理,11point使用簡單的均值計算,而最大值積分則要精細一些,因此後者測出來的AP值要高於前者,而且我認為也要準確一些。
目前來看,如果想要作出簡單的P-R曲線,還是要使用11point的方法,我們只需要打印出recall為0,0.1,0.2…1.0這11個閾值之下的precision值就可以。這需要修改4個檔案:solver.cpp,caffe.prototxt,bbox_util.hpp和bbox_util.cpp。
首先修改bbox_util.hpp和bbox_util.cpp中的ComputeAP函式宣告,增加一個vector變數來儲存11個精度值,然後在函式體的for迴圈中增加push_back()語句。
void ComputeAP(const vector<pair<float, int> >& tp, const int num_pos,
const vector<pair<float, int> >& fp, const string ap_version,
vector<float>* prec, vector<float>* rec, float* ap, vector<float> *temp) // add temp parameter
...
for (int j = 10; j >= 0; --j) {
*ap += max_precs[j] / 11;
temp->push_back(max_precs[j]); // save max_precs
}
然後在solver.cpp中新增相應的列印語句。
vector<float> prec, rec, p_r; // add p_r vector
ComputeAP(label_true_pos, label_num_pos, label_false_pos,
param_.ap_version(), &prec, &rec, &(APs[label]), &p_r); // add parameters
mAP += APs[label];
if (param_.show_per_class_result()) {
LOG(INFO) << "class" << label << ": " << APs[label];
if(param_.show_pr_value()) // add bool parameter
{
for(int i=0;i<p_r.size();i++)
{
LOG(INFO) << "p-r value: " << p_r[i]; // print p_r value(11points)
}
}
}
我們在solver.cpp中使用了show_pr_value
這個bool變數來控制是否列印資訊。因此需要在caffe.proto中增加一條,以便solver.prototxt可以解析該資訊。
...
// If true, display per class result.
optional bool show_per_class_result = 44 [default = false];
// If true, display pr value of per class
optional bool show_pr_value = 45 [default = false]; # add a line
可能有同學覺得自己加語句有些麻煩,也可以到這裡下載:修改SSD原始碼列印AP,然後替換原有的檔案,接下來需要重新編譯caffe-ssd,最後呢,我們就在solver.prototxt中新增show_pr_value: true
語句,同時注意ap_version:
"11point"
。
執行命令執行測試過程,我們可以得到以下資訊:
I0127 21:33:55.290652 22867 solver.cpp:332] Iteration 0, loss = 0.735246
I0127 21:33:55.290686 22867 solver.cpp:433] Iteration 0, Testing net (#0)
I0127 21:33:55.302778 22867 net.cpp:693] Ignoring source layer mbox_loss
I0127 21:34:06.567648 22867 solver.cpp:540] class1: 0.722297
I0127 21:34:06.567668 22867 solver.cpp:545] p-r value: 0
I0127 21:34:06.567674 22867 solver.cpp:545] p-r value: 0
I0127 21:34:06.567679 22867 solver.cpp:545] p-r value: 0.5
I0127 21:34:06.567693 22867 solver.cpp:545] p-r value: 0.657895
I0127 21:34:06.567698 22867 solver.cpp:545] p-r value: 0.84
I0127 21:34:06.567703 22867 solver.cpp:545] p-r value: 0.947368
I0127 21:34:06.567706 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.567720 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.567725 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.567729 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.567734 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.567929 22867 solver.cpp:540] class2: 0.878479
I0127 21:34:06.567936 22867 solver.cpp:545] p-r value: 0
I0127 21:34:06.567940 22867 solver.cpp:545] p-r value: 0.793226
I0127 21:34:06.567955 22867 solver.cpp:545] p-r value: 0.945498
I0127 21:34:06.567960 22867 solver.cpp:545] p-r value: 0.969359
I0127 21:34:06.567975 22867 solver.cpp:545] p-r value: 0.984076
I0127 21:34:06.567979 22867 solver.cpp:545] p-r value: 0.986207
I0127 21:34:06.567984 22867 solver.cpp:545] p-r value: 0.99
I0127 21:34:06.567989 22867 solver.cpp:545] p-r value: 0.994898
I0127 21:34:06.567994 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.567999 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.568004 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.568164 22867 solver.cpp:540] class3: 0.723391
I0127 21:34:06.568184 22867 solver.cpp:545] p-r value: 0
I0127 21:34:06.568190 22867 solver.cpp:545] p-r value: 0
I0127 21:34:06.568204 22867 solver.cpp:545] p-r value: 0.353846
I0127 21:34:06.568212 22867 solver.cpp:545] p-r value: 0.786408
I0127 21:34:06.568228 22867 solver.cpp:545] p-r value: 0.8625
I0127 21:34:06.568233 22867 solver.cpp:545] p-r value: 0.954545
I0127 21:34:06.568239 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.568244 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.568249 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.568255 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.568260 22867 solver.cpp:545] p-r value: 1
I0127 21:34:06.568267 22867 solver.cpp:553] Test net output #0: detection_eval = 0.774722
I0127 21:34:06.568280 22867 solver.cpp:337] Optimization Done.
I0127 21:34:06.568286 22867 caffe.cpp:254] Optimization Done.
畫P-R曲線
有了每一個類別的Precision和Recall資訊,很容易畫出P-R曲線,一個簡單的python程式就可以了,這個程式比較簡陋,以後有機會再把它優化下。
# pr_curve.py
# coding:utf-8
import numpy as np
import matplotlib.pyplot as plt
data=np.loadtxt('pr.txt')
mean=np.mean(data[:,1:],axis=1)
tick=[0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]
plt.figure()
plt.subplot(2,2,1)
plt.title('Cyclist, AP=0.722')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.axis([0, 1, 0, 1.05])
plt.xticks(tick)
plt.yticks(tick)
plt.plot(data[:,0],data[:,1])
plt.subplot(2,2,2)
plt.title('Car, AP=0.878')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.axis([0, 1, 0, 1.05])
plt.xticks(tick)
plt.yticks(tick)
plt.plot(data[:,0],data[:,2])
plt.subplot(2,2,3)
plt.title('Person, AP=0.723')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.axis([0, 1, 0, 1.05])
plt.xticks(tick)
plt.yticks(tick)
plt.plot(data[:,0],data[:,3])
plt.subplot(2,2,4)
plt.title('Overall, mAP=0.774')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.axis([0, 1, 0, 1.05])
plt.xticks(tick)
plt.yticks(tick)
plt.plot(data[:,0],mean)
plt.show()
畫出的曲線如下所示:
KITTI官網中畫AP影象用了41個點,而這個僅僅11個點,曲線看起來並不平滑,有時間再研究下怎麼得到更精確的資料。