สอนใช้ MMDetection สร้างโมเดล Object Detection

Nuttaset kuapanich
21 min readMar 18, 2024

--

เนื้อหาด้านล่างนี้เป็นการสอนพื้นฐาน ถ้าต้องการข้ามไปดูสาธิตการใช้งาน สามารถเข้าไปใน ลิงค์นี้ ครับ

หากใครยังไม่ได้ติดตั้ง MMDetection สามารถติดตั้งตามขั้นตอนในนี้ครับ

หัวข้อในบทความนี้

0. แนะนำ Configuration File

0.1 การเข้าถึง Config File

0.2 การแก้ไขค่าใน Config File

0.3 การถ่ายทอดตัวแปรระหว่าง Config File

0.4 การแก้ไขค่าระหว่างถ่ายทอด Config File

0.5 การลบ key บางตัวระหว่างถ่ายทอด Config File

0.6 การถ่ายทอดเฉพาะบางตัวแปรระหว่าง Config File

0.7 การบันทึก Config File ใหม่

0.8 การรับค่า Path ของ Config File

0.9 การแก้ไขค่า Config File ผ่าน Command Line

0.10 การใช้ Environment Variable กำหนดค่าใน Config File

0.11 ตัวอย่างตัวแปรใน Config File ที่สำคัญ

  1. Dataset Format

2. Training Model

2.1 แนะนำ Pretrained Model

2.2 การเรียกใช้งาน Pretrained Model

2.3 การตรวจเช็คตัวแปรก่อน Train Pretrained Model

2.4 การ Train Pretrained Model

2.5 การสร้างกราฟ Loss Function จากการ Train

3. Testing Model

3.1 การสั่งผ่าน Python

3.2 การสั่งผ่าน Command Line

0. แนะนำ Configuration File

เนื่องจาก MMDetection (เรียกสั้นๆว่า mmdet) เป็น toolbox ที่ใช้ configuration file (เรียกสั้นๆว่า config file) กำหนดค่าต่างๆไม่ว่าจะเป็นตอน train หรือ test ตอน test เป็นหลัก ดังนั้นจึงจำเป็นที่ผู้ใช้ควรมีความเข้าใขการใช้ config file เบื้องต้น

0.1 การเข้าถึง Config File

mmdet ใช้ config system ของ MMEngine (มีทั้ง Python, Json, YAML) ในภาษา python เข้าถึง config file โดยใช้ Config

from mmengine.config import Config

การอ่าน และวิเคราะห์ config file ใช้ Config.fromfile() โดยค่าใน config file อยู่ในรูปแบบ set ของ dictionary (คู่ของ key กับ value) เช่นต้องการอ่าน config file ที่ชื่อว่า config_sgd.py โดยในไฟล์มีเนื้อหาคือ

optimizer = dict(type='SGD', lr=0.1, momentum=0.9, weight_decay=0.0001)

ใช้คำสั่ง

from mmengine.config import Config
cfg = Config.fromfile("config_sgd.py")
print(cfg)

ได้ output คือ

Config (path: config_sgd.py): {'optimizer': {'type': 'SGD', 'lr': 0.1, 'momentum': 0.9, 'weight_decay': 0.0001}}

0.2 การแก้ไขค่าใน Config File

จากเนื้อหาใน config_sgd.py สามารถแก้ไขค่าโดยระบุ key และกำหนด value ใหม่ที่ต้องการกำหนด เช่น

print(cfg.optimizer)
cfg.optimizer.lr = 0.01
print(cfg.optimizer)

ได้ output คือ

{'type': 'SGD', 'lr': 0.1, 'momentum': 0.9, 'weight_decay': 0.0001}
{'type': 'SGD', 'lr': 0.01, 'momentum': 0.9, 'weight_decay': 0.0001}

สามารถใช้ Config ไปกำหนดค่าร่วมกับ Registry (function หรือ class ที่ใช้สร้าง module) เช่นใช้ config_sgd.py สร้าง registry OPTIMIZERS

from mmengine import Config, optim
from mmengine.registry import OPTIMIZERS
import torch.nn as nn

cfg = Config.fromfile("config_sgd.py")

model = nn.Conv2d(1, 1, 1)
cfg.optimizer.params = model.parameters()
optimizer = OPTIMIZERS.build(cfg.optimizer)
print(optimizer)

ได้ output คือ

SGD (
Parameter Group 0
dampening: 0
differentiable: False
foreach: None
lr: 0.1
maximize: False
momentum: 0.9
nesterov: False
weight_decay: 0.0001
)

0.3 การถ่ายทอดตัวแปรระหว่าง Config File

ใน config file ถ้ามีการระบุตัวแปร list _base_ หมายความว่า config file นั้นรับตัวแปรมาจาก config file อื่นมาต่อเข้ากับของตัวเองด้วย เช่นใน config file ชื่อ optimizer_cfg.py มีเนื้อหาคือ

optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)

config file ชื่อ runtime_cfg.py มีเนื้อหาคือ

gpu_ids = [0, 1]

config file ชื่อ resnet50_runtime.py มีเนื้อหาคือ

_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)

ดังนั้นตัวแปรที่มีอยู่ใน resnet50_runtime.pyได้รับการถ่ายทอดจาก optimizer_cfg.py และ runtime_cfg.py

cfg = Config.fromfile("resnet50_runtime.py")
print(cfg)

ได้ output คือ

