评估计算recall、precision、AP、F1、mAP(PyTorch-YOLOv3代码解析二)

本文详细介绍了如何使用PyTorch实现YOLOv3目标检测中的评估函数ap_per_class,包括计算AP(Average Precision)、召回率、精确度和F1分数。涉及数据预处理、TP统计以及多幅图像的批量评估过程。
摘要由CSDN通过智能技术生成

目标检测评估计算(utils.py)

代码github地址:https://github.com/eriklindernoren/PyTorch-YOLOv3

1. 检测的评估函数

# reference: https://github.com/eriklindernoren/PyTorch-YOLOv3/blob/f917503ffe4a21d2b1148d8cb13b89b834517d76/utils/utils.py

def ap_per_class(tp, conf, pred_cls, target_cls):
    """ 通过召回率与精确度曲线计算mAP
    Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
    # 参数说明
        tp: True positives (list).
        conf: 置信度[0,1] (list).
        pred_cls: 预测的目标类别 (list).
        target_cls: 真正的目标类别 (list).
    # 返回
          [precision,recall,average precision,f1, classes_num]
    """

    # 按照预测的置信度做降序排列, 得到排序的索引
    i = np.argsort(-conf)
    tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]

    # 去除重复项
    unique_classes = np.unique(target_cls)

    # 为每个类别创建精度-召回曲线, 计算AP
    ap, p, r = [], [], []
    for c in tqdm.tqdm(unique_classes, desc="Computing AP"):
        # 找出等于c的位置
        i = pred_cls == c
        # 类别c的人工标注目标的数量
        n_gt = (target_cls == c).sum()
        # 类别c的预测目标数量
        n_p = i.sum()

        if n_p == 0 and n_gt == 0:
            continue
        elif n_p == 0 or n_gt == 0:
            ap.append(0)
            r.append(0)
            p.append(0)
        else:
            # 累加计算FPs与TPs
            fpc = (1 - tp[i]).cumsum()  #
            tpc = (tp[i]).cumsum()

            # Recall
            recall_curve = tpc / (n_gt + 1e-16)  # TP/(TP + FN) (TP+FN)为当前类人工标注目标数量
            r.append(recall_curve[-1]) # 这个类别的召回率

            # Precision
            precision_curve = tpc / (tpc + fpc)  # TP/(TP + FP) (TP+FP)为预测框的数量
            p.append(precision_curve[-1]) # 这个类别的精确率

            # 从召回率-精确率曲线计算AP
            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")

2. batch预测数据的统计

(1) 数据加载。通过统计非极大值抑制后得到的outputs与人工标注框的targets条目,得到TP(目标预测正确)。统计的数据为一个batch的,保存的数据为statistics_data.pt,输出数据的shape。

# 非极大值抑制后的outputs,与targets
batch_statistics = torch.load('statistics_data.pt',map_location='cpu')
outputs = batch_statistics['outputs']
targets = batch_statistics['targets']
print('outputs_shape: ',[x.shape for x in outputs])
print('targets_size: ', targets.shape,end='\n\n')

输出结果:

数据statistics_data.pt保存的是8幅图像预测框与人工标注框数据,其中outputs的数据由非极大值抑制获取。outputs数据格式[x1,y1,x2,y2,conf,class_score,class_idx],targets数据格式[batch_idx,class_idx,x1,y1,x2,y2]。数据中的坐标是相对于网络中的输入尺寸。

(2) 统计TP。当某幅图像的某个预测框与targets中的真实框的IoU大于某个阈值,则表示该预测框能够作为targets中真实框的预测值。

# output为某幅图像的预测框,初始化tp
true_positives = np.zeros(output.shape[0])
# 找出targets中batch_idx=i的class_idx与标注框坐标
annotations = targets[targets[:, 0] == i][:, 1:]
    
