纯文本风格配置#

MMEngine 实现了抽象的配置类(Config),为用户提供统一的配置访问接口。配置类能够支持不同格式的配置文件,包括 pythonjsonyaml,用户可以根据需求选择自己偏好的格式。配置类提供了类似字典或者 Python 对象属性的访问接口,用户可以十分自然地进行配置字段的读取和修改。为了方便算法框架管理配置文件,配置类也实现了一些特性,例如配置文件的字段继承等。

from pathlib import Path
import sys
temp_dir = Path(".temp")
sys.path.extend([str(temp_dir)])
temp_dir.mkdir(exist_ok=True)

配置文件读取#

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

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

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

对于以上三种格式的文件,假设文件名分别为 config.pyconfig.jsonconfig.yml,调用 Config.fromfile('config.xxx') 接口加载这三个文件都会得到相同的结果,构造了包含 3 个字段的配置对象。

config.py 为例,先将示例配置文件下载到本地:

%%file {temp_dir}/config.py
test_int = 1
test_list = [1, 2, 3]
test_dict = dict(key1='value1', key2=0.1)
Writing .temp/config.py
from mmengine.config import Config

cfg = Config.fromfile(f'{temp_dir}/config.py')
print(cfg)
Config (path: .temp/config.py): {'test_int': 1, 'test_list': [1, 2, 3], 'test_dict': {'key1': 'value1', 'key2': 0.1}}

配置文件的使用#

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

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]

注意,配置文件中定义的嵌套字段(即类似字典的字段),在 Config 中会将其转化为 ConfigDict 类,该类继承了 Python 内置字典类型的全部接口,同时也支持以对象属性的方式访问数据。

在算法库中,可以将 ConfigRegistry 结合起来使用,达到通过配置文件来控制模块构造的目的。这里举在配置文件中定义优化器的例子。

假设已经定义了优化器的注册器 OPTIMIZERS,包括了各种优化器。那么首先写 config_sgd.py

%%file {temp_dir}/config_sgd.py
optimizer = dict(type='SGD', lr=0.1, momentum=0.9, weight_decay=0.0001)
Writing .temp/config_sgd.py

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

from mmengine import Config, optim
from mmengine.registry import OPTIMIZERS

import torch.nn as nn

cfg = Config.fromfile(f'{temp_dir}/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
    differentiable: False
    foreach: None
    fused: None
    lr: 0.1
    maximize: False
    momentum: 0.9
    nesterov: False
    weight_decay: 0.0001
)
/media/pc/data/lxw/envs/anaconda3x/envs/xxx/lib/python3.12/site-packages/mmengine/optim/optimizer/zero_optimizer.py:11: DeprecationWarning: `TorchScript` support for functional optimizers is deprecated and will be removed in a future PyTorch release. Consider using the `torch.compile` optimizer instead.
  from torch.distributed.optim import \

配置文件的继承#

有时候,两个不同的配置文件之间的差异很小,可能仅仅只改了一个字段,就需要将所有内容复制粘贴一次,而且在后续观察的时候,不容易定位到具体差异的字段。又有些情况下,多个配置文件可能都有相同的一批字段,不得不在这些配置文件中进行复制粘贴,给后续的修改和维护带来了不便。

为了解决这些问题,给配置文件增加了继承的机制,即配置文件 A 可以将另一个配置文件 B 作为自己的基础,直接继承了 B 中所有字段,而不必显式复制粘贴。

继承机制概述#

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

%%file {temp_dir}/optimizer_cfg.py
optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)
Writing .temp/optimizer_cfg.py
%%file {temp_dir}/resnet50.py

_base_ = ['optimizer_cfg.py']
model = dict(type='ResNet', depth=50)
Writing .temp/resnet50.py

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

