第十一章 MMDetection3D解析系列二_Config(车道线感知)

一 前言

近期参与到了手写AI的车道线检测的学习中去,以此系列笔记记录学习与思考的全过程。车道线检测系列会持续更新,力求完整精炼,引人启示。所需前期知识,可以结合手写AI进行系统的学习。

二 介绍

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

三 配置文件读取

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

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

Json 格式:
{
  "test_int": 1,
  "test_list": [1, 2, 3],
  "test_dict": {"key1": "value1", "key2": 0.1}
}

YAML 格式:
test_int: 1
test_list: [1, 2, 3]
test_dict:
  key1: "value1"
  key2: 0.1

对于以上三种格式的文件,假设文件名分别为 config.py,config.json,config.yml,调用 Config.fromfile(‘config.xxx’) 接口加载这三个文件都会得到相同的结果,构造了包含 3 个字段的配置对象。
我们以 config.py 为例,我们先将示例配置文件下载到本地:然后通过配置类的 fromfile 接口读取配置文件:

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’] 或者类似 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 内置字典类型的全部接口,同时也支持以对象属性的方式访问数据。

五 配置文件的继承

5.1 继承机制概述

optimizer_cfg.py:
optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)

resnet50.py:
cfg = Config.fromfile('resnet50.py')
print(cfg.optimizer)

{'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}

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

runtime_cfg.py:
gpu_ids = [0, 1]
resnet50_runtime.py:
_base_ = ['optimizer_cfg.py', 'runtime_cfg.py']
model = dict(type='ResNet', depth=50)
{'type': 'SGD', 'lr': 0.02, 'momentum': 0.9, 'weight_decay': 0.0001}

5.2 修改继承字段

由于 optimizer 这个字段是一个字典,我们只需要重新定义这个字典里面需修改的下级字段即可。这个规则也适用于增加一些下级字段。

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

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

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

5.3 删除字典中的 key

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

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)
{'type': 'SGD', 'lr': 0.01}

这时候,optimizer 这个字典中就只有 type 和 lr 这两个 key,momentum 和 weight_decay 将不再被继承。

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

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

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

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

解析后发现,a 的值变成了 resnet50.py 中定义的 model
我们可以在 json、yaml、python 三种类型的配置文件中,使用这种方式来获取 base 中定义的变量。

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

modify_base_var.py:
_base_ = ['resnet50.py']
a = _base_.model
a.type = 'MobileNet'

cfg = Config.fromfile('modify_base_var.py')
print(cfg.a)
{'type': 'MobileNet', 'depth': 50}

六 配置文件的导出

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

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

