본문 바로가기

컴린이 탈출기/Vision

공부하며 정리해보는 Detectron2 튜토리얼 🌠 (2) - Model, Training, Evaluation, Yacs configs, Lazy configs

반응형

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

 

Welcome to detectron2’s documentation! — detectron2 0.5 documentation

© Copyright 2019-2020, detectron2 contributors Revision 64e84c5b.

detectron2.readthedocs.io

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



이전 글

공부하며 정리해보는 Detectron2 튜토리얼 🌠 (1) - Dataset, Dataloader, Data Augmentation

 

Use Models

Build Models from Yacs Config

yacs config로부터 모델을 빌드할 때는 build_model, build_backbone, build_roi_heads 함수를 이용할 수 있다.

from detectron2.modeling import build_model
model = build_model(cfg)  # returns a torch.nn.Module

build_model은 랜덤한 웨이트로 채워진 모델 구조를 리턴한다.

 

Load/Save a checkpoint

from detectron2.checkpoint import DetectionCheckpointer
DetectionCheckpointer(model).load(file_path_or_url)  # load a file, usually from cfg.MODEL.WEIGHTS

checkpointer = DetectionCheckpointer(model, save_dir="output")
checkpointer.save("model_999")  # save to output/model_999.pth

Detectron2의 checkpointe는 pytorch의 .pth 포맷, model zoo의 pkl 포맷을 인식한다. 

 

Use a Model

모델은 outputs = model(inputs) 방식으로 불러올 수 있다. 이때 input은 list [dict] 형식. 각각의 dict는 하나의 이미지와 대응되고, 모델의 종류, 학습/평가 모드에 따라 필요한 key를 가지고 있다. 

# Training

학습 모드에서는 모든 모델이 학습 관련 통계량을 저장하는 EvenStorage 하에서 사용되어야 한다. 

from detectron2.utils.events import EventStorage
with EventStorage() as storage:
  losses = model(inputs)

 

# Inference

단순히 Inference만을 하고자 할 때는 간단한 end-to-end predictor를 리턴하는  DefaultPredictor wrapper를 사용하면 된다.  모델 로드, 전처리 등 기능을 포함하고 있고, 배치가 아닌 하나의 이미지에 대해 동작한다.

model.eval()
with torch.no_grad():
  outputs = model(inputs)

 

Model Input Format

유저들은 임의의 input 포맷을 지원하는 custom 모델을 사용할 수 있다. 아래는 Detectron2 내의 모든 builtin 모델이 지원하는 기본적인 input 포맷에 대한 설명이다. 이 모델들은 모두 list [dict] 형태를 input으로 취한다.

# image

(C, H, W) 포맷의 Tensor. 채널들의 의미는 cfg.INPUT.FORMAT으로 정의될 수 있다. cfg.MODEL.PIXEL_(MEAN, STD)를 통해 Image normalization을 할 수 있다.

# height, weight

원하는 output의 height, weight 값이다. image 필드의 height, width 값과 반드시 같을 필요는 없다. 예를 들어 전처리 단계에 resize가 있다면 image 필드는 resize된 이미지를 포함하게 된다. 하지만 원본과 같은 resolution의 아웃풋을 원한다면, height와 weight 값을 모델에게 제공하면 된다. 

builtin 모델의 inference 시에는 image key가 반드시 필요하고, wieght와 height는 옵셔널하다.

# instances

학습에 사용되는 instance 객체로 gt_boxes / gt_classes / gt_masks / gt_keypoints와 같은 필드가 있다.

# sem_seg

(H, W) 포맷의 Tensor [int]로, semantic segmentation의 GT이다. 카테고리 라벨은 0부터 시작한다. 

# proposal

Fast R-CNN 계열의 모델에서만 쓰이는 Instance 객체로, proposal_boxes / objectness_logits 필드가 있다. 

 

+ How it connects to data loader

: 기본 DatasetMapper의 아웃풋은 위의 포맷을 따르는 dict이다. Dataloader가 batching (이미지를 batch로 만들었다 이런 뜻인 듯!) 한 후에는 builtin 모델들이 지원하는 list [dict] 포맷이 된다.

 

Model Output Format