cfg = Config.fromfile(f'{temp_dir}/resnet50.py')
print(cfg.optimizer)
{'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}

这里 _base_ 是配置文件的保留字段,指定了该配置文件的继承来源。支持继承多个文件,将同时获得这多个文件中的所有字段,但是要求继承的多个文件中没有相同名称的字段,否则会报错。

%%file {temp_dir}/runtime_cfg.py

gpu_ids = [0, 1]
Writing .temp/runtime_cfg.py
%%file {temp_dir}/resnet50_runtime.py

_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)
Writing .temp/resnet50_runtime.py

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

cfg = Config.fromfile(f'{temp_dir}/resnet50_runtime.py')
print(cfg.optimizer)
{'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}

通过这种方式,我们可以将配置文件进行拆分,定义一些通用配置文件,在实际配置文件中继承各种通用配置文件,可以减少具体任务的配置流程。

修改继承字段#

有时候,我们继承配置文件之后,可能需要对其中个别字段进行修改,例如继承了 optimizer_cfg.py 之后,想将学习率从 0.02 修改为 0.01

这时候,只需要在新的配置文件中,重新定义需要修改的字段即可。注意由于 optimizer 这个字段是字典,只需要重新定义这个字典里面需修改的下级字段即可。这个规则也适用于增加一些下级字段。

%%file {temp_dir}/resnet50_lr0.01.py

_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)
optimizer = dict(lr=0.01)
Writing .temp/resnet50_lr0.01.py

读取这个配置文件之后,就可以得到期望的结果。

cfg = Config.fromfile(f'{temp_dir}/resnet50_lr0.01.py')
print(cfg.optimizer)
{'type': 'SGD', 'lr': 0.01, 'momentum': 0.9, 'weight_decay': 0.0001}

对于非字典类型的字段,例如整数,字符串,列表等,重新定义即可完全覆盖,例如下面的写法就将 gpu_ids 这个字段的值修改成了 [0]

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

删除字典中的 key#

有时候我们对于继承过来的字典类型字段,不仅仅是想修改其中某些 key,可能还需要删除其中的一些 key。这时候在重新定义这个字典时,需要指定 _delete_=True,表示将没有在新定义的字典中出现的 key 全部删除。

%%file {temp_dir}/resnet50_delete_key.py

_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)
optimizer = dict(_delete_=True, type='SGD', lr=0.01)
Writing .temp/resnet50_delete_key.py

这时候,optimizer 这个字典中就只有 typelr 这两个 key,momentumweight_decay 将不再被继承。

cfg = Config.fromfile(f'{temp_dir}/resnet50_delete_key.py')
print(cfg.optimizer)
{'type': 'SGD', 'lr': 0.01}

引用被继承文件中的变量#

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

%%file {temp_dir}/refer_base_var.py
_base_ = ['resnet50.py']
a = {{_base_.model}}
Writing .temp/refer_base_var.py

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

cfg = Config.fromfile(f'{temp_dir}/refer_base_var.py')
print(cfg.a)
{'type': 'ResNet', 'depth': 50}

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

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

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

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

%%file {temp_dir}/modify_base_var.py

_base_ = ['resnet50.py']
a = _base_.model
a.type = 'MobileNet'
Writing .temp/modify_base_var.py
cfg = Config.fromfile(f'{temp_dir}/modify_base_var.py')
print(cfg.a)
{'type': 'MobileNet', 'depth': 50}

配置文件的导出#

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

cfg = Config.fromfile(f'{temp_dir}/resnet50.py')
cfg.dump(f'{temp_dir}/resnet50_dump.py')

此外,dump 不仅能导出加载自文件的 cfg,还能导出加载自字典的 cfg

cfg = Config(dict(a=1, b=2))
cfg.dump(f'{temp_dir}/dump_dict.py')

命令行修改配置#

有时候只希望修改部分配置,而不想修改配置文件本身,例如实验过程中想更换学习率,但是又不想重写配置文件,常用的做法是在命令行传入参数来覆盖相关配置。考虑到想修改的配置通常是一些内层参数,如优化器的学习率、模型卷积层的通道数等,因此 MMEngine 提供了一套标准的流程,让我们能够在命令行里轻松修改配置文件中任意层级的参数。

  1. 使用 argparse 解析脚本运行的参数

  2. 使用 argparse.ArgumentParser.add_argument() 方法时,让 action 参数的值为 DictAction,用它来进一步解析命令行参数中用于修改配置文件的参数

  3. 使用配置类的 merge_from_dict 方法来更新配置

