본문 바로가기

컴린이 탈출기/Vision

공부하며 정리해보는 MMDetection 튜토리얼 🤖 (1) - Config, Dataset, Data Pipelines

반응형

https://mmdetection.readthedocs.io/en/latest/

 

Welcome to MMDetection’s documentation! — MMDetection 2.15.0 documentation

© Copyright 2018-2021, OpenMMLab Revision 604bfe96.

mmdetection.readthedocs.io

MMDetection 공식 문서를 공부하며 정리한 글입니다!
오역한 부분이나 자연스러운 표현을 위해 의역한 부분이 있을 수 있습니다.
잘못된 내용에 대한 댓글로 부탁드립니다. :)



MMDetection이란?

MMDetection은 Pytorch 기반의 Object Detection 오픈소스 라이브러리이다. 전체 프레임워크를 모듈 단위로 분리해 관리할 수 있다는 것이 가장 큰 특징이다.

Framework of MMDetection / 2-stage Detector 중 하나인 Faster R-CNN의 구조

1 stage 모델의 경우 Backbone / Neck / DenseHead 모듈로, 2 stage 모델의 경우 여기에 RoIHead 모듈을 추가, 총 4개의 모듈로 구분된다.

◾ Backbone - 입력 이미지를 특징 맵으로 변형시켜 줌 (ex. VGG, ResNet, ResNeXt 등)
◾ Neck - Backbone과 Head를 연결. 특징 맵을 정제, 재구성한다. (ex. FPN)
DenseHead - 특징 맵의 Dense location을 수행하는 부분이다. (ex. RPN; Region Proposal Network)
RoIHead - RoI 특징을 입력으로 받아 box 분류, 좌표 등을 예측하는 부분이다.

 

FPN

 

이러한 각각의 모듈은 Config 파일을 통해 통제된다.


Learn about Config 

MMDet은 모듈, 상속식의 디자인을 Config 시스템을 통해 통합시켰다. 이 시스템은 다양한 실험을 진행하기에 아주 편리하다. 

Config File Structure