Config (path: resnet50_runtime.py): {'optimizer': {'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}, 'gpu_ids': [0, 1], 'model': {'type': 'ResNet', 'depth': 50}}

0.4 การแก้ไขค่าระหว่างถ่ายทอด Config File

ให้กำหนดตัวแปรใน config file พร้อมระบุค่า โดยใช้ชื่อซ้ำกับตัวแปรที่จะรับการถ่ายทอด แต่ต้องการแก้ไขค่า (ก็คือสร้างของใหม่มาทับของเก่า) เช่นใน resnet50.py มีเนื้อหาคือ

_base_ = ['optimizer_cfg.py']
model = dict(type='ResNet', depth=50)

และใน resnet50_lr0.01.py มีเนื้อหาคือ

_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)
optimizer = dict(lr=0.01)

ดังนั้นค่า lrของ resnet50_lr0.01.py ถูกแก้ไขจาก 0.02 เป็น 0.01

cfg1 = Config.fromfile('resnet50.py')
cfg2 = Config.fromfile('resnet50_lr0.01.py')
print(cfg1.optimizer)
print(cfg2.optimizer)

ได้ output คือ

{'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}
{'type': 'SGD', 'lr': 0.01, 'momentum': 0.9, 'weight_decay': 0.0001}

0.5 การลบ key บางตัวออกระหว่างถ่ายทอด Config File

กำหนด _delete_=True ในตัวแปรที่ต้องการลบ key ออก (ลบ key ออกทั้งหมด) หลังจากนั้นกำหนด key ที่ต้องการเหลือเอาไว้ เช่นใน resnet50_delete_key.py มีเนื้อหาคือ

_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)
optimizer = dict(_delete_=True, type='SGD', lr=0.01)

ดังนั้นใน resnet50_delete_key.py ซึ่งได้รับการถ่ายทอดตัวแปรจาก optimizer_cfg.py แต่ไม่มี key momentum และ weight_decay แล้ว

cfg1 = Config.fromfile("resnet50.py")
cfg2 = Config.fromfile("resnet50_delete_key.py")
print(cfg1.optimizer)
print(cfg2.optimizer)

ได้ output คือ

{'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}
{'type': 'SGD', 'lr': 0.01}

0.6 การถ่ายทอดเฉพาะบางตัวแปรระหว่าง Config File

ใช้ {{_base_.ตัวแปรของconfig file ที่ถ่ายทอดให้}} เพื่อรับการถ่ายทอดเฉพาะตัวแปรนั้น เช่นใน resnet50.py มีเนื้อหาคือ

_base_ = ['optimizer_cfg.py']
model = dict(type='ResNet', depth=50)

refer_base_var.py มีเนื้อหาคือ

_base_ = ["resnet50.py"]
a = {{_base_.model}}

ดังนั้นในตัวแปร a ของ refer_base_var.py มีค่าเหมือนกับตัวแปร model ของ resnet50.py ทุกประการ

cfg1 = Config.fromfile("resnet50.py")
cfg2 = Config.fromfile("refer_base_var.py")
print(cfg1.model)
print(cfg2.a)

ได้ output คือ

{'type': 'ResNet', 'depth': 50}
{'type': 'ResNet', 'depth': 50}

0.7 การบันทึก Config File ใหม่

เป็นสิ่งสำคัญมาก หลังจากที่ config file ใหม่รับการถ่ายทอดตัวแปร, แก้ไขค่าแล้ว ถ้าไม่บันทึก config file นั้น สิ่งที่แก้ไขไปก็ไม่ถูกบันทึก การบันทึกใช้ method dump เช่น

cfg = Config.fromfile("resnet50.py")
cfg.optimizer.lr = 0.05
cfg.dump("resnet50_dump.py")

ดังนั้นใน resnet50_dump.py มีตัวแปรที่มาจาก restnet50.py พร้อมค่าตัวแปรหลังจากการแก้ไข ใน resnet50_dump.py มีเนื้อหาคือ (ในไฟล์ไม่มีการใช้ _base_ แต่แสดงรายการตัวแปรทั้งหมดที่มีออกมาโดยตรง

model = dict(depth=50, type='ResNet')
optimizer = dict(lr=0.05, momentum=0.9, type='SGD', weight_decay=0.0001)

0.8 การรับค่า Path ของ Config File

มี predefined fields ที่เกี่ยวข้องกับ path ดังนี้

  • {{fileDirname}}: ชื่อ directory ของไฟล์ปัจจุบัน เช่น /home/your-username/your-project/folder
  • {{fileBasename}}: ชื่อไฟล์ของไฟล์ปัจจุบันโดยมีนามสกุลไฟล์ เช่น file.py
  • {{fileBasenameNoExtension}}: ชื่อไฟล์ของไฟล์ปัจจุบันโดยไม่มีนามสกุลไฟล์ เช่น file
  • {{fileExtname}}: นามสกุลไฟล์ของไฟล์ปัจจุบัน เช่น .py เช่น
work_dir = "./work_dir/{{fileBasenameNoExtension}}"
cfg = Config.fromfile("./predefined_var.py")
print(cfg.work_dir)

output คือ

./work_dir/predefined_var

0.9 การแก้ไขค่า Config File ผ่าน Command Line

ใช้ --cfg-options ในการกำหนด แต่เป็นการกำหนดชั่วคราวเฉพาะในคำสั่งที่ใช้ run เท่านั้น ไม่มีการแก้ไขข้อมูลใน config file เช่น ต้องการสั่ง train โดยใช้ไฟล์ demo_train.py (ไม่จำเป็นต้องดู code) และ config file คือ config_example.py มีเนื้อหาคือ

model = dict(type='CustomModel', in_channels=[1, 2, 3])
optimizer = dict(type='SGD', lr=0.01)

ถ้าต้องการสั่ง train (อธิบายวิธีการ train ภายหลัง) โดยกำหนด lr =0.1 พิมพ์ใน command line ว่า

python demo_train.py ./config_example.py --cfg-options optimizer.lr=0.1

ได้ output คือ

Config (path: ./config_example.py): {'model': {'type': 'CustomModel', 'in_channels': [1, 2, 3]}, 'optimizer': {'type': 'SGD', 'lr': 0.1}}

ถ้าค่าในตัวแปรไม่ใช้ตัวเลข ต้องใส้ “” ครอบ เช่นต้องการกำหนด in_channels = [1, 1, 1] พิมพ์ใน command line ว่า

python demo_train.py ./config_example.py --cfg-options model.in_channels="[1, 1, 1]"

ได้ output คือ

Config (path: ./config_example.py): {'model': {'type': 'CustomModel', 'in_channels': [1, 1, 1]}, 'optimizer': {'type': 'SGD', 'lr': 0.01}}

ถ้าต้องการแก้ไขค่า config มากกว่า 1 รายการ ต้องเรียกใช้ --cfg-options ซ้ำ เช่น

python demo_train.py ./config_example.py --cfg-options optimizer.type="Adam" --cfg-options model.in_channels="[1, 1, 1]"

ได้ output คือ

Config (path: ./example.py): {'model': {'type': 'CustomModel', 'in_channels': [1, 1, 1]}, 'optimizer': {'type': 'Adam', 'lr': 0.01}}

0.10 การใช้ Environment Variable กำหนดค่าใน Config File

คล้ายกับการใช้ --cfg-optionsการกำหนดค่า environment variable ทำผ่าน command line และไม่มีการแก้ไขข้อมูลใน config file

มีรูปแบบคือ {{$ENV_VAR:DEF_VAL}} โดย ENV_VAR คือชื่อตัวแปรที่ต้องใช้กำหนดใน command line และ DEF_VAL คือค่า default กรณีไม่ได้กำหนดค่าให้ตัวแปร ENV_VAR เช่นใน replace_data_root.py มีเนื้อหาคือ

dataset_type = "CocoDataset"
data_root = "{{$DATASET:/data/coco/}}"
dataset=dict(ann_file= data_root + "train.json:)

ถ้าสั่ง train โดยไม่กำหนดค่าของ DATASET , data_root จะเท่ากับค่า default ซึ่งในที่นี้คือ “/data/coco/”

python demo_train.py replace_data_root.py

ได้ output คือ

Config (path: replace_data_root.py): {'dataset_type': 'CocoDataset', 'data_root': '/data/coco/', 'dataset': {'ann_file': '/data/coco/train.json'}}

ถ้าต้องการกำหนดค่า DATASET ต้องทำก่อนใข้คำสั่ง python

DATASET=/new/dataset/path/ python demo_train.py replace_data_root.py

ได้ output คือ

Config (path: replace_data_root.py): {'dataset_type': 'CocoDataset', 'data_root': '/new/dataset/path/', 'dataset': {'ann_file': '/new/dataset/path/train.json'}}

เมื่อกำหนด environment variable ก็ยังสามารถใช้ --cfg-options ได้ โดยค่าของ environment variable จะถูกเรียกใช้ก่อน และอาจถูกแทนที่ด้วย --cfg-options กรณีกำหนดค่าให้ตัวแปรที่ซ้ำกัน เช่น

DATASET=/old/dataset/path/ python demo_train.py replace_data_root.py --cfg-options data_root="/new/dataset/path/"

ดังนั้นค่าของ data_root ถูกแก้ไขเป็น “/new/dataset/path/” แต่ค่าของ ann_file ยังคงเป็น “old/dataset/path/train.json”

ได้ output คือ

Config (path: replace_data_root.py): {'dataset_type': 'CocoDataset', 'data_root': '/new/dataset/path/', 'dataset': {'ann_file': '/old/dataset/path/train.json'}}

0.11 ตัวอย่างตัวแปรใน Config File ที่สำคัญ

ตัวอย่างต่อไปนี้เป็นของ Mask R-CNN

  • Model
model = dict(
type='MaskRCNN', # The name of detector

data_preprocessor=dict( # The config of data preprocessor, usually includes image normalization and padding
type='DetDataPreprocessor', # The type of the data preprocessor, refer to https://mmdetection.readthedocs.io/en/latest/api.html#mmdet.models.data_preprocessors.DetDataPreprocessor
mean=[123.675, 116.28, 103.53], # Mean values used to pre-training the pre-trained backbone models, ordered in R, G, B
std=[58.395, 57.12, 57.375], # Standard variance used to pre-training the pre-trained backbone models, ordered in R, G, B
bgr_to_rgb=True, # whether to convert image from BGR to RGB
pad_mask=True, # whether to pad instance masks
pad_size_divisor=32), # The size of padded image should be divisible by ``pad_size_divisor``

backbone=dict( # The config of backbone
type='ResNet', # The type of backbone network. Refer to https://mmdetection.readthedocs.io/en/latest/api.html#mmdet.models.backbones.ResNet
depth=50, # The depth of backbone, usually it is 50 or 101 for ResNet and ResNext backbones.
num_stages=4, # Number of stages of the backbone.
out_indices=(0, 1, 2, 3), # The index of output feature maps produced in each stage
frozen_stages=1, # The weights in the first stage are frozen
norm_cfg=dict( # The config of normalization layers.
type='BN', # Type of norm layer, usually it is BN or GN
requires_grad=True), # Whether to train the gamma and beta in BN
norm_eval=True, # Whether to freeze the statistics in BN
style='pytorch', # The style of backbone, 'pytorch' means that stride 2 layers are in 3x3 Conv, 'caffe' means stride 2 layers are in 1x1 Convs.
init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50')), # The ImageNet pretrained backbone to be loaded

neck=dict(
type='FPN', # The neck of detector is FPN. We also support 'NASFPN', 'PAFPN', etc. Refer to https://mmdetection.readthedocs.io/en/latest/api.html#mmdet.models.necks.FPN for more details.
in_channels=[256, 512, 1024, 2048], # The input channels, this is consistent with the output channels of backbone
out_channels=256, # The output channels of each level of the pyramid feature map
num_outs=5), # The number of output scales

rpn_head=dict(
type='RPNHead', # The type of RPN head is 'RPNHead', we also support 'GARPNHead', etc. Refer to https://mmdetection.readthedocs.io/en/latest/api.html#mmdet.models.dense_heads.RPNHead for more details.
in_channels=256, # The input channels of each input feature map, this is consistent with the output channels of neck
feat_channels=256, # Feature channels of convolutional layers in the head.
anchor_generator=dict( # The config of anchor generator
type='AnchorGenerator', # Most of methods use AnchorGenerator, SSD Detectors uses `SSDAnchorGenerator`. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/task_modules/prior_generators/anchor_generator.py#L18 for more details
scales=[8], # Basic scale of the anchor, the area of the anchor in one position of a feature map will be scale * base_sizes
ratios=[0.5, 1.0, 2.0], # The ratio between height and width.
strides=[4, 8, 16, 32, 64]), # The strides of the anchor generator. This is consistent with the FPN feature strides. The strides will be taken as base_sizes if base_sizes is not set.
bbox_coder=dict( # Config of box coder to encode and decode the boxes during training and testing
type='DeltaXYWHBBoxCoder', # Type of box coder. 'DeltaXYWHBBoxCoder' is applied for most of the methods. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/task_modules/coders/delta_xywh_bbox_coder.py#L13 for more details.
target_means=[0.0, 0.0, 0.0, 0.0], # The target means used to encode and decode boxes
target_stds=[1.0, 1.0, 1.0, 1.0]), # The standard variance used to encode and decode boxes
loss_cls=dict( # Config of loss function for the classification branch
type='CrossEntropyLoss', # Type of loss for classification branch, we also support FocalLoss etc. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/losses/cross_entropy_loss.py#L201 for more details
use_sigmoid=True, # RPN usually performs two-class classification, so it usually uses the sigmoid function.
loss_weight=1.0), # Loss weight of the classification branch.
loss_bbox=dict( # Config of loss function for the regression branch.
type='L1Loss', # Type of loss, we also support many IoU Losses and smooth L1-loss, etc. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/losses/smooth_l1_loss.py#L56 for implementation.
loss_weight=1.0)), # Loss weight of the regression branch.
roi_head=dict( # RoIHead encapsulates the second stage of two-stage/cascade detectors.
type='StandardRoIHead',
bbox_roi_extractor=dict( # RoI feature extractor for bbox regression.
type='SingleRoIExtractor', # Type of the RoI feature extractor, most of methods uses SingleRoIExtractor. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/roi_heads/roi_extractors/single_level_roi_extractor.py#L13 for details.
roi_layer=dict( # Config of RoI Layer
type='RoIAlign', # Type of RoI Layer, DeformRoIPoolingPack and ModulatedDeformRoIPoolingPack are also supported. Refer to https://mmcv.readthedocs.io/en/latest/api.html#mmcv.ops.RoIAlign for details.
output_size=7, # The output size of feature maps.
sampling_ratio=0), # Sampling ratio when extracting the RoI features. 0 means adaptive ratio.
out_channels=256, # output channels of the extracted feature.
featmap_strides=[4, 8, 16, 32]), # Strides of multi-scale feature maps. It should be consistent with the architecture of the backbone.
bbox_head=dict( # Config of box head in the RoIHead.
type='Shared2FCBBoxHead', # Type of the bbox head, Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/roi_heads/bbox_heads/convfc_bbox_head.py#L220 for implementation details.
in_channels=256, # Input channels for bbox head. This is consistent with the out_channels in roi_extractor
fc_out_channels=1024, # Output feature channels of FC layers.
roi_feat_size=7, # Size of RoI features
num_classes=80, # Number of classes for classification
bbox_coder=dict( # Box coder used in the second stage.
type='DeltaXYWHBBoxCoder', # Type of box coder. 'DeltaXYWHBBoxCoder' is applied for most of the methods.
target_means=[0.0, 0.0, 0.0, 0.0], # Means used to encode and decode box
target_stds=[0.1, 0.1, 0.2, 0.2]), # Standard variance for encoding and decoding. It is smaller since the boxes are more accurate. [0.1, 0.1, 0.2, 0.2] is a conventional setting.
reg_class_agnostic=False, # Whether the regression is class agnostic.
loss_cls=dict( # Config of loss function for the classification branch
type='CrossEntropyLoss', # Type of loss for classification branch, we also support FocalLoss etc.
use_sigmoid=False, # Whether to use sigmoid.
loss_weight=1.0), # Loss weight of the classification branch.
loss_bbox=dict( # Config of loss function for the regression branch.
type='L1Loss', # Type of loss, we also support many IoU Losses and smooth L1-loss, etc.
loss_weight=1.0)), # Loss weight of the regression branch.
mask_roi_extractor=dict( # RoI feature extractor for mask generation.
type='SingleRoIExtractor', # Type of the RoI feature extractor, most of methods uses SingleRoIExtractor.
roi_layer=dict( # Config of RoI Layer that extracts features for instance segmentation
type='RoIAlign', # Type of RoI Layer, DeformRoIPoolingPack and ModulatedDeformRoIPoolingPack are also supported
output_size=14, # The output size of feature maps.
sampling_ratio=0), # Sampling ratio when extracting the RoI features.
out_channels=256, # Output channels of the extracted feature.
featmap_strides=[4, 8, 16, 32]), # Strides of multi-scale feature maps.
mask_head=dict( # Mask prediction head
type='FCNMaskHead', # Type of mask head, refer to https://mmdetection.readthedocs.io/en/latest/api.html#mmdet.models.roi_heads.FCNMaskHead for implementation details.
num_convs=4, # Number of convolutional layers in mask head.
in_channels=256, # Input channels, should be consistent with the output channels of mask roi extractor.
conv_out_channels=256, # Output channels of the convolutional layer.
num_classes=80, # Number of class to be segmented.
loss_mask=dict( # Config of loss function for the mask branch.
type='CrossEntropyLoss', # Type of loss used for segmentation
use_mask=True, # Whether to only train the mask in the correct class.
loss_weight=1.0))), # Loss weight of mask branch.

train_cfg = dict( # Config of training hyperparameters for rpn and rcnn
rpn=dict( # Training config of rpn
assigner=dict( # Config of assigner
type='MaxIoUAssigner', # Type of assigner, MaxIoUAssigner is used for many common detectors. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/task_modules/assigners/max_iou_assigner.py#L14 for more details.
pos_iou_thr=0.7, # IoU >= threshold 0.7 will be taken as positive samples
neg_iou_thr=0.3, # IoU < threshold 0.3 will be taken as negative samples
min_pos_iou=0.3, # The minimal IoU threshold to take boxes as positive samples
match_low_quality=True, # Whether to match the boxes under low quality (see API doc for more details).
ignore_iof_thr=-1), # IoF threshold for ignoring bboxes
sampler=dict( # Config of positive/negative sampler
type='RandomSampler', # Type of sampler, PseudoSampler and other samplers are also supported. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/task_modules/samplers/random_sampler.py#L14 for implementation details.
num=256, # Number of samples
pos_fraction=0.5, # The ratio of positive samples in the total samples.
neg_pos_ub=-1, # The upper bound of negative samples based on the number of positive samples.
add_gt_as_proposals=False), # Whether add GT as proposals after sampling.
allowed_border=-1, # The border allowed after padding for valid anchors.
pos_weight=-1, # The weight of positive samples during training.
debug=False), # Whether to set the debug mode
rpn_proposal=dict( # The config to generate proposals during training
nms_across_levels=False, # Whether to do NMS for boxes across levels. Only work in `GARPNHead`, naive rpn does not support do nms cross levels.
nms_pre=2000, # The number of boxes before NMS
nms_post=1000, # The number of boxes to be kept by NMS. Only work in `GARPNHead`.
max_per_img=1000, # The number of boxes to be kept after NMS.
nms=dict( # Config of NMS
type='nms', # Type of NMS
iou_threshold=0.7 # NMS threshold
),
min_bbox_size=0), # The allowed minimal box size
rcnn=dict( # The config for the roi heads.
assigner=dict( # Config of assigner for second stage, this is different for that in rpn
type='MaxIoUAssigner', # Type of assigner, MaxIoUAssigner is used for all roi_heads for now. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/task_modules/assigners/max_iou_assigner.py#L14 for more details.
pos_iou_thr=0.5, # IoU >= threshold 0.5 will be taken as positive samples
neg_iou_thr=0.5, # IoU < threshold 0.5 will be taken as negative samples
min_pos_iou=0.5, # The minimal IoU threshold to take boxes as positive samples
match_low_quality=False, # Whether to match the boxes under low quality (see API doc for more details).
ignore_iof_thr=-1), # IoF threshold for ignoring bboxes
sampler=dict(
type='RandomSampler', # Type of sampler, PseudoSampler and other samplers are also supported. Refer to https://github.com/open-mmlab/mmdetection/blob/main/mmdet/models/task_modules/samplers/random_sampler.py#L14 for implementation details.
num=512, # Number of samples
pos_fraction=0.25, # The ratio of positive samples in the total samples.
neg_pos_ub=-1, # The upper bound of negative samples based on the number of positive samples.
add_gt_as_proposals=True
), # Whether add GT as proposals after sampling.
mask_size=28, # Size of mask
pos_weight=-1, # The weight of positive samples during training.
debug=False)), # Whether to set the debug mode

test_cfg = dict( # Config for testing hyperparameters for rpn and rcnn
rpn=dict( # The config to generate proposals during testing
nms_across_levels=False, # Whether to do NMS for boxes across levels. Only work in `GARPNHead`, naive rpn does not support do nms cross levels.
nms_pre=1000, # The number of boxes before NMS
nms_post=1000, # The number of boxes to be kept by NMS. Only work in `GARPNHead`.
max_per_img=1000, # The number of boxes to be kept after NMS.
nms=dict( # Config of NMS
type='nms', #Type of NMS
iou_threshold=0.7 # NMS threshold
),
min_bbox_size=0), # The allowed minimal box size
rcnn=dict( # The config for the roi heads.
score_thr=0.05, # Threshold to filter out boxes
nms=dict( # Config of NMS in the second stage
type='nms', # Type of NMS
iou_thr=0.5), # NMS threshold
max_per_img=100, # Max number of detections of each image
mask_thr_binary=0.5))) # Threshold of mask prediction
  • Dataloader

กรณีที่ข้อมูลมีเฉพาะส่วน train กับ test กำหนดให้ test_dataloader = val_dataloader ดังตัวอย่างด้านล่าง

dataset_type = 'CocoDataset'  # Dataset type, this will be used to define the dataset
data_root = 'data/coco/' # Root path of data
backend_args = None # Arguments to instantiate the corresponding file backend

train_pipeline = [ # Training data processing pipeline
dict(type='LoadImageFromFile', backend_args=backend_args), # First pipeline to load images from file path
dict(
type='LoadAnnotations', # Second pipeline to load annotations for current image
with_bbox=True, # Whether to use bounding box, True for detection
with_mask=True, # Whether to use instance mask, True for instance segmentation
poly2mask=True), # Whether to convert the polygon mask to instance mask, set False for acceleration and to save memory
dict(
type='Resize', # Pipeline that resizes the images and their annotations
scale=(1333, 800), # The largest scale of the images
keep_ratio=True # Whether to keep the ratio between height and width
),
dict(
type='RandomFlip', # Augmentation pipeline that flips the images and their annotations
prob=0.5), # The probability to flip
dict(type='PackDetInputs') # Pipeline that formats the annotation data and decides which keys in the data should be packed into data_samples
]
test_pipeline = [ # Testing data processing pipeline
dict(type='LoadImageFromFile', backend_args=backend_args), # First pipeline to load images from file path
dict(type='Resize', scale=(1333, 800), keep_ratio=True), # Pipeline that resizes the images
dict(
type='PackDetInputs', # Pipeline that formats the annotation data and decides which keys in the data should be packed into data_samples
meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape',
'scale_factor'))
]
train_dataloader = dict( # Train dataloader config
batch_size=2, # Batch size of a single GPU
num_workers=2, # Worker to pre-fetch data for each single GPU
persistent_workers=True, # If ``True``, the dataloader will not shut down the worker processes after an epoch end, which can accelerate training speed.
sampler=dict( # training data sampler
type='DefaultSampler', # DefaultSampler which supports both distributed and non-distributed training. Refer to https://mmengine.readthedocs.io/en/latest/api/generated/mmengine.dataset.DefaultSampler.html#mmengine.dataset.DefaultSampler
shuffle=True), # randomly shuffle the training data in each epoch
batch_sampler=dict(type='AspectRatioBatchSampler'), # Batch sampler for grouping images with similar aspect ratio into a same batch. It can reduce GPU memory cost.
dataset=dict( # Train dataset config
type=dataset_type,
data_root=data_root,
ann_file='annotations/instances_train2017.json', # Path of annotation file
data_prefix=dict(img='train2017/'), # Prefix of image path
filter_cfg=dict(filter_empty_gt=True, min_size=32), # Config of filtering images and annotations
pipeline=train_pipeline,
backend_args=backend_args))
val_dataloader = dict( # Validation dataloader config
batch_size=1, # Batch size of a single GPU. If batch-size > 1, the extra padding area may influence the performance.
num_workers=2, # Worker to pre-fetch data for each single GPU
persistent_workers=True, # If ``True``, the dataloader will not shut down the worker processes after an epoch end, which can accelerate training speed.
drop_last=False, # Whether to drop the last incomplete batch, if the dataset size is not divisible by the batch size
sampler=dict(
type='DefaultSampler',
shuffle=False), # not shuffle during validation and testing
dataset=dict(
type=dataset_type,
data_root=data_root,
ann_file='annotations/instances_val2017.json',
data_prefix=dict(img='val2017/'),
test_mode=True, # Turn on the test mode of the dataset to avoid filtering annotations or images
pipeline=test_pipeline,
backend_args=backend_args))
test_dataloader = val_dataloader # Testing dataloader config

แต่ถ้าข้อมูลแบ่งเป็น train, valid, test ก็สร้าง test_dataloader ขึ้นมาเฉพาะ

test_dataloader = dict(
batch_size=1,
num_workers=2,
persistent_workers=True,
drop_last=False,
sampler=dict(type='DefaultSampler', shuffle=False),
dataset=dict(
type=dataset_type,
data_root=data_root,
ann_file=data_root + 'annotations/image_info_test-dev2017.json',
data_prefix=dict(img='test2017/'),
test_mode=True,
pipeline=test_pipeline))
  • Evaluator

กรณีที่ข้อมูลมีเฉพาะส่วน train กับ test กำหนดให้ test_evaluator = val_evaluator ดังตัวอย่างด้านล่าง

val_evaluator = dict(  # Validation evaluator config
type='CocoMetric', # The coco metric used to evaluate AR, AP, and mAP for detection and instance segmentation
ann_file=data_root + 'annotations/instances_val2017.json', # Annotation file path
metric=['bbox', 'segm'], # Metrics to be evaluated, `bbox` for detection and `segm` for instance segmentation
format_only=False,
backend_args=backend_args)
test_evaluator = val_evaluator # Testing evaluator config

แต่ถ้าข้อมูลแบ่งเป็น train, valid, test ก็สร้าง test_evaluator ขึ้นมาเฉพาะ

test_evaluator = dict(
type='CocoMetric',
ann_file=data_root + 'annotations/image_info_test-dev2017.json',
metric=['bbox', 'segm'], # Metrics to be evaluated
format_only=True, # Only format and save the results to coco json file
outfile_prefix='./work_dirs/coco_detection/test') # The prefix of output json files
  • Training, Validation และ Testing
train_cfg = dict(
type='EpochBasedTrainLoop', # The training loop type. Refer to https://github.com/open-mmlab/mmengine/blob/main/mmengine/runner/loops.py
max_epochs=12, # Maximum training epochs
val_interval=1) # Validation intervals. Run validation every epoch.
val_cfg = dict(type='ValLoop') # The validation loop type
test_cfg = dict(type='TestLoop') # The testing loop type
  • Optimization

optim_wrapper ใช้กำหนดฟังก์ชั่นของ optimizer

optim_wrapper = dict(  # Optimizer wrapper config
type='OptimWrapper', # Optimizer wrapper type, switch to AmpOptimWrapper to enable mixed precision training.
optimizer=dict( # Optimizer config. Support all kinds of optimizers in PyTorch. Refer to https://pytorch.org/docs/stable/optim.html#algorithms
type='SGD', # Stochastic gradient descent optimizer
lr=0.02, # The base learning rate
momentum=0.9, # Stochastic gradient descent with momentum
weight_decay=0.0001), # Weight decay of SGD
clip_grad=None, # Gradient clip option. Set None to disable gradient clip. Find usage in https://mmengine.readthedocs.io/en/latest/tutorials/optimizer.html
)

param_scheduler ใช้ปรับ hyperparameter เช่น learning rate, momentum

param_scheduler = [
# Linear learning rate warm-up scheduler
dict(
type='LinearLR', # Use linear policy to warmup learning rate
start_factor=0.001, # The ratio of the starting learning rate used for warmup
by_epoch=False, # The warmup learning rate is updated by iteration
begin=0, # Start from the first iteration
end=500), # End the warmup at the 500th iteration
# The main LRScheduler
dict(
type='MultiStepLR', # Use multi-step learning rate policy during training
by_epoch=True, # The learning rate is updated by epoch
begin=0, # Start from the first epoch
end=12, # End at the 12th epoch
milestones=[8, 11], # Epochs to decay the learning rate
gamma=0.1) # The learning rate decay ratio
]
  • Hook

สามารถใส่ hook config เพื่อเพิ่มการดำเนินการระหว่างลูป training, validation และ testing โดย hook config มี 2 แบบคือ

  1. default_hooks
default_hooks = dict(
timer=dict(type='IterTimerHook'), # Update the time spent during iteration into message hub
logger=dict(type='LoggerHook', interval=50), # Collect logs from different components of Runner and write them to terminal, JSON file, tensorboard and wandb .etc
param_scheduler=dict(type='ParamSchedulerHook'), # update some hyper-parameters of optimizer
checkpoint=dict(type='CheckpointHook', interval=1), # Save checkpoints periodically
sampler_seed=dict(type='DistSamplerSeedHook'), # Ensure distributed Sampler shuffle is active
visualization=dict(type='DetVisualizationHook')) # Detection Visualization Hook. Used to visualize validation and testing process prediction results

2. custom_hooks

custom_hooks = []
  • Runtime
default_scope = 'mmdet'  # The default registry scope to find modules. Refer to https://mmengine.readthedocs.io/en/latest/advanced_tutorials/registry.html

env_cfg = dict(
cudnn_benchmark=False, # Whether to enable cudnn benchmark
mp_cfg=dict( # Multi-processing config
mp_start_method='fork', # Use fork to start multi-processing threads. 'fork' usually faster than 'spawn' but maybe unsafe. See discussion in https://github.com/pytorch/pytorch/issues/1355
opencv_num_threads=0), # Disable opencv multi-threads to avoid system being overloaded
dist_cfg=dict(backend='nccl'), # Distribution configs
)

vis_backends = [dict(type='LocalVisBackend')] # Visualization backends. Refer to https://mmengine.readthedocs.io/en/latest/advanced_tutorials/visualization.html
visualizer = dict(
type='DetLocalVisualizer', vis_backends=vis_backends, name='visualizer')
log_processor = dict(
type='LogProcessor', # Log processor to process runtime logs
window_size=50, # Smooth interval of log values
by_epoch=True) # Whether to format logs with epoch type. Should be consistent with the train loop's type.

log_level = 'INFO' # The level of logging.
load_from = None # Load model checkpoint as a pre-trained model from a given path. This will not resume training.
resume = False # Whether to resume from the checkpoint defined in `load_from`. If `load_from` is None, it will resume the latest checkpoint in the `work_dir`.

1. Dataset Format

format ของ dataset ที่ mmdet รองรับได้แก่ COCO, Pascal VOC, CityScapes, OpenImages และ WIDER FACE

เช่นของ COCO format คือ

'images': [
{
'file_name': 'COCO_val2014_000000001268.jpg',
'height': 427,
'width': 640,
'id': 1268
},
...
],

'annotations': [
{
'segmentation': [[192.81,
247.09,
...
219.03,
249.06]], # If you have mask labels, and it is in polygon XY point coordinate format, you need to ensure that at least 3 point coordinates are included. Otherwise, it is an invalid polygon.
'area': 1035.749,
'iscrowd': 0,
'image_id': 1268,
'bbox': [192.81, 224.8, 74.73, 33.43],
'category_id': 16,
'id': 42986
},
...
],

'categories': [
{'id': 0, 'name': 'car'},
]
  • images: เก็บรายการข้อมูลรูปภาพ เช่น file_name, height, width และ id
  • annotations: เก็บรายการ annotation
  • categories: เก็บรายการชื่อ class และ id

สามารถโหลด dataset จาก mmdetection/tools/misc/download_dataset.py เช่น

python mmdetection/tools/misc/download_dataset.py --dataset-name coco2017 --unzip
python mmdetection/tools/misc/download_dataset.py --dataset-name voc2007
python mmdetection/tools/misc/download_dataset.py --dataset-name lvis

หากอยู่ในจีนสามารถโหลด dataset จาก OpenDataLab

2. Training Model

2.1 แนะนำ Pretrained Model

pretrained model/backbone model (model zoo) ถูกเก็บไว้ที่ Aliyun (Alibaba Cloud) โดยโมเดลที่ใช้เป็น PyTorch style (ไฟล์ .pth)

รายการโมเดลที่สามารถโหลดได้สามารถดูได้จากใน ลิงค์นี้ โดย weight ของ pretrained model สามารถแบ่งประเภทตาม img_norm_cfg (image normalization configuration) ได้ดังนี้

  • TorchVision: ได้แก่ ResNet50, ResNet101
    img_norm_cfg คือ dict(mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
  • Pycls: ได้แก่ RegNetX
    img_norm_cfg คือ dict(mean=[103.530, 116.280, 123.675], std=[57.375, 57.12, 58.395], to_rgb=False)
  • MSRA: ได้แก่ ResNet50_Caffe, ResNet101_Caffe
    img_norm_cfg คือ dict(mean=[103.530, 116.280, 123.675], std=[1.0, 1.0, 1.0], to_rgb=False)
  • Caffe2: ได้แก่ ResNext101_32x8d
    img_norm_cfg คือ dict(mean=[103.530, 116.280, 123.675], std=[57.375, 57.120, 58.395], to_rgb=False)
  • อื่นๆ: เช่น
    SSD มี img_norm_cfg คือ dict(mean=[123.675, 116.28, 103.53], std=[1, 1, 1], to_rgb=True)
    YOLOv3 มี img_norm_cfg คือ img_norm_cfg is dict(mean=[0, 0, 0], std=[255., 255., 255.], to_rgb=True)

รายละเอียดของแต่ละโมเดลสามารถดูได้ในนี้ RPN, Faster R-CNN, Mask R-CNN, Fast R-CNN (with pre-computed proposals), RetianNet, Cascade R-CNN และ Cascade Mask R-CNN, Hybrid Task Cascade (HTC), SSD, Group Normalization (GN), Weight Standardization (WS), Deformable Convolution v2 (DCN), Content-Aware ReAssembly of FEatures (CARAFE), Instaboost, Libra R-CNN, Guided Anchoring, FCOS, FoveaBox, RepPoints, FreeAnchor, Grid R-CNN (plus), GHM, GCNet, HRNet, Mask Scoring R-CNN, เทรนจาก Scratch, NAS-FPN, ATSS, FSAF, RegNetX, Res2Net, GRoIE, Dynamic R-CNN, PointRend, DetectoRS, Generalized Focal Loss (GFL), CornerNet, YOLOv3, PAA, SABL, CentripetalNet, ResNeSt, DETR, Deformable DETR, AutoAssign, YOLOF, Seesaw Loss, CenterNet, YOLOX, PVT, SOLO, QueryInst, PanopticFPN, MaskFormer, DyHead, Mask2Former, Efficientnet

2.2 การเรียกใช้งาน Pretrained Model

ใข้ api DetInferencer เรียกใช้ pretrained model ของ mmdet

from mmdet.apis import DetInferencer

ดูรายการ pretrained model ที่สามารถโหลดได้ใช้ DetInferencer.list_models("mmdet")

from mmdet.apis import DetInferencer

models = DetInferencer.list_models("mmdet")
print(models)

ได้ output คือ (ออกมายาวมาก)

กำหนดโมเดลที่ต้องการโดยระบุที่ model หลังจากนั้น weight จะถูกโหลดจาก model zoo ของ OpenMMLab เช่นต้องการโหลด RTMDet model

inferencer = DetInferencer(model="rtmdet_tiny_8xb32-300e_coco")

นอกจากนี้ยังสามารใช้โมเดลของ mmdet แต่ใช้ weight จากที่อื่นก็ได้ โดยระบุที่ weights

inferencer = DetInferencer(model="rtmdet_tiny_8xb32-300e_coco", weights="path/to/rtmdet.pth")

สามารถใช้โมเดลที่มาจาก config file อื่นได้ โดยกำหนด path ของไฟล์นั้นที่ model

inferencer = DetInferencer(model='path/to/rtmdet_config.py', weights='path/to/rtmdet.pth')

กำหนดอุปกรณ์ที่ใช้ประมวลผลที่ device (ใช้รูปแบบเดียวกับ torch.device) เช่น

inferencer = DetInferencer(model='rtmdet_tiny_8xb32-300e_coco', device='cuda:1')
inferencer = DetInferencer(model='rtmdet_tiny_8xb32-300e_coco', device='cpu')

หลังจากโหลด pretrained model เสร็จแล้ว สามารถนำรูปภาพที่โหลดมาเองให้โมเดลทำนายได้

Input สามารถกำหนดได้ในรูปแบบต่อไปนี้

  • path/URL ของรูปภาพ
inferencer('demo/demo.jpg')
  • numpy array ของรูปภาพ (BGR format)
import mmcv
array = mmcv.imread('demo/demo.jpg')
inferencer(array)

#ใช้ matplotlib แสดงรูปภาพ (ทดลองรันโค๊ตอีกที)
import matplotlib.pyploy as plt
plt.figure(figsize=(15, 10))
plt.imshow(mmcv.bgr2rgb(array))
plt.axis() = False
plt.show()
  • list ของที่อยู่รูปภาพรูปภาพ
inferencer(['img_1.jpg', 'img_2.jpg])
inferencer(['img_1.jpg', array])
  • directory ของรูปภาพ ทุกรูปภาพจะถูกประมวลผล
inferencer('path/to/your_imgs/')

Output ออกมาในรูปแบบ dictionary โดยมี key ได้แก่

  • predictions เก็บผลการทำนาย
  • visualization เก็บรูปภาพผลลัพธ ์
{
'predictions' : [
# Each instance corresponds to an input image
{
'labels': [...], # int list of length (N, )
'scores': [...], # float list of length (N, )
'bboxes': [...], # 2d list of shape (N, 4), format: [min_x, min_y, max_x, max_y]
},
...
],
'visualization' : [
array(..., dtype=uint8),
]
}

ถ้าต้องการบันทึกรูปภาพผลลัพธ์ กำหนดที่ out_dir เช่นใน directory custom_images_input/shenzhen_dongmen.jpg มีรูปภาพคือ

from mmdet.apis import DetInferencer

my_model = DetInferencer(model="cascade-rcnn_r50-caffe_fpn_1x_coco", device="cuda")
my_model("custom_images_input/shenzhen_dongmen.jpg", out_dir= "custom_images_output/")

ดังนั้นรูปภาพผลลัพธ์ถูกเก็บไว้ที่ custom_images_outputs/vis/shenzhen_dongmen.jpg

2.3 การตรวจเช็คตัวแปรก่อน Train Pretrained Model

สิ่งสำคัญที่ต้องตรวจสอบใน config file เพื่อให้แน่ใจว่าโมเดลสามารถเทรน dataset ของเราได้แก่

  • classes
  • num classes
  • batch size
  • data root
  • ann file
  • data prefix

classes

ใน mmdetection/mmdet/datasets/coco.py มี dict ที่เก็บรายการ class ไว้อยู่ชื่อว่า METAINFO ซึ่งด้านในเก็บ class ของ coco dataset เอาไว้

เราต้องแก้ไขข้อมูล class ด้านในให้ตรงกับ class ในไฟล์ json ไม่งั้นเมื่อสั่ง train จะเกิน Error ขึ้นว่า

ValueError: need at least one array to concatenate

เช่นในไฟล์ json มีอยู่ class เดียวชื่อว่า coconut ก็แก้ไขเป็น (ถ้ามีเพียง class เดียว หลังชื่อ class ต้องตามหลังด้วย ,)

ในลิสต์ palette เก็บรายการสีของกล่องที่ใช้แสดงผลการ detect แต่ละ class (ในการณีจำนวน class ของ json มีไม่มาก ไม่ต้องแก้ไขส่วนนี้ก็ได้)

*หมายเหตุ

ถ้าแก้ไขที่ coco.py แล้ว แต่ยังขึ้นว่า ValueError แบบด้านบนอีก อาจเป็นเพราะว่ามีการติดตั้ง mmdetection มากกว่า 1 ครั้ง, ให้ไปแก้ไข coco.py ที่โฟลเดอร์ mmdetection แรกที่ติดตั้งลงไป

num classes

คือจำนวน class ที่ออกจากโมเดล ซึ่งอยู่ใน roi_head ของตัวแปร model ใน config file เช่นของ faster-rcnn_r50_fpn.py (mmdetection/configs/_base_/models/faster-rcnn_r50_fpn.py)

ต้องแก้ไข num_classes ให้ตรงกับจำนวน class ในไฟล์ json

batch size

โดยทั่วไป batch size ไม่ได้มีความสัมพันธ์โดยตรงกับ learning rate แต่กรณีที่สั่งใช้ auto_scale_lr (พิพม์ --auto-scale-lr ตอนสั่ง train ใน command line) เช่นใช้คำสั่ง

python tools/train.py \
${CONFIG_FILE} \
--auto-scale-lr \
[optional arguments]

learning rate จะถูกปรับอัตโนมัติตาม batch size และ จำนวน GPU ตาม linear scaling rule, โดยค่าเริ่มต้นของ learning rate คือสำหรับ GPU 8 ตัวที่ส่ง 2 รูปต่อ GPU (batch size = 8*2 = 16)

ถ้าต้องการแก้ไขให้ไปแก้ที่ตัวแปร auto_scale_lr เช่นใน schedule_1x.py (mmdetection/mmdet/configs/_base_/schedules/schedule_1x.py)

data root, ann file และ data prefix

สำคัญมาก เพราะถ้าระบุผิด mmdet จะไม่สามารถหา dataset เจอ โดยกำหนดไว้ที่ dataloader ของส่วน train, valid และ test ได้แก่ train_dataloader , val_dataloader และ test_dataloader เช่น ใน coco_detection.py (mmdetection/configs/_base_/datasets/coco_detection.py)

train dataloader
validation dataloader
test dataloader

2.4 การ Train Pretrained Model

ถ้าต้องการ train pretrained model ต้องระบุ path ของโมเดลที่ต้องการ train ต่อที่ตัวแปร load_from ใน config file, ไม่งั้น parameter เริ่มต้นของโมเดลจะถูกสุ่มขึ้นมา เช่น

load_from = '.cache/torch/hub/checkpoints/cascade_rcnn_r50_caffe_fpn_1x_coco_bbox_mAP-0.404_20200504_174853-b857be87.pth'

กรณีสั่ง train บน GPU มีวิธีสั่ง train 2 แบบ คือ

  1. Train บน GPU ตัวเดียว

ใช้ tools/train.py มีรูปแบบคือ

python tools/train.py \
${CONFIG_FILE} \
[optional arguments]

2. Train บน GPU หลายตัว

ใช้ tools/dist_train.sh มีรูปแบบคือ

bash ./tools/dist_train.sh \
${CONFIG_FILE} \
${GPU_NUM} \
[optional arguments]
  • ระบุ directory ที่ใช้เก็บ log file และ checkpoint ผ่าน --work-dir
  • กำหนด GPU ที่ต้องการใช้ train ด้วย CUDA_VISIBLE_DEVICES โดยต้องสั่งก่อนเรียกใช้ python เช่น กำหนดใช้ GPU หมายเลข 2 train
CUDA_VISIBLE_DEVICES=2 python mmdetection/tools/train.py modified_config.py \
--auto-scale-lr \
--work-dir=work_dirs2/ > log.txt 2>&1

กรณี train บน CPU กำหนด

export CUDA_VISIBLE_DEVICES=-1

2.5 การสร้างกราฟ Loss Function จากการ Train

ใช้ tools/analysis_tools/analyze_logs.py โดยต้องติดตั้ง seaborn ก่อน หากใช้ conda สามารถติดตั้งโดยใช้คำสั่ง

conda install -y anaconda::seaborn

analyze_logs.py ใช้ข้อมูลจากไฟล์ json ที่ได้จากการ train มาสร้างกราฟ โดยมีรูปแบบคำสั่งคือ

python tools/analysis_tools/analyze_logs.py plot_curve [--keys ${KEYS}] 
[--eval-interval ${EVALUATION_INTERVAL}] [--title ${TITLE}]
[--legend ${LEGEND}] [--backend ${BACKEND}] [--style ${STYLE}]
[--out ${OUT_FILE}]

ไฟล์ json ที่ได้จากการ train จะถูกเก็บไว้ในโฟลเดอร์เดียวกับที่ใช้เก็บโมเดลแต่ละ checkpoint เช่นกำหนดว่าโมเดลถูกเก็บไว้ที่ work_dir ไฟล์ jsonจะถูกเก็บไว้ที่ work_dir/20240303_090505/vis_data/20240303_090505.json หรือ scalars.json

ข้อมูลที่อยู่ในไฟล์ json ที่ได้จากการ train

คำสั่งในการสร้างกราฟเช่น

python mmdetection/tools/analysis_tools/analyze_logs.py plot_curve work_dirs/20240303_090505/vis_data/scalars.json --keys bbox_mAP --legend bbox_mAP --title "Bounding Box Mean Average Precision" --out bbox_mAP.png
กราฟที่ออกมาจากคำสั่งด้านบน

3. Testing Model

3.1 การสั่งผ่าน Python

เมื่อ train โมเดลเสร็จแล้ว สามารถนำโมเดลมาใช้กับรูปภาพที่ต้องการได้ โดยคำสั่ง python คล้ายกับตอนที่ลองนำ pretrained model มาใช้ เช่นใช้ config file คือ modified_config.py (อันเดียวกับที่ใช้ train โมเดล) ใช้โมเดลคือ work_dirs/modified_config/epoch_12.pth โดยรูปภาพที่ต้องการทดสอบอยู่ที่ custom_images_input/test.jpg และนำภาพผลลัพธ์เก็บไว้ที่ custom_images_output/ ได้คำสั่งคือ

from mmdet.apis import DetInferencer

model = DetInferencer(model="modified_config.py", weights="work_dirs/modified_config/epoch_12.pth", device="cuda")
model("custom_images_input/test.jpg", out_dir= "custom_images_output/")

3.2 การสั่งผ่าน Command Line

  1. กรณีต้องการ result submission เป็นไฟล์ json ที่เก็บผลลัพธ์จาก test set ทั้งหมด ใช้ tools/test.py มีรูปแบบคำสั่งคือ
python tools/test.py \
${CONFIG_FILE} \
${CHECKPOINT_FILE}

โดยก่อนสร้าง test ควรตรวจสอบก่อนว่าที่ตัวแปร test_evaluator ใน config file มี key ชื่อว่า outfile_prefix แล้วหรือไม่ เพราะว่าใช้กำหนด path สำหรับเก็บผลลัพธ์ เช่น

from mmengine.config import Config

cfg = Config.fromfile("modified_config.py")
cfg.test_evaluator.outfile_prefix="CascadeRCNN/result"
cfg.dump('test_modified_config.py')

คำสั่งผ่าน command line เช่น

CUDA_VISIBLE_DEVICES=2 python mmdetection/tools/test.py \
> test_modified_config.py \
> work_dirs/epoch_12.pth >> test_log2.txt

ได้ผลลัพธ์คือ CascadeRCNN/result.bbox.json

เนื้อหาใน result.bbox.json

2. การณีต้องการผลลัพธ์เป็นไฟล์ที่เก็บทั้งข้อมูลรูปภาพของ test set, ค่าความมั่นใจ และตำแหน่งที่ detect ของแต่ละ class เก็บในรูปแบบไฟล์ pickel ใช้รูปแบบคำสั่งคือ

python tools/test.py \
${CONFIG_FILE} \
${CHECKPOINT_FILE} \
[--out ${RESULT_FILE}] \
[--show]

โดยที่ --out ระบุเป็นชื่อไฟล์นามสกุล .pkl หรือ .pickle เช่น

CUDA_VISIBLE_DEVICES=3 python mmdetection/tools/test.py \
my_config/cascade_rcnn.py my_trained_model/cascade_rcnn/epoch_3.pth \
--out my_test_results/cascade_rcnn.pkl

วิธีอ่านไฟล์เพื่อแสดงรูปภาพ พร้อมผลลัพธ์การ detect เช่น

import pickle
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import cv2

# อ่านไฟล์ pickel
with open("my_test_results/cascade_rcnn.pkl", "rb") as f:
data = pickle.load(f)

# สร้าง function แสดงรูปภาพพร้อมผลลัพธ์
def show_img(data):

image_path = data["img_path"]
image = cv2.imread(image_path)

fig, ax = plt.subplots(1)

ax.axis("off")
ax.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))

bboxes = data["pred_instances"]["bboxes"]
labels = data["pred_instances"]["labels"]

for bbox, label in zip(bboxes, labels):
xmin, ymin, xmax, ymax = bbox
# ตำแหน่งกล่องสี่เหลี่ยมกำหนดเป็น (พิกัดx, พิกัดy), ความกว้าง, ความสูง
rect = patches.Rectangle((xmin, ymin), xmax-xmin, ymax-ymin, linewidth=1, edgecolor='r', facecolor="none")

ax.add_patch(rect)

plt.show()

show_img(data[9])

--

--

Nuttaset kuapanich

กำลังศึกษาระดับปริญญาตรี คณะปัญญาประดิษฐ์ มหาวิยาลัยซุนยัดเซ็น Email: kuapanich@mail2.sysu.edu.cn