%%file {temp_dir}/demo_train.py

import argparse

from mmengine.config import Config, DictAction


def parse_args():
    parser = argparse.ArgumentParser(description='Train a model')
    parser.add_argument('config', help='train config file path')
    parser.add_argument(
        '--cfg-options',
        nargs='+',
        action=DictAction,
        help='override some settings in the used config, the key-value pair '
        'in xxx=yyy format will be merged into config file. If the value to '
        'be overwritten is a list, it should be like key="[a,b]" or key=a,b '
        'It also allows nested list/tuple values, e.g. key="[(a,b),(c,d)]" '
        'Note that the quotation marks are necessary and that no white space '
        'is allowed.')

    args = parser.parse_args()
    return args


def main():
    args = parse_args()
    cfg = Config.fromfile(args.config)
    if args.cfg_options is not None:
        cfg.merge_from_dict(args.cfg_options)
    print(cfg)


if __name__ == '__main__':
    main()
Writing .temp/demo_train.py
%%file {temp_dir}/example.py

model = dict(type='CustomModel', in_channels=[1, 2, 3])
optimizer = dict(type='SGD', lr=0.01)
Writing .temp/example.py

在命令行里通过 . 的方式来访问配置文件中的深层配置,例如我们想修改学习率,只需要在命令行执行:

