[2] 데이터 수집 및 정제

Seong Woong Kim·2022년 11월 1일
0

[Project] Airstrip Safer

목록 보기
2/4
post-thumbnail

👉 0. 개요

본 문서에서는 프로젝트에서 학습용 Data를 어떻게 얻고, 과정을 진행하면서 생긴 이슈들을 어떻게 접근하고 해결하려 하였으며, 정제하고 Load하였는지를 다룬다.

모델 학습과 관련된 자세한 사항은 다음 글에서 기술하고 여기에선 데이터 관점 위주로 작성하려 한다.

본 프로젝트는 활주로 내 사고를 일으키는 객체들을 이상 객체로 분류하여 탐지하는 Task인데, 다행히 활주로 내 존재하는 다양한 객체들을 담은 Dataset을 찾을 수 있었다.

💥 MAIN ISSUES 💥

  1. 심각한 클래스 불균형
  2. 추가 데이터셋의 정제 미흡과 Label file format 간의 불일치로 인한 지속된 성능 정체

👉 이번 Project의 주요 문제들은 데이터셋에서 일어나서, 약간 길어질 수 있다. 그래도 주요 이슈와 시도한 점, 결과를 개조식으로 서술하려고 노력했다.





👉 1. Dataset 선정

AI Hub Dataset

활주로 내 이상물체 감지 객체 이미지

Train & Validation 용 총 742GB 용량의 큰 데이터셋이다.
활주로 내 활동하는 사물과 차량과 인력들이 적절히 포함되어 있고, annotation도 문제가 없어 선정하였다.

각 이미지의 크기는 1920 x 1080이고, 총 지정 객체 6개의 클래스, 이상 객체 15개 클래스총 21개 클래스로 이루어진 데이터셋이다.

지정 객체

Civil aircraft, Rotocraft, Light aircraft, Road surface facility, Road surface disorder, Obstacle

이상 객체

Bird, Mammals, Worker, Box, Pallet, Toyinka, Ramp bus, Step car, Fire truck, Road sweeper, Weeding vehicle, Special vehicle, Forklift, Cargo loader, Tug car

각 이미지에 대한 Label file은 Json 파일에 Pascal VOC 형태로 이미지당 하나의 파일로 구성되어있다.

{
  "image": {
    "date": "20201109",
    "path": "S1-N0101M00296",
    "filename": "S1-N0101M00303.jpg",
    "copyrighter": "미디어그룹사람과숲(컨)",
    "location": "01",
    "H_DPI": 96,
    "V_DPI": 96,
    "bit": 24,
    "resolution": [
      1920,
      1080
    ]
  },
  "annotations": [
    {
      "data ID": "S1",
      "large classification": "01",
      "middle classification": "01",
      "box": [
        676,
        354,
        1220,
        520
      ],
      "class": "04",
      "flags": ","
    },
    
   .....
   
    {
      "data ID": "S1",
      "large classification": "02",
      "middle classification": "03",
      "box": [
        379,
        844,
        435,
        866
      ],
      "class": "08",
      "flags": ","
    }
  ]
}


Dataset Undersampling - 1차 데이터셋

우리는 하드웨어 용량 한계를 고려하여 Validation용 데이터만 다운로드하여 111GB 용량의 72,207장의 이미지를 확보하였다.

확보한 이미지들 내 295,056개의 객체가 존재했다.

데이터셋 클래스별 분포를 확인해봤는데 심각한 💥Class Imbalance💥가 발견됐다.

그래서 팀원들끼리 🛠️ 우선 해결하기 위해서 최초로 2가지를 시도했다. 🛠️

  • 내가 속한 팀은 Cycle GANMOGAN을 이용해 Image Generation을 하는 방안을 시도했는데, Image를 생성하고 난 뒤, 이미지 내 객체의 Bounding Box 생성 문제가 생겨 실패했다.
  • 다른 팀은 부족한 클래스의 이미지 크롤링을 시도했는데, 필터링을 하고 난 뒤, 쓸만한 이미지가 없어 실패했다.

또한, Google Colab으로 진행 중이었는데, 데이터 용량으로 인해 구글 드라이브의 I/O가 너무 느리고, 오류가 발생했다.

그래서 위의 문제들을 다소 해결하고자 Under Sampling의 필요성을 느끼고 수행했다.

