本博客内容涉及较多对代码的修改,这个前提是对train的过程有比较详细的了解,这部分详见【mmdetection实践】(二)训练自己的网络。在【mmdetection实践】(二)训练自己的网络我使用distributed模式,总是会莫名奇妙的卡住,所以就想采用non-distributed模式进行多卡训练。
non-distributed的直接使用tools/train.py进行训练,所以查看其中内容,可以看到
def parse_args():
parser = argparse.ArgumentParser(description='Train a detector')
parser.add_argument(
'--gpus',
type=int,
default=1,
help='number of gpus to use '
'(only applicable to non-distributed training)')
其中有一条是跟gpus有关,所以在调用train.py时,可以将“–gpus 4”作为参数传入,将gpu使用个数设定为4。或者,直接修改代码中的default值,设置成电脑的gpu个数,以后就可以不用每次都输入“–gpus”了。
在【mmdetection实践】(二)训练自己的网络中提到,dist_train.sh的调用可以传入参数–validate来控制在训练中进行评价。但这个功能并没有集成到non-distributed模式中,这个也可以参考:
Built-in validation is not implemented yet in not-distributed training. Use distributed training or test.py and *eval.py scripts instead.’
所以想要在train的过程中进行eval,就需要自己添加一部分代码。总体思想就是在train的过程中,经过一定的epoch,就调用一次eval。
在【mmdetection实践】(二)训练自己的网络中看到了在CONFIG_FILE中workflow的应用,可以通过设定workflow来控制train和val的循环。例如:
workflow = [('train', 3), ('val' , 1)]
这意味着每3个train之后增加1个val构成一个循环。但这样做也有明显的缺点,先看以下log:
2019-11-07 15:35:39,911 - INFO - workflow: [('train', 3), ('val', 1)], max: 10 epochs
2019-11-07 15:35:49,115 - INFO - Epoch [1][1/1] lr: 0.00333, eta: 0:01:22, time: 9.202, data_time: 0.423, memory: 2203, acc: 99.1943, loss_cls: 0.4014, loss_bbox: 0.0097, loss_rpn_cls: 0.6872, loss_rpn_bbox: 0.0047, loss: 1.1030
2019-11-07 15:35:50,462 - INFO - Epoch [2][1/1] lr: 0.00335, eta: 0:00:41, time: 1.290, data_time: 0.755, memory: 2527, acc: 99.3408, loss_cls: 0.1254, loss_bbox: 0.0139, loss_rpn_cls: 0.6817, loss_rpn_bbox: 0.0049, loss: 0.8259
2019-11-07 15:35:51,855 - INFO - Epoch [3][1/1] lr: 0.00336, eta: 0:00:27, time: 1.283, data_time: 0.768, memory: 2527, acc: 99.3164, loss_cls: 0.0519, loss_bbox: 0.0152, loss_rpn_cls: 0.6702, loss_rpn_bbox: 0.0053, loss: 0.7424
2019-11-07 15:35:53,141 - INFO - Epoch(train) [3][1] acc: 98.9990, loss_cls: 0.0670, loss_bbox: 0.0228, loss_rpn_cls: 0.6433, loss_rpn_bbox: 0.0051, loss: 0.7383
从log中可以看到,3个train epoch之后跟了一个“Epoch(train)”,这里应该标错了,这应该就是指Epoch(val),可以看到val中的量有acc和loss。这里就需要解释以下acc的意义,acc的意义是指在最后所有bbox中,分类正确的有多少。这里看到,经过3个epoch的train,就达到了98.999%,这个值得意思是网络最终生成的bbox中其中分类正确的有98.999%,要知道每张图片其中只有一个是物体,其他都是背景。所以这样看,acc虽然值很高,但其实效果也很不好,并不能真正反映网络的效果。
所以另外一种方式是自己添加eval代码,这就需要对train和test的过程有一定的了解,具体的详解可以看我另外一篇博客【mmdetection实践】(二)训练自己的网络。
具体的来说,就是需要找到train过程中控制epoch的地方,在特定的epoch过后插入一个eval的epoch。查看train的代码,幸运的是mmdet/apis/train.py中留出了控制epoch的接口。
#./mmdet/apis/train.py
def _non_dist_train(model, dataset, cfg, validate=False):
··· # 省略了一些代码
# 进行训练,其中cfg.total_epochs控制了run一次的的epoch数量
runner.run(data_loaders, cfg.workflow, cfg.total_epochs)
所以,我们只需要将总的total_epochs切分成很多小块,在每个小块之后加入eval的一个epoch。
直接看我修改后的代码
def _non_dist_train(model, dataset, cfg, validate=False):
# prepare data loaders
dataset = dataset if isinstance(dataset, (list, tuple)) else [dataset]
data_loaders = [
build_dataloader(
ds,
cfg.data.imgs_per_gpu,
cfg.data.workers_per_gpu,
cfg.gpus,
dist=False) for ds in dataset
]
# put model on gpus
model = MMDataParallel(model, device_ids=range(cfg.gpus)).cuda()
# build runner
optimizer = build_optimizer(model, cfg.optimizer)
runner = Runner(model, batch_processor, optimizer, cfg.work_dir,
cfg.log_level)
# fp16 setting
fp16_cfg = cfg.get('fp16', None)
if fp16_cfg is not None:
optimizer_config = Fp16OptimizerHook(
**cfg.optimizer_config, **fp16_cfg, distributed=False)
else:
optimizer_config = cfg.optimizer_config
runner.register_training_hooks(cfg.lr_config, optimizer_config,
cfg.checkpoint_config, cfg.log_config)
if cfg.resume_from:
runner.resume(cfg.resume_from)
elif cfg.load_from:
runner.load_checkpoint(cfg.load_from)
# 以上代码未做修改,基本就是设置一些与train有关的东西,包括dataloader,model等
evals = [] # 用来记录eval的结果
evaluator = Evaluator(cfg, distributed=False) # 初始化eval所用的dataloader等
evals.append(evaluator.eval_one_epoch(model, epoch=0)) # eval一个epoch,并把结果放入evals中
its = int(round(cfg.total_epochs / cfg.eval_in_train.interval)) # 每eval_in_train.interval个train的epoch之后加一个eval的epoch,并计算进行trian+eval一共its个循环
for i in range(its):
runner.run(data_loaders, cfg.workflow, (i+1)*cfg.eval_in_train.interval)
evals.append(evaluator.eval_one_epoch(model, epoch=runner.epoch)) # eval一个epoch,并把结果放入evals中
# 记录eval的结果,放入results_ap.txt中
evals = np.vstack(evals)
output_file = os.path.join(cfg.work_dir, 'results_ap.txt')
w_i = 10
with open(output_file, 'w') as f:
f.write('epoch' + 'ap'.ljust(w_i) + 'ap(0.5)'.ljust(w_i) + 'ap(0.75)'.ljust(w_i) + 'ap(s)'.ljust(w_i) + 'ap(m)'.ljust(w_i) + 'ap(l)'.ljust(w_i)+
'ar'.ljust(w_i) + 'ar(0.5)'.ljust(w_i) + 'ar(0.75)'.ljust(w_i) + 'ar(s)'.ljust(w_i) + 'ar(m)'.ljust(w_i) + 'ar(l)'.ljust(w_i))
f.write('\n')
for eval_data in evals:
for i in range(len(eval_data)):
f.write(('%.2f' % eval_data[i]).ljust(w_i))
f.write('\n')
上述代码值得注意的是runner.run中有一个对epoch进行计数的变量,在初始化时为0,每训练一个epoch就加1。而其中传入参数是指,停止时的epoch,当停止时的epoch小于内部计数时,不做训练。例如下列代码
def test_runner():
runner = Runner(···) # 初始化
runner.run(data_loaders, cfg.workflow, 1) # 内部计数初始为0,执行总epoch数为1,所以本次执行1次train的epoch,内部计数变成1
runner.run(data_loaders, cfg.workflow, 2) # 内部计数为1,执行总epoch数为2,所以本次执行1次train的epoch,内部计数变成2
runner.run(data_loaders, cfg.workflow, 1) # 内部计数为2,执行总epoch数为1,所以本次执行0次train的epoch,内部计数2
为了控制多少个train加入一个eval,在CONFIG_FILE中加入下面的代码,在计算its时使用
evaluation = dict(interval=1)
这部分代码参考tools/test.py
import argparse
import os
import os.path as osp
import shutil
import tempfile
import mmcv
import torch
import torch.distributed as dist
from mmcv.parallel import MMDataParallel, MMDistributedDataParallel
from mmcv.runner import get_dist_info, load_checkpoint
from mmdet.apis import init_dist
from mmdet.core import coco_eval, results2json, wrap_fp16_model
from mmdet.datasets import build_dataloader, build_dataset
from mmdet.models import build_detector
import numpy as np
class Evaluator:
def __init__(self, cfg, distributed):
self.dataset = build_dataset(cfg.data.test) # 使用cfg.data.test构建eval使用的数据集
data_loader = build_dataloader(
self.dataset,
imgs_per_gpu=1,
workers_per_gpu=cfg.data.workers_per_gpu,
dist=distributed,
shuffle=False,)
self.data_loader = data_loader
self.length = len(self.dataset)
self.output_dir = os.path.join(cfg.work_dir, cfg.eval_in_train.output_dir)
if not os.path.exists(self.output_dir):
os.mkdir(self.output_dir)
def eval_one_epoch(self, model, epoch):
model.eval()
# 下面两行是对non-distributed模式的妥协
model = model.module # 先将model从多卡的Parallel格式退出来
model = MMDataParallel(model, device_ids=[0]) # 然后将model放入单卡的Parallel的模式
results = []
prog_bar = mmcv.ProgressBar(self.length)
for i, data in enumerate(self.data_loader):
with torch.no_grad():
result = model(return_loss=False, rescale=False, **data)
if len(result[0]) == 0: # 对COCO计算的妥协,当一张图什么都预预测不到的时候,result为空,这时候在计算eval统计数据的时候会出现错误
result = [np.array([[0, 0, 0, 0, 0.0]])]
results.append(result)
batch_size = data['img'][0].size(0)
for _ in range(batch_size):
prog_bar.update()
length = 0
for i in range(len(results)):
length += len(results[i][0])
print('\n results length: {}'.format(length))
out_file = os.path.join(self.output_dir, 'eval_%06d_epoch.pkl' % epoch)
eval_types = ['bbox']
outputs = np.zeros(13)
# mmcv.dump(results, out_file)
result_files = results2json(self.dataset, results, out_file)
outputs[1:] = coco_eval(result_files, eval_types, self.dataset.coco)
outputs[0] = epoch
return outputs
在val过程中,由于需要计算统计结果,所以需要真实的类别和bbox等信息,需要在CONFIG_FILE中修改test的dataset加载过程中的一些设置,可以将test的设置模仿val。
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']),
])
]
test过程中,distributed模式中的最后一个卡不运算,找不到原因。
# tools/test.py
def multi_gpu_test(model, data_loader, tmpdir=None):
model.eval()
results = []
dataset = data_loader.dataset
rank, world_size = get_dist_info()
if rank == 0:
prog_bar = mmcv.ProgressBar(len(dataset))
for i, data in enumerate(data_loader):
with torch.no_grad():
result = model(return_loss=False, rescale=True, **data) # 最后一张显卡上的model不推理
results.append(result)
if rank == 0:
batch_size = data['img'][0].size(0)
for _ in range(batch_size * world_size):
prog_bar.update()
# collect results from all ranks
results = collect_results(results, len(dataset), tmpdir)
return results
TODO:这个问题仍然没有解决!!!