なんの記事か
Kaggle Advent Calendar 2021の2枚目の18日目の記事です。
最近流行りのSwinTransformerを使って物体検出を触ってみます。SIIM2021 3rd解法では Swin Transformer with RepPoints(mmdetection)がアンサンブルの種として使用されています。自分で手を動かせばわかることがあるかもしれないと思い過去コンペで追試をしてみます。
行った追試は以下の通りです。
- swin_tiny mask_r_cnnの公式configをいじって使う
- augmentationを追加
- さらにaugmentationを強める
- backboneを大きいモデルに変更する
- 他のモデルを試す
- cocoの事前学習が有効か見てみる
追試に用いたデータ
kaggleの過去コンペ&csv提出ができる&自分のドメインに近い、という理由でVinBig を追試の対象としました。
アノテーションはdiscussionに多く挙げられていた「複数のアノテーターのbboxはナイーブにWBFで統合する」という方法で前処理し、single foldで学習、subをしています。手元のmAP0.5とprivateでのスコアを確認していきます。(スコアの確認が歯切れの悪い形になっています。ご了承ください)
以下全てのsubに2cls fillter をつけています。コンペ特有の課題(複数のアノテーターへの対処)は本記事では扱いません。推論のコードはこれを参考にしました。
実験
1. swin_tiny mask_r_cnnの公式configをいじって使う
mmdetectionの公式のレポジトリにはmask_Rcnn系列のbackboneにswin_tiny/smallを用いた場合のconfig/log/weightが存在します。
まずこれをそのまま使用したいのですがmaskの情報はvinbigには無いのでmask headを消しておきます。
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=14,
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)),
mask_roi_extractor=dict(
type='SingleRoIExtractor',
roi_layer=dict(type='RoIAlign', output_size=14, sampling_ratio=0),
out_channels=256,
featmap_strides=[4, 8, 16, 32]),
#mask_head=dict(
# type='FCNMaskHead',
# num_convs=4,
# in_channels=256,
# conv_out_channels=256,
# num_classes=14,
# loss_mask=dict(
# type='CrossEntropyLoss', use_mask=True, loss_weight=1.0))
),
次にbackboneを変更します。
backbone=dict(
type='SwinTransformer',
embed_dims=96,
depths=[2, 2, 6, 2],
num_heads=[3, 6, 12, 24],
window_size=7,
mlp_ratio=4,
qkv_bias=True,
qk_scale=None,
drop_rate=0.,
attn_drop_rate=0.,
drop_path_rate=0.2,
patch_norm=True,
out_indices=(0, 1, 2, 3),
with_cp=False,
convert_weights=True,
init_cfg=dict(type='Pretrained', checkpoint="https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_tiny_patch4_window7_224.pth'")),
neck=dict(
type='FPN',
in_channels=[96, 192, 384, 768],
out_channels=256,
num_outs=5),
load_from = ”~/mmdetection/checkpoints/mask_rcnn_swin-t-p4-w7_fpn_ms-crop-3x_coco_20210906_131725-bacf6f7b.pth”のように指定すれば事前学習済みの重みを使用できます。
結果は以下です。
val_mAP@0.5 | private LB | |
---|---|---|
tiny | 0.3600 | 0.223 |
small | 0.3560 | 0.221 |
2. augmentationを追加
albumentationsの使用例があったのでそのままコピペします。SIIM2021の3rd解法のものを借用したものも試しました。
val_mAP@0.5 | private LB | |
---|---|---|
small | 0.3560 | 0.221 |
small+aug | 0.3790 | 0.240 |
small+aug_3rd | 0.3840 | 0.234 |
augmentationの追加は気兼ねなくできる&伸び代もありそうなので良さそうです。
3. さらにaugmentationを強める
mmdetetionでのmosaic/mixupの使用にはyoloxのconfigをみれば良さそうです。datasetのtypeをMultiImageMixDatasetにするなどの修正が必要です。
img_scale = (1024,1024) # (1333, 800)
train_pipeline = [
#dict(type='LoadImageFromFile'),
#dict(type='LoadAnnotations', with_bbox=True),
dict(type='Mosaic', img_scale=img_scale, pad_val=114.0),
#dict(
# type='RandomAffine',
# scaling_ratio_range=(0.1, 2),
# border=(-img_scale[0] // 2, -img_scale[1] // 2)),
dict(
type='Albu',
transforms=albu_train_transforms,
bbox_params=dict(
type='BboxParams',
format='pascal_voc',
label_fields=['gt_labels'],
min_visibility=0.0,
filter_lost_elements=True),
keymap={
'img': 'image',
#'gt_masks': 'masks',
'gt_bboxes': 'bboxes'
},
update_pad_shape=False,
skip_img_without_anno=True),
#dict(
# type='MixUp',
# img_scale=img_scale,
# ratio_range=(0.8, 1.6),
# pad_val=114.0),
dict(type='Resize', img_scale=img_scale, 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']),
]
train_dataset = dict(
type='MultiImageMixDataset',
dataset=dict(
type=dataset_type,
classes=classes,
ann_file= '~/vinbig/dataset/fold0/annotations/train.json',
img_prefix= '~/vinbig/dataset/fold0/train2017/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True)
],
filter_empty_gt=False,
),
pipeline=train_pipeline)
data = dict(
samples_per_gpu=8,
workers_per_gpu=12,
#train=dict(
# type=dataset_type,
# classes=classes,
# ann_file='~/vinbig/dataset/fold0/annotations/train.json',
# img_prefix='~/vinbig/dataset/fold0/train2017/',
# pipeline=train_pipeline),
train = train_dataset,
val=dict(
type=dataset_type,
classes=classes,
ann_file='~/vinbig/dataset/fold0/annotations/val.json',
img_prefix= "~/vinbig/dataset/fold0/val2017/",
pipeline=test_pipeline),
val_mAP@0.5 | private LB | |
---|---|---|
small+aug | 0.3790 | 0.240 |
small+mosaic | 0.3310 | 0.208 |
適当に使って良いものでは無さそうです。
4. backboneを大きいモデルに変更する
SIIM2021の3rd解法のレポジトリにはモデルの大きさを大きくしてみるという実験を行った痕跡が残されているので倣ってみます。(swin_rsna000.py swin_rsna001.py)
backboneとneckの値をいじればなんとかなりそうです。以下はtiny→baseの変更をおこなったものです。
backbone=dict(
type='SwinTransformer',
#embed_dims=96,
embed_dim=128,
#embed_dim=192, #large
#depths=[2, 2, 6, 2],
depths=[2, 2, 18, 2], #largeもおなじ
#num_heads=[3, 6, 12, 24],
num_heads=[4, 8, 16, 32],
#num_heads=[6, 12, 24, 48], #large
#window_size=7,
window_size=12,
mlp_ratio=4,
qkv_bias=True,
qk_scale=None,
drop_rate=0.,
attn_drop_rate=0.,
drop_path_rate=0.2,
patch_norm=True,
out_indices=(0, 1, 2, 3),
with_cp=False,
convert_weights=True,
init_cfg=dict(type='Pretrained', checkpoint="https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_base_patch4_window12_384_22kto1k.pth'")),
neck=dict(
type='FPN',
#in_channels=[96, 192, 384, 768],
in_channels=[128, 256, 512, 1024],
#in_channels=[192, 384, 768, 1536],, #large
out_channels=256,
num_outs=5),
base large smallの結果は以下です。モデルを大きくすると収束が遅くなるのでは無いかと思ったのでlargeを長めに学習させるものも試しました。augmentationはSIIM2021の3rd解法のものを使用しています。
TH | val_mAP@0.5 | private LB |
---|---|---|
small | 0.3840 | 0.234 |
base | 0.3770 | 0.246 |
lagre | 0.3330 | 0.230 |
lagre(3x) | 0.3640 | 0.234 |
largeは収束が遅い&脳死で使って良いわけでは無さそうです。
5. 他のモデルを試す
CascadeRcnn,retinanet,HTCもsubしてみます。各々backboneとneckを書き換えれば使用可能です。
TH | val_mAP@0.5 | private LB |
---|---|---|
Cascade | 0.3840 | 0.226 |
retinanet | 0.3790 | 0.232 |
HTC | 0.3640 | 0.229 |
使いこなせないですね...
6. cocoの事前学習が有効か見てみる
mask R cnnでのcoco2017の事前学習済みの重みはmmdetectionに公開されていますが、他のモデルについては公式のmmdetectionには見当たりません。ネットを探しても見つからない場合、自分でcoco2017の事前学習をすることになります。A1001枚でswin smallのretinanetを12epoch学習させるのに2.5日ほどかかるので非常にしんどいです。
事前学習でのmAPが高ければ高いほどよいのか確認するため8epoch終了時(学習途中)と12epoch終了時(最終epoch)の2つの重みを用いました。2つのcocoでのmAP_valは以下の通りです。
TH | val_mAP@0.5:0.95 | val_mAP@0.5 | val_mAP@0.75 | val_mAP small | val_mAP medium | val_mAP large |
---|---|---|---|---|---|---|
8epoch | 0.407 | 0.619 | 0.438 | 0.245 | 0.450 | 0.542 |
12epoch | 0.457 | 0.670 | 0.492 | 0.300 | 0.498 | 0.605 |
結果は以下です。
TH | val_mAP@0.5 | private LB |
---|---|---|
without pretraining | 0.3790 | 0.232 |
8epoch | 0.3820 | 0.239 |
12epoch | 0.3940 | 0.238 |
cocoの事前学習を用いればLBが上昇しました。事前学習でのmAPが高ければ高いほどよいとも言いきれ無さそうです。
まとめ
mmdetectionを使用してSwinTransformerを物体検出に適用しました。勘所は不明ですがmmdetectionを気兼ねなく使えるようになったのはよかったと思います。追試はモチベがなかなか出ない問題がありますが一日のsub数など気にせず実験のフィードバックが得られるので楽しかったです。
おまけ
mmdetection以外でもswinで物体検出が可能です。
yolov5のbackboneなどを自分で変えて実験できます。coco2017で事前学習した重みは載っていません。SIIM2021の2ndのレポジトリに似たようなものがありました。
これをvinbigに試しても散々でした。
nvnn氏曰く「SIIM2021の検出タスクは解像度が低くても&cocoの精度が低いものでもcv/lbがそんなに変わらなかったのでcocoで事前学習されていないモデルでも構わず使用した。有効かどうかはデータセット依存だよ」とのことです。”そんなに変わらない”の肌感は不明です。。。
detectron2でswin_Tinyのretinanetを36epoch事前学習させた重みなどがあります。詳細は未確認です。