그 결과, 총 15,000장의 이미지를 구성하였다. --> 1차 데이터셋





👉 2. MMDetection Custom Dataset Generation

본 프로젝트에서는 MMDetection을 활용하여 Deformable DETR 모델을 사용한다.

모델을 선정한 Dataset으로 학습시키기 위해서는 MMDetection이 지원하는 Dataset Format (COCO Format, Middle Format)으로 Customize해서 Dataset을 생성해야 한다.

그 후, MMDetection의 CustomDataset Class를 상속 받아서 dataset registry에 만든 Dataset을 등록하면 그 Dataset을 받아서 Data Loader를 생성하는 형식이다.

필자는 MMDetection Document를 참고해서 모든 데이터셋이 호환이 되는 Middle Format으로 Dataset을 생성했다.



MMDetection Middle Format Code

Middle Format의 annotation은 dict의 list로 구성되며, 각 dict는 하나의 이미지와 대응된다.

각 dict는 filename(상대경로), width, height, 그리고 추가 필드인 ann(annotation)Key로 가진다.

ann은 4개의 부분으로 구성되는데,

  • bboxes: np.ndarray 형식으로 크기는 (n, 4)이다.
  • labels: np.ndarray 형식으로 크기는 (n, )이다.
  • bboxes_ignore , labels_ignore : 일부 데이터셋은 crowd/difficult/ignored bboxes로 구분하는데, 여기서는 이를 위해 제공한다.
                              <Middle Format>

[
    {
        'filename': 'a.jpg',
        'width': 1280,
        'height': 720,
        'ann': {
            'bboxes': <np.ndarray, float32> (n, 4),
            'labels': <np.ndarray, int64> (n, ),
            'bboxes_ignore': <np.ndarray, float32> (k, 4), (optional field)
            'labels_ignore': <np.ndarray, int64> (k, )     (optional field)
        }
    },
    ...
]

필자는 1차 데이터셋의 각 json file에 접근해 filename, width, height, bboxes를 추출해서 각 이미지에 대응되는 dict에 추가되도록 코드를 짰다.

  • filename : Dataset의 filename의 중복이 없으므로, filename은 json 파일명을 그대로 가져왔다.
  • width,height : json의 image['resolution']에 있는 정보를 각각 입력했다.
  • ann['bboxes'], ann['labels'] : json의 annotations['box'], annotations['class']의 정보를 가져왔다.
    - 1차 데이터셋 json에 있는 bbox 좌표 형식은 좌상단, 우하단 (x1,y1,x2,y2) 형식이다.

필자는 이렇게 생성된 각 이미지에 대한 json 파일명을 list로 만들어서 하나의 txt 파일로 만든 후, 그것을 학습의 Annotation 파일로 사용했다. 코드는 여기에서 확인할 수 있다.

Middle Format의 MMDetection 학습을 진행하기 위해 최종으로 만든 데이터셋 생성 코드는 다음과 같다.

import cv2
import mmcv
import copy
import json
import numpy as np
import os.path as osp
from tqdm import tqdm

from mmdet.datasets.custom import CustomDataset
from mmdet.datasets.builder import DATASETS

