mmcv组件5

MMCV核心组件Runner

​ 在2.0.0版本以后,Runnner从mmcv中移至mmengine,大体用法不发生改变,调用方式发生如下改变:

1
2
from mmcv.runner import Runner # < 2.0.0
from mmengine.runner import Runner # >= 2.0.0

官方文档提示:

我们希望你在教程中更多地关注整体结构,而非具体模块的实现。这种“自顶向下”的思考方式是我们所倡导的。别担心,之后你将有充足的机会和指引,聚焦于自己想要改进的模块

​ Runner 是 OpenMMLab 系列框架中训练部分的引擎,其核心功能和特性如下:

  • 负责 OpenMMLab 中所有框架的训练过程调度,是一个基础但功能丰富的类
  • 支持定制工作流以满足训练过程中各状态自由切换,目前支持训练和验证两个工作流
  • 提供了 Epoch 和 Iter 为基础的迭代模式以满足不同场景,例如 MMDetection 默认采用 Epoch (配置文件中相关参数都是以 Epoch 为单位),而 MMSegmentation 默认采用 Iter (配置文件中相关参数都是以 Iter 为单位)
  • 配合各类 Hook,对外提供了灵活的扩展能力,注入不同类型的 Hook,就可以在训练过程中以一种优雅的方式实现扩展功能

Runner 分析

Runner 初始化

考虑到 Epoch 和 Iter 模式有很多共有逻辑,为了复用,抽象出一个 BaseRunner。BaseRunner 初始化是一个常规初始化过程,其参数如下:

1
2
3
4
5
6
7
8
def __init__(self,
model,
optimizer=None,
work_dir=None,
logger=None,
meta=None, # 提供了该参数,则会保存到 ckpt 中
max_iters=None, # 这两个参数非常关键,如果没有给定,则内部自己计算
max_epochs=None):

​ 当然这只是主要的参数,看源码我们可以看到构造函数有三十几个参数,Runner初始化沿袭了mmcv的使用Config初始化的习惯,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from torch.utils.data import DataLoader, default_collate
from torch.optim import Adam
from mmengine.runner import Runner

@MODELS.register_module()
class MyAwesomeModel(BaseModel):
...
@DATASETS.register_module()
class MyDataset(Dataset):
...
@METRICS.register_module()
class Accuracy(BaseMetric):
...