!python {temp_dir}/demo_train.py {temp_dir}//example.py --cfg-options optimizer.lr=0.1
Config (path: .temp//example.py): {'model': {'type': 'CustomModel', 'in_channels': [1, 2, 3]}, 'optimizer': {'type': 'SGD', 'lr': 0.1}}

成功地把学习率从 0.01 修改成 0.1。如果想改变列表、元组类型的配置,如上例中的 in_channels,则需要在命令行赋值时给 ()[] 外加上双引号:

!python {temp_dir}/demo_train.py {temp_dir}//example.py --cfg-options model.in_channels="[1, 1, 1]"
Config (path: .temp//example.py): {'model': {'type': 'CustomModel', 'in_channels': [1, 1, 1]}, 'optimizer': {'type': 'SGD', 'lr': 0.01}}

备注

上述流程只支持在命令行里修改字符串、整型、浮点型、布尔型、None、列表、元组类型的配置项。对于列表、元组类型的配置,里面每个元素的类型也必须为上述七种类型之一。

导入自定义 Python 模块#

将配置与注册器结合起来使用时,如果往注册器中注册了一些自定义的类,就可能会遇到一些问题。因为读取配置文件的时候,这部分代码可能还没有被执行到,所以并未完成注册过程,从而导致构建自定义类的时候报错。

例如新实现了一种优化器 CustomOptim

%%file {temp_dir}/my_module.py

from mmengine.registry import OPTIMIZERS

@OPTIMIZERS.register_module()
class CustomOptim:
    ...
Overwriting .temp/my_module.py

为这个优化器的使用写了新的配置文件:

%%file {temp_dir}/custom_imports.py

optimizer = dict(type='CustomOptim')
Writing .temp/custom_imports.py

那么就需要在读取配置文件和构造优化器之前,增加一行 import my_module 来保证将自定义的类 CustomOptim 注册到 OPTIMIZERS 注册器中:为了解决这个问题,给配置文件定义了保留字段 custom_imports,用于将需要提前导入的 Python 模块,直接写在配置文件中。对于上述例子,就可以将配置文件写成如下:

%%file {temp_dir}/custom_imports.py

custom_imports = dict(imports=['my_module'], allow_failed_imports=False)
optimizer = dict(type='CustomOptim')
Overwriting .temp/custom_imports.py

这样就不用在训练代码中增加对应的 import 语句,只需要修改配置文件就可以实现非侵入式导入自定义注册模块。

cfg = Config.fromfile(f'{temp_dir}/custom_imports.py')
from mmengine.registry import OPTIMIZERS

custom_optim = OPTIMIZERS.build(cfg.optimizer)
print(custom_optim)
<my_module.CustomOptim object at 0x7f5d7d3a2390>

跨项目继承配置文件#

为了避免基于已有算法库开发新项目时需要复制大量的配置文件,MMEngine 的配置类支持配置文件的跨项目继承。例如基于 MMDetection 开发新的算法库,需要使用以下 MMDetection 的配置文件:

configs/_base_/schedules/schedule_1x.py
configs/_base_/datasets.coco_instance.py
configs/_base_/default_runtime.py
configs/_base_/models/faster-rcnn_r50_fpn.py

如果没有配置文件跨项目继承的功能,就需要把 MMDetection 的配置文件拷贝到当前项目,而现在只需要安装 MMDetection(如使用 mim install mmdet),在新项目的配置文件中按照以下方式继承 MMDetection 的配置文件:

%%file {temp_dir}/cross_repo.py

_base_ = [
    'mmdet::_base_/schedules/schedule_1x.py',
    'mmdet::_base_/datasets/coco_instance.py',
    'mmdet::_base_/default_runtime.py',
    'mmdet::_base_/models/faster-rcnn_r50_fpn.py',
]
Writing .temp/cross_repo.py

可以像加载普通配置文件一样加载 cross_repo.py

cfg = Config.fromfile(f'{temp_dir}/cross_repo.py')
print(cfg.train_cfg)
{'type': 'EpochBasedTrainLoop', 'max_epochs': 12, 'val_interval': 1, '_scope_': 'mmdet'}

通过指定 mmdet::,Config 类会去检索 mmdet 包中的配置文件目录,并继承指定的配置文件。实际上,只要算法库的 setup.py 文件符合 MMEngine 安装规范,在正确安装算法库以后,新的项目就可以使用上述用法去继承已有算法库的配置文件而无需拷贝。

跨项目获取配置文件#

MMEngine 还提供了 get_configget_model 两个接口,支持对符合 MMEngine 安装规范 的算法库中的模型和配置文件做索引并进行 API 调用。

  • 通过 get_model 接口可以获得构建好的模型。

  • 通过 get_config 接口可以获得配置文件。

get_model 的使用样例如下所示,使用和跨项目继承配置文件相同的语法,指定 mmdet::,即可在 mmdet 包中检索对应的配置文件并构建和初始化相应模型。用户可以通过指定 pretrained=True 获得已经加载预训练权重的模型以进行训练或者推理。

from mmengine.hub import get_model

model = get_model(
    'mmdet::faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py', pretrained=True)
print(type(model))
Loads checkpoint by http backend from path: https://download.openmmlab.com/mmdetection/v2.0/faster_rcnn/faster_rcnn_r50_fpn_1x_coco/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth
<class 'mmdet.models.detectors.faster_rcnn.FasterRCNN'>
2024-11-17 23:54:37.454992: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2024-11-17 23:54:38.213724: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2024-11-17 23:54:38.292457: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2024-11-17 23:54:39.584892: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2024-11-17 23:55:00.430832: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
Downloading: "https://download.openmmlab.com/mmdetection/v2.0/faster_rcnn/faster_rcnn_r50_fpn_1x_coco/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth" to /home/ai/.cache/torch/hub/checkpoints/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth

get_config 的使用样例如下所示,使用和跨项目继承配置文件相同的语法,指定 mmdet::,即可实现去 mmdet 包中检索并加载对应的配置文件。用户可以基于这样得到的配置文件进行推理修改并自定义自己的算法模型。同时,如果用户指定 pretrained=True,得到的配置文件中会新增 model_path 字段,指定了对应模型预训练权重的路径。

from mmengine.hub import get_config

cfg = get_config(
    'mmdet::faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py', pretrained=True)
print(cfg.model_path)
https://download.openmmlab.com/mmdetection/v2.0/faster_rcnn/faster_rcnn_r50_fpn_1x_coco/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth