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
设置为0,把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个点,曲线看起来并不平滑,有时间再研究下怎么得到更精确的数据。