if len(annotations):
    # 真实的目标类别代号,标注框
    target_labels = annotations[:, 0]
    target_boxes = annotations[:, 1:]
    print('target_boxes: ',target_boxes,',target_labels: ',target_labels)   
    detected_boxes = []
    # 遍历预测框索引,预测框坐标,预测类别代号 
    for pred_i,(pred_box, pred_cls) in enumerate(zip(output[:,:4],output[:,-1])):
        if len(detected_boxes) == len(target_boxes): break
        if pred_cls not in target_labels: continue 
        # 预测框去拟合目标框(通过最大的IoU加阈值判断)   
        iou, box_index = bbox_iou(pred_box.unsqueeze(0), target_boxes).max(0)
        print('pred_{}'.format(pred_i),iou, ',box_index: ',box_index)
        if iou >= iou_threshold and box_index not in detected_boxes:
            # 预测真实值则赋值相应位置为1
            true_positives[pred_i] = 1
            detected_boxes += [box_index]
   

输出结果

[true_positives, pred_confs, pred_cls]:

多幅图像的统计

# batch统计
batch_metrics = []
for i,output in enumerate(outputs):
    true_positives = np.zeros(output.shape[0])
    annotations = targets[targets[:, 0] == i][:, 1:]
    
    if len(annotations):
        target_boxes = annotations[:, 1:]
        target_labels = annotations[:, 0]
        detected_boxes = []
        
        for pred_i,(pred_box, pred_cls) in enumerate(zip(output[:,:4],output[:,-1])):
            if len(detected_boxes) == len(target_boxes): break
            if pred_cls not in target_labels: continue 
            
            iou, box_index = bbox_iou(pred_box.unsqueeze(0), target_boxes).max(0)
            if iou >= iou_threshold and box_index not in detected_boxes:
                true_positives[pred_i] = 1
                detected_boxes += [box_index]
    # 每幅图像的TP,预测的目标置信度,预测类别代号
    batch_metrics.append([true_positives,output[:,4],output[:,-1]])

3. Recall、Precision、F1、AP、mAP计算

(1) 获取batch统计的结果与排序

batch_metrics的数据结构(list):

[[true_positives1, pred_confs1, pred_cls1],

[true_positives2, pred_confs2, pred_cls2],

[true_positives3, pred_confs3, pred_cls3],

......]

通过解包重组: list(zip(*batch_metrics)),然后得到如下:

[(true_positives1, true_positives2, ......),

(pred_confs1, pred_confs2, ......),

(pred_cls1, pred_cls2, ......) ]

# 解包一个batch的数据
true_positives, pred_confs, pred_cls = [np.concatenate(x, 0) for x in list(zip(*batch_metrics))]

按照置信度降序排列统计的数据

# 按照置信度降序排序,排序tp,conf,cls
idx = np.argsort(-pred_confs)
tp, pre_confs, pred_cls = true_positives[idx],pred_confs[idx],pred_cls[idx]

(2) 评估一个batch的预测结果。通常情况是通过第2步的统计,统计出整个验证图片库的[true_positives, pred_confs, pred_cls]数据,然后再计算相应的评估值。

a. 召回率、精确度曲线

# 人工标注的类别
unique_cls = np.unique(target_cls)

# 每个类别创建精度-召回曲线, 计算AP
ap, p, r = [], [], []
for num, c in enumerate(unique_cls):
    idx = pred_cls==c
    # 类别c的人工标注目标的数量
    n_gt = (target_cls == c).sum().numpy()
    n_p = idx.sum()
    print('n_gt: ',n_gt,',n_p: ',n_p)
    
    if n_p == 0 and n_gt == 0:
        continue
    elif n_p == 0 or n_gt == 0:
        ap.append(0)
        r.append(0)
        p.append(0)
    else:
        # 累加计算FPs与TPs
        fpc = (1 - tp[idx]).cumsum()
        tpc = tp[idx].cumsum()
        
        # 召回率
        recall_curve = tpc/(n_gt + 1e-16) # TP/(TP+FN)
        r.append(recall_curve[-1])
        
        # 精确率
        precision_curve = tpc/(tpc+fpc) # TP/(TP+FP)
        p.append(precision_curve[-1])
        print('recall_curve: ',recall_curve,'\nprecision_curve: ',precision_curve)