@DATASETS.register_module(force=True)
class AirplaneDataset(CustomDataset):
    
   # Class 지정 
   # Class 통합 후 최종 클래스 -> 아래 글에 언급 
    CLASSES = ['Aircraft','Rotocraft','Road surface facility','Obstacle (FOD)','Bird','Mammals','Worker',
               'Box','Pallet','Toyinka','Ramp bus','Step car','Fire truck','Road sweeper','Weeding vehicle',
               'Special vehicle','Forklift','Cargo loader','Tug Car'] 


    def load_annotations(self, ann_file):
        '''
        annotation에 대한 모든 파일명을 가지고 있는 텍스트 파일을 __init__(self, ann_file)로 입력 받고, 
        이 self.ann_file이 load_annotations()의 인자로 입력
        '''
        label2cat = {i:k for i, k in enumerate(self.CLASSES)}
        image_list = mmcv.list_from_file(self.ann_file)  # ann_file을 다 받아서 리스트를 만듬
        mode_name = self.img_prefix.split('/')[-1].split('_')[0]
        
        # Middle Format 데이터를 담을 list 객체
        data_infos = []

        for image_id in tqdm(image_list, desc= 'Making Middle Format per Image'): 
            
            # self.img_prefix: 절대경로
            # 절대경로가 필요한 이유 : opencv imread를 통해서 이미지의 height, width 구함
            filename = f'{self.img_prefix}/{image_id}.jpg' 

            # 원본 이미지의 width, height 를 직접 로드하여 구함.
            image = cv2.imread(filename)
            height, width = image.shape[:2] #  height, width
            
            # 개별 image의 annotation 정보 저장용 Dict 생성. key값 filename 에는 image의 파일명만 들어감(디렉토리는 제외)
            # 이미지 하나는 하나의 data_info를 가지게 됨
            data_info = {'filename' : str(image_id) + '.jpg',
                         'width' : width, 'height' : height}

            # 개별 annotation이 있는 서브 디렉토리의 prefix 변환. 
            # annotation 정보는 label folder에서 가지고 있음
            label_prefix = self.img_prefix.replace(f'{mode_name}_data',f'{mode_name}_label')

            # json 파일 로드해서 bbox 저장
            bbox_classid = []
            bboxes = []

            file = open(osp.join(label_prefix, str(image_id)+'.json'))
            jsonFile = json.load(file)
            jsonObject = jsonFile.get('annotations')

            for j_list in jsonObject:
                bbox = j_list.get('box')[:4]
                cls_id = int(j_list.get('class')) # 1부터 클래스 아이디 불러옴

                # bbox 좌표를 저장
                bboxes.append(bbox)

                # 오브젝트의 클래스 id 저장
                # json 파일 내 클래스가 1부터 시작해서 0부터 시작하는 걸로 변경
                bbox_classid.append(cls_id-1)

            gt_bboxes = []
            gt_labels = []
            gt_bboxes_ignore = []
            gt_labels_ignore = []

            # 위 이미지에서 key 'ann'을 만드는 작업
            # loop로 한번에 담기 위해서 생성
            for class_id, bbox in zip(bbox_classid, bboxes):

                if class_id in label2cat:
                    gt_bboxes.append(bbox) # 리스트의 리스트

                    # label이 int로 이뤄져야되서 int인 클래스 아이디 그대로 불러옴.
                    # 클래스별 int로 label array를 넘겨주면 mmdetection에서 알아서 클래스 별 아이디를 만들어서 할당.
                    gt_labels.append(class_id) 
                    
                else: # don't care (class에 포함되지 않는 것)은 여기에 집어넣기
                    gt_bboxes_ignore.append(bbox)
                    gt_labels_ignore.append(-1)
        

            # 개별 image별 annotation 정보를 가지는 Dict 생성. 해당 Dict의 value값은 모두 np.array임. 
            # 위의 것들을 한꺼번에 담는 annotation를 만듬 ->   위에서 작업한 것들을 한 middle format의 ann을 만드는 중
            data_anno = {
                'bboxes': np.array(gt_bboxes, dtype=np.float32).reshape(-1,4),
                'labels': np.array(gt_labels), # 1차원
                'bboxes_ignore' : np.array(gt_bboxes_ignore, dtype=np.float32).reshape(-1,4),
                'labels_ignore' : np.array(gt_labels_ignore, dtype=np.compat.long)
            }

           # 위에서 image에 대한 메타 정보를 가지는  data_info dict에 ann이라는 키와 data_anno를 value로 추가함
            data_info.update(ann=data_anno)  
            
            # 전체 annotation 파일들에 대한 정보를 가지는 data_infos에 data_info Dict를 추가
            data_infos.append(data_info)

        return data_infos # 리스트 객체 반환





👉 3. 1차 학습 및 본격적인 💥 Class Imbalance 💥 해결 시도


1차 학습

팀원들과 상의 끝에, 모든 Task에서 Class Imbalance는 존재하니깐 일단 이 문제를 안고, 학습을 진행하여 학습의 추이와 모델의 Convergence를 본 후, 보완해가자는 의견이 나왔다.

그래서 1차 데이터셋으로 Train과 Valid의 비율을 8:2로 나누고 기본 파라미터로 58 epochs 정도 1차 학습을 진행했다.