학습 모드에서는 builtin 모델은 loss와 함께 dict [str→ScalarTensor]을 리턴한다. 반면 Inference 모드에서 builtin 모델은 list [dict]를 리턴한다. 이때 하나의 dict가 하나의 이미지에 대응한다. 하나의 dict는 모델의 태스크에 맞는 다음과 같은 필드를 가지고 있다.

# instances

Instance 객체는 pred_boxes / scores / pred_classes / pred_masks / pred_keypoints와 같은 필드를 갖고 있다.

# sem_seg

(num_categories, H, W) 사이즈의 Tensor# proposalproposal boxes / objectness_logits 필드를 갖는 Instance 객체# panoptic_seg(pred:Tensor, segments_info : Optional [list [dict]]) 형식의 튜플. 이때 pred 텐서는 각각의 픽셀에 대한 segment id 값을 가진 (H, W) 쉐입의 텐서이다.

 

Partially execute a model

후처리 전의 output, 특정 레이어의 인풋과 같이 모델 중간의 tensor를 얻고자 할 때는 다음과 같은 옵션을 사용할 수 있다.

1. 우리가 필요로 하는 아웃풋을 리턴하는 모델의 일부 구성 요소(ex. 모델 헤드), 즉 서브 모델을 작성한다.

2.  forward()를 통해 부분적으로 모델을 실행한다. 다음은 mask head 이전에 mask feature를 얻는 코드이다.

images = ImageList.from_tensors(...)  # preprocessed input tensor
model = build_model(cfg)
model.eval()
features = model.backbone(images.tensor)
proposals, _ = model.proposal_generator(images, features)
instances, _ = model.roi_heads(images, features, proposals)
mask_features = [features[f] for f in model.roi_heads.in_features]
mask_features = model.roi_heads.mask_pooler(mask_features, [x.pred_boxes for x in instances])

3. 특정 모듈의 input 혹은 output을 얻도록 도와주는 forward hook을 이용한다. 

 

Write Models

완전히 새로운 모델(builtin 모델이 아닌!)을 사용하고자 하더라도 대부분의 경우 존재하는 모델의 요소들을 수정 혹은 확장하는 식의 방식이 좋다. 따라서 유저들이 표준 모델의 특정 내부 구성요소의 동작을 재정의할 수 있는 방법을 알아보자.

Register New components

Detectron2는 backbone feature extractor, box head와 같이 유저들이 자주 커스터마이징하고 싶어 하는 모듈에 대해 registration 매커니즘을 제공한다.

예를 들어 새로운 backbone을 추가하고자 한다면, Backbone 클래스의 인터페이스에 따라 새로운 Backbone을 구현, Backbone_REGISTRY에 등록하면 된다.

from detectron2.modeling import BACKBONE_REGISTRY, Backbone, ShapeSpec

@BACKBONE_REGISTRY.register()
class ToyBackbone(Backbone):
  def __init__(self, cfg, input_shape):
    super().__init__()
    # create your own backbone
    self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=16, padding=3)

  def forward(self, image):
    return {"conv1": self.conv1(image)}

  def output_shape(self):
    return {"conv1": ShapeSpec(channels=64, stride=16)}

위와 같은 코드를 import 한 후 다음과 같은 코드를 작성하면 된다. (class 이름을 이용!)

cfg = ...   # read a config
cfg.MODEL.BACKBONE.NAME = 'ToyBackbone'   # or set it in the config file
model = build_model(cfg)  # it will find `ToyBackbone` defined above

 

또 다른 예로, 일반적인 R-CNN 구조의 ROI 헤드에 새로운 기능을 추가하려면 새 ROI 헤드의 서브 클래스를 구현, ROI_HEADS_REGISTRY에 넣으면 된다.

 

Construct Models with Explicit Arguments

예를 들어 커스텀 loss 함수를 Faster R-CNN의 box head에서 사용하고자 할 때는, 다음과 같이 하면 된다.

1. 현재 loss는 FASTRCNNOutputLayers에서 계산된다. 따라서 우리는 이것의 변형 혹은 서브클래스를 커스텀, 실행하면 된다.

2. builtin 된 FastRCNNOutputLayers 대신 StandardROIHeads를 box_predictor = MyRCNNOutput() 아규먼트를 주고  호출한다. 