runner = Runner(
# 你的模型
model=MyAwesomeModel(
...),
# 模型检查点、日志等都将存储在工作路径中
work_dir='exp/my_awesome_model',

# 训练所用数据
train_dataloader=DataLoader(
dataset=MyDataset(
...),
# 训练相关配置
train_cfg=dict(
by_epoch=True, # 根据 epoch 计数而非 iteration
max_epochs=10,
val_begin=2, # 从第 2 个 epoch 开始验证
val_interval=1), # 每隔 1 个 epoch 进行一次验证

# 优化器封装,MMEngine 中的新概念,提供更丰富的优化选择。
# 通常使用默认即可,可缺省。有特殊需求可查阅文档更换,如
# 'AmpOptimWrapper' 开启混合精度训练
optim_wrapper=dict(
optimizer=dict(
type=Adam,
lr=0.001)),
# 参数调度器,用于在训练中调整学习率/动量等参数
param_scheduler=dict(
type='MultiStepLR',
by_epoch=True,
milestones=[4, 8],
gamma=0.1),

# 验证所用数据
val_dataloader=DataLoader(
dataset=MyDataset(
is_train=False,
size=1000),
shuffle=False,
collate_fn=default_collate,
batch_size=1000,
pin_memory=True,
num_workers=2),
# 验证相关配置,通常为空即可
val_cfg=dict(),
# 验证指标与验证器封装,可自由实现与配置
val_evaluator=dict(type=Accuracy),

# 以下为其他进阶配置,无特殊需要时尽量缺省
# 钩子属于进阶用法,如无特殊需要,尽量缺省
default_hooks=dict(
# 最常用的默认钩子,可修改保存 checkpoint 的间隔
checkpoint=dict(type='CheckpointHook', interval=1)),

# `luancher` 与 `env_cfg` 共同构成分布式训练环境配置
launcher='none',
env_cfg=dict(
cudnn_benchmark=False, # 是否使用 cudnn_benchmark
backend='nccl', # 分布式通信后端
mp_cfg=dict(mp_start_method='fork')), # 多进程设置
log_level='INFO',

# 加载权重的路径 (None 表示不加载)
load_from=None,
# 从加载的权重文件中恢复训练
resume=False
)

# 开始训练你的模型吧
runner.train()

​ 即使不了解实现细节,你也一定大体理解了这个训练流程,并感叹于执行器代码的紧凑与可读性(也许)。这也是 MMEngine 所期望的:结构化、模块化、标准化的训练流程,使得复现更加可靠、对比更加清晰

配置参数项太多了,不知道怎么配置怎么办?

把执行器作为备忘录。需要使用的时候查api官方文档

对比手动构建和注册机制构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from mmengine.model import BaseModel
from mmengine.runner import Runner
from mmengine.registry import MODELS # 模型根注册器,你的自定义模型需要注册到这个根注册器中

@MODELS.register_module() # 用于注册的装饰器
class MyAwesomeModel(BaseModel): # 你的自定义模型
def __init__(self, layers=18, activation='silu'):
...

# 基于注册机制的例子
runner = Runner(
model=dict(
type='MyAwesomeModel',
layers=50,
activation='relu'),
...
)

# 基于手动构建的例子
model = MyAwesomeModel(layers=18, activation='relu')
runner = Runner(
model=model,
...
)

摘自官方文档:

​ 为什么我要传入字典让 Runner 来构建实例,这样又有什么好处?如果你有产生这样的疑问,那我们就会很自豪的回答:“当然!(没有好处)”。事实上,基于注册机制的构建方式只有在结合配置文件时才会发挥它的最大优势。这里直接传入字典的写法也并非使用执行器的最佳实践。在这里,我们希望你能够通过这个例子读懂并习惯这种写法,方便理解我们马上将要讲到的执行器最佳实践——配置文件。敬请期待!

​ 如果你作为初学者无法立刻理解,使用手动构建的方式依然不失为一种好选择,甚至在小规模使用、试错和调试时是一种更加推荐的方式,因为对于 IDE 更加友好

锐评:配置文件格式小括号叠小括号写得真恶心,而且官方也不想着优化配置文件格式

执行器最佳实践——配置文件

​ MMEngine 提供了一套支持 Python 语法的、功能强大的配置文件系统。你可以从之前的示例代码中近乎(我们将在下面说明)无缝地转换到配置文件。下面给出一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 以下代码存放在 example_config.py 文件中
# 基本拷贝自上面的示例,并将每项结尾的逗号删去
model = dict(type='MyAwesomeModel',
layers=2,
activation='relu')
work_dir = 'exp/my_awesome_model'

train_dataloader = dict(
dataset=dict(type='MyDataset',
...),
sampler=dict(
type='DefaultSampler',
...)
train_cfg = dict(
by_epoch=True,
...)
optim_wrapper = dict(
optimizer=dict(
type='Adam',
lr=0.001))
param_scheduler = dict(
type='MultiStepLR',
by_epoch=True,
milestones=[4, 8],
gamma=0.1)

val_dataloader = dict(
dataset=dict(type='MyDataset', ...),
sampler=dict(
type='DefaultSampler', ...),
collate_fn=dict(type='default_collate'),
batch_size=1000,
...)
val_cfg = dict()
val_evaluator = dict(type='Accuracy')

default_hooks = dict(
checkpoint=dict(type='CheckpointHook', interval=1))
launcher = 'none'
env_cfg = dict(
cudnn_benchmark=False,
...)
log_level = 'INFO'
load_from = None
resume = False

此时,我们只需要在训练代码中加载配置,然后运行即可

1
2
3
4
5
from mmengine.config import Config
from mmengine.runner import Runner
config = Config.fromfile('example_config.py')
runner = Runner.from_cfg(config)
runner.train()

Runner 的使用:

Runner 的使用过程可以分成 4 个步骤:

  1. Runner 对象初始化
  2. 注册各类 Hook 到 Runner 中
  3. 调用 Runner 的 resume 或者 load_checkpoint 方法对权重进行加载
  4. 运行给定的工作流,此时才真正开启了工作流
1
2
3
4
5
6
7
8
9
10
from mmengine.runner import Runner
# 假设你有一个配置文件 'config.yaml'
runner = Runner.from_cfg('config.yaml')

runner.train()
runner.test()

# 检查点
runner.save_checkpoint('checkpoint.pth')
runner.load_checkpoint('checkpoint.pth')