MMCV组件4

MMCV核心组件Config

配置文件读取

配置类提供了统一的接口 Config.fromfile(),来读取和解析配置文件。

合法的配置文件应该定义一系列键值对,这里举几个不同格式配置文件的例子。

Python 格式:

1
2
3
test_int = 1
test_list = [1, 2, 3]
test_dict = dict(key1='value1', key2=0.1)

Json 格式:

1
2
3
4
5
{
"test_int": 1,
"test_list": [1, 2, 3],
"test_dict": {"key1": "value1", "key2": 0.1}
}

YAML 格式:

1
2
3
4
5
test_int: 1
test_list: [1, 2, 3]
test_dict:
key1: "value1"
key2: 0.1

对于以上三种格式的文件,假设文件名分别为 config.pyconfig.jsonconfig.yml,调用 Config.fromfile('config.xxx') 接口加载这三个文件都会得到相同的结果

1
2
3
4
5
6
from mmengine.config import Config

cfg = Config.fromfile('learn_read_config.py')
print(cfg)
>>>
Config (path: learn_read_config.py): {'test_int': 1, 'test_list': [1, 2, 3], 'test_dict': {'key1': 'value1', 'key2': 0.1}}

配置文件的使用

​ 通过读取配置文件来初始化配置对象后,就可以像使用普通字典或者 Python 类一样来使用这个变量了。这里提供了字典访问和属性访问两种接口cfg['key'] 或者 cfg.key。这两种接口都支持读写

​ 关于为什么能用属性的方法进行成员的访问,进入源码之后我们可以看到原因是使用了第三方库addict实现,这里就不展开看源码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
print(cfg.test_int)
print(cfg.test_list)
print(cfg.test_dict)
cfg.test_int = 2

print(cfg['test_int'])
print(cfg['test_list'])
print(cfg['test_dict'])
cfg['test_list'][1] = 3
print(cfg['test_list'])
>>>
1
[1, 2, 3]
{'key1': 'value1', 'key2': 0.1}
2
[1, 2, 3]
{'key1': 'value1', 'key2': 0.1}
[1, 3, 3]

​ 在算法库中,可以将配置与注册器结合起来使用,达到通过配置文件来控制模块构造的目的。例如我们已经定义了一个优化器的注册器 OPTIMIZERS,包括了各种优化器。那么首先写一个 config_sgd.py

1
optimizer = dict(type='SGD', lr=0.1, momentum=0.9, weight_decay=0.0001)

然后在算法库中可以通过如下代码构造优化器对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from mmengine import Config, optim
from mmengine.registry import OPTIMIZERS

import torch.nn as nn

cfg = Config.fromfile('config_sgd.py')

model = nn.Conv2d(1, 1, 1)
cfg.optimizer.params = model.parameters()
optimizer = OPTIMIZERS.build(cfg.optimizer)
print(optimizer)
>>>
SGD (
Parameter Group 0
dampening: 0
foreach: None
lr: 0.1
maximize: False
momentum: 0.9
nesterov: False
weight_decay: 0.0001
)

配置文件的继承

​ 有时候两个不同的配置文件之间的差异很小,可能仅仅只改了一个字段,我们就需要将所有内容复制粘贴一次,而且在后续观察的时候,不容易定位到具体差异的字段

继承机制概述

这里我们举一个例子来说明继承机制。定义如下两个配置文件,

optimizer_cfg.py

1
optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)

resnet50.py

1
2
_base_ = ['optimizer_cfg.py']
model = dict(type='ResNet', depth=50)

虽然我们在 resnet50.py 中没有定义 optimizer 字段,但由于我们写了 _base_ = ['optimizer_cfg.py'],会使这个配置文件获得 optimizer_cfg.py 中的所有字段。

1
2
3
4
cfg = Config.fromfile('resnet50.py')
print(cfg.optimizer)
>>>
{'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}

_base_ 是配置文件的保留字段,指定了该配置文件的继承来源。支持继承多个文件,将同时获得这多个文件中的所有字段

runtime_cfg.py

1
gpu_ids = [0, 1]

resnet50_runtime.py

1
2
_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)

这时,读取配置文件 resnet50_runtime.py 会获得 3 个字段 modeloptimizergpu_ids

1
2
3
4
cfg = Config.fromfile('resnet50_runtime.py')
print(cfg.optimizer)
>>>
{'optimizer': {'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}, 'gpu_ids': [0, 1], 'model': {'type': 'ResNet', 'depth': 50}}

修改继承字段(在引用配置文件的地方修改配置)

​ 有时候,我们继承一个配置文件之后,可能需要对其中个别字段进行修改,有两种修改方式,继承式修改和覆盖式修改

继承式修改和覆盖式修改的区别在于dict函数第一个参数是否有_delete_=True,若没有则为继承式修改,只会修改指定的配置,反之则删光之前的所有配置,开始一个新的配置

​ 例如需要改变_base_文件中optimizer的配置:

resnet50_lr0.01.py

1
2
3
4
5
6
_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)
# 继承式修改:
optimizer = dict(lr=0.01)
# 删除式修改:
optimizer = dict(_delete_=True, type='SGD', lr=0.01)

引用被继承文件中的变量(在配置文件内部修改配置)

​ 有时我们想重复利用 _base_ 中定义的字段内容,就可以通过 {{_base_.xxxx}} 获取来获取对应变量的拷贝。例如:

1
2
3
refer_base_var.py
_base_ = ['resnet50.py']
a = {{_base_.model}}

​ 解析后发现,a 的值变成了 resnet50.py 中定义的 model

1
2
3
4
cfg = Config.fromfile('refer_base_var.py')
print(cfg.a)
>>>
{'type': 'ResNet', 'depth': 50}

​ 我们可以在 jsonyamlpython 三种类型的配置文件中,使用这种方式来获取 _base_ 中定义的变量

​ 尽管这种获取 _base_ 中定义变量的方式非常通用,但是在语法上存在一些限制,无法充分利用 python 类配置文件的动态特性。比如我们想在 python 类配置文件中,修改 _base_ 中定义的变量:

1
2
3
_base_ = ['resnet50.py']
a = {{_base_.model}}
a['type'] = 'MobileNet'

配置类是无法解析这样的配置文件的(解析时报错)。配置类提供了一种更 pythonic 的方式,让我们能够在 python 类配置文件中修改 _base_ 中定义的变量(python 类配置文件专属特性,目前不支持在 jsonyaml 配置文件中修改 _base_ 中定义的变量)

modify_base_var.py

1
2
3
4
_base_ = ['resnet50.py']
a = _base_.model
a.type = 'MobileNet'
# 这样即可修改成功

配置文件的导出

在启动训练脚本时,用户可能通过传参的方式来修改配置文件的部分字段,为此我们提供了 dump 接口来导出更改后的配置文件。与读取配置文件类似,用户可以通过 cfg.dump('config.xxx') 来选择导出文件的格式。dump 同样可以导出有继承关系的配置文件,导出的文件可以被独立使用,不再依赖于 _base_ 中定义的文件。

基于继承一节定义的 resnet50.py,我们将其加载后导出:

1
2
cfg = Config.fromfile('resnet50.py')
cfg.dump('resnet50_dump.py')
1
2
3
resnet50_dump.py
optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)
model = dict(type='ResNet', depth=50)

类似的,我们可以导出 json、yaml 格式的配置文件

1
2
3
4
5
6
7
8
9
resnet50_dump.yaml
model:
depth: 50
type: ResNet
optimizer:
lr: 0.02
momentum: 0.9
type: SGD
weight_decay: 0.0001

高级用法:以后再说