1차 학습을 한 결과, Class Imbalance가 치명적인 문제로 밝혀졌다.

  • Data가 충분한 Class들에 대해선 준수한 Recall값과 AP값을 보였다.
  • Data가 부족한 Classes들에 대해서는 모델이 충분히 학습하지 못한 까닭에 개수에 비해 많은 예측 Boxes들이 형성되는데 모델이 Ground Truth를 맞추질 못해 Recall과 AP 성능이 좋지 않았다.
  • Epoch가 증가하면서 전체적인 Recall은 상승하는데 AP가 감소하는 현상을 보였다.
    • 실제로 Target 값의 분포가 불균형한 상태일 때 이러한 양상을 많이 보인다고 한다.

P.S. 1차 학습 후 팀원 전원이 중도 하차를 결정했다. 이 이후부터 프로젝트 끝까지 필자 혼자 진행했다.



💥 Class Imbalance 💥를 해소하기 위한 본격적인 시도 - 2차 데이터셋

본격적으로 Class Imbalance를 해결하기 위해 여러 방안들을 고민했다.

  1. Dataset Regeneration
  2. 부족한 클래스의 이미지 데이터 크롤링으로 보충
  3. 데이터 증강으로 이미지 데이터를 보충
  4. 불필요한 클래스 병합
  5. Class Imbalance 상태에서 모델의 일반화를 위해 최적의 Hyperparameters, Loss,IoU Loss, Lr Scheduler 등을 여러 기법과 Weight들을 탐색

5번은 모델 학습과 연관이 깊어서 다음 글에서 자세히 기술하겠다.



🛠️ 1. Dataset Regeneration

상황

팀원이 구글 드라이브에 1차 데이터셋을 등록해서 공유해놓은 상태에서 데이터를 사용하고 있었는데, 팀원이 프로젝트 중도 하차 후 데이터를 삭제했다.

그래서 데이터셋 재다운로드가 불가피해진 상황이었다.

시도

다운로드를 다시 받을 때, 이미지 내 클래스 객체의 비율을 생각해서 수량을 조절했다.

그리고 Colab RAM 용량을 고려해서 데이터를 1차 데이터셋보다 적게 다운로드 받았다.

결과

1차 데이터셋보다 클래스 불균형이 다소 적은 데이터셋이 구축됐다.

  • 타겟 객체가 많은 클래스의 이미지는 감소하고, 타겟 객체가 적은 클래스들의 이미지는 최대한 많이 추가하려고 했다.

그래서 Train : 12,000 / Valid : 4,000 / Test 2,000장으로 구성된 2차 데이터셋이 생성됐다.



🛠️ 2. Image Data Crawling & Labeling

상황

2차 데이터셋을 살펴보니 대체로 Fire truck, Road sweeper, Special vehicle, Step car, Weed vehicle 이렇게 5개의 클래스의 데이터가 현저히 부족한 것을 발견했다.

시도

🎈 Crawling 🎈

부족한 클래스의 이미지를 크롤링해서 데이터의 수량을 보충하려 하였다.

크롤링을 하기 전 2가지를 고려하였다.

  • 크롤링하려는 이미지의 사이즈
    • 원본 이미지의 사이즈와 많이 차이가 나게 되면, 모델이 이미지 내 객체를 수월하게 판별하지 못할 것으로 판단했다.
  • 크롤링하려는 이미지의 배경
    • 이 부분은 나중에 알게 된 것인데, 이미지의 배경도 모델이 학습하는 데 영향을 주는 것을 배웠다.

구글에서 검색하면 다른 포털 사이트의 이미지도 검색이 되니깐 구글에서 크롤링하기로 결정하고, 구글 이미지를 한 번 클릭하면 우측에 나오는 비교적 사이즈가 큰 이미지를 수집하기로 했다.

전체 크롤링 코드는 여기서 확인할 수 있다.


🎈 Labeling 🎈

이미지를 크롤링한 후, Roboflow에서 Labeling을 진행했다.

Labeling을 다 마친 데이터셋을 COCO Format으로 Export했다.

Custom Dataset Format인 Pascal VOC Format에 맞추기 위해 이미지별로 json 파일 생성을 진행했다.

  • COCO Format의 Json 파일에 접근해서 images Keyid 값annotations Keyimage id 값 Matching을 활용했다.
  • 대응되는 image와 annotation의 파일명, 박스 정보를 받아와서 파일명 변경, 박스 정보 입력, 클래스값을 입력하여 각 이미지에 대응되는 새로운 json 파일을 생성했다.

진행 코드는 여기서 확인할 수 있다.

     << Roboflow에서 추출한 COCO 형식의 json 파일 예시 >> 

