最近在处理检测问题的时候,需要对网络参数进行评估,之前对mAP的详细细节没有深入了解,这里记录一下
-----------------------
准确率(accuracy),精确率(precision),召回率(recall)
MeanAverage Precision(平均精确率),即mAP
Precision=TP/(TP+FP)
TP(检测为P, 实际为p ,检测正确,P->P) TruePostive
FP(检测为P,实际为N,检测错误,N->P) False Postive
即Precision 为检测认为是P的样本里,真实为P的概率,即检测结果哩的真实率
Recall =TP/(TP+FN)
TP(检测为P,检测正确,实际为正,P->P) True Postive
FN(检测为N,检测错误,说明实际为正,被检测为负,即漏掉了P->N) False Negive
即Recall 为检测认为是P的样本哩占全部真实为P的概率,即反映了查全率
假如原图中有10张人脸,检测到12张人脸,其中正确的人脸为8张,错误的为4张,则漏检的2张。
TP= 8, FP=4, Precision=66.7% 期望Precision越高越好,即错检测的尽可能少
TP=8, FN=2, Recall =80% 即检测的查全率为80% 希望越多越好。
Precision:根据上述方法,对于某个类别A,我们先计算每张图片中A类别TP和FP的数量,并遍历所有图像对其进行累加,即可得到类别A在整个数据集中TP和FP的数量,计算TP/(TP+FP)即可得到类别A的Precision (计算Precision的时候只需要用到TP和FP),但是会发现Precision的数值是受模型预测出的边界框的数量(上述计算式的分母部分,即我们认为的检测出的Postive的样本的数量)影响的,如果我们控制模型输出预测框的数量,就可以得到不同的Precision,所以我们可以设置不同的score阈值,最终得到不同数量的TP和FP。
Recall:对于某个类别A,按上述方法进行累加TP的数量,计算TP/(n_gt)即可得到Recall,其中n_gt表示类别A在所有图片中gt的数量之和。同理,如果控制模型输出的预测框的数量,就会改变TP的数量,也就会改变Recall的值
综上,想要得到PR曲线,可以通过改变score的阈值来控制模型输出的预测框数量,从而得到不同的TP、FP、FN。不过在实际操作中,并不需要手动来设置score的阈值,因为每个预测框都有一个score,我们只需要将其按从小到大进行排序,然后每次选择最后一个score作为阈值即可,这样如果类别A在所有图片中的预测框数量之和有100个,就可以计算100组类别A的Precision-Recall值。
假设现在数据集中一共有5张图片,
①第1张图片中有2个A类别的gt,有三个A类别的预测框,score分别为(0.3, 0.5, 0.9),按照上述计算TP的方法(按score从大到小的顺序匹配)发现score为0.3和0.9的与gt相匹配,则将这两个记为TP。建立用于计算PR曲线的数组metric和记录A类别gt总数的变量ngt,向数组中加入(0.3, 1), (0.5, 0), (0.9, 1)三组
数据(每组数据的第一个代表预测框的score,第二个代表这个预测框是否是TP),并将n_gt累加2;使用相同的方法计算接下来的几张图片,
(0.3,1)(0.5, 0)(0.9,1) gt=2
②第2张图片中没有A类别的物体(gt数量为0),则n_gt+=0,但有一个关于A类别的预测框,score为0.45,则向metric中加入(0.45, 0);
(0.3,1)(0.5, 0)(0.9,1)(0.45, 0) gt=2
③第3张图片中有1个A类别的物体,但没有预测框,则n_gt+=1;
(0.3,1)(0.5, 0)(0.9,1)(0.45, 0) gt=3
④第4张图片中有3个A类别的物体,有5个预测框,其中有3个与gt相匹配,最终n_gt+=3,metric中加入(0.85, 1), (0.8, 1), (0.7, 1), (0.35, 0), (0.1, 0);
(0.3,1)(0.5, 0)(0.9,1)(0.45, 0) (0.85, 1), (0.8, 1), (0.7, 1), (0.35, 0), (0.1, 0) gt=6
⑤第5张图片中没有A类别的物体,也没有A类别的预测框。
(0.3,1)(0.5, 0)(0.9,1)(0.45, 0) (0.85, 1), (0.8, 1), (0.7, 1), (0.35, 0), (0.1, 0) gt=6
对score进行从大到小排序,之所以使用由大向小排列是因为判断条件为>=score,是因为小的阈值向上兼容大的阈值
accTP accFP precition recall
(0.9, 1) 1 0 1/1 1/6
#score =0.9 正确分类1个(0.9, 1),错分0个.
precision=1/(1+0)=1, recall =1/6
(0.85,1) 2 0 2/2 2/6
#score =0.85 正确分类2个(0.85,1)(0.9,1),错分0个.
precision=2/(2+0)=1, recall =2/6
(0.8, 1) 3 0 3/3 3/6
#score =0.80 正确分类3个(0.85,1)(0.9,1),(0.8,1),错分0个.
precision=3/(3+0)=1, recall =3/6
(0.7, 1) 4 0 4/4 4/6
#score =0.70 正确分类4个(0.85,1)(0.9,1),(0.8,1)(0.7,1),错分0个.
precision=4/(4+0)=1, recall =4/6
(0.5, 0) 4 1 4/5 4/6
#score =0.50 正确分类4个(0.85,1)(0.9,1),(0.8,1)(0.7,1),错分1个
(0.5, 0). precision=4/(4+1)=4/5, recall =4/6
(0.45,0) 4 2 4/6 4/6
#score =0.45 正确分类4个(0.85,1)(0.9,1),(0.8,1)(0.7,1),错2个
(0.5,0),(0.45,0). precision=4/(4+2)=4/6, recall =4/6
(0.35,0) 4 3 4/7 4/6
#score =0.35 正确分类4个(0.85,1)(0.9,1),(0.8,1)(0.7,1),错3个
(0.5,0),(0.45,0),(0.35,0). precision=4/(4+3)=4/7, recall=4/6
(0.3, 1) 5 3 5/8 5/6
#score =0.3 正确分类4个(0.85,1)(0.9,1),(0.8,1)(0.7,1)(0.3,1),错3个
(0.5,0),(0.45,0),(0.35,0). precision=5/(5+3)=5/8, recall=5/6
(0.1 0) 5 4 5/9 5/6
#score =0.1 正确分类4个(0.85,1)(0.9,1),(0.8,1)(0.7,1)(0.3,1),错3个
(0.5,0),(0.45,0),(0.35,0),(0.1,0). precision=5/(5+4)=5/9, recall=5/6
在计算面积的时候,我们使用均匀分片的方法,用样条面积相加生成最终的面积,因此,合适的样条,以Recall固定间隔进行采样,以上面数据为例子则分别为
[0-1/6],[1/6-2/6],[2/6-3/6],[3/6-4/6],[4/6-5/6],[5/6-6/6], 由于是离散的点,则分别取recall=1/6, 2/6, 3/6, 4/6, 5/6, 6/6的对应的对应得precision,由于计算得是面积,
因此取每个Recall值对应的最大Precision进行计算即可

