指标:mAP


mAP 是目标检测、重识别等领域的重要指标。计算 mAP 的关键是计算 AP,而计算 AP 关键的是 TP, FP, FN 的定义,如果我们能够将一个输出序列标注为 TP, FN,并且得到它们的置信度,那么剩余的其实直接套公式计算即可。

Recall/Precision 的计算


1. TP/FP/TN/FN的定义

首先我们要知道 T/F, P/N 的定义:

  • Positive or Negative: 表示分类给出的结果
  • True or False: 表示分类结果是否正确

那么组合以下:

  • TP: True Positive;
  • FP: False Positive;
  • TN: True Negative;
  • FN: False Negative; 表示分为负样本,但是分错的样本 => 真实值为正样本

所以易得 Precision 和 Recall 的公式:

Precision=TPTP+FPRecall=TPTP+FN{ \begin{aligned} Precision &= \frac{TP}{TP + FP} \\ Recall &= \frac{TP}{TP + FN} \\ \end{aligned} }

通俗地说:

  • Recall: 真实的样本被分对了多少
  • Precision: 分类的结果中有多少是对的

多分类情况下,对各类,recall 选出真实值为第 i 类的样本,输出 accuracy,precision 选出预测结果为第 i 类的样本,输出 accuracy。

计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np

num_classes = 3
num_samples = 100
samples = np.random.randint(0, num_classes, (num_samples, )) # ground truth
results = np.random.randint(0, num_classes, (num_samples, ))

# precision
for i in range(num_classes):
pos = np.where(results == i)
sample = samples[pos]
acc = ((sample == i).sum() / sample.size)
print(acc)

# recall
for i in range(num_classes):
pos = np.where(samples == i)
result = results[pos]
recall = ((result == i).sum() / result.size)
print(recall)

# total accuracy
accuracy = (samples == results).sum() / results.size

# 验证
from sklearn.metrics import recall_score, precision_score \
accuracy_score
print(recall_score(samples, results, average=None))
print(precision_score(samples, results, average=None))
print(accuracy_score(samples, results))

AP/mAP


  • AP: 仅在二分类下使用,表示 Precision-Recall(P-R)曲线下的面积
  • mAP: 多分类下使用,每类的 AP 的平均

二分类情况下AP:

理论计算

给定 ground truth: {0,1}i=1N{\{0, 1\}_{i=1}^{N}},模型预测的概率为 [0,1]i=1N{[0, 1]_{i=1}^{N}},调整 threshold 可以得到不同的 precision/recall,构成 P-R 曲线,计算曲线下的面积即为 AP。

理解:precision, recall 仅针对正类,根据 precision 和 recall 的公式:

Precision=TPTP+FPRecall=TPTP+FN{ \begin{aligned} Precision &= \frac{TP}{TP + FP} \\ Recall &= \frac{TP}{TP + FN} \\ \end{aligned} }

  • recall 的分母和预测的结果无关,是定值,所以当 threshold 降低时,TP 的数量是非严格单调递增的,所以 recall 也是非严格单调递增的。特别的,当threshold=1时,recall=0,当 threshold=0时,recall=1
  • precision 的分母和分子都和预测结果有关,所以值是波动的,特别的,当 TP+FP=0 时,规定 precision = 1

实际计算


计算

可以用 scikit 的 sklearn.metrics.average_precision_score 计算 AP,输入为 y_true, y_pred

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np
from sklearn.metrics import average_precision_score, precision_recall_curve

samples = np.random.rand(100, )
samples[np.where(samples > 0.5)] = 1
samples[np.where(samples <= 0.5)] = 0
results = np.random.rand(100, )

# AP
ap = average_precision_score(samples, results)
precisions, recalls, thresholds = precision_recall_curve(samples, results)

import matplotlib.pyplot as plt
plt.plot(recalls, precisions) # recall 为 x 轴
plt.xticks(np.linspace(0, 1, 11))
plt.yticks(np.linspace(0, 1, 11))
plt.xlabel("recalls")
plt.ylabel("precisions")
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.tight_layout()
plt.title("P-R curve")
plt.show()