roi_heads = StandardROIHeads(
  cfg, backbone.output_shape(),
  box_predictor=MyRCNNOutput(...)
)

3. (optional) 만약 이것을 config file로부터 가능하게 하기 위해서는 다음과 같은 registration이 필요하다.

@ROI_HEADS_REGISTRY.register()
class MyStandardROIHeads(StandardROIHeads):
  def __init__(self, cfg, input_shape):
    super().__init__(cfg, input_shape,
                     box_predictor=MyRCNNOutput(...))

 

Training

학습하는 방법에는 다음과 같은 두 스타일이 있다.

Custom Training Loop

모델과 dataloader가 준비된 상태에선, 직접 training loop를 작성하면 된다. 이 스타일은 사용자로 하여금 전체 학습 로직을 관리할 수 있게 한다. tools/plain_train_net.py에서 예를 확인할 수 있다.

 

GitHub - facebookresearch/detectron2: Detectron2 is FAIR's next-generation platform for object detection, segmentation and other

Detectron2 is FAIR's next-generation platform for object detection, segmentation and other visual recognition tasks. - GitHub - facebookresearch/detectron2: Detectron2 is FAIR's next-genera...

github.com

그리고 Training logic customization은 유저들이 쉽게 할 수 있다.

 

Trainer Abstraction

Detectron2는 또한 hook 시스템이 있는 trainer abstraction을 제공한다. 그리고 여기에는 다음과 같은 두 개의 인스턴스가 있다.

# SimpleTrainer

SimpleTrainer는 single-cost single-optimizer single-data-source training을 위한 가장 기본적인 루프를 제공한다. Checkpointing, logging 등은 hook system을 통해 실행될 수 있다.

hook.before_train()
for iter in range(start_iter, max_iter):
    hook.before_step()
    trainer.run_step()
    hook.after_step()
iter += 1
hook.after_train()

 

# DefaultTrainer

DefaultTrainer는 tools/train_net.py와 많은 스크립트에서 사용되는, yacs config를 이용해 초기화된 SimpleTrainer이다. 이것은 optimizer를 위한 config, learning rate schedule, loggin, 평가, checkpointing 등 더 많은 옵션을 가지고 있다.

DefaultTrainer를 커스터마이징하기 위해선 다음과 같은 방법을 따라야 한다.

1. 단순한 커스터마이징을 할 때에는 tools/train_net.py와 같이 서브 클래스를 덮어쓰면 된다.

class Trainer(DefaultTrainer):
    """
    We use the "DefaultTrainer" which contains pre-defined default logic for
    standard training workflow. They may not work for you, especially if you
    are working on a new research project. In that case you can write your
    own training loop. You can use "tools/plain_train_net.py" as an example.
    """

    @classmethod
    def build_evaluator(cls, cfg, dataset_name, output_folder=None):
        return build_evaluator(cfg, dataset_name, output_folder)

    @classmethod
    def test_with_TTA(cls, cfg, model):
        logger = logging.getLogger("detectron2.trainer")
        # In the end of training, run an evaluation with TTA
        # Only support some R-CNN models.
        logger.info("Running inference with test-time augmentation ...")
        model = GeneralizedRCNNWithTTA(cfg, model)
        evaluators = [
            cls.build_evaluator(
                cfg, name, output_folder=os.path.join(cfg.OUTPUT_DIR, "inference_TTA")
            )
            for name in cfg.DATASETS.TEST
        ]
        res = cls.test(cfg, model, evaluators)
        res = OrderedDict({k + "_TTA": v for k, v in res.items()})
        return res


def setup(args):
    """
    Create configs and perform basic setups.
    """
    cfg = get_cfg()
    cfg.merge_from_file(args.config_file)
    cfg.merge_from_list(args.opts)
    cfg.freeze()
    default_setup(cfg, args)
    return cfg


