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

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,这个很好写,类似下面就行:


     
     
  1. /home/mx/paper-use/caffe/build/tools/caffe train \
  2. - -solver=’solver.prototxt’ \
  3. - -weights=’KITTI_SSD_300x300_ft_iter_61000.caffemodel’ \
  4. - -gpu 0 2>&1 | tee test_result.log

然后是solver.prototxt文件,大致不变,需要指定训练和测试的网络文件,然后把最大迭代次数max_iter设置为0,把test_iter设置为测试图片数量除以测试的batchsize,这样就可以直接进入测试阶段。

模型测试中会用到train.prototxt和test.prototxt文件,其中需要修改的地方只是所有的路径,列举如下:


     
     
  1. # train.prototxt
  2. data_param {
  3. source: "/home/mx/paper-use/caffe/examples/KITTI/KITTI_trainval_lmdb"
  4. batch_size: 1 # 根据显卡调整,越大越好
  5. backend: LMDB
  6. }
  7. label_map_file: "/home/mx/paper-use/caffe/data/KITTI/labelmap_kitti.prototxt"
  8. # test.prototxt
  9. data_param {
  10. source: "/home/mx/paper-use/caffe/examples/KITTI/KITTI_test_lmdb"
  11. batch_size: 1
  12. backend: LMDB
  13. }
  14. annotated_data_param {
  15. batch_sampler {
  16. }
  17. label_map_file: "/home/mx/paper-use/caffe/data/KITTI/labelmap_kitti.prototxt"
  18. }
  19. save_output_param {
  20. output_directory: "/home/mx/paper-use/test-kitti-model/main"
  21. output_name_prefix: "comp4_det_test_"
  22. output_format: "VOC"
  23. label_map_file: "/home/mx/paper-use/caffe/data/KITTI/labelmap_kitti.prototxt"
  24. name_size_file: "/home/mx/paper-use/caffe/data/KITTI/test_name_size.txt"
  25. num_test_image: 899 # 测试图片的数量
  26. }
  27. name_size_file: "/home/mx/paper-use/caffe/data/KITTI/test_name_size.txt"

最后直接执行./test.sh 命令来执行测试过程,打印的重要信息如下所示:


     
     
  1. I0127 20: 25: 05.363581 9889 solver .cpp: 332] Iteration 0, loss = 1.2211
  2. I0127 20: 25: 05.363626 9889 solver .cpp: 433] Iteration 0, Testing net ( #0)
  3. I0127 20: 25: 05.376278 9889 net .cpp: 693] Ignoring source layer mbox_loss
  4. I0127 20: 25: 16.671056 9889 solver .cpp: 553] Test net output #0: detection_eval = 0.774722
  5. I0127 20: 25: 16.671093 9889 solver .cpp: 337] Optimization Done.
  6. 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的源代码后,可发现如下语句:


     
     
  1. ...
  2. if (param_.show_per_class_result()) {
  3. LOG(INFO) << "class" << label << ": " << APs[label];
  4. }
  5. ...

什么意思?这就是说,只要在solver.prototxt中加入如下命令,同时令ap_version: "11point"
,就能在终端中打印每一类的AP信息。

show_per_class_result: true
     
     
  • 1

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


     
     
  1. I0127 20: 43: 58.062511 13937 solver .cpp: 332] Iteration 0, loss = 1.22286
  2. I0127 20: 43: 58.062556 13937 solver .cpp: 433] Iteration 0, Testing net ( #0)
  3. I0127 20: 43: 58.074950 13937 net .cpp: 693] Ignoring source layer mbox_loss
  4. I0127 20: 44: 09.344396 13937 solver .cpp: 540] class1: 0.722297
  5. I0127 20: 44: 09.344655 13937 solver .cpp: 540] class2: 0.878479
  6. I0127 20: 44: 09.344856 13937 solver .cpp: 540] class3: 0.723391
  7. I0127 20: 44: 09.344866 13937 solver .cpp: 553] Test net output #0: detection_eval = 0.774722
  8. I0127 20: 44: 09.344885 13937 solver .cpp: 337] Optimization Done.
  9. I0127 20: 44: 09.344889 13937 caffe .cpp: 254] Optimization Done.