cfg = Config.fromfile('resnet50.py')
cfg.dump('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 格式的配置文件

model:
  depth: 50
  type: ResNet
optimizer:
  lr: 0.02
  momentum: 0.9
  type: SGD
  weight_decay: 0.0001
{"optimizer": {"type": "SGD", "lr": 0.02, "momentum": 0.9, "weight_decay": 0.0001}, "model": {"type": "ResNet", "depth": 50}}

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

cfg = Config(dict(a=1, b=2))
cfg.dump('dump_dict.py')

dump_dict.py
a=1
b=2

七 其他进阶用法

7.1 命令行修改配置

考虑到我们想修改的配置通常是一些内层参数,如优化器的学习率、模型卷积层的通道数等,因此 MMEngine 提供了一套标准的流程,让我们能够在命令行里轻松修改配置文件中任意层级的参数。

  1. 使用 argparse 解析脚本运行的参数
  2. 使用 argparse.ArgumentParser.add_argument 方法时,让 action 参数的值
  3. 为 DictAction,用它来进一步解析命令行参数中用于修改配置文件的参数使用配置类的 merge_from_dict 方法来更新配置
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='要在使用的配置中覆盖某些设置,应采用 xxx=yyy 格式的键值对,
        它们将被合并到配置文件中。如果要被覆盖的值是一个列表,它应该像
        key="[a,b]" 或 key=a,b 这样。它还允许嵌套的列表/元组值,例如 
        key="[(a,b),(c,d)]"。请注意,必须使用引号,并且不允许有空白空间。')

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

示例配置文件如下:

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

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

python demo_train.py ./example.py --cfg-options optimizer.lr=0.1

Config (path: ./example.py): {'model': {'type': 'CustomModel', 'in_channels': [1, 2, 3]}, 'optimizer': {'type': 'SGD', 'lr': 0.1}}

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

python demo_train.py ./example.py --cfg-options model.in_channels="[1, 1, 1]"

上述流程只支持在命令行里修改字符串、整型、浮点型、布尔型、None、列表、元组类型的配置项。对于列表、元组类型的配置,里面每个元素的类型也必须为上述七种类型之一。
DictAction 的行为与 “extend” 相似,支持多次传递,并保存在同一个列表中。如

python demo_train.py ./example.py --cfg-options optimizer.type="Adam" --cfg-options model.in_channels="[1, 1, 1]"

Config (path: ./example.py): {'model': {'type': 'CustomModel', 'in_channels': [1, 1, 1]}, 'optimizer': {'type': 'Adam', 'lr': 0.01}}

7.2 导入自定义 Python 模块

例如我们新实现了一种优化器 CustomOptim,相应代码在 my_module.py 中。

from mmengine.registry import OPTIMIZERS

@OPTIMIZERS.register_module()
class CustomOptim:
    pass

我们为这个优化器的使用写了一个新的配置文件 custom_imports.py:

optimizer = dict(type='CustomOptim')

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

custom_imports.py

custom_imports = dict(imports=['my_module'], allow_failed_imports=False)
optimizer = dict(type='CustomOptim')

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

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

from mmengine.registry import OPTIMIZERS

custom_optim = OPTIMIZERS.build(cfg.optimizer)
print(custom_optim)

八 纯 Python 风格的配置文件(Beta)

8.1 基本语法

  1. 模块构建
python风格:无需注册
# 纯文本
# 注册流程
from torch.optim import SGD
from mmengine.registry import OPTIMIZERS
OPTIMIZERS.register_module(module=SGD, name='SGD')

# 配置文件
from torch.optim import SGD
optimizer = dict(type=SGD, lr=0.1)

optimizer = dict(type='SGD', lr=0.1)

# 构建流程完全一致
# 构建流程完全一致
import torch.nn as nn
from mmengine.registry import OPTIMIZERS


cfg = Config.fromfile('optimizer.py')
model = nn.Conv2d(1, 1, 1)
cfg.optimizer.params = model.parameters()
optimizer = OPTIMIZERS.build(cfg.optimizer)

从上面的例子可以看出,纯 Python 风格的配置文件和纯文本风格的配置文件的区别在于:
纯 Python 风格的配置文件无需注册模块
纯 Python 风格的配置文件中,type 字段不再是字符串,而是直接指代模块。相应的配置文件需要多出 import 语法
需要注意的是,OpenMMLab 系列算法库在新增模块时仍会保留注册过程,用户基于 MMEngine 构建自己的项目时,如果使用纯 Python 风格的配置文件,则无需注册。看到这你会或许会好奇,这样没有安装 PyTorch 的环境不就没法解析样例配置文件了么,这样的配置文件还叫配置文件么?不要着急,这部分的内容我们会在后面介绍。
2. 继承

from mmengine.config import read_base
with read_base():
    from .optimizer import *

_base_ = [./optimizer.py]

纯 Python 风格的配置文件通过 import 语法来实现继承,这样做的好处是,我们可以直接跳转到被继承的配置文件中,方便阅读和跳转。变量的继承规则(增删改查)完全对齐 Python 语法,例如我想修改 base 配置文件中 optimizer 的学习率:

from mmengine.config import read_base


with read_base():
    from .optimizer import *

# optimizer 为 base 配置文件定义的变量
optimizer.update(
    lr=0.01,
)

当然了,如果你已经习惯了纯文本风格的继承规则,且该变量在 base 配置文件中为 dict 类型,也可以通过 merge 语法来实现和纯文本风格配置文件一致的继承规则:

from mmengine.config import read_base


with read_base():
    from .optimizer import *

# optimizer 为 base 配置文件定义的变量
optimizer.merge(
    _delete_=True,
    lr=0.01,
    type='SGD'
)

# 等价的 python 风格写法如下,与 Python 的 import 规则完全一致
# optimizer = dict(
#     lr=0.01,
#     type='SGD'
# )

纯 Python 风格的配置文件中,字典的 update 方法与 dict.update 稍有不同。纯 Python 风格的 update 会递归地去更新字典中的内容,例如:

x = dict(a=1, b=dict(c=2, d=3))

x.update(dict(b=dict(d=4)))
# 配置文件中的 update 规则:
# {a: 1, b: {c: 2, d: 4}}
# 普通 dict 的 update 规则:
# {a: 1, b: {d: 4}}

与纯文本风格的配置文件相比,纯 Python 风格的配置文件的继承规则完全对齐 import 语法,更容易理解,且支持配置文件之间的跳转。你或许会好奇既然继承和模块的导入都使用了 import 语法,为什么继承配置文件还需要额外的 with read_base(): 这个上下文管理器呢?一方面这样可以提升配置文件的可读性,可以让继承的配置文件更加突出,另一方面也是受限于 lazy_import 的规则,这个会在后面讲到。

8.2 配置文件的导出

optimizer = dict(type='torch.optim.SGD', lr=0.1)

optimizer:
    type: torch.optim.SGD
    lr: 0.1

{"optimizer": "torch.optim.SGD", "lr": 0.1}


optimizer = dict(type='SGD', lr=0.1)

optimizer:
    type: SGD
    lr: 0.1

{"optimizer": "SGD", "lr": 0.1}

可以看到,纯 Python 风格导出的 type 字段会包含模块的全量信息。导出的配置文件也可以被直接加载,通过注册器来构建实例。

九 什么是lazy import

正如前面所提到的,解析配置文件需要依赖配置文件中引用的三方库,这其实是一件非常不合理的事。例如我基于 MMagic 训练了一个模型,想使用 MMDeploy 的 onnxruntime 后端部署。由于部署环境中没有 torch,而配置文件解析过程中需要 torch,这就导致了我无法直接使用 MMagic 的配置文件作为部署的配置,这是非常不方便的。为了解决这个问题,我们引入了 lazy_import 的概念。

要聊 lazy_import 的具体实现是一件比较复杂的事,在此我们仅对其功能做简要介绍。lazy_import 的核心思想是,将配置文件中的 import 语句延迟到配置文件被解析时才执行,这样就可以避免配置文件中的 import 语句导致的三方库依赖问题。配置文件解析过程时,Python 解释器实际执行的等效代码如下

from torch.optim import SGD
optimizer = dict(type=SGD)

lazy_obj = LazyObject('torch.optim', 'SGD')
optimizer = dict(type=lazy_obj)

LazyObject 作为 Config 模块的內部类型,无法被用户直接访问。用户在访问 type 字段时,会经过一系列的转换,将 LazyObject 转化成真正的 torch.optim.SGD 类型。这样一来,配置文件的解析不会触发三方库的导入,而用户使用配置文件时,又可以正常访问三方库的类型。

要想访问 LazyObject 的内部类型,可以通过 Config.to_dict 接口:

cfg = Config.fromfile('optimizer.py').to_dict()
print(type(cfg['optimizer']['type']))
# mmengine.config.lazy.LazyObject

此时得到的 type 就是 LazyObject 类型。

然而对于 base 文件的继承(导入,import),我们不能够采取 lazy import 的策略,这是因为我们希望解析后的配置文件能够包含 base 配置文件定义的字段,需要真正的触发 import。因此我们对 base 文件的导入加了一层限制,即必须在 with read_base()’ 的上下文中导入。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小酒馆燃着灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值