最近刚看完Faster R-CNN的源码,对于mAP这个目标检测的衡量指标之前也大致的了解一下,但是它与准确度(Accuray)、精度(Precision)、召回率(recall)等的关系是怎么样的?这些都还没有了解,所以刚好顺着Faster R-CNN的源码,把这个问题搞清楚一些。

一、目标检测问题

每个模型在“验证/测试”数据集上来评估性能,性能衡量使用各种统计量如准确度(accuracy),精度(precision),召回率(recall)等。选择的统计量通常针对特定应用场景和用例。 对于每个应用场景,选择一个能够客观比较模型的度量指标非常重要。

大多数时候,这些指标很容易理解和计算。例如,在二元分类中,精确度和召回率是一个一个简单直观的统计量。然而,目标检测是一个非常不同且有趣的问题。即使你的目标检测器在图片中检测到猫,但如果你无法定位,它也没有用处。由于你要预测的是图像中各个物体是否出现及其位置,如何计算mAP将非常有趣。

在目标检测问题中,给定一个图像,找到它所包含的物体,找到他们的位置并对它们进行分类。目标检测模型通常是在一组特定的类集合上进行训练的,所以模型只会定位和分类图像中的那些类。另外,对象的位置通常采用矩形边界框表示。因此,目标检测涉及图像中物体的定位和分类。
image.png

二、评估目标检测模型

1.为什么是mAP?

目标检测问题中的每个图片都可能包含一些不同类别的物体。如前所述,需要评估模型的物体分类和定位性能。因此,用于图像分类问题的标准指标precision不能直接应用于此。所以我们需要一种新的评估方式,即mAP(Mean Average Precision),来衡量模型的性能。

2.Ground Truth的定义


对于任何算法,评估指标需要知道ground truth(真实标签)数据。 我们只知道训练、验证和测试数据集的ground truth。对于目标检测问题,ground truth包括图像中物体的类别以及该图像中每个物体的真实边界框。
image.png

训练时,模型需要的输入是原始的图像,以及Ground Truth的坐标值和类别。

image.png

在计算mAP之前我们再来复习以下比较重要的概念。

3.IOU的概念

IOU的概念应该比较简单,就是衡量监测框和标签框的重合程度。

image.png

2.TP TN FP FN的概念

TP(True Positives)意思是“被分为正样本,并且分对了”,
TN(True Negatives)意思是“被分为负样本,而且分对了”,
FP(False Positives)意思是“被分为正样本,但是分错了”,
FN(False Negatives)意思是“被分为负样本,但是分错了”。

按下图来解释,左半矩形是正样本,右半矩形是负样本。一个2分类器,在图上画了个圆,分类器认为圆内是正样本,圆外是负样本。那么左半圆分类器认为是正样本,同时它确实是正样本,那么就是“被分为正样本,并且分对了”即TP,左半矩形扣除左半圆的部分就是分类器认为它是负样本,但是它本身却是正样本,就是“被分为负样本,但是分错了”即FN。右半圆分类器认为它是正样本,但是本身却是负样本,那么就是“被分为正样本,但是分错了”即FP。右半矩形扣除右半圆的部分就是分类器认为它是负样本,同时它本身确实是负样本,那么就是“被分为负样本,而且分对了”即TN

image.png

3.Precision(精度)和Recall(召回率)的概念

有了上面TP TN FP FN的概念,这个Precision和Recall的概念一张图就能说明。
image.png

image.png 翻译成中文就是“分类器认为是正类并且确实是正类的部分占所有分类器认为是正类的比例”,衡量的是一个分类器分出来的正类的确是正类的概率。两种极端情况就是,如果精度是100%,就代表所有分类器分出来的正类确实都是正类。如果精度是0%,就代表分类器分出来的正类没一个是正类。光是精度还不能衡量分类器的好坏程度,比如50个正样本和50个负样本,我的分类器把49个正样本和50个负样本都分为负样本,剩下一个正样本分为正样本,这样我的精度也是100%,但是傻子也知道这个分类器很垃圾。