def main(args):
    cfg = setup(args)

    if args.eval_only:
        model = Trainer.build_model(cfg)
        DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load(
            cfg.MODEL.WEIGHTS, resume=args.resume
        )
        res = Trainer.test(cfg, model)
        if cfg.TEST.AUG.ENABLED:
            res.update(Trainer.test_with_TTA(cfg, model))
        if comm.is_main_process():
            verify_results(cfg, res)
        return res

    """
    If you'd like to do anything fancier than the standard training logic,
    consider writing your own training loop (see plain_train_net.py) or
    subclassing the trainer.
    """
    trainer = Trainer(cfg)
    trainer.resume_or_load(resume=args.resume)
    if cfg.TEST.AUG.ENABLED:
        trainer.register_hooks(
            [hooks.EvalHook(0, lambda: trainer.test_with_TTA(cfg, trainer.model))]
        )
    return trainer.train()

2. 학습 동안의 다른 태스크를 위해서는 hook system을 이용해야 한다. 만약 학습 중에 hello를 프린트하기 위해서는 다음과 같이 하면 된다.

class HelloHook(HookBase):
  def after_step(self):
    if self.trainer.iter % 100 == 0:
      print(f"Hello at iteration {self.trainer.iter}!")

 

Logging of Metrics

학습을 진행하는 동안, Detectron2의 모델과 trainer는 중앙화된(centeralized) EvenStroage에 메트릭을 저장한다. 이 로그에는 다음과 같은 코드를 통해 접근할 수 있다.

from detectron2.utils.events import get_event_storage

# inside the model:
if self.training:
  value = # compute the value from inputs
  storage = get_event_storage()
  storage.put_scalar("some_accuracy", value)

 

Evaluation

모델을 직접 사용해 평가를 진행할 수도 있지만, Detectron2에서는 DatasetEvaluator라는 인터페이스를 제공한다. DatasetEvaluator를 이용하면 데이터셋에 맞는 metric을 계산할 수 있고, input/output 페어를 이용한 다른 작업들을 수행할 수 있다. 예를 들어, 다음과 같이 detect 된 객체가 몇 개인지 리턴할 수 있다.

class Counter(DatasetEvaluator):
  def reset(self):
    self.count = 0
  def process(self, inputs, outputs):
    for output in outputs:
      self.count += len(output["instances"])
  def evaluate(self):
    # save self.count somewhere, or print it, or return it.
    return {"count": self.count}

 

Use evaluators

evaluator는 다음과 같은 수동적인(manual) 방식으로 사용될 수 있다.

def get_all_inputs_outputs():
  for data in data_loader:
    yield data, model(data)

evaluator.reset()
for inputs, outputs in get_all_inputs_outputs():
  evaluator.process(inputs, outputs)
eval_results = evaluator.evaluate()


한편 Evaluator는 또한 inference_on_dataset과 함께 쓰일 수 있다.

eval_results = inference_on_dataset(
    model,
    data_loader,
    DatasetEvaluators([COCOEvaluator(...), Counter()]))

이것은 data_loader의 모든 인풋과 모델을 실행시킬 것이며, evaluator를 호출해 처리한다. 이전의 매뉴얼 한 방법과 비교했을 때, 이 함수의 장점은 DatasetEvaluator와 병합되어 사용될 수 있다는 점과 한 번의 dataset 포워딩을 통해 평가를 마칠 수 있다는 점이다.  이 함수는 또한 모델과 데이터셋에 대한 정확한 속도 벤치마크를 제공한다.

 

Evaluators for custom dataset

Detectron2의 많은 evaluator들은 특정 데이터셋을 위해 작성되었다. 하지만 COCOEvaluator와 SemSegEvaluator는 각각 box detection, instance segmentation, keypoint detection의 AP와 Semantic Segmentation 메트릭을 평가할 수 있기 때문에 custom 데이터셋을 평가할 때도 사용될 수 있다.

 

Yacs Configs

Detectron2는 key-value에 기반한 config 시스템을 제공한다. 이 시스템은 YAML과 yacs를 이용한다. Yaml은 매우 제한적인 언어로 Detectron2의 모든 기능을 config를 통해 사용할 수 없다. 따라서 config 단에서 사용할 수 없는 것은 API를 이용해야 한다.

조금 더 강력한 LazyConfig 시스템이 도입됨에 따라, 앞으로는 Yacs/Yaml 기반의 config system에 새로운 기능이 추가되지 않는다.

 

Basic Usage