{
 'info': {'year': '2022',
 'version': '3',
 'description': 'Exported from roboflow.ai',
 'contributor': '',
 'url': 'https://public.roboflow.ai/object-detection/undefined',
 'date_created': '2022-09-05T12:47:53+00:00'},
 'licenses': [
   			{'id': 1,
            'url': 'https://creativecommons.org/licenses/by/4.0/',
            'name': 'CC BY 4.0'}
            ],

 'categories': [
 			{'id': 0, 'name': 'Step-car', 'supercategory': 'none'},
            {'id': 1, 'name': 'Step car', 'supercategory': 'Step-car'}
            ],

'images': [
            {'id': 0, 👈 이걸 활용 
            'license': 1,
            'file_name': 'airport-step-car_173_jpg.rf.3d8188ea5df2295147ff0e09e8adbc30.jpg',
            'height': 480,
            'width': 640,
            'date_captured': '2022-09-05T12:47:53+00:00'},
            
            {'id': 1, 👈 이걸 활용 
            'license': 1,
            'file_name': 'airport-step-car_6_jpg.rf.566299ef20aa0ec399bf14d3c087bcd0.jpg',
            'height': 267,
            'width': 400,
            'date_captured': '2022-09-05T12:47:53+00:00'},
            ]

'annotations': [

            {'id': 0,
            'image_id': 0, 👈 이걸 활용 
            'category_id': 1,
            'bbox': [149, 21, 354.59, 446.36],
            'area': 158274.7924,
            'segmentation': [],
            'iscrowd': 0},
            
            {'id': 1,
            'image_id': 1, 👈 이걸 활용 
            'category_id': 1,
            'bbox': [57, 16, 289.24, 214.78],
            'area': 62122.9672,
            'segmentation': [],
            'iscrowd': 0},
            
            ]
            }
            '''

결과

2차 데이터셋 중 부족한 클래스들의 이미지를 300장 정도 보충할 수 있었다.

  • 이 수량으론 부족하다고 판단해 Crawling한 이미지에서 Augmentations을 적용해서 추가적인 데이터를 생성해야 한다고 판단

하지만 이 Labeling과 Export이 그 당시에는 인지하지 못했지만
추후에 💥다른 문제💥를 초래하게 된다.

  • Labeling Boudning box의 여백이 Loose한 점 + 여백이 통일되지 않음
  • 추가한 이미지의 배경이 원본 데이터셋과 다른 배경도 있던 점
  • 아래 3번, 데이터 증강과 연관이 있지만, Augmentation을 적용해서 데이터셋을 생성할 때 과도한 Noise를 줘서 데이터셋의 질과 전체 데이터셋의 동일한(최대한의) 분포를 저하한 점


🛠️ 3. Image Generation through Augmentation

상황

크롤링해서 추가한 데이터의 수량이 부족하다고 판단해서 Augmentations을 적용해 추가적인 데이터를 확보하는 게 필요하다고 판단

시도

Roboflow에서 Dataset을 Export할 때, 추가적인 Augmentations을 추가해서 데이터의 수량을 증가시켜 Export

결과

5개 클래스에 대한 크롤링 데이터셋에 같은 Augmentations을 적용해서 총 1,110장을 확보해서 2차 데이터셋에 추가할 수 있었다.

  • Class Imbalance의 비율이 다소 감소


🛠️ 4. Classes Unification

상황

데이터셋의 클래스 중 유사하거나 동일한 카테고리인데 별도의 클래스로 구별된 것을 발견하고 클래스를 통합해서 클래스의 개수를 줄여서 Class Imbalance를 조금 줄여보자고 판단

시도

유사하거나 동일한 카테고리에 속한 클래스들의 통합을 시도했다.

Civil aircraft + Light aircraft -------------> Aircraft (1차 학습 때)
Road surface disorder + Obstacle ------------> Obstacle (FOD)

해당하는 Class에 속하는 데이터의 Json 파일에 접근해서 Class 숫자를 변경했다.

그리고, 나머지 Class들의 Number를 일괄적으로 변경하는 코드를 작성했다.

Class 통합을 진행했던 코드는 여기에서 class_conversion 함수에서 확인 가능하다.

결과

총 21개의 클래스가 19개의 클래스로 클래스의 개수가 변경됐다.

Aircraft, Rotocraft, Road surface facility, Obstacle (FOD), Bird, Mammals, Worker, Box, Pallet, Toyinka, Ramp bus, Step car, Fire truck, Road sweeper, Weeding vehicle, Special vehicle, Forklift, Cargo loader, Tug Car

클래스를 통합함으로써 유사하거나 같은 카테고리의 객체의 Classification을 통합하고, Class Imbalance의 정도를 미세하게라도 감소시킬 수 있었다.





👉 4. 2차 학습 및 💥 새로 추가한 데이터셋으로 인한 성능 향상 정체 💥 해결 시도


2차 학습

모델 학습 글에서 자세히 설명하겠지만, Class Balanced한 Mini Dataset을 구축한 다음, Ablation Study (Experiments)를 진행해서 얻은 최적의 하이퍼 파라미터와 IoU Loss, Lr Scheduler 등을 산출했다.

그리고 새로 추가한 데이터셋이 포함된 2차 데이터셋을 가지고 2차 학습을 진행했다.

좋은 성능과 결과를 당연하게 예측했지만, 전혀 예상하지 못했던 문제가 발생했다.

  • Epoch 40지점부터 계속 학습을 진행할수록 mAP 0.66 ~ 0.67를 달성한 시점부터 성능이 향상되는 게 아니라, 💥성능 향상이 정체되는 것이다. 💥

클래스별 RecallAP를 살펴봤지만, 평균 80 정도로 양호했고, 데이터가 부족한 클래스들도 1차 데이터셋에 비해 좋은 결과를 보였다.

그런데 왜 성능 향상 정체가????????????????

학습을 하면서 좀 더 지켜보자라고 생각했지만, Google Colab을 이용해서 학습을 진행하는 특성상, GPU가 할당이 안되면 학습 자체가 불가능하고, 학습에 필요한 데이터셋 생성에 걸리는 시간만 평균 6시간이여서 최대한 빨리 해결해야 했다.

데이터셋을 생성하다가 런타임이 끊기는 경우도 종종 발생했다.😂😂😂



💥 성능 향상 정체 💥를 해결하기 위한 본격적인 시도 - 3차 데이터셋

하드웨어 한계상 계속 지켜볼 수 없던 상황이라 원인을 최대한 빨리 찾고 싶었다.

처음에는 데이터셋의 문제라고 도저히 생각되지 않았다.

그래서 Loss나 Model의 Encoder, Decoder Layer 개수, 하이퍼 파라미터 등등 수정도 해보고, 현업에 계시는 선배들에게 자문도 구했다.

여러 가지를 시도해 본 끝에 성능 향상 정체의 원인을 찾을 수 있었다.

원인은 크게 2가지였다.

  1. 크롤링해서 추가한 데이터셋이 기존 데이터셋의 분포와 배경을 상당히 더럽힌(?) 것
  2. COCO Format에서 Pascal VOC Format으로 변경할 때, Bounding Box 좌표의 차이를 구현하지 못한 것


🛠️ 1. 새롭게 추가한 데이터셋 문제 해결

상황

크롤링으로 수집하여 추가했던 새로운 데이터들에 3가지의 문제가 있었다.

  1. 기존 데이터셋과 많이 다른 배경과 양상을 띠는 이미지들 존재
  2. Labeling할 때 Bounding Box 여백 불일치
  3. 과도한 AUgmenations, 특히 Boudning Box Rotation을 적용하는데 그에 따른 Bounding Box 좌표 변경이 적용이 안 됨

👉 이에 따라 기존 데이터셋과 많이 다른 데이터 분포와 배경이 포함된 데이터들이 추가됐다.
👉 그리고 과도한 Augmentations 적용은 노이즈로써 작용이 되어서 그것이 모델이 훈련하는 데에 혼란을 초래해 일정 epoch가 넘어가면 성능 향상이 중지되었다.


1. 기존 데이터셋과 많이 다른 배경과 양상을 띠는 이미지들 존재

데이터를 크롤링할 당시, 부족한 객체를 보충하는 것에 너무 집중한 나머지, 기존 데이터셋을 고려하여 "양질"의 데이터를 추가해야되는 것을 망각했다.

지금 보면 초보적인 실수이지만, 추가했던 데이터셋을 살펴보니 배경이 아예 없는 이미지, 활주로에 존재하지 않는 물체, 건물이 포함되서 Labeling된 이미지, Watermark가 포함된 이미지들을 발견할 수 있었다.


2. Labeling할 때 Bounding Box 여백 불일치

Roboflow에서 Labeling을 할 때, Bounding Box의 여백을 일관되게 해야 하는데, 그렇게 하지 못했던 것을 발견했다.

객체가 tight 하게 혹은 loose 하게 지정되어 있었다.

현업에 있는 선배님이 이러한 Bounding box의 여백 불일치도 모델 성능 저하의 원인이 될 수 있다고 하셨다.


3. 과도한 AUgmenations, 특히 Boudning Box Rotation을 적용하는데 그에 따른 Bounding Box 좌표 변경이 적용이 안 됨

크롤링한 데이터셋의 Labeling을 마치고, Augmentations을 적용해서 데이터 증강을 시도할 때 적용했던 것들이 Noise로써 작용이 되어서 모델 학습에 혼란을 초래한 것 같았다.

특히, Bounding Box Rotation도 적용하였는데 Rotation에 따른 Bounding Box 좌표 변경이 적용이 안 된 상태였던 것을 확인했다.

시도

1. 기존 데이터셋과 많이 다른 배경과 양상을 띠는 이미지들 제거

  • 크롤링해서 생성했던 5개 클래스에 대한 데이터셋을 하나씩 살펴보며, 모델 학습에 방해가 될 만한 것들을 다 제거했다.

2. Augmenations 간소화해서 데이터 증강

  • 불필요한 데이터들을 제거한 데이터셋을 Augmenations을 활용하여 증강할 때, Augmentations은 Rotation 1°만 적용하여 생성했다.

결과

최초 크롤링으로 수집한 데이터를 추가한 2차 데이터셋에서 모델 성능 향상에 방해가 되는 요소들을 제거한 3차 데이터셋을 구축했다.

추가됐던 1,110장에서 약 500장 정도가 남아서 3차 데이터셋의 수는 소량 감소했다.



🛠️ 2. Bounding Box Format의 불일치 문제 해결

상황

COCO Format JSon 파일에서 Pascal VOC 형식으로 각 이미지에 대해 Json 파일을 만들 때, COCO Format Bounding Box 데이터 형식을 그대로 적용한 것을 발견

  • (x1,y1,bbox_width,bbox_height)(x1,y1,x2,y2)로 변경하지 않고 bounding box 좌표 정보 원본 json 파일에 입력

시도

이미지마다 대응되는 json 파일을 만들 때, boudning box 정보를 입력하는 부분을 수정하였다.

....
    
# BBox 형태를 기존 COCO형태 [x1,y1,bbox_w,bbox_h]에서 Pascal VOC 형태로 수정

base_format['box'] = [0 for _ in range(4)] # bbox 담을 4 크기의 리스트 생성
base_format['box'][0] = anno_data['bbox'][0] # x1
base_format['box'][1] = anno_data['bbox'][1] # y1
base_format['box'][2] = anno_data['bbox'][0] + anno_data['bbox'][2] # (x1 + bbox_width)
base_format['box'][3] = anno_data['bbox'][1] + anno_data['bbox'][3] # (y1 + bbox_height)

data_dict['annotations'].append(base_format)
                                                
json.dump(data_dict , f_out, indent='\t') # img명을 가진 json 파일 각각 생성

....

결과

Bouding Box Format 불일치의 문제를 해결했다.





👉 5. 3차 학습으로 확인한 결과 개선


이렇게 모든 수정과 개선 과정을 거친 `3차 데이터셋`으로 학습을 진행했다.

Ablation Study로 산출한 최적의 하이퍼 파라미터 등을 적용하여 2차 학습과 비슷한 Epochs로 학습의 추이를 살펴봤다.

그 결과, Class Imbalance의 문제로 인한 성능 저하가 기존보다는 다소 해소되었고, 불균형한 클래스들의 객체는 2차 데이터셋보다 줄었지만, 클래스별 더 높은 Recall과 AP값을 보여줬다.

최종적으로, 2차 학습 유사 Epochs 대비 mAP 약 78로 수정 전보다 향상된 성능을 확인할 수 있었다.

profile
성장과 연구하는 자세를 추구하는 AI 연구개발자

2개의 댓글

comment-user-thumbnail
2023년 1월 7일

너무 좋은 글이네요~ 잘 보고 갑니다.

1개의 답글