【mmdetection踩坑记录】(二)non-distributed模式多卡训练

2022-10-30 08:16:58

本博客内容涉及较多对代码的修改,这个前提是对train的过程有比较详细的了解,这部分详见【mmdetection实践】(二)训练自己的网络。在【mmdetection实践】(二)训练自己的网络我使用distributed模式,总是会莫名奇妙的卡住,所以就想采用non-distributed模式进行多卡训练。

使用non-distributed模式进行多卡训练

non-distributed的直接使用tools/train.py进行训练,所以查看其中内容,可以看到

defparse_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”了。

在训练过程中进行eval

【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。

使用work_flow来进行控制

【mmdetection实践】(二)训练自己的网络中看到了在CONFIG_FILE中workflow的应用,可以通过设定workflow来控制train和val的循环。例如:

workflow=[('train',3),('val',1)]

这意味着每3个train之后增加1个val构成一个循环。但这样做也有明显的缺点,先看以下log:

2019-11-0715:35:39,911- INFO- workflow:[('train',3),('val',1)],max:10 epochs2019-11-0715: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.10302019-11-0715: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.82592019-11-0715: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.74242019-11-0715: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的代码

所以另外一种方式是自己添加eval代码,这就需要对train和test的过程有一定的了解,具体的详解可以看我另外一篇博客【mmdetection实践】(二)训练自己的网络

思路

具体的来说,就是需要找到train过程中控制epoch的地方,在特定的epoch过后插入一个eval的epoch。查看train的代码,幸运的是mmdet/apis/train.py中留出了控制epoch的接口。

#./mmdet/apis/train.pydef_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。

控制训练的epoch和eval的epoch

直接看我修改后的代码

def_non_dist_train(model, dataset, cfg, validate=False):# prepare data loaders
    dataset= datasetifisinstance(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 dsin 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_cfgisnotNone:
        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 iinrange(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=10withopen(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_datain evals:for iinrange(len(eval_data)):
                f.write(('%.2f'% eval_data[i]).ljust(w_i))
            f.write('\n')

上述代码值得注意的是runner.run中有一个对epoch进行计数的变量,在初始化时为0,每训练一个epoch就加1。而其中传入参数是指,停止时的epoch,当停止时的epoch小于内部计数时,不做训练。例如下列代码

deftest_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)

构建Evaluator

这部分代码参考tools/test.py

import argparseimport osimport os.pathas ospimport shutilimport tempfileimport mmcvimport torchimport torch.distributedas distfrom mmcv.parallelimport MMDataParallel, MMDistributedDataParallelfrom mmcv.runnerimport get_dist_info, load_checkpointfrom mmdet.apisimport init_distfrom mmdet.coreimport coco_eval, results2json, wrap_fp16_modelfrom mmdet.datasetsimport build_dataloader, build_datasetfrom mmdet.modelsimport build_detectorimport numpyas npclassEvaluator: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)ifnot os.path.exists(self.output_dir):
            os.mkdir(self.output_dir)defeval_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, datainenumerate(self.data_loader):with torch.no_grad():
                result= model(return_loss=False, rescale=False,**data)iflen(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 _inrange(batch_size):
                prog_bar.update()

        length=0for iinrange(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]= epochreturn outputs

其他问题

在val的过程中没有gt-bbox

在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']),])]

tools/test.py中使用distributed模式

test过程中,distributed模式中的最后一个卡不运算,找不到原因。

# tools/test.pydefmulti_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, datainenumerate(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 _inrange(batch_size* world_size):
                prog_bar.update()# collect results from all ranks
    results= collect_results(results,len(dataset), tmpdir)return results

TODO:这个问题仍然没有解决!!!

  • 作者:麒麒哈尔
  • 原文链接:https://blog.csdn.net/wqwqqwqw1231/article/details/103097356
    更新时间:2022-10-30 08:16:58