CfgNode의 몇 가지 기본적인 사용법이다. 자세한건 이 도큐먼트 참고!

from detectron2.config import get_cfg
cfg = get_cfg()    # obtain detectron2's default config
cfg.xxx = yyy      # add new configs for your own custom components
cfg.merge_from_file("my_cfg.yaml")   # load values from a file

cfg.merge_from_list(["MODEL.WEIGHTS", "weights.pth"])   # can also load values from a list of str
print(cfg.dump())  # print formatted configs
with open("output.yaml", "w") as f:
  f.write(cfg.dump())   # save config to file

기본 yaml 문법에 더해, config 파일은 가장 먼저 불러와지는 기본 config 파일인 __BASE__:base.yaml 필드에 정의될 수 있다. base config의 값들은 sub config들에 의해 덮어 쓰일 수 있다.

Detectron2의 많은 builtin tool은 command line을 통한 덮어쓰기를 허용한다. 커맨드 라인으로 제공되는 key-value 페어는 현재 config 파일의 값을 덮어쓴다.

./demo.py --config-file config.yaml [--other-options] \
  --opts MODEL.WEIGHTS /path/to/weights INPUT.MIN_SIZE_TEST 1000

 

Best Practice with Configs

1. Config를 코드처럼 다뤄라! 

Config가 copy 되거나 duplicate 하는 것을 피해야 한다. 따라서 __BASE__를 통해 config들 간 중복되는 부분을 공유해서 써야 한다.

2. 단순한 Config를 작성하자!

실험 세팅에 영향을 주지 않는 key를 포함시키지 말자.

 

Lazy Configs

전통적인 yacs 기반의 config 시스템은 기본적이고 표준적인 기능을 제공한다. 하지만 이 방식은 많은 새로운 프로젝트에 대해 유연하지 않기 때문에 Lazy config라는 대안을 개발하게 되었다. 

Python Syntax

이 Config는 여전히 dictionary 형태를 취하지만, dict를 선언하기 위해 Yaml을 사용하는 대신 곧바로 Python을 사용한다. 이것은 유저로 하여금 dict의 쉬운 수정, 더 많은 데이터 타입 등의 Yaml이 가질 수 없는 장점들을 갖게 한다.

이 Python config 파일은 다음과 같이 불러온다.

# config.py:
a = dict(x=1, y=2, z=dict(xx=1))
b = dict(x=3, y=4)

# my_code.py:
from detectron2.config import LazyConfig
cfg = LazyConfig.load("path/to/config.py")  # an omegaconf dictionary
assert cfg.a.z.xx == 1

LazyConfig를 로드하면 cfg는 글로벌한 범위에 정의된 모든 dict를 포함하는 dict가 된다. (무슨 말이지...) 또한 LazyConfig.save는 config 객체를 yaml으로 저장한다.

 

Recursive instantiation

LazyConfig 시스템은 함수와 클래스를 호출하기 위해 dict를 사용하는 패턴인 Recursie instantiation을 사용한다. LazyCall은 이 dict를 생성하는 것을 도와주는 함수이다.  다음은 LazyCall을 이용하는 코드이다.

from detectron2.config import LazyCall as L
from my_app import Trainer, Optimizer
cfg = L(Trainer)(
  optimizer=L(Optimizer)(
    lr=0.01,
    algo="SGD"
  )
)

 

dict 생성은 다음과 같이한다.

cfg = {
  "_target_": "my_app.Trainer",
  "optimizer": {
    "_target_": "my_app.Optimizer",
    "lr": 0.01, "algo": "SGD"
  }
}

이러한 dict를 통해 객체를 표현하고, instantiate 함수는 이것을 실제 객체로 바꾼다.

from detectron2.config import instantiate
trainer = instantiate(cfg)
# equivalent to:
# from my_app import Trainer, Optimizer
# trainer = Trainer(optimizer=Optimizer(lr=0.01, algo="SGD"))

이러한 패턴은 다음과 같은 복잡한 객체를 표현할 때 아주 강력하다.

# A Full Mask R-CNN described in recursive instantiation