image.png,翻译成中文就是“分类器认为是正类并且确实是正类的部分占所有确实是正类的比例**”,衡量的是一个分类能把所有的正类都找出来的能力。两种极端情况,如果召回率是100%,就代表所有的正类都被分类器分为正类。如果召回率是0%,就代表没一个正类被分为正类。

4.举例计算mAP

在对多标签图像分类时,首先用训练好的模型得到所有测试样本的confidence score,每一类(如car)的confidence score都保存到一个文件中(如test_car.txt)。假设该文件包含20个测试样本(即对应图1中的矩形),每个id, confidence score, ground truth label如下:

接下来按照confidence score从大到小排序:
image.png

上面虽然有分数,但是我们没有判定每个id是属于哪个类别。这里有两种方法。

  • 设定阈值,比如score>=0.50
  • 或者每一类的前五必为该类。

这里我们选用前五这种方法来计算:

前五如下:
image.png

意思就是说,前五被认为应该全是Positive,但是只有gt_label=1时才是正真的Positive。
true positives就是指第4和第2张图片共2张照片,false positives就是指第13,19,6张图片共3张照片。

前五之后如下:
image.png

就是说,按照这个标准来讲,前五之后的所有都被认为是Negative,那么其中,false negatives是指第9,16,7,20张图片共4张照片,true negatives是指第1,18,5,15,10,17,12,14,8,11,3张图片共11张照片。

1).计算Precision和Recall的值:

image.png,则Precision=2/5=40%,意思是对于car这一类别,我们选定了5个样本,其中正确的有2个,即准确率为40%。

image.png,则Recall=2/(2+4)=30%,意思是在所有测试样本中,共有6个car,但是因为我们只召回了2个,所以召回率为30%。

2).做PR图

实际多类别分类任务中,通常不满足只通过top-5来衡量一个模型的好坏,而是需要知道从top-1到top-N(N是所有测试样本个数,本文中为20)对应的precision和recall。显然随着我们选定的样本越来也多,recall一定会越来越高,而precision整体上会呈下降趋势。把recall当成横坐标,precision当成纵坐标,即可得到常用的precision-recall曲线。例子中precision-recall曲线如下:
image.png

图中一共有20个点,也就是使用每个点作为界限点。

3).VOC计算方法

PASCAL VOC CHALLENGE的计算方法:

  1. 07年的方法:首先设定一组阈值,[0, 0.1, 0.2, …, 1]。然后对于recall大于每一个阈值(比如recall>0.3),都会得到一个对应的最大precision。这样,就计算出了11个precision。AP即为这11个precision的平均值。这种方法英文叫做11-point interpolated average precision。计算曲线的下面积 则为AP。


  1. 10年之后的方法:新的计算方法假设这N个样本中有M个正例,那么会得到M个recall值(1/M, 2/M, …, M/M),对于每个recall值r,可以计算出对应(r’ > r)的最大precision,然后对这M个precision值取平均即得到最后的AP值。计算方法如下:

对于上面的例子中。一共有20个测试,但是只有6个正的测试样本(以下是第二种计算mAP的方法)。

image.png

上表中的最后一栏就是car这类的AP。而mAP就是10个种类的AP求平均值。
从表中gt_label可以看出正例是6个,其他是负例。Recall分别为1/6,2/6,3/6,4/6,5/6,6/6。对于每个recall值,都对应着很多种top取法,所以每个recall值对应的诸多取法中(包括等于此recall的取法)有一个最大的precision,把每种recall对应最大的precision求和取平均即AP。

比如2/6的recall,查找上表,能得到recall2/6值的种类:从第2个开始到第5个,而到上表第6个,因为对应的是正例,所以就不是recall为2/6的范围了(因为前面已经有2个正例,如果再加一个正例,recall值就是3/6了),这几个取法对应最大的precision是2/2。同理,recall 4/6的取法就是第四个正例开始(4/7)到第5个正例前(4/10)之间的范围,对应最大的pricision是4/7。