car的精度最高,达到了87.8%,而person和cyclist则要低一些,说明我们可以在后两类的提升上想想办法。

进一步看,SSD源码中涉及到mAP计算的部分在bbox_util.cpp中,可以发现一个ComputeAP函数,粘贴如下:


     
     
  1. void ComputeAP( const vector<pair<float, int> >& tp, const int num_pos,
  2. const vector<pair<float, int> >& fp, const string ap_version,
  3. vector<float> * prec, vector<float> * rec, float * ap) {
  4. const float eps = 1e-6;
  5. CHECK_EQ(tp.size(), fp.size()) << "tp must have same size as fp.";
  6. const int num = tp.size();
  7. // Make sure that tp and fp have complement value.
  8. for ( int i = 0; i < num; ++i) {
  9. CHECK_LE( fabs(tp[i].first - fp[i].first), eps);
  10. CHECK_EQ(tp[i].second, 1 - fp[i].second);
  11. }
  12. prec->clear();
  13. rec->clear();
  14. *ap = 0;
  15. if (tp.size() == 0 || num_pos == 0) {
  16. return;
  17. }
  18. // Compute cumsum of tp.
  19. vector<int> tp_cumsum;
  20. CumSum(tp, &tp_cumsum);
  21. CHECK_EQ(tp_cumsum.size(), num);
  22. // Compute cumsum of fp.
  23. vector<int> fp_cumsum;
  24. CumSum(fp, &fp_cumsum);
  25. CHECK_EQ(fp_cumsum.size(), num);
  26. // Compute precision.
  27. for ( int i = 0; i < num; ++i) {
  28. prec->push_back( static_cast< float>(tp_cumsum[i]) /
  29. (tp_cumsum[i] + fp_cumsum[i]));
  30. }
  31. // Compute recall.
  32. for ( int i = 0; i < num; ++i) {
  33. CHECK_LE(tp_cumsum[i], num_pos);
  34. rec->push_back( static_cast< float>(tp_cumsum[i]) / num_pos);
  35. }
  36. if (ap_version == "11point") {
  37. // VOC2007 style for computing AP.
  38. vector<float> max_precs( 11, 0.);
  39. int start_idx = num - 1;
  40. for ( int j = 10; j >= 0; --j) {
  41. for ( int i = start_idx; i >= 0 ; --i) {
  42. if ((*rec)[i] < j / 10.) {
  43. start_idx = i;
  44. if (j > 0) {
  45. max_precs[j - 1] = max_precs[j];
  46. }
  47. break;
  48. } else {
  49. if (max_precs[j] < (*prec)[i]) {
  50. max_precs[j] = (*prec)[i];
  51. }
  52. }
  53. }
  54. }
  55. for ( int j = 10; j >= 0; --j) {
  56. *ap += max_precs[j] / 11;
  57. }
  58. } else if (ap_version == "MaxIntegral") {
  59. // VOC2012 or ILSVRC style for computing AP.
  60. float cur_rec = rec->back();
  61. float cur_prec = prec->back();
  62. for ( int i = num - 2; i >= 0; --i) {
  63. cur_prec = std::max< float>((*prec)[i], cur_prec);
  64. if ( fabs(cur_rec - (*rec)[i]) > eps) {
  65. *ap += cur_prec * fabs(cur_rec - (*rec)[i]);
  66. }
  67. cur_rec = (*rec)[i];
  68. }
  69. *ap += cur_rec * cur_prec;
  70. } else if (ap_version == "Integral") {
  71. // Natural integral.
  72. float prev_rec = 0.;
  73. for ( int i = 0; i < num; ++i) {
  74. if ( fabs((*rec)[i] - prev_rec) > eps) {
  75. *ap += (*prec)[i] * fabs((*rec)[i] - prev_rec);
  76. }
  77. prev_rec = (*rec)[i];
  78. }
  79. } else {
  80. LOG(FATAL) << "Unknown ap_version: " << ap_version;
  81. }
  82. }