from detectron2.config import LazyCall as L
from detectron2.layers import ShapeSpec
from detectron2.modeling.meta_arch import GeneralizedRCNN
from detectron2.modeling.anchor_generator import DefaultAnchorGenerator
from detectron2.modeling.backbone.fpn import LastLevelMaxPool
from detectron2.modeling.backbone import BasicStem, FPN, ResNet
from detectron2.modeling.box_regression import Box2BoxTransform
from detectron2.modeling.matcher import Matcher
from detectron2.modeling.poolers import ROIPooler
from detectron2.modeling.proposal_generator import RPN, StandardRPNHead
from detectron2.modeling.roi_heads import (
    StandardROIHeads,
    FastRCNNOutputLayers,
    MaskRCNNConvUpsampleHead,
    FastRCNNConvFCHead,
)

model = L(GeneralizedRCNN)(
    backbone=L(FPN)(
        bottom_up=L(ResNet)(
            stem=L(BasicStem)(in_channels=3, out_channels=64, norm="FrozenBN"),
            stages=L(ResNet.make_default_stages)(
                depth=50,
                stride_in_1x1=True,
                norm="FrozenBN",
            ),
            out_features=["res2", "res3", "res4", "res5"],
        ),
        in_features="${.bottom_up.out_features}",
        out_channels=256,
        top_block=L(LastLevelMaxPool)(),
    ),
    proposal_generator=L(RPN)(
        in_features=["p2", "p3", "p4", "p5", "p6"],
        head=L(StandardRPNHead)(in_channels=256, num_anchors=3),
        anchor_generator=L(DefaultAnchorGenerator)(
            sizes=[[32], [64], [128], [256], [512]],
            aspect_ratios=[0.5, 1.0, 2.0],
            strides=[4, 8, 16, 32, 64],
            offset=0.0,
        ),
        anchor_matcher=L(Matcher)(
            thresholds=[0.3, 0.7], labels=[0, -1, 1], allow_low_quality_matches=True
        ),
        box2box_transform=L(Box2BoxTransform)(weights=[1.0, 1.0, 1.0, 1.0]),
        batch_size_per_image=256,
        positive_fraction=0.5,
        pre_nms_topk=(2000, 1000),
        post_nms_topk=(1000, 1000),
        nms_thresh=0.7,
    ),
    roi_heads=L(StandardROIHeads)(
        num_classes=80,
        batch_size_per_image=512,
        positive_fraction=0.25,
        proposal_matcher=L(Matcher)(
            thresholds=[0.5], labels=[0, 1], allow_low_quality_matches=False
        ),
        box_in_features=["p2", "p3", "p4", "p5"],
        box_pooler=L(ROIPooler)(
            output_size=7,
            scales=(1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32),
            sampling_ratio=0,
            pooler_type="ROIAlignV2",
        ),
        box_head=L(FastRCNNConvFCHead)(
            input_shape=ShapeSpec(channels=256, height=7, width=7),
            conv_dims=[],
            fc_dims=[1024, 1024],
        ),
        box_predictor=L(FastRCNNOutputLayers)(
            input_shape=ShapeSpec(channels=1024),
            test_score_thresh=0.05,
            box2box_transform=L(Box2BoxTransform)(weights=(10, 10, 5, 5)),
            num_classes="${..num_classes}",
        ),
        mask_in_features=["p2", "p3", "p4", "p5"],
        mask_pooler=L(ROIPooler)(
            output_size=14,
            scales=(1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32),
            sampling_ratio=0,
            pooler_type="ROIAlignV2",
        ),
        mask_head=L(MaskRCNNConvUpsampleHead)(
            input_shape=ShapeSpec(channels=256, width=14, height=14),
            num_classes="${..num_classes}",
            conv_dims=[256, 256, 256, 256, 256],
        ),
    ),
    pixel_mean=[103.530, 116.280, 123.675],
    pixel_std=[1.0, 1.0, 1.0],
    input_format="BGR",
)

 

Using Model Zoo LazyConfigs

Detectron2는 LazyConfig 시스템을 이용하는 model zoo config 일부를 제공한다. (ex - common baselines)

Detectron2 설치 후, model zoo API의 model_zoo.get_config를 통해 로드할 수 있다. 이 레퍼런스를 이용하면 custom config 구조/필드를 쉽게 정의할 수 있다.

반응형