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

文章目录

  • 使用non-distributed模式进行多卡训练
  • 在训练过程中进行eval
    • 使用work_flow来进行控制
    • 自己添加eval的代码
      • 思路
      • 控制训练的epoch和eval的epoch
      • 构建Evaluator
    • 其他问题
      • 在val的过程中没有gt-bbox
      • tools/test.py中使用distributed模式

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

在训练过程中进行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-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的代码

所以另外一种方式是自己添加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。

控制训练的epoch和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)

构建Evaluator

这部分代码参考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的过程中没有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.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:这个问题仍然没有解决!!!

你可能感兴趣的:(mmdetection)