深入实际计算

  1. 如果我们有 (precision, recall) 对,计算 AP:

实际上 average_precision_score 不进行插值计算面积,而是采用以下的公式:

AP=n(RnRn1)Pn{ AP = \sum_{n}(R_n - R_{n-1}) P_n }

其中 Rn,Pn{R_n, P_n} 是不同 thresholds 下得到的 P-R 对(并且按 recall 的升序排列),相当于计算多个矩形的面积,而且矩形的高是右边的数值(如果用左边的话会感觉虚高)。 而且从公式中可以看成是 precisions 的加权平均。

1
2
3
4
5
6
7
8
# 假设已有 precisions 和 recalls
# way1
tres = 0.0
for i in range(len(precisions)-1):
tres += (recalls[i+1] - recall[i]) * precisions[i+1]
# way2: 更简洁
ttres = np.sum((recalls[1:] - recalls[:-1]) * precisions[1:])
assert (tres - ttres).sum() == 0
  1. 更加快速的计算方式:

如何得到 precision 和 recall 对? 一种广泛使用的方法是将阈值直接取为 y_pred,而且 y_pred 从大到小排序,每次得到一对 precision 和 recall,这种方法可以很更快地计算 TP, FP 等值。

如下表:是一个向下累加的过程:

y_pred y_true threshold TP FP TP + FP FN + TP recall precision
0.98255475 0 0.98255475 0 1 1 2 0 0
0.96782305 0 0.96782305 0 2 2 2 0 0
0.95248108 1 0.95248108 1 2 3 2 0.33 0.5
0.94590941 1 0.94590941 2 2 4 2 0.5 1
0.90657107 0 0.90657107 2 3 5 2 0.4 1

随着阈值从大到小被取为不同的 y_pred:

  • TP 其实就是 y_true 的累加值
  • TP + FP 其实就是当前阈值下所有预测结果为 1 的样本数量,也就是表格中阈值以上的所有样本的数量,就是 1,2,3, …
  • TP + FN 就是 y_true 的和,表示所有真实值为 1 的样本个数,是不变的
  • 最后我们还需要添加 threshold=1 的情况(并不在上面表格中),此时 precsion, recall 为 (1, 0)。

完整代码即为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import numpy as np
from sklearn.metrics import average_precision_score

# 样本
samples = np.random.rand(100, )
samples[np.where(samples > 0.5)] = 1
samples[np.where(samples <= 0.5)] = 0
results = np.random.rand(100, )

# AP
ap = average_precision_score(samples, results)

# handcraft AP
indices = results.argsort()[::-1]
y_true = samples[indices]
y_pred = results[indices]

# precisions and recalls pair
precisions = np.ones((len(y_true) + 1, ))
recalls = np.zeros_like(precisions)
tp = np.cumsum(y_true)
tp_plus_fp = np.cumsum(np.ones((len(y_true), )))
precisions[1:] = tp / tp_plus_fp
recalls[1:] = tp / y_true.sum()

# AP
tap = ((recalls[1:] - recalls[:-1]) * precisions[1:]).sum()
assert (tap - ap).sum() == 0 # 和之前的结果一致

在不同任务下的 AP 计算可以采用相似的办法,不同点在于 TP, FP 怎么取。

mAP


mAP (mean Average Precision),顾名思义,就是 AP 的平均。在多分类任务下,我们对每个类别计算一个 AP 值,然后取平均,得到 mAP。

假设我们得到的输出为矩阵 YRN×C{Y \in \mathbb{R}^{N \times C}},N 表示样本个数,C 表示类别个数,yi,j{y_{i,j}} 表示为第 i{i} 个样本被分为第 j{j} 类的置信度。

  • 首先根据置信度,我们根据每个样本的置信度将它分类为置信度最高的那一类,并得到相应的置信度。
  • 对单个类别计算 AP,我们仅针对所有被模型分为该类的样本,y_pred 就是每个样本的置信度,而 y_true 就是该样本是否被正确分类。带入 average_precision_score 即可得到。