相应的Precision-Recall曲线(这条曲线是单调递减的)如下:

image.png

AP衡量的是学出来的模型在每个类别上的好坏,mAP衡量的是学出的模型在所有类别上的好坏,得到AP后mAP的计算就变得很简单了,就是取所有AP的平均值。

5.目标检测计算mAP

检测出来的bbox包含score和bbox,按照score降序排序,所以每添加一个样本,就代表阈值降低一点(真实情况下score降低,iou不一定降低)。这样就是可以有很多种阈值,每个阈值情况下计算一个prec和recall。

具体过程如下:

  1. 使用区域选择算法得到候选区域
  2. 对候选区域,计算每一个候选区域和标定框(groud truth)的iou
  3. 设定一个iou阈值,大于这个的标为正样本,小于的标为负样本,由此得到一个类似于分类时的测试集。
  4. 将给定的测试集(正负样本),通过分类器,算出每一个图片是正样本的score
  5. 设定一个score阈值,大于等于此值的视作正样本,小于的作为负样本
  6. 根据上一步的结果可以算出准确率和召回率
  7. 调节score阈值,算出召回率从0到1时的准确率,得到一条曲线计算曲线的下面积 则为AP(这是07年方法,10年的方法参考上面),这条曲线就是对每个类的单独计算出来的。通过计算所有类的AP就可以计算mAP了。

三、python版本的VOC计算方式

