SSD算法评估:AP, mAP和Precision-Recall曲线

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Jesse_Mx/article/details/79169991

前言

对于目标检测算法来说,最终模型的评价至关重要。本文将针对SSD检测框架,简要叙述如何在模型的测试阶段,针对标注好的测试集,得到mAP,每一类的AP,以及画出P-R曲线。这里博主不再赘述mAP的概念及其计算公式,只说怎么修改caffe-ssd的代码。

模型测试

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

于是做了实验,发现果然打印了每一类的检测精度:

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个recall值之下的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可以解析该信息,我这里最新的可用编号是45。

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

猜你喜欢

转载自blog.csdn.net/Jesse_Mx/article/details/79169991
今日推荐