config/_base_ 폴더 (https://github.com/open-mmlab/mmdetection/tree/master/configs/_base_) 안에는 4개의 기본 구성 요소, dataset, model, schedule, default_runtime가 위치한다. 같은 폴더 안에 있는 configs들은 오직 하나의 primitive(원시) config만을 갖도록 권장된다. 그리고 다른 모든 config들은 이 primitive config를 상속하면 된다.  

쉬운 이해를 위해 contributor들에게도 존재하는 메서드를 상속할 것을 추천한다.  Faster R-CNN에 기반한 구조라면 _base_ = ../faster_rcnn/faster_rcnn_r50_fpn_1x_coco_.py 를 명시하여 기본 Faster R-CNN 구조를 먼저 상속받고, 이후에 필요한 필드를 수정하는 식이다.

+

models config를 먼저 간단히 살펴보면, backbone / neck / rpn_head (=DenseHead) / roi_head로 크게 구분되는 모델과 관련된 부분과 train/test에 관한 부분으로 구성되어 있다.

# https://github.com/open-mmlab/mmdetection/blob/master/configs/_base_/models/faster_rcnn_r50_fpn.py

# model settings
model = dict(
    type='FasterRCNN',
    backbone=dict(
        type='ResNet',
        depth=50,
        num_stages=4,
        out_indices=(0, 1, 2, 3),
        frozen_stages=1,
        norm_cfg=dict(type='BN', requires_grad=True),
        norm_eval=True,
        style='pytorch',
        init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50')),
    neck=dict(
        type='FPN',
        in_channels=[256, 512, 1024, 2048],
        out_channels=256,
        num_outs=5),
    rpn_head=dict(
        type='RPNHead',
        in_channels=256,
        feat_channels=256,
        anchor_generator=dict(
            type='AnchorGenerator',
            scales=[8],
            ratios=[0.5, 1.0, 2.0],
            strides=[4, 8, 16, 32, 64]),
        bbox_coder=dict(
            type='DeltaXYWHBBoxCoder',
            target_means=[.0, .0, .0, .0],
            target_stds=[1.0, 1.0, 1.0, 1.0]),
        loss_cls=dict(
            type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0),
        loss_bbox=dict(type='L1Loss', loss_weight=1.0)),
    roi_head=dict(
        type='StandardRoIHead',
        bbox_roi_extractor=dict(
            type='SingleRoIExtractor',
            roi_layer=dict(type='RoIAlign', output_size=7, sampling_ratio=0),
            out_channels=256,
            featmap_strides=[4, 8, 16, 32]),
        bbox_head=dict(
            type='Shared2FCBBoxHead',
            in_channels=256,
            fc_out_channels=1024,
            roi_feat_size=7,
            num_classes=80,
            bbox_coder=dict(
                type='DeltaXYWHBBoxCoder',
                target_means=[0., 0., 0., 0.],
                target_stds=[0.1, 0.1, 0.2, 0.2]),
            reg_class_agnostic=False,
            loss_cls=dict(
                type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0),
            loss_bbox=dict(type='L1Loss', loss_weight=1.0))),
            
    # model training and testing settings
    train_cfg=dict(
        rpn=dict(
            assigner=dict(
                type='MaxIoUAssigner',
                pos_iou_thr=0.7,
                neg_iou_thr=0.3,
                min_pos_iou=0.3,
                match_low_quality=True,
                ignore_iof_thr=-1),
            sampler=dict(
                type='RandomSampler',
                num=256,
                pos_fraction=0.5,
                neg_pos_ub=-1,
                add_gt_as_proposals=False),
            allowed_border=-1,
            pos_weight=-1,
            debug=False),
        rpn_proposal=dict(
            nms_pre=2000,
            max_per_img=1000,
            nms=dict(type='nms', iou_threshold=0.7),
            min_bbox_size=0),
        rcnn=dict(
            assigner=dict(
                type='MaxIoUAssigner',
                pos_iou_thr=0.5,
                neg_iou_thr=0.5,
                min_pos_iou=0.5,
                match_low_quality=False,
                ignore_iof_thr=-1),
            sampler=dict(
                type='RandomSampler',
                num=512,
                pos_fraction=0.25,
                neg_pos_ub=-1,
                add_gt_as_proposals=True),
            pos_weight=-1,
            debug=False)),
    test_cfg=dict(
        rpn=dict(
            nms_pre=1000,
            max_per_img=1000,
            nms=dict(type='nms', iou_threshold=0.7),
            min_bbox_size=0),
        rcnn=dict(
            score_thr=0.05,
            nms=dict(type='nms', iou_threshold=0.5),
            max_per_img=100)
        # soft-nms is also supported for rcnn testing
        # e.g., nms=dict(type='soft_nms', iou_threshold=0.5, min_score=0.05)
    ))

 

활용 맛보기

from mmcv import Config
from mmdet.datasets import build_dataset
from mmdet.models import build_detector
from mmdet.apis import train_detector
from mmdet.datasets import (build_dataloader, build_dataset,
                            replace_ImageToTensor)

# config 파일 불러오기
cfg = Config.fromfile('./configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py')

# 일부 필드 변경
classes = ("UNKNOWN", "General trash", "Paper", "Paper pack", "Metal", "Glass", 
           "Plastic", "Styrofoam", "Plastic bag", "Battery", "Clothing")

cfg.data.train.classes = classes

cfg.seed = 2021

# 모델 build
model = build_detector(cfg.model)

# 데이터셋 build
datasets = [build_dataset(cfg.data.train)]

# train
train_detector(model, datasets[0], cfg)

모델을 학습 또는 inference할 때는 위와 같이 config 파일을 불러오고, 일부 필드를 변경한 뒤, 모델과 데이터셋을 build, 학습/inference를 진행한다.

그리고 이 때 불러온 config는 다음과 같은 파일이다. (상속)

# faster_rcnn_r50_fpn_1x_coco.py

_base_ = [
    '../_base_/models/faster_rcnn_r50_fpn.py',
    '../_base_/datasets/coco_detection.py',
    '../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py'
]

 

Customize Datasets

Reorganized new data formats to existing format

사용하고자 하는 데이터셋이 COCO 포맷일 경우, 다음과 같은 annotation을 체크하자!

+ COCO 포맷은 Info, Licences, Images, Categories, Annotations, 크게 5가지로 구분된 정보를 담고 있다. 

 

COCO Data format과 Pycocotools

COCO Dataset은 Object Detection, Segmentation, Keypoint Detection 등을 위한 데이터셋으로, 매년 전 세계의 여러 대학과 기업이 참가하는 대회에서 사용되고 있습니다. COCO Dataset 자체를 이용하기도 하지만..

comlini8-8.tistory.com

 

1. COCO 데이터의 Categories 필드의 길이는 Classes 필드의 튜플 길이와 일치해야 한다. 이 값은 Class의 개수를 의미한다.

2. Config 파일의 Classes 필드는 COCO 데이터의 Categories annotation과 같은 요소, 같은 순서를 가져야 한다.

3. COCO 데이터의 Annotations 필드는 유효해야 한다. 즉, category_id 내에 있는 모든 값은 categories의 id에 속해야 한다.

'annotations': [
    {
        'segmentation': [[192.81,
            247.09,
            ...
            219.03,
            249.06]],  # if you have mask labels
        'area': 1035.749,
        'iscrowd': 0,
        'image_id': 1268,
        'bbox': [192.81, 224.8, 74.73, 33.43],
        'category_id': 16,
        'id': 42986
    },
    ...
],

# MMDetection automatically maps the uncontinuous `id` to the continuous label indices.
'categories': [
    {'id': 1, 'name': 'a'}, {'id': 3, 'name': 'b'}, {'id': 4, 'name': 'c'}, {'id': 16, 'name': 'd'}, {'id': 17, 'name': 'e'},
 ]

 

Customize datasets by dataset wrappers

MMDetection은 학습을 위해 데이터셋을 섞거나 분포를 수정하기 위한 많은 dataset wrapper를 지원한다. 현재 MMDet이 지원하는 세 개의 dataset wrapper는 다음과 같다.

◾ RepeatDataset : 전체 데이터셋을 단순히 반복한다.
◾ ClassBalancedDataset : Class balanced 한 방식으로 데이터셋을 반복한다.
◾ ConcatDataset : 데이터셋을 Concat 한다.

이 dataset wrapper는 다음과 같이 사용된다.

dataset_A_train = dict(
    type='RepeatDataset',
    times=N,
    dataset=dict(
        type='CocoDataset',
        ...
        pipeline=train_pipeline
    )
)
dataset_A_val = dict(
    ...
    pipeline=test_pipeline
)
dataset_A_test = dict(
    ...
    pipeline=test_pipeline
)
dataset_B_train = dict(
    type='RepeatDataset',
    times=M,
    dataset=dict(
        type='CocoDataset',
        ...
        pipeline=train_pipeline
    )
)

data = dict(
    imgs_per_gpu=2,
    workers_per_gpu=2,
    train = [
        dataset_A_train,
        dataset_B_train
    ],
    val = dataset_A_val,
    test = dataset_A_test
)

 

+ 헷갈리는 💦 data의 구성은 다음과 같다. 아래 코드는 가장 큰 기본 골격이다.

data = dict(
	samples_per_gpu = 4,
    workers_per_gpu = 2,
    train = dict(),
    val = dict(),
    test = dict()
)

 

그리고 train, val, test 자리에는 다음과 같은 dict가 들어간다.

dataset_type = 'CocoDataset'
classes = ('person', 'bicycle', 'car')

train_pipeline [
	dict(type='LoadImageFromFile'),
    dict(type='LoadAnnotations', with_bbox = True),
    dict(type='Resize', img_scale = (512,512), keep_ratio = True),
    dict(type='RandomFlip', flip_ratio = 0.5),
    dict(type='Normalize', **img_norm_cfg),
    dict(type='DefaultFormatBundle'),
    dict(type='collect', keys=['img', 'gt_bboxes', 'gt_labels']),
]

train = dict(
	type = dataset_type,
    classes = classes,
    ann_file = data_root + 'train.json',
    img_prefix = data_root,
    pipeline = train_pipeline
    )

이때 dataset_type에는 'CocoDataset'과 같은 레벨의 값이 들어가고, classes에는 class 명들을 담은 튜플이 들어간다. pipeline에는 위에서도 볼 수 있듯이 이미지 로드부터 augmentation 등 데이터 파이프라인 정보가 들어간다. 이에 대해서는 데이터셋 내용 이후에 자세히 알아보도록 하자.

 

Modify Dataset Classes

우리는 다음과 같이 annotation의 class 이름을 수정해 기존 데이터셋 유형을 사용할 수 있다. 이 때 데이터셋은 classes에 속하지 않는 GT box들을 자동으로 필터링할 것이다.

classes = ('person', 'bicycle', 'car')
data = dict(
    train=dict(classes=classes),
    val=dict(classes=classes),
    test=dict(classes=classes))

 

Customize Data Pipelines

Design of Data pipelines

전형적인 컨벤션을 따라 MMDet 역시 데이터를 로드할 때 Dataset과 Dataloader를 사용한다. Dataset은 데이터의 아이템을 dict로 리턴한다. Object Detection의 데이터들은 대부분 같은 사이즈가 아니기 때문에 (이미지 사이즈, GT bbox 사이즈 등) DataContainer라는 새로운 개념을 도입하였다. 

데이터 준비 파이프라인은 데이터셋과 구분된다. 보통 데이터셋은 annotation을 어떻게 처리할지를 정의하고, 데이터 파이프라인은 data dict를 준비하는 모든 단계를 정의한다. 파이프라인은 일련의 동작들로 구성되어 있고, 각각의 오퍼레이션들은 dict를 인풋으로 취하고, 다음 transform을 위한 dict를 아웃풋으로 리턴한다.


전체 파이프라인은 다음과 같고, 오퍼레이션들은 데이터 로드 / 전처리 / 포맷팅 / TTA로 크게 구분된다.

Pipeline figure

 

img_norm_cfg = dict(
    mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
train_pipeline = [
    dict(type='LoadImageFromFile'),
    dict(type='LoadAnnotations', with_bbox=True),
    dict(type='Resize', img_scale=(1333, 800), keep_ratio=True),
    dict(type='RandomFlip', flip_ratio=0.5),
    dict(type='Normalize', **img_norm_cfg),
    dict(type='Pad', size_divisor=32),
    dict(type='DefaultFormatBundle'),
    dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']),
]
test_pipeline = [
    dict(type='LoadImageFromFile'),
    dict(
        type='MultiScaleFlipAug',
        img_scale=(1333, 800),
        flip=False,
        transforms=[
            dict(type='Resize', keep_ratio=True),
            dict(type='RandomFlip'),
            dict(type='Normalize', **img_norm_cfg),
            dict(type='Pad', size_divisor=32),
            dict(type='ImageToTensor', keys=['img']),
            dict(type='Collect', keys=['img']),
        ])
]

 각각의 오퍼레이션에 대한 자세한 내용은 docs 하단에서 확인할 수 있다.

 

이어지는 글

공부하며 정리해보는 MMDetection 튜토리얼 🤖 (2) - Model, Runtime Setting (Optimizer, Scheduler 등), Finetuning Models, Weight Initialization

 

Reference

https://github.com/open-mmlab/mmdetection

https://wordbe.tistory.com/entry/MMDetection-%EB%85%BC%EB%AC%B8-%EC%A0%95%EB%A6%AC-%EB%B0%8F-%EB%AA%A8%EB%8D%B8-%EA%B5%AC%ED%98%84

반응형