下面摘自前段时间阅读Faster R-CNN源码中,计算mAP的部分代码。

  1. from __future__ import absolute_import
  2. from __future__ import division
  3. from __future__ import print_function
  4. import xml.etree.ElementTree as ET
  5. import os
  6. import pickle
  7. import numpy as np
  8. def parse_rec(filename):
  9. """
  10. Parse a PASCAL VOC xml file
  11. 读取XML文件中的各种信息
  12. """
  13. tree = ET.parse(filename)
  14. objects = []
  15. for obj in tree.findall('object'):
  16. obj_struct = {}
  17. obj_struct['name'] = obj.find('name').text
  18. obj_struct['pose'] = obj.find('pose').text
  19. obj_struct['truncated'] = int(obj.find('truncated').text)
  20. obj_struct['difficult'] = int(obj.find('difficult').text)
  21. bbox = obj.find('bndbox')
  22. obj_struct['bbox'] = [int(bbox.find('xmin').text),
  23. int(bbox.find('ymin').text),
  24. int(bbox.find('xmax').text),
  25. int(bbox.find('ymax').text)]
  26. objects.append(obj_struct)
  27. return objects
  28. def voc_ap(rec, prec, use_07_metric=False):
  29. """ ap = voc_ap(rec, prec, [use_07_metric])
  30. Compute VOC AP given precision and recall.
  31. If use_07_metric is true, uses the
  32. VOC 07 11 point method (default:False).
  33. """
  34. if use_07_metric:
  35. # 11 point metric
  36. # 2010年以前按recall等间隔取11个不同点处的精度值做平均(0., 0.1, 0.2, …, 0.9, 1.0)
  37. ap = 0.
  38. for t in np.arange(0., 1.1, 0.1):
  39. if np.sum(rec >= t) == 0:
  40. p = 0
  41. else:
  42. # 取最大值等价于2010以后先计算包络线的操作,保证precise非减
  43. p = np.max(prec[rec >= t])
  44. ap = ap + p / 11.
  45. else:
  46. # 2010年以后取所有不同的recall对应的点处的精度值做平均
  47. # correct AP calculation
  48. # first append sentinel values at the end
  49. mrec = np.concatenate(([0.], rec, [1.]))
  50. mpre = np.concatenate(([0.], prec, [0.]))
  51. # compute the precision envelope
  52. # 计算包络线,从后往前取最大保证precise非减
  53. for i in range(mpre.size - 1, 0, -1):
  54. mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
  55. # to calculate area under PR curve, look for points
  56. # where X axis (recall) changes value
  57. # 找出所有检测结果中recall不同的点
  58. i = np.where(mrec[1:] != mrec[:-1])[0]
  59. # and sum (\Delta recall) * prec
  60. # 用recall的间隔对精度作加权平均
  61. ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
  62. return ap
  63. '''
  64. 接下来才是最主要的函数
  65. '''
  66. def voc_eval(detpath,
  67. annopath,
  68. imagesetfile,
  69. classname,
  70. cachedir,
  71. ovthresh=0.5,
  72. use_07_metric=False):
  73. """
  74. rec, prec, ap = voc_eval(detpath,
  75. annopath,
  76. imagesetfile,
  77. classname,
  78. [ovthresh],
  79. [use_07_metric])
  80. Top level function that does the PASCAL VOC evaluation.
  81. detpath: Path to detections
  82. detpath.format(classname) should produce the detection results file.
  83. annopath: Path to annotations
  84. annopath.format(imagename) should be the xml annotations file.
  85. imagesetfile: Text file containing the list of images, one image per line.
  86. classname: Category name (duh)
  87. cachedir: Directory for caching the annotations
  88. [ovthresh]: Overlap threshold (default = 0.5)
  89. [use_07_metric]: Whether to use VOC07's 11 point AP computation
  90. (default False)
  91. """
  92. # assumes detections are in detpath.format(classname)
  93. # assumes annotations are in annopath.format(imagename)
  94. # assumes imagesetfile is a text file with each line an image name
  95. # cachedir caches the annotations in a pickle file
  96. # first load gt
  97. if not os.path.isdir(cachedir):
  98. os.mkdir(cachedir)
  99. cachefile = os.path.join(cachedir, '%s_annots.pkl' % imagesetfile)
  100. # read list of images
  101. with open(imagesetfile, 'r') as f:
  102. lines = f.readlines()
  103. imagenames = [x.strip() for x in lines]
  104. if not os.path.isfile(cachefile):
  105. # load annotations
  106. recs = {}
  107. for i, imagename in enumerate(imagenames):
  108. recs[imagename] = parse_rec(annopath.format(imagename))
  109. if i % 100 == 0:
  110. print('Reading annotation for {:d}/{:d}'.format(
  111. i + 1, len(imagenames)))
  112. # save
  113. print('Saving cached annotations to {:s}'.format(cachefile))
  114. with open(cachefile, 'wb') as f:
  115. pickle.dump(recs, f)
  116. else:
  117. # load
  118. with open(cachefile, 'rb') as f:
  119. try:
  120. recs = pickle.load(f)
  121. except:
  122. recs = pickle.load(f, encoding='bytes')
  123.  
  124. # extract gt objects for this class
  125. # 提取所有测试图片中当前类别所对应的所有ground_truth
  126. class_recs = {}
  127. npos = 0
  128. # 遍历所有测试图片
  129. for imagename in imagenames:
  130. # 找出所有当前类别对应的object
  131. R = [obj for obj in recs[imagename] if obj['name'] == classname]
  132. # 该图片中该类别对应的所有bbox
  133. bbox = np.array([x['bbox'] for x in R])
  134. difficult = np.array([x['difficult'] for x in R]).astype(np.bool)
  135. # 该图片中该类别对应的所有bbox的是否已被匹配的标志位
  136. det = [False] * len(R)
  137. # 累计所有图片中的该类别目标的总数,不算diffcult
  138. npos = npos + sum(~difficult)
  139. class_recs[imagename] = {'bbox': bbox,
  140. 'difficult': difficult,
  141. 'det': det}
  142. # read dets
  143. # 读取相应类别的检测结果文件,每一行对应一个检测目标
  144. detfile = detpath.format(classname)
  145. with open(detfile, 'r') as f:
  146. lines = f.readlines()
  147. splitlines = [x.strip().split(' ') for x in lines]
  148. image_ids = [x[0] for x in splitlines]
  149. # 读取图像的ID
  150. confidence = np.array([float(x[1]) for x in splitlines])
  151. # 读取测试图像的置信度
  152. BB = np.array([[float(z) for z in x[2:]] for x in splitlines])
  153. # 读取特使图像的Predict BBox
  154. nd = len(image_ids) #该类别检测结果的总数(所有检测出的bbox的数目)
  155. # 用于标记每个检测结果是TP(True positive)还是FP
  156. tp = np.zeros(nd)
  157. fp = np.zeros(nd)
  158. if BB.shape[0] > 0:
  159. # sort by confidence
  160. # 将该类别的检测结果按照置信度大小降序排列
  161. sorted_ind = np.argsort(-confidence)
  162. sorted_scores = np.sort(-confidence)
  163. BB = BB[sorted_ind, :]
  164. image_ids = [image_ids[x] for x in sorted_ind]
  165. # go down dets and mark TPs and FPs
  166. for d in range(nd):
  167. # 取出该条检测结果所属图片中的所有ground truth
  168. R = class_recs[image_ids[d]]
  169. bb = BB[d, :].astype(float)
  170. ovmax = -np.inf
  171. BBGT = R['bbox'].astype(float)
  172. if BBGT.size > 0:
  173. # 计算与该图片中所有ground truth的最大重叠度
  174. # compute overlaps
  175. # intersection
  176. ixmin = np.maximum(BBGT[:, 0], bb[0])
  177. iymin = np.maximum(BBGT[:, 1], bb[1])
  178. ixmax = np.minimum(BBGT[:, 2], bb[2])
  179. iymax = np.minimum(BBGT[:, 3], bb[3])
  180. iw = np.maximum(ixmax - ixmin + 1., 0.)
  181. ih = np.maximum(iymax - iymin + 1., 0.)
  182. inters = iw * ih
  183. # union
  184. uni = ((bb[2] - bb[0] + 1.) * (bb[3] - bb[1] + 1.) +
  185. (BBGT[:, 2] - BBGT[:, 0] + 1.) *
  186. (BBGT[:, 3] - BBGT[:, 1] + 1.) - inters)
  187. overlaps = inters / uni
  188. ovmax = np.max(overlaps)
  189. jmax = np.argmax(overlaps)
  190. # 如果最大的重叠度大于一定的阈值
  191. if ovmax > ovthresh:
  192. # 如果最大重叠度对应的ground truth为difficult就忽略
  193. if not R['difficult'][jmax]:
  194. # 如果对应的最大重叠度的ground truth以前没被匹配过则匹配成功,即TP
  195. if not R['det'][jmax]:
  196. tp[d] = 1.
  197. R['det'][jmax] = 1
  198. # 若之前有置信度更高的检测结果匹配过这个ground truth,则此次检测结果为FP
  199. else:
  200. fp[d] = 1.
  201. # 该图片中没有对应类别的目标ground truth或者与所有ground truth重叠度都小于阈值
  202. else:
  203. fp[d] = 1.
  204. # 按置信度取不同数量检测结果时的累计fp和tp
  205. # compute precision recall
  206. # np.cumsum([1, 2, 3, 4]) -> [1, 3, 6, 10]
  207. fp = np.cumsum(fp)
  208. tp = np.cumsum(tp)
  209. # 召回率为占所有真实目标数量的比例,非减的,注意npos本身就排除了difficult,因此npos=tp+fn
  210. rec = tp / float(npos)
  211. # 精度为取的所有检测结果中tp的比例
  212. # avoid divide by zero in case the first detection matches a difficult
  213. # ground truth
  214. prec = tp / np.maximum(tp + fp, np.finfo(np.float64).eps)
  215. ap = voc_ap(rec, prec, use_07_metric)
  216. return rec, prec, ap

参考:
https://www.jianshu.com/p/d7a06a720a2b
https://zhuanlan.zhihu.com/p/37910324
https://www.zhihu.com/question/53405779/answer/419532990
https://blog.csdn.net/hsqyc/article/details/81702437