因为A类别一共有6个gt,所以Recall的值应该是从1/6~6/6共6个,也就是要取6组PR值计算平均Precision,因为这个例子中没有出现Recall=6/6的情况,
所以R=6/6时的Precision算作0,即类别A的AP=(1/1 + 2/2 + 3/3 + 4/4+ 5/8 + 0) / 6 = 0.7708
def ap_per_class(tp, conf, pred_cls, target_cls):
""" Compute the average precision, given the recall and precision curves.
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
# Arguments
tp: True positives (list).
conf: Objectness value from 0-1 (list).
pred_cls: Predicted object classes (list).
target_cls: True object classes (list).
# Returns
The average precision as computed in py-faster-rcnn.
"""
# Sort by objectness
i = np.argsort(-conf)#np.argsort为从小到大排序,使用-conf,即等于对conf从大到小排序
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]#基于conf从大到小的排序结果对 tp, conf,pred_cls进行调整顺序
# Find unique classes
unique_classes = np.unique(target_cls) #对类别进行去重,并按从小到大排序,因为计算AP是针对每类进行
# Create Precision-Recall curve and compute AP for each class
ap, p, r = [], [], []
for c in tqdm.tqdm(unique_classes, desc="Computing AP"):
i = pred_cls == c #对于类别c,找到预测类别为c的所有的索引,索引记为i(i的0,1组成的序列)
n_gt = (target_cls == c).sum() # Number of ground truth objects 真实的类别为c的数目,即c类的gt数目
n_p = i.sum() # Number of predicted objects 即预测为c类的总的数目 n_p 即为fp和np的和
if n_p == 0 and n_gt == 0: #本身没有c类,也没有被预测为c类的样本,则跳过该类别
continue
elif n_p == 0 or n_gt == 0:
ap.append(0)
r.append(0) #r为recall的容器
p.append(0) #p为precision的容器
#本身没有c类,或者没有被预测为c类,两者必有之一,
#如果没有c类,则p=0, r=0,ap=0
#如果没有被预测到c类,则tp一定为0,则p=0,r=0,ap=0
else:
# Accumulate FPs and TPs
fpc = (1 - tp[i]).cumsum()
#i的值为(1,0),列表记录着索引对应位置是否是c类别框,i为1 则表示该点被预测为c,tp里面保存的为是否true-postive, tp[i]=0,则为false-postive, fpc则为计算累加的fp的总数目
tpc = (tp[i]).cumsum()
#i的值为(1,0),列表记录着索引对应位置是否是c类别框,i为1 则表示该点被预测为c,tp里面保存的为是否true-postive, tp[i]=1,则为true-postive, tpc则为计算累加的tp的总数目
# Recall
recall_curve = tpc / (n_gt + 1e-16)#计算recall的值
r.append(recall_curve[-1])#recall值放入到r中,r为recall的容器
# Precision
precision_curve = tpc / (tpc + fpc)#计算precision值
p.append(precision_curve[-1])#precision值放入到p中,p为precision的容器
# AP from recall-precision curve
ap.append(compute_ap(recall_curve, precision_curve))
# Compute F1 score (harmonic mean of precision and recall)
p, r, ap = np.array(p), np.array(r), np.array(ap)
f1 = 2 * p * r / (p + r + 1e-16)
return p, r, ap, f1, unique_classes.astype("int32")
def compute_ap(recall, precision):
""" Compute the average precision, given the recall and precision curves.
Code originally from https://github.com/rbgirshick/py-faster-rcnn.
# Arguments
recall: The recall curve (list).
precision: The precision curve (list).
# Returns
The average precision as computed in py-faster-rcnn.
"""
# correct AP calculation
# first append sentinel values at the end
mrec = np.concatenate(([0.0], recall, [1.0]))
mpre = np.concatenate(([0.0], precision, [0.0]))
# compute the precision envelope
for i in range(mpre.size - 1, 0, -1):
mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
# to calculate area under PR curve, look for points
# where X axis (recall) changes value
i = np.where(mrec[1:] != mrec[:-1])[0]
# and sum (\Delta recall) * prec
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
return ap