最近有在分析map的代码,整体逻辑是首先对所有测试集进行推理,保留所有预测框的结果。然后对所有真实框进行统计。在对各个类别预测框和真实框统计预测正确的个数tp、预测错误的个数fp,计算准确率和召回率,再计算ap。
AP为 PR 曲线之下的面积,计算公式为:
也就是统计所有召回率对应的精确率的积分。
召回率 (Recall),是正确识别出的物体占总物体数的比率。
准确度 (Percision) 也叫查准率,是在识别出的物体中,正确的正向预测 (True Positive,TP) 所占的比率。
也就是意味着,为了更好的统计ap,需要将召回率和准确率的统计尽可能稀疏,以达到更精确的目的。
常规的思路是:
- 对每个图片进行推理,获得预测框
- 将预测框和真实框进行IOU匹配,大于阈值的判定为tp,小于阈值的判定为fp
- 计算各个图像的召回率和准确率
- 将所有召回率从小到大排列,获取其准确率,计算ap
但在实际应用中,为了更好地评估模型的整体性能,通常采用如下步骤:
- 对整个数据集(包括所有图片)进行推理,得到所有的预测框及其置信度。
- 将所有的预测框按置信度从高到低进行排序。
- 依次遍历排序后的预测框,计算它们与真实框的 IoU。根据设定的 IoU 阈值(如 0.5),将预测框标记为 tp或 fp。
- 维护一个累计计数器,统计整个数据集中的 TP 和 FP。通过遍历所有预测框来计算每个预测框的精确率和召回率。
- 随着预测框的遍历,记录每个步骤的精确率和召回率。
这样操作下来,将所有预测框收集在一起处理,能够更全面地评估模型的表现,避免了仅基于单个图像的片面性。而且这种方法允许更平滑的精确度-召回率曲线,进而使 AP 的计算更为准确。
而召回率和准确率的每一个元素可以理解为,经过置信度排序以后,如果置信度设置为该idx的置信度时,该模型的召回率和准确率。
具体代码可去Bubbliiiing大神的仓库获取,主体逻辑代码如下:
sum_AP = 0.0
ap_dictionary = {}
lamr_dictionary = {}
with open(RESULTS_FILES_PATH + "/results.txt", 'w') as results_file:
results_file.write("# AP and precision/recall per class\n")
count_true_positives = {}
for class_index, class_name in enumerate(gt_classes):
count_true_positives[class_name] = 0
# 获取所有预测框
dr_file = TEMP_FILES_PATH + "/" + class_name + "_dr.json"
dr_data = json.load(open(dr_file))
nd = len(dr_data)
tp = [0] * nd
fp = [0] * nd
score = [0] * nd
score_threhold_idx = 0
# 遍历预测框
for idx, detection in enumerate(dr_data):
file_id = detection["file_id"]
score[idx] = float(detection["confidence"])
if score[idx] >= score_threhold:
score_threhold_idx = idx
# 找到对应真实框
gt_file = TEMP_FILES_PATH + "/" + file_id + "_ground_truth.json"
ground_truth_data = json.load(open(gt_file))
ovmax = -1
gt_match = -1
bb = [float(x) for x in detection["bbox"].split()]
# 计算IOU
for obj in ground_truth_data:
if obj["class_name"] == class_name:
bbgt = [ float(x) for x in obj["bbox"].split() ]
bi = [max(bb[0],bbgt[0]), max(bb[1],bbgt[1]), min(bb[2],bbgt[2]), min(bb[3],bbgt[3])]
iw = bi[2] - bi[0] + 1
ih = bi[3] - bi[1] + 1
if iw > 0 and ih > 0:
ua = (bb[2] - bb[0] + 1) * (bb[3] - bb[1] + 1) + (bbgt[2] - bbgt[0]
+ 1) * (bbgt[3] - bbgt[1] + 1) - iw * ih
ov = iw * ih / ua
if ov > ovmax:
ovmax = ov
gt_match = obj
min_overlap = MINOVERLAP
# IOU大于阈值,判定为预测正确,tp=1,类别真值数量+1
if ovmax >= min_overlap:
if "difficult" not in gt_match:
if not bool(gt_match["used"]):
tp[idx] = 1
gt_match["used"] = True
count_true_positives[class_name] += 1
with open(gt_file, 'w') as f:
f.write(json.dumps(ground_truth_data))
if show_animation:
status = "MATCH!"
# 重复预测框,判定为预测错误,fp=1
else:
fp[idx] = 1
if show_animation:
status = "REPEATED MATCH!"
# IOU小于阈值,判定为预测错误,fp=1
else:
fp[idx] = 1
if ovmax > 0:
status = "INSUFFICIENT OVERLAP"
# 统计idx之前的所有fp个数
cumsum = 0
for idx, val in enumerate(fp):
fp[idx] += cumsum
cumsum += val
# 统计idx之前的所有tp的个数
cumsum = 0
for idx, val in enumerate(tp):
tp[idx] += cumsum
cumsum += val
# 计算召回率,tp个数/真值数
rec = tp[:]
for idx, val in enumerate(tp):
rec[idx] = float(tp[idx]) / np.maximum(gt_counter_per_class[class_name], 1)
# 计算准确率,tp/tp+fp
prec = tp[:]
for idx, val in enumerate(tp):
prec[idx] = float(tp[idx]) / np.maximum((fp[idx] + tp[idx]), 1)
ap, mrec, mprec = voc_ap(rec[:], prec[:])
-
统计各个类别的所有框的json:airplane_dr.json,遍历所有预测框,把该类别的所有框都写进来,格式是{“confidence”:confidence, “file_id”:file_id, “bbox”:bbox,经过按照confidence从高到低排序
-
遍历所有dr.json,找到file_id,取出来置信度confidence,如果大于置信度阈值(0.5),记录索引。找对应的真实框,计算IOU,如果IOU大于IOU阈值(0.5或者50%),tp[idx]=1,否则fp[idx]=1
通过这一过程,可以筛选出所有tp和fp的框 -
再通过
cumsum = 0
for idx, val in enumerate(fp):
fp[idx] += cumsum
cumsum += val`
这样操作可以将fp这个list理解为,当置信度设置为该idx预测框的置信度时,模型对于所有测试集,预测出的所有错误框的个数。
-
tp也是如此操作,表示当置信度设置为该idx预测框的置信度时,模型对于所有测试集,预测出的所有正确框的个数。
-
计算召回率
rec = tp[:]
for idx, val in enumerate(tp):
rec[idx] = float(tp[idx]) / np.maximum(gt_counter_per_class[class_name], 1)
float(tp[idx]): 将真正例的数量转换为浮点数,以便进行精确的除法运算。
gt_counter_per_class[class_name]: 表示某个特定类别(class_name)的真实目标(Ground Truth)数量。
将每个idx的数量除以该类别所有真实目标的数量,得到每个idx的召回率
可以看到召回率的排序应该是从小到大的,因为tp的统计是该类别置信度越高的框排序在前
所以召回率的每个元素表示为,当置信度设置为该idx预测框的置信度时,模型的召回率。
- 计算准确率
prec = tp[:]
for idx, val in enumerate(tp):
prec[idx] = float(tp[idx]) / np.maximum((fp[idx] + tp[idx]), 1)
计算每个idx中真正数除以检测总数,表示准确率
可以看到准确率的排序应该是从大到小的
因为tp和fp的统计是该类别置信度越高的框排序在前,且tp统计的是,当置信度设置为该idx预测框的置信度时,模型对于所有测试集,预测出的所有错误框的个数,fp的统计是模型预测错误的个数,所以获取的准确率也是模型对于该置信度表现下的准确率
-
将准确率前后都填上0.0
prec.insert(0, 0.0) # insert 0.0 at begining of list prec.append(0.0) # insert 0.0 at end of list mpre = prec[:]
然后在从后往前,进行递减排序
for i in range(len(mpre)-2, -1, -1):
mpre[i] = max(mpre[i], mpre[i+1])
这块暂时不理解,为什么前后要加0.0在比对而不是 把idx=0的在前面复制一个,后面加个0就ok了
-
将召回率前面加个0,后面加个1
rec.insert(0, 0.0) # insert 0.0 at begining of list rec.append(1.0) # insert 1.0 at end of list mrec = rec[:]
在统计召回率有变换的索引
i_list = []
for i in range(1, len(mrec)):
if mrec[i] != mrec[i-1]:
i_list.append(i)
-
计算ap
ap为PR曲线即精确率-召回率下面的面积,也就是召回率有变化的位置之前召回率的宽度乘以该范围内最高的精确率
ap = 0.0
for i in i_list:
ap += ((mrec[i]-mrec[i-1])*mpre[i])
由于计算tp和fp的统计是根据所有框进行置信度从大到小排序后,统计该idx之前所有框中tp和fp的个数。
也就意味着,置信度低的框,其tp的个数统计趋于平稳,fp会增加。
召回率rec则是将之前所有tp的个数都除以真值gt的总个数进行统计。所以召回率是逐步增加的。
准确率prec的计算是tp/(tp + fp),那么prec是逐步降低的。
根据ap的计算公式可以看到,rec没有变化的区间内,区间内取准确率是取最后的索引值
相同的Recall只会保留最大的Precision的那一个,也就是只会保留图中所示有方框的5个点。最后计算面积即可,有一点注意就是实际上计算的是每一个小矩形的面积,宽为相邻的两个点的recall差,高为从该点开始往后的所有的precision最大的值。
那么经过观察,绝大多数统计map时,置信度会设置成尽可能多,通常为就是0.001,但是推理时置信度通常会设置为0.5。
这样操作,个人感觉是为了尽可能的保留更多的预测框,尽管这可能会增加 TP,但同时也会显著增加 FP。这使得 TP 和 FP 的关系变得更加复杂,可能导致精确率的变化,影响最终的评估结果。