从函数可知,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()语句。


     
     
  1. void ComputeAP( const vector<pair<float, int> >& tp, const int num_pos,
  2. const vector<pair<float, int> >& fp, const string ap_version,
  3. vector<float> * prec, vector<float> * rec, float * ap, vector<float> *temp) // add temp parameter
  4. ...
  5. for ( int j = 10 ; j >= 0 ; --j) {
  6. *ap += max_precs[j] / 11;
  7. temp->push_back(max_precs[j]); // save max_precs
  8. }

然后在solver.cpp中添加相应的打印语句。


     
     
  1. vector<float> prec, rec, p_r; // add p_r vector
  2. ComputeAP(label_true_pos, label_num_pos, label_false_pos,
  3. param_.ap_version(), &prec, &rec, &(APs[label]), &p_r); // add parameters
  4. mAP += APs[label];
  5. if (param_.show_per_class_result()) {
  6. LOG(INFO) << "class" << label << ": " << APs[label];
  7. if(param_.show_pr_value()) // add bool parameter
  8. {
  9. for( int i= 0;i<p_r.size();i++)
  10. {
  11. LOG(INFO) << "p-r value: " << p_r[i]; // print p_r value(11points)
  12. }
  13. }
  14. }

我们在solver.cpp中使用了show_pr_value这个bool变量来控制是否打印信息。因此需要在caffe.proto中增加一条,以便solver.prototxt可以解析该信息。


     
     
  1. ...
  2. // If true, display per class result.
  3. optional bool show_per_class_result = 44 [ default = false];
  4. // If true, display pr value of per class
  5. optional bool show_pr_value = 45 [ default = false]; # add a line

可能有同学觉得自己加语句有些麻烦,也可以到这里下载:修改SSD源码打印AP,然后替换原有的文件,接下来需要重新编译caffe-ssd,最后呢,我们就在solver.prototxt中添加show_pr_value: true 语句,同时注意ap_version: "11point"

运行命令执行测试过程,我们可以得到以下信息:


     
     
  1. I0127 21: 33: 55.290652 22867 solver .cpp: 332] Iteration 0, loss = 0.735246
  2. I0127 21: 33: 55.290686 22867 solver .cpp: 433] Iteration 0, Testing net ( #0)
  3. I0127 21: 33: 55.302778 22867 net .cpp: 693] Ignoring source layer mbox_loss
  4. I0127 21: 34: 06.567648 22867 solver .cpp: 540] class1: 0.722297
  5. I0127 21: 34: 06.567668 22867 solver .cpp: 545] p-r value: 0
  6. I0127 21: 34: 06.567674 22867 solver .cpp: 545] p-r value: 0
  7. I0127 21: 34: 06.567679 22867 solver .cpp: 545] p-r value: 0.5
  8. I0127 21: 34: 06.567693 22867 solver .cpp: 545] p-r value: 0.657895
  9. I0127 21: 34: 06.567698 22867 solver .cpp: 545] p-r value: 0.84
  10. I0127 21: 34: 06.567703 22867 solver .cpp: 545] p-r value: 0.947368
  11. I0127 21: 34: 06.567706 22867 solver .cpp: 545] p-r value: 1
  12. I0127 21: 34: 06.567720 22867 solver .cpp: 545] p-r value: 1
  13. I0127 21: 34: 06.567725 22867 solver .cpp: 545] p-r value: 1
  14. I0127 21: 34: 06.567729 22867 solver .cpp: 545] p-r value: 1
  15. I0127 21: 34: 06.567734 22867 solver .cpp: 545] p-r value: 1
  16. I0127 21: 34: 06.567929 22867 solver .cpp: 540] class2: 0.878479
  17. I0127 21: 34: 06.567936 22867 solver .cpp: 545] p-r value: 0
  18. I0127 21: 34: 06.567940 22867 solver .cpp: 545] p-r value: 0.793226
  19. I0127 21: 34: 06.567955 22867 solver .cpp: 545] p-r value: 0.945498
  20. I0127 21: 34: 06.567960 22867 solver .cpp: 545] p-r value: 0.969359
  21. I0127 21: 34: 06.567975 22867 solver .cpp: 545] p-r value: 0.984076
  22. I0127 21: 34: 06.567979 22867 solver .cpp: 545] p-r value: 0.986207
  23. I0127 21: 34: 06.567984 22867 solver .cpp: 545] p-r value: 0.99
  24. I0127 21: 34: 06.567989 22867 solver .cpp: 545] p-r value: 0.994898
  25. I0127 21: 34: 06.567994 22867 solver .cpp: 545] p-r value: 1
  26. I0127 21: 34: 06.567999 22867 solver .cpp: 545] p-r value: 1
  27. I0127 21: 34: 06.568004 22867 solver .cpp: 545] p-r value: 1
  28. I0127 21: 34: 06.568164 22867 solver .cpp: 540] class3: 0.723391
  29. I0127 21: 34: 06.568184 22867 solver .cpp: 545] p-r value: 0
  30. I0127 21: 34: 06.568190 22867 solver .cpp: 545] p-r value: 0
  31. I0127 21: 34: 06.568204 22867 solver .cpp: 545] p-r value: 0.353846
  32. I0127 21: 34: 06.568212 22867 solver .cpp: 545] p-r value: 0.786408
  33. I0127 21: 34: 06.568228 22867 solver .cpp: 545] p-r value: 0.8625
  34. I0127 21: 34: 06.568233 22867 solver .cpp: 545] p-r value: 0.954545
  35. I0127 21: 34: 06.568239 22867 solver .cpp: 545] p-r value: 1
  36. I0127 21: 34: 06.568244 22867 solver .cpp: 545] p-r value: 1
  37. I0127 21: 34: 06.568249 22867 solver .cpp: 545] p-r value: 1
  38. I0127 21: 34: 06.568255 22867 solver .cpp: 545] p-r value: 1
  39. I0127 21: 34: 06.568260 22867 solver .cpp: 545] p-r value: 1
  40. I0127 21: 34: 06.568267 22867 solver .cpp: 553] Test net output #0: detection_eval = 0.774722
  41. I0127 21: 34: 06.568280 22867 solver .cpp: 337] Optimization Done.
  42. I0127 21: 34: 06.568286 22867 caffe .cpp: 254] Optimization Done.