见以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
from sklearn.metrics import average_precision_score

num_classes = 10
num_samples = 1000

# 样本
samples = np.random.randint(0, num_classes, (num_samples, ))
results = np.random.rand(num_samples, num_classes)
results = results / results.sum(axis=1).reshape(num_samples, 1)
y_predict_clss = np.argmax(results, axis=1)
y_predict = np.max(results, axis=1)

APs = []
for i in range(num_classes): # AP for each class
indices = np.where(y_predict_clss == i)
y_pred = y_predict[indices]
y_true = (samples[indices] == i)
ap = average_precision_score(y_true, y_pred)
APs.append(ap)
mAP = np.asarray(APs).mean()

其他任务下的 AP/mAP


Detection下的AP/mAP

参考 mmdetection 的evaluation

mAP 计算输入:

  • 模型输出 boxes + pred_logits(N 类),选取 confidence 最高的那类,得到 boxes + confidence + pred_classes
  • ground truth: 每个 gt 框的 gt_boxes + gt_classes

计算过程:

  1. 首先对单个 class i 计算 AP:

    1. 筛选出每个 image 中 pred_classes 为 i 的数据: cls_dets
    2. 筛选出每个 image 中 gt_classes 为 i 的数据: cls_gts
  2. 对每张 image 进行 TP/FP 的分配:

    1. 如果 gt 框的数量为 0,那么所有的 det 都分配为 FP(当然 det 的数量也可能为 0,那就什么都没有)。
    2. 计算所有 gt_boxes 和 det_boxes 的 iou,得到 iou 的矩阵(MxN, M 为 det_boxes 的数量,N 对应 gt_boxes)
    3. 对每个 det_boxes,选取和它最重叠的 gt_boxes ,得到两两的 pairs,进入下面的分配。
    4. 按照 confidence 从大到小排序 det_boxes
    5. 对每个 det_boxes,如果:
      • 和 gt_boxes 的 iou 大于 iou_thresh: 如果 gt_boxes 已经在之前被匹配,那么标记为 fp,否则标记为 tp。
  3. 得到所有的 tp 和 fp,以及每个 boxes 的 confidence,各 concat 成一个向量。按照 confidence 从大到小的顺序排序 tp 和 fp,然后按照累加和的计算公式计算 tp 和 fp。

1
2
precisions = tp / (tp + fp)
recalls = tp / num_gt
  1. 计算 ap,然后计算 mAP。

除此之外,可能需要考虑的其他问题:

  • 规定 iou_thresh 如果小于某个值 min_area,对它忽略,因为可能检测到未标记的 gt 样本。
  • 计算 recalls 和 precisions 需要注意分母需要 eps。
  • 等等

根据以上思路的简单样例代码

ReID下的mAP


ReID 下的 mAP 比较好理解,计算也更简单:就是对每个 probe,计算 AP,最后再对所有 probe 取平均。

单个 probe 的 AP 计算:首先根据 ground truth,可以得到每个 gallery 的 gt_label 作为y_true,其次 gallery 和 probe 的 feature 计算相似度,得到预测概率作为 y_pred,就可以用于计算 AP。

Person Search 下的 mAP


Person Search 下的 mAP 其实和 ReID 的指标非常类似,因为 mAP 在 Person Search 中也是用于衡量搜索的好坏。唯一不同是TP 的定义上还要考虑定位准不准。简单来说,在 Person Search 中,对一个 probe target,在一张 gallery 图像中,首先按照相似度将该图像内的所有 candidate persons 排序,然后按照相似度从大到小的第一个和 gt boxes IoU 大于给定阈值(0.5)的 person 作为 TP,取它的相似度作为置信度分数,其余的都作为 FP。

mAP 涨点


从 AP/mAP 的定义就可以看出,实际上 AP/mAP 不关心最终的置信度是多少,而关心样本置信度的相对大小( recall threshold 从大取到小),我把这个样本的排列称为 rank_listAP 涨点的关键就是要使得 TP 尽可能地排在 rank_list 的前面,当所有的 TP 都排在最前面时,AP 就可以达到最大值 1 。