一个类别召回率、精确度曲线输出:

b. AP计算

AP是召回率-精确度曲线的面积,采用的方法为积分求面积。通过plot绘制的曲线如下图所示:

积分的方法需要找出图中recall存在梯度的位置,即上图红色线与红色线之间在recall方向存在梯度(也就是相邻的两个点的recall值不同)。把整个面积区域划分成坐标点-1个条形图(图中8个点,则有7个条形图),然后求和所有条形图的面积得到AP的面积。实现代码如下:

# 计算AP,检测评估函数compute_ap
mrec = np.concatenate(([0.0], recall_curve, [1.0]))
mpre = np.concatenate(([1.0], precision_curve, [0.0]))
        
for i in range(mpre.size - 1, 0, -1):
    mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
        
# 找出召回率有梯度的位置
idx = np.where(mrec[1:] != mrec[:-1])[0]
# 计算面积 sum(delta_recall*precision)
ap_c = np.sum((mrec[idx+1] - mrec[idx])*mpre[idx + 1])

c. 计算F1

f1 = 2 * p * r / (p + r + 1e-16)

d. 总体代码

# 评估一个batch的预测结果
true_positives, pred_confs, pred_cls = [np.concatenate(x, 0) for x in list(zip(*batch_metrics))]
target_cls = targets[:,1]

# 按照置信度降序排序,排序tp,conf,cls
idx = np.argsort(-pred_confs)
tp, pre_confs, pred_cls = true_positives[idx],pred_confs[idx],pred_cls[idx]

# 得到人工标注的类别
unique_cls = np.unique(target_cls)

# 为每个类别创建精度-召回曲线, 计算AP
ap, p, r = [], [], []
for num, c in enumerate(unique_cls):
    idx = pred_cls==c
    # 类别c的人工标注目标的数量
    n_gt = (target_cls == c).sum().numpy()
    n_p = idx.sum()
    
    if n_p == 0 and n_gt == 0:
        continue
    elif n_p == 0 or n_gt == 0:
        ap.append(0)
        r.append(0)
        p.append(0)
    else:
        # 累加计算FPs与TPs
        fpc = (1 - tp[idx]).cumsum()
        tpc = tp[idx].cumsum()
        
        # 召回率
        recall_curve = tpc/(n_gt + 1e-16) # TP/(TP+FN)
        r.append(recall_curve[-1])
        
        # 精确率
        precision_curve = tpc/(tpc+fpc) # TP/(TP+FP)
        p.append(precision_curve[-1])
        
        # 计算AP
        mrec = np.concatenate(([0.0], recall_curve, [1.0]))
        mpre = np.concatenate(([1.0], precision_curve, [0.0]))
        
        for i in range(mpre.size - 1, 0, -1):
            mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
        
        # 找出召回率有梯度的位置
        idx = np.where(mrec[1:] != mrec[:-1])[0]
        
        # 计算面积
        ap_c = np.sum((mrec[idx+1] - mrec[idx])*mpre[idx + 1])
        ap.append(ap_c)

# batch中所有类别       
p, r, ap = np.array(p), np.array(r), np.array(ap)
f1 = 2 * p * r / (p + r + 1e-16)

#[类别,召回率,精确率,F1, AP]
str_list = ['%d \t %.3f \t %.3f \t %.3f \t %.3f'%(x[0],x[1],x[2],x[3],x[4]) for x in np.array([unique_cls,r,p,f1,ap]).T]
print('cls_num | recall | precision | F1 | AP')
for x in str_list:
    print(x)

print('mAP:', np.mean(ap))

输出结果

  • 6
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值