mAP(Mean Average Precision)

저번 mAP(Mean Average Precision) [1] 포스팅 에서는 MAP를 알기위해 필요한 지식에 대해 주로 다루었고 이번 포스팅에서는 mAP계산 과정을 실제 코드와 함께 알아보자.

Review


우선 강아지라는 1개의 클래스에 대해서만 생각해보자.  mAP를 계산하기 위해서는 prediction box와 test set의 ground truth가 필요하다.  3개의 이미지에는 4개의 Ground truth가 있으며 7개의 prediction이 confidence와 함께 주어진다.




그리고 각 이미지에 대해 ground truth와 prediction의 IOU가 0.5 이상인 것은 TP(True Positive) 이하인 것은 FP(False Positive)로 정한다.


모든 prediction에 대한 TP or FP를 정했으면 confidence가 높은 순으로 정렬을 한다. (정렬을 하는 정확한 이유는 찾을 수 없었는데 내 생각으로는 나중에 Precision-Recall 그래프의 모양이 너무 구겨지지 않도록 하기 위해서 사용하는 것 같다.  -> confidence가 높을수록 TP일 확률이 높기 때문에) 


7개의 prediction의 Precision과 Recall에 대해 계산을 한다. (계산 방법은 mAP(Mean Average Precision) [1] 포스팅에 자세히 나와있다.)



이렇게 Precision-Recall 그래프를 그린 후 아래 면적을 통해 강아지의 AP를 얻을 수 있다.


해당 과정을 통해 모든 class에 대해 AP를 구한 후에 그것을 평균을 내면 mAP를 얻을 수 있다.


여러 Object Detection 논문을 보면 알 수 있듯이 단지 IOU 0.5 만을 기준으로 mAP를 계산하지 않고 0.5~0.95 사이의 IOU까지 0.5씩 올려가면서 성능결과를 제시한다. (코드에서는 0.5에 대한 mAP 계산을 할 예정.)

지금까지 mAP를 구하는 과정을 살펴 보았고 이제 implementation을 해보자


코드구현

import torch
from collections import Counter

def mean_average_precision(pred_boxes, true_boxes, iou_threshold=0.5, box_format='corners', num_classes=20):
    # pred_boxes (list):  [[train_idx(image_num), class_pred, prob_score, x1, y1, x2, y2], ...]
    average_precisions = [] # 각 클래스별로 AP가 추가될 리스트
    epsilon = 1e-6 # stability numeric
    for c in range(num_classes):
        detections = [] # 각 클래스의 detection이 담길 리스트
        ground_truths = [] # 각 클래스의 ground truth가 담길 리스트
        
        for detection in pred_boxes:
            if detection[1] == c:
                detections.append(detection)
                
        for true_box in true_boxes: 
            if true_box[1] == c:
                ground_truths.append(true_box)
                
        # img 0 has 3 bboxes
        # img 1 has 5 bboxes
        # amount_bboxes = {0:3, 1:5}
        amount_bboxes = Counter(gt[0] for gt in ground_truths) # gt의 각 이미지(key)의 개수(value)를 셈
        
        for key, val in amount_bboxes.items():
            amount_bboxes[key] = torch.zeros(val) # 개수를 1차원 tensor로 변환
        # amount_boxes = {0: torch.tensor([0,0,0]), 1:torch.tensor([0,0,0,0,0])}
        
        detections.sort(key=lambda x: x[2], reverse=True) # detections의 confidence가 높은 순으로 정렬
        TP = torch.zeros((len(detections))) # detections 개수만큼 1차원 TP tensor를 초기화
        FP = torch.zeros((len(detections))) # 마찬가지로 1차원 FP tensor 초기화
        total_true_bboxes = len(ground_truths) # recall의 TP+FN으로 사용됨
        
        for detection_idx, detection in enumerate(detections): # 정렬한 detections를 하나씩 뽑음
            # ground_truth_img : detection과 같은 이미지의 ground truth bbox들을 가져옴
            ground_truth_img = [bbox for bbox in ground_truths if bbox[0] == detection[0]]         
            best_iou = 0 # 초기화
            
            for idx, gt in enumerate(ground_truth_img): # 현재 detection box를 이미지의 ground truth들과 비교
                iou = intersection_over_union(
                        torch.Tensor(detection[3:]).view(1,-1),
                        torch.Tensor(gt[3:]).view(1,-1),
                        box_format=box_format)
                
                if iou > best_iou: #ground truth들과의 iou중 가장 높은놈의 iou를 저장
                    best_iou = iou
                    best_gt_idx = idx # 인덱스도 저장
            
            if best_iou > iou_threshold: # 그 iou가 0.5 이상이면 헤당 인덱스에 TP = 1 저장, 이하면 FP = 1 저장 
                if amount_bboxes[detection[0]][best_gt_idx] == 0:
                    TP[detection_idx] = 1
                    amount_bboxes[detection[0]][best_gt_idx] = 1
                else:
                    FP[detection_idx] = 1 # 이미 해당 물체를 detect한 물체가 있다면 즉 인덱스 자리에 이미 TP가 1이라면 FP=1적용
            else:
                FP[detection_idx] = 1
            
        # [1, 1, 0, 1, 0] -> [1, 2, 2, 3, 3]    
        TP_cumsum = torch.cumsum(TP, dim=0)
        FP_cumsum = torch.cumsum(FP, dim=0)
        recalls = TP_cumsum / (total_true_bboxes + epsilon)
        precisions = torch.divide(TP_cumsum, (TP_cumsum + FP_cumsum + epsilon)) # TP_cumsum + FP_cumsum을 하면 1씩 증가하게됨
        
        recalls = torch.cat((torch.tensor([0]), recalls)) # x축의 시작은 0 이므로 맨앞에 0추가
        precisions = torch.cat((torch.tensor([1]), precisions)) # y축의 시작은 1 이므로 맨앞에 1 추가
        average_precisions.append(torch.trapz(precisions, recalls)) # 현재 클래스에 대해 AP를 계산해줌
        
    return sum(average_precisions) / len(average_precisions) # MAP

End

이번 포스팅에서는 mAP의 계산과정과 그 코드를 함께 살펴보았다. 모든 Object Detection 모델의 성능평가는 mAP로 하기 때문에 반드시 알아야 하는 개념이다.

reference : www.youtube.com/watch?v=FppOzcDvaDI&t=1s

업데이트:

댓글남기기