前言
本文主要闡述如何使用mmdetection
訓練自己的數(shù)據(jù),包括配置文件的修改,訓練時的數(shù)據(jù)增強,加載預訓練權(quán)重以及繪制損失函數(shù)圖等。這里承接上一篇文章,默認已經(jīng)準備好了COCO
格式數(shù)據(jù)集且已安裝mmdetection
,環(huán)境也已經(jīng)配置完成。
這里說明一下,因為mmdetection
更新至2.x
版本之后有些用法不一樣了,所以對本文重新更新一下,這里使用的mmdetection
的版本是2.27.0
,只要是2.x
版本本文都適用的。
1、配置文件修改
配置文件獲取方式一:
首先就是根據(jù)任務選定一個訓練模型,這里我選用yolox-s
作為我的訓練模型,進入mmdetection/configs/yolox
文件夾,可以看到有以下文件:
這里可以看到有yolox-s
的配置文件yolox_s_8x8_300e_coco.py
,這里請注意:不要在默認的配置文件中修改內(nèi)容,最好將要修改的配置文件復制一份,在副本文件中修改內(nèi)容?。?! 復制一份配置文件之后,就可以根據(jù)需要進行修改了!
配置文件獲取方式二(推薦):
方式二就是使用openmmlab
的包管理工具mim
來獲取配置文件和預訓練權(quán)重,這里的mim在安裝mmdetection時就安裝上了,就不進行安裝說明了,如果有問題請參考mmdetection文檔。
這里以jupyternotebook
為例進行講述,在終端使用時請去掉感嘆號!
# 以yolox為例獲取配置文件 --model后面就寫你想獲取的模型配置文件
!mim search mmdet --model 'yolox'
當上述運行后,會出現(xiàn)下面的內(nèi)容:
這里我要使用yolox-s
,那么我就選中對應的config id:yolox_s_8x8_300e_coco
,然后執(zhí)行下面的代碼:
# --dest后面有個空格,然后再加一個點,這個得是英文的點
!mim download mmdet --config yolox_s_8x8_300e_coco --dest .
這樣就將配置文件和預訓練權(quán)重下載到你的當前文件執(zhí)行的目錄下了:
之后在這個配置文件里面改你所需的東西就行了
1.1 model部分
這里大部分參數(shù)可以沿用默認的,或許有修改的是bbox_head
中的num_classes=80
,這個是類別數(shù),COCO
數(shù)據(jù)集是80類,你可以看自己數(shù)據(jù)集是多少類別,然后改成對應的,比如我的數(shù)據(jù)集有2類,那么就改成num_classes=2
。
另外就是test_cfg
下的nms=dict(type='nms', iou_threshold=0.65)
,iou_threshold=0.65
可以修改,你可以把iou
閾值改成你想要的,比如iou_threshold=0.40
。
如果想使用預訓練權(quán)重,那么可以這樣設置,就是在model字典開頭,加上init_cfg=dict(type='Pretrained', checkpoint='這里輸入你的預訓練權(quán)重文件路徑')
一些分類頭和FPN
的修改和BackBone
的替換并不在本文之內(nèi)。
1.2 dataset部分
2.x
之后的mmdetection
在dataset
部分有一些不同,這里重新說明一下自定義數(shù)據(jù)集的設置
在mmdetection
文件夾中創(chuàng)建data
文件夾,然后創(chuàng)建子文件夾,把子文件夾的名稱設為coco
,將你的訓練、驗證、測試數(shù)據(jù)導入其中。具體樣式如下:
返回配置文件,然后在下列填入你的數(shù)據(jù)集路徑:
你數(shù)據(jù)集的類別可能不是coco
的80類,那么就需要把類別給改了,具體操作如下:
- 進入
mmdetection/mmdet/datasets
,打開coco.py
,我們要修改其內(nèi)容(這里我們默認數(shù)據(jù)集格式是COCO
) - 按照下圖樣式,把原來的CLASSES注掉,新起一個CLASSES,里面填你的類別,這里需要注意:如果你的數(shù)據(jù)集只有一個類別,那么記得在類別后面加一個逗號,不然會報錯!??! 請注意:這里類別的名字得和你的圖片目標標簽名字一樣,別你的標簽是 Cat和Dog,然后在這里變成了 cat和dog?。?! 其他的地方都不需要動!??!
這里對coco.py
修改之后,還需修改一個地方,請把目光轉(zhuǎn)到mmdetection-2.27.0/mmdet/core/evaluation/class_names.py
這個文件下面,將你的類別數(shù)量也進行修改,找到def coco_classes():
,改成你自己的類別:
當我們把上述兩個文件修改之后記得重新編譯一下代碼:
!python setup.py install
這樣dataset
初步構(gòu)建完成,下面針對train dataset
進行修改
1.2.1 train dataset部分
訓練部分數(shù)據(jù)增強
說起train dataset
肯定離不開數(shù)據(jù)增強,我這里沒有使用mmdetection
內(nèi)置的數(shù)據(jù)增強,如果你想看其內(nèi)置哪些增強,可以在mmdetection/mmdet/datasets/pipelines/transforms.py
中查看。我這里使用albumentations
庫進行數(shù)據(jù)增強(主要是功能真的很強大,太香了),如果你也想使用這個開源庫,那么請先安裝它:
pip install -U albumentations
然后在train_pipelines
添加或修改你的增強策略。具體可以參考我的代碼:
- 首先在配置文件開頭添加如下代碼:
### Albumentations Start ###
img_norm_cfg = dict(
mean=[95.4554, 107.3959, 69.8863], std=[56.0811, 55.2941, 55.2364], to_rgb=True)
albu_train_transforms = [
dict(
type='RandomBrightnessContrast',
brightness_limit=[-0.2, 0.3],
contrast_limit=[-0.2, 0.3],
p=0.5),
dict(type='RandomRotate90', p=0.5),
dict(type='GaussianBlur', blur_limit=(3, 7), sigma_limit=(0, 0.99), p=0.5),
dict(type='MotionBlur', blur_limit=(3, 7), p=0.5)
]
### Albumentations End ###
type='RandomBrightnessContrast',type='RandomRotate90'
都是增強策略,這個可以查看albumentations官方文檔,根據(jù)自己需求添加,添加格式和我上面的代碼一樣。
然后 mean=[95.4554, 107.3959, 69.8863], std=[56.0811, 55.2941, 55.2364]
,這個是你數(shù)據(jù)集的均值和標準差,可以自己編寫一個Python
程序自動計算一下,如果你懶得編寫,那么可以參考我的這個,就是計算起來稍微有點慢。
import torch
from torch.utils.data import DataLoader, Dataset
import os
from pathlib import Path
import numpy as np
from PIL import Image
def cal_mean_std(path: str):
channels_sum, channels_squared_sum, nums = 0, 0, 0
path_list = os.listdir(path)
for img_path in path_list:
image_path = os.path.join(path, img_path)
# image = torch.from_numpy(np.array(Image.open(image_path)) / 255).permute([2, 0, 1]).float()
image = torch.from_numpy(np.array(Image.open(image_path))).permute([2, 0, 1]).float()
channels_sum += torch.mean(image, dim=[1, 2])
channels_squared_sum += torch.mean(image**2, dim=[1, 2])
nums += 1
mean = channels_sum / nums
std = (channels_squared_sum / nums - mean**2)**0.5
return mean, std
if __name__ == '__main__':
path = os.path.abspath("F:/VOC2012/VOCdevkit/VOC2012/JPEGImages")
mean, std = cal_mean_std(path=path)
print(f'mean : {mean}, std : {std}')
到這里train_pipelines
添加完成
train dataset后續(xù)
作完數(shù)據(jù)增強后就應該將其添加到train_dataset
中了,照我這樣添加就好了:
train_pipeline = [
dict(
type='Albu',
transforms=albu_train_transforms,
bbox_params=dict(
type='BboxParams',
format='pascal_voc',
label_fields=['gt_labels'],
min_visibility=0.1,
filter_lost_elements=True),
keymap={
'img': 'image',
'gt_bboxes': 'bboxes'
},
update_pad_shape=False,
skip_img_without_anno=True),
dict(type='FilterAnnotations', min_gt_bbox_wh=(1, 1), keep_empty=False),
dict(type='DefaultFormatBundle'),
dict(
type='Collect',
keys=['img', 'gt_bboxes', 'gt_labels'],
meta_keys=('filename', 'ori_shape', 'img_shape', 'img_norm_cfg',
'pad_shape', 'scale_factor'))
]
train_dataset = dict(
type='MultiImageMixDataset',
dataset=dict(
type=dataset_type,
ann_file=data_root + 'annotations/instances_train2017.json',
img_prefix=data_root + 'train2017/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True)
],
filter_empty_gt=False,
),
pipeline=train_pipeline)
1.2.2 dataset后續(xù)
dataset
的其他部分如下所示,修改的地方并不多,samples_per_gpu=16
說的是單張GPU
的batch size
,這個看你GPU
顯存大小了,數(shù)值大占顯存就多,數(shù)值過小就訓練不佳;worker_per_gpu=1
表示線程數(shù),這個看你CPU
的數(shù)量了,你要是有15個,那就設15,榨干服務器性能。
1.3 其他部分
其他部分無外乎就是學習率、優(yōu)化器、迭代次數(shù),多少輪驗證一次等等
如果電腦GPU一般,我推薦可以訓練10輪驗證一次,然后checkpoint
可以設置比如50輪保存一次,比較節(jié)省內(nèi)存
這里全部弄完之后,就能開始訓練了
python tools/train.py configs/yolox/yolox_s_peach_coco.py --auto-scale-lr
2、繪制訓練損失圖
按照上述訓練完成之后,會在mmdetection
文件夾下生成一個叫做work_dirs
的文件夾,里面存放著訓練日志、訓練模型的權(quán)重、配置文件。繪制train loss
圖的話,我們用到的是以.log.json
結(jié)尾的文件。
輸入這行命令:
python tools\analysis_tools\analyze_logs.py plot_curve yolox.log.json --keys loss --start-epoch 1 --eval-interval 10 --legend loss
–eval-interval 是多少輪驗證一次,請和訓練配置文件設置一致
運行之后如果你得到了這樣的錯誤信息,list index out of range
,列表越界問題
不要擔心,請按我下面的代碼。替換analyze_loss.py
中的def plot_curve(log_dicts, arg)
函數(shù):
def plot_curve(log_dicts, args):
if args.backend is not None:
plt.switch_backend(args.backend)
sns.set_style(args.style)
# if legend is None, use {filename}_{key} as legend
legend = args.legend
if legend is None:
legend = []
for json_log in args.json_logs:
for metric in args.keys:
legend.append(f'{json_log}_{metric}')
assert len(legend) == (len(args.json_logs) * len(args.keys))
metrics = args.keys
num_metrics = len(metrics)
for i, log_dict in enumerate(log_dicts):
epochs = list(log_dict.keys())
for j, metric in enumerate(metrics):
print(f'plot curve of {args.json_logs[i]}, metric is {metric}')
if metric not in log_dict[epochs[int(args.eval_interval) - 1]]:
if 'mAP' in metric:
raise KeyError(
f'{args.json_logs[i]} does not contain metric '
f'{metric}. Please check if "--no-validate" is '
'specified when you trained the model.')
raise KeyError(
f'{args.json_logs[i]} does not contain metric {metric}. '
'Please reduce the log interval in the config so that '
'interval is less than iterations of one epoch.')
if 'mAP' in metric:
xs = []
ys = []
for epoch in epochs:
ys += log_dict[epoch][metric]
if 'val' in log_dict[epoch]['mode']:
xs.append(epoch)
plt.xlabel('epoch')
plt.plot(xs, ys, label=legend[i * num_metrics + j], marker='o')
else:
xs = []
ys = []
num_iters_per_epoch = log_dict[epochs[0]]['iter'][-1]
for epoch in epochs:
iters = log_dict[epoch]['iter']
if log_dict[epoch]['mode'][-1] == 'val':
iters = iters[:-1]
# xs.append(
# np.array(iters) + (epoch - 1) * num_iters_per_epoch)
xs.append(np.array([epoch]))
ys.append(np.array(log_dict[epoch][metric][:len(iters)]))
xs = np.concatenate(xs)
ys = np.concatenate(ys)
# plt.xlabel('iter')
plt.xlabel('epoch')
plt.plot(
xs, ys, label=legend[i * num_metrics + j], linewidth=0.5)
plt.legend()
if args.title is not None:
plt.title(args.title)
if args.out is None:
plt.show()
else:
print(f'save curve to: {args.out}')
plt.savefig(args.out)
plt.cla()
這樣你就能畫出下面這樣的訓練損失函數(shù)圖啦!
全部配置信息
這里貼上的我配置文件,代碼行數(shù)有點多,介意的小伙伴可以跳過此處
optimizer = dict(
type='SGD',
lr=0.01,
momentum=0.9,
weight_decay=0.0005,
nesterov=True,
paramwise_cfg=dict(norm_decay_mult=0.0, bias_decay_mult=0.0))
optimizer_config = dict(grad_clip=None)
lr_config = dict(
policy='YOLOX',
warmup='exp',
by_epoch=False,
warmup_by_epoch=True,
warmup_ratio=1,
warmup_iters=5,
num_last_epochs=15,
min_lr_ratio=0.05)
runner = dict(type='EpochBasedRunner', max_epochs=300)
checkpoint_config = dict(interval=10)
log_config = dict(interval=50, hooks=[dict(type='TextLoggerHook')])
custom_hooks = [
dict(type='YOLOXModeSwitchHook', num_last_epochs=15, priority=48),
dict(type='SyncNormHook', num_last_epochs=15, interval=10, priority=48),
dict(
type='ExpMomentumEMAHook',
resume_from=None,
momentum=0.0001,
priority=49)
]
dist_params = dict(backend='nccl')
log_level = 'INFO'
load_from = None
resume_from = None
workflow = [('train', 1)]
opencv_num_threads = 0
mp_start_method = 'fork'
auto_scale_lr = dict(enable=False, base_batch_size=64)
img_scale = (640, 640)
model = dict(
type='YOLOX',
input_size=(640, 640),
random_size_range=(15, 25),
random_size_interval=10,
backbone=dict(type='CSPDarknet', deepen_factor=0.33, widen_factor=0.5,
init_cfg=dict(type='Pretrained', checkpoint='yolox_s_8x8_300e_coco_20211121_095711-4592a793.pth')),
neck=dict(
type='YOLOXPAFPN',
in_channels=[128, 256, 512],
out_channels=128,
num_csp_blocks=1),
bbox_head=dict(
type='YOLOXHead', num_classes=1, in_channels=128, feat_channels=128),
train_cfg=dict(assigner=dict(type='SimOTAAssigner', center_radius=2.5)),
test_cfg=dict(score_thr=0.01, nms=dict(type='nms', iou_threshold=0.65)))
data_root = 'data/coco/'
dataset_type = 'CocoDataset'
train_pipeline = [
dict(type='Mosaic', img_scale=(640, 640), pad_val=114.0),
dict(
type='RandomAffine', scaling_ratio_range=(0.1, 2),
border=(-320, -320)),
dict(
type='MixUp',
img_scale=(640, 640),
ratio_range=(0.8, 1.6),
pad_val=114.0),
dict(type='YOLOXHSVRandomAug'),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Resize', img_scale=(640, 640), keep_ratio=True),
dict(
type='Pad',
pad_to_square=True,
pad_val=dict(img=(114.0, 114.0, 114.0))),
dict(type='FilterAnnotations', min_gt_bbox_wh=(1, 1), keep_empty=False),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
]
train_dataset = dict(
type='MultiImageMixDataset',
dataset=dict(
type='CocoDataset',
ann_file='data/coco/annotations/instances_train2017.json',
img_prefix='data/coco/train2017/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True)
],
filter_empty_gt=False),
pipeline=[
dict(type='Mosaic', img_scale=(640, 640), pad_val=114.0),
dict(
type='RandomAffine',
scaling_ratio_range=(0.1, 2),
border=(-320, -320)),
dict(
type='MixUp',
img_scale=(640, 640),
ratio_range=(0.8, 1.6),
pad_val=114.0),
dict(type='YOLOXHSVRandomAug'),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Resize', img_scale=(640, 640), keep_ratio=True),
dict(
type='Pad',
pad_to_square=True,
pad_val=dict(img=(114.0, 114.0, 114.0))),
dict(
type='FilterAnnotations', min_gt_bbox_wh=(1, 1), keep_empty=False),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
])
test_pipeline = [
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(640, 640),
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(
type='Pad',
pad_to_square=True,
pad_val=dict(img=(114.0, 114.0, 114.0))),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img'])
])
]
data = dict(
samples_per_gpu=24,
workers_per_gpu=15,
persistent_workers=True,
train=dict(
type='MultiImageMixDataset',
dataset=dict(
type='CocoDataset',
ann_file='data/coco/annotations/instances_train2017.json',
img_prefix='data/coco/train2017/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True)
],
filter_empty_gt=False),
pipeline=[
dict(type='Mosaic', img_scale=(640, 640), pad_val=114.0),
dict(
type='RandomAffine',
scaling_ratio_range=(0.1, 2),
border=(-320, -320)),
dict(
type='MixUp',
img_scale=(640, 640),
ratio_range=(0.8, 1.6),
pad_val=114.0),
dict(type='YOLOXHSVRandomAug'),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Resize', img_scale=(640, 640), keep_ratio=True),
dict(
type='Pad',
pad_to_square=True,
pad_val=dict(img=(114.0, 114.0, 114.0))),
dict(
type='FilterAnnotations',
min_gt_bbox_wh=(1, 1),
keep_empty=False),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
]),
val=dict(
type='CocoDataset',
ann_file='data/coco/annotations/instances_val2017.json',
img_prefix='data/coco/val2017/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(640, 640),
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(
type='Pad',
pad_to_square=True,
pad_val=dict(img=(114.0, 114.0, 114.0))),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img'])
])
]),
test=dict(
type='CocoDataset',
ann_file='data/coco/annotations/instances_test2017.json',
img_prefix='data/coco/test2017/',
pipeline=[
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(640, 640),
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(
type='Pad',
pad_to_square=True,
pad_val=dict(img=(114.0, 114.0, 114.0))),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img'])
])
]))
max_epochs = 300
num_last_epochs = 15
interval = 100
evaluation = dict(
save_best='auto', interval=100, dynamic_intervals=[(285, 1)], metric='bbox')
報錯問題
如果在訓練時出現(xiàn)報錯信息如下:
AssertionError: The
num_classes
(2) in Shared2FCBBoxHead of MMDataParallel does not matches the length ofCLASSES
80) in CocoDataset
那么首先檢查一下是不是在修改了coco.py
和class_names.py
之后忘記重新編譯了,重新編譯一下可能就好了,編譯代碼請看上面1.2部分。
如果重新編譯也解決不了,那么就需要去環(huán)境里面把源文件給改了,如果在本地運行的話請在安裝的虛擬環(huán)境里面找,比如我安裝mmdetection
的虛擬環(huán)境安裝在D盤的ai環(huán)境,像這樣D:\Anaconda\envs\ai\Lib\site-packages\mmdet
,和上面的步驟一樣,在這里面找到cooc.py
和class_names.py
然后改了,之后就能正常運行了。
如果你是在云GPU上訓練模型,那么請這樣查找環(huán)境:pip show mmdet
之后的步驟和前面一樣,我就不進行贅述了。
總結(jié)
本文講述了配置文件的修改和數(shù)據(jù)增強的添加,另外對繪制損失函數(shù)圖會出現(xiàn)的一個問題進行了解決,歡迎大家提供不同意見,共同學習!下篇文章將講述如何修改bbox的字體顏色等等。文章來源:http://www.zghlxwxcb.cn/news/detail-458538.html
參考鏈接
mmdetection文檔
一個報錯問題的解決
mmdetection項目文章來源地址http://www.zghlxwxcb.cn/news/detail-458538.html
到了這里,關(guān)于使用MMDetection訓練自己的數(shù)據(jù)集的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!