画P-R曲线

有了每一个类别的Precision和Recall信息,很容易画出P-R曲线,一个简单的python程序就可以了,这个程序比较简陋,以后有机会再把它优化下。


     
     
  1. # pr_curve.py
  2. # coding:utf-8
  3. import numpy as np
  4. import matplotlib.pyplot as plt
  5. data=np.loadtxt( 'pr.txt')
  6. mean=np.mean(data[:, 1:],axis= 1)
  7. tick=[ 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
  8. plt.figure()
  9. plt.subplot( 2, 2, 1)
  10. plt.title( 'Cyclist, AP=0.722')
  11. plt.xlabel( 'Recall')
  12. plt.ylabel( 'Precision')
  13. plt.axis([ 0, 1, 0, 1.05])
  14. plt.xticks(tick)
  15. plt.yticks(tick)
  16. plt.plot(data[:, 0],data[:, 1])
  17. plt.subplot( 2, 2, 2)
  18. plt.title( 'Car, AP=0.878')
  19. plt.xlabel( 'Recall')
  20. plt.ylabel( 'Precision')
  21. plt.axis([ 0, 1, 0, 1.05])
  22. plt.xticks(tick)
  23. plt.yticks(tick)
  24. plt.plot(data[:, 0],data[:, 2])
  25. plt.subplot( 2, 2, 3)
  26. plt.title( 'Person, AP=0.723')
  27. plt.xlabel( 'Recall')
  28. plt.ylabel( 'Precision')
  29. plt.axis([ 0, 1, 0, 1.05])
  30. plt.xticks(tick)
  31. plt.yticks(tick)
  32. plt.plot(data[:, 0],data[:, 3])
  33. plt.subplot( 2, 2, 4)
  34. plt.title( 'Overall, mAP=0.774')
  35. plt.xlabel( 'Recall')
  36. plt.ylabel( 'Precision')
  37. plt.axis([ 0, 1, 0, 1.05])
  38. plt.xticks(tick)
  39. plt.yticks(tick)
  40. plt.plot(data[:, 0],mean)
  41. plt.show()

画出的曲线如下所示:

这里写图片描述

KITTI官网中画AP图像用了41个点,而这个仅仅11个点,曲线看起来并不平滑,有时间再研究下怎么得到更精确的数据。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值