为了帮助大家方便阅读,以下是本文内容的索引,满满的干货,期待你能用心看完。
一、引言
二、介绍
三、YACS 基本用法
3.1、如何通过 YACS 创建 config
3.2、Congfig System 的核心需求:增删查改
3.3、如何合理地更新和保护定义好的 Config
3.4、YACS 小结
四、LazyConfig 基本用法
4.1、LazyConfig 样例介绍
4.2、什么是 LazyConfig 中的”lazy”?
4.3、LazyConfig:在保证基本功能的前提下更加灵活
五、LazyConfig 与 YACS 的简单对比
六、总结
七、参考文献
近期 IDEA-CVR 团队基于 detectron2 开发面向 transformer-based 目标检测 codebase —— detrex,目前已经集成了从 DETR 到 DINO 的一系列目标检测模型,detrex 希望提供一个更易用的平台方便模型的调用与对比,并且在已复现的模型上均与原始模型性能持平或者更好。为了帮助大家更好地上手 detectron2 以及 detrex,我们会陆续推出新的 Blog 将我们在开发过程中的经验与思考分享给大家。这篇是关于 codebase 基本组件“配置系统(Config System)”的选择与思考,希望对读者有所帮助。
detrex 项目地址: https://github.com/IDEA-Research/detrex
配置系统(Config System)是 open-source library 的一个很重要的组件。一个好的配置系统给用户的使用体验带来的好处是很明显的:
所以作为一个 open-source codebase,首先需要明确自身需要采用哪套 Config System,这是一切的基础。而yacs和LazyConfig是 detectron2 使用过的两套配置系统。这篇文章将针对这两套配置系统进行相关的介绍,并探讨这两套系统背后对应的一些开源思想。文章不会从特别细枝末节的功能开始讲解,而是从一些常见的使用场景出发,希望让读者读起来轻松一些。有些必要的功能可以在后续使用时接触到了再进行了解。文章会给出一些简单的 example,帮助大家理解与感受 detectron2 下第二代 LazyConfig 为用户带来的便捷性。
大家可能已经注意到,detectron2 的最新版本下已经用了最新的 LazyConfig 完全替代了之前的 YACS 版本的.yaml格式的 config,但是为了兼容早期的 baseline,并没有完全删除 YACS 的使用,呈现出了兼容的状态,我们基于 detectron2 开发的 detrex,完全采用了 LazyConfig 的配置系统,在新的这套配置下,一个完整的训练代码只有百行不到,并且 config 的可读性相对于之前的 YACS 来说有了本质的提升,可以极大地减少用户阅读代码的负担。这篇 blog 表达的内容有限,只是作为第一篇 blog 让大家更好的理解 LazyConfig 以及 YACS 的区别与好处,并且帮助大家更好地上手 detectron2 与 detrex。后续还会有更多的 blog 对 detrex 和 detectron2 的设计做更多的介绍。
yacs是 4 年前 rbg 大佬团队开发的用于Detectron, maskrcnn-benchmark以及早期Detectron2的一个轻量化配置系统,其使用与可读性较好的.yaml文件有着紧密的联系。在讨论一个 config system 的时候,我们首先可以了解一下这个 config 的基本格式。
3.1、如何通过 YACS 创建 config
所有的配置在yacs下都可以通过CfgNode这个类来定义,如果我们希望得到下面这样.yaml排版格式的 config 配置:
MODEL:
BACKBONE: R50
NORM: BN
NAME: Test
我们可以看到 config 最外层总共有两个参数,一个MODEL一个NAME, 并且 MODEL 下还有两个子节点BACKBONE以及NORM, 那么我们用yacs便可以轻松地创建出这样层级关系的 config:
from yacs.config import CfgNode as CN
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
cfg.MODEL = CN()
cfg.MODEL.BACKBONE = "R50"
cfg.MODEL.NORM = "BN"
当我们print(cfg), 就可以得到刚刚想要的 config 格式了:
MODEL:
BACKBONE: R50
NORM: BN
NAME: Test
3.2、Config System 的核心需求: 增删查改
配置系统的一个基本需求当然是让用户可以很方便地对其参数进行访问与更新。 在 yacs 中,我们可以很直观地对这些配置系统进行相应的增删查改。依旧以之前我们创建的 config 为例:
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
cfg.MODEL = CN()
cfg.MODEL.BACKBONE = "R50"
cfg.MODEL.NORM = "BN"
现在我们定义了一组我们代码仓库需要的配置,我们可以对其进行以下几个基本操作:
# 1. 访问某个参数
print(cfg.NAME)
>>> Test
# 2. 修改某个参数
cfg.NAME = "Update"
print(cfg.NAME)
>>> Update
# 3. 新增一个参数
cfg.NEW_PARAM = "New"
print(cfg.NEW_PARAM)
>>> New
删除一个参数倒是没有特别的方法,目前看来只能从定义的部分直接删除,但是其提供了一个cfg.clear()接口可以将所有的配置清空,一般很少使用。
3.3、如何合理地更新和保护定义好的 Config
我们了解完了一个 config 配置的定义和基本操作(增删查改)后,随之而来的问题便是,面对这么一个灵活的配置系统,应该如何有效地避免我们在代码的某处不小心对其进行了修改,从而导致实验出错。yacs提供了几个接口帮助我们尽可能地避免这个问题。
CfgNode.clone(): 返回一份复制的 config,对其进行修改不会影响你最初定义的 config 内容:
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
# 将 cfg 中的内容完整地复制给 new_cfg
new_cfg = cfg.clone()
print(new_cfg.NAME)
>>> Test
# 对 new_cfg 进行修改, 不会影响原 cfg 中的参数
new_cfg.NAME = "New"
print(new_cfg.NAME)
>>> New
print(cfg.NAME)
>>> Test
.clone()方法,一方面可以作为对原来 config 的一种保护,另一方面也是快速创建一个新的 config 的方式。
CfgNode.freeze()与CfgNode.defrost(): 作为一个开关, 可以保护定义好的 params 不可被修改对于配置参数的保护,yacs提供了另一种方法,即可以通过freeze()方法冻结整个配置系统,使其在 freeze 之后无法进行任何修改,这也保证了整个过程中我们的配置不会有任何的改动。
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
# 保护 cfg 下定义的所有参数不可被修改
cfg.freeze()
cfg.NAME = "New"
>>> AttributeError: Attempted to set NAME to New, but CfgNode is immutable
# 可以通过 defrost 让超参重新变得可修改
cfg.defrost()
cfg.NAME = "New"
print(cfg.NAME)
>>> New
3.4、YACS 小结
yacs作为一个极其 lightweight 的 config system,可以说是麻雀虽小五脏俱全。其中一些基本的应用如果熟悉了,有助于帮助到大家平时的实验:
虽然yacs的功能很全面了,但还是难免会存在一些问题。detectron2的早期设计,旨在尽可能把整套训练所涉及到的参数都通过 config system 控制,但是yacs在本身的语法上有所限制,并且 detectron2 最初将所有的基本 config 都定义到了 detectron2/config/default.py 下,并在训练过程中会将所有的 config 都 dump 下来,虽然带有一部分的注释,但是在可读性上依旧不是很好,存在一部分臃肿冗余的参数。
在最近的 detectron2 更新中,引入了一种 LazyConfig 机制,全面替换了之前的yacs config system。在基本功能都有所保证的情况下,LazyConfig 相比于之前的yacs具有了更简洁、更准确、更高效的特性。detectron2 在结构设计上不断能有新的简洁的设计,在代码设计上不断地做减法,让人很敬佩。接下来我们简单介绍一下 LazyConfig 的使用,帮助新接触 detectron2 的用户熟悉一下这套配置系统。
4.1、LazyConfig 样例介绍
我们首先通过一个例子入手,直观地比较一下两种 config 方式的区别,假设我们现在的需求是,需要一个简单的卷积层,并且需要可以灵活地控制其中的参数,例如stride, kernel_size, padding等,我们对比一下两套配置方案是如何完成这样的事情:
YACS Config System:
在 YACS 下,需要在 config 中指定所有我们需要调整的参数,这意味着我们的 config 中如果面对需要调整的新的参数,就必须新增一项内容:
import torch.nn as nn
from yacs.config import CfgNode as CN
# 创建一个 config node, 并且罗列我们需要控制的参数
cfg = CN()
cfg.in_channels = 16
cfg.out_channels = 16
cfg.kernel_size = 3
cfg.stride = 1
cfg.padding = 1
# 通过 cfg 传入超参, 实例化一个卷积层
conv_layer = nn.Conv2d(
in_channels=cfg.in_channels,
out_channels=cfg.out_channels,
kernel_size=cfg.kernel_size,
stride=cfg.stride,
padding=cfg.padding,
)
# 打印 cfg
print(cfg)
>>> in_channels: 16
kernel_size: 3
out_channels: 16
padding: 1
stride: 1
# 打印 conv_layer
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
LazyConfig System:
在 LazyConfig System 下,以上一个 10 行代码才能完成的事情,只需要 2 行即可:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 通过 instantiate 实例化
conv_layer = instantiate(conv_config)
# 打印 conv_layer
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
4.2、什么是 LazyConfig 中的 “lazy” ?
LazyConfig 中的核心思想是 Lazy,Lazy 表示一种延迟状态。这里的表述可能会让人云里雾里。我们结合上一小节的 example,对 lazyconfig 中的核心思想做进一步解释,我们可以看到在上文创建卷积层的这个 example 中,我们新接触到了两个函数,LazyCall以及instantiate, 这也是 LazyConfig 中的核心用法,我们和直接通过nn.Conv2d创建一层卷积层进行一下简单的对比:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
conv_layer = instantiate(conv_config)
# 直接通过 nn.Conv2d 创建 conv_layer
conv_layer = nn.Conv2d(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
用最通俗的语言表达就是:LazyCall 将实例化这个过程拆分成了两个步骤,将一步即可实例化的过程拆分成了两个状态,也就是所谓的”延迟”:
1.通过LazyCall将需要实例化的对象包裹一下,传入对应的参数
2.通过instantiate来进行实例化
我们可以打印一下使用LazyCall包裹的对象,观察一下具体的内容:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
print(conv_config)
>>> {'in_channels': 16, 'out_channels': 16, 'kernel_size': 3, 'stride': 1, 'padding': 1, '_target_': <class 'torch.nn.modules.conv.Conv2d'>}
我们可以观察到 LazyCall 返回给我们一个类似字典的数据结构。当然其不是真正的 dict,我们去打印 type 可以发现是一个omegaconf.dictconfig.DictConfig对象。我们暂时可以不需要去了解,感兴趣的小伙伴可以去看看omegaconf这个 repo。这个DictConfig对象包含了几个 key:
那么所谓的Lazy顾名思义,在我们真正实例化这个类之前,它将一直以DictConfig对象的形式存在。也就是说我们不马上去实例化这个对象,在需要的时候再调用instantiate函数进行实例化即可。这种形式给我们带来了几个好处:
4.3、LazyConfig:在保证基本功能的前提下更加灵活
在解释完什么是 Lazy 之后,我们直接进入最基本的使用,让我们看看 LazyConfig 是如何满足一个配置系统的基本需求的:
config 的修改:
如果我们需要得到一个卷积核大小为 5 的卷积层:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 修改 kernel_size
conv_config.kernel_size = 5
# 实例化这个对象, 得到 kernel_size=5 的卷积层
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(5, 5), stride=(1, 1), padding=(1, 1))
config 的增加:
如果我们需要将这个卷积替换为分组卷积,意味着需要新指定一个groups参数:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 新增一个 groups 参数, 用来控制分组卷积的组数
conv_config.groups = 16
# 实例化这个对象, 得到 groups=16 的分组卷积
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), groups=16)
LazyCall 的 flexible 也会带来一个问题,当你新增一个不属于需要实例化的那个类的参数的时候,会报相应的错误,LazyCall 不存在超参检查的机制,需要用户对传入的参数足够了解:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 新增了一个不属于 nn.Conv2d 的参数并且实例化
conv_config.embed_dim = 16
conv_layer = instantiate(conv_config)
会提示以下错误:
Error when instantiating torch.nn.modules.conv.Conv2d!
TypeError: __init__() got an unexpected keyword argument 'embed_dim'
config 的删除:
如果我们不需要传入 padding 这个参数,使用默认值,但是我们原有的 config 中已经指定了的话,那么我们可以在实例化之前 del 这个参数,而在 yacs 中需要删除原始 config 中对应的字段。
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 删除 padding 这个参数
del conv_config.padding
# 实例化这个对象
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1))
直接替换我们实例化的对象:
更有趣的功能是,如果我们不需要 Conv2d 了,需要一个一维卷积(Conv1d),我们可以直接修改 _target_ 参数:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 修改 target 参数, 将实例化的对象改为 Conv1D
conv_config._target_ = nn.Conv1D
# 实例化这个对象, 得到 groups=16 的分组卷积
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv1d(16, 16, kernel_size=(3,), stride=(1,), padding=(1,))
非常神奇地发现我们实例化的对象从 Conv2d 变成了 Conv1d。
在上面的内容中,我们简单介绍了yacs和LazyConfig的基本使用。我们可以很直观地看出来,很多时候我们在使用 YACS 作为 config system,构建整套 codebase 需要的组件时,我们的 config 会变得越发臃肿且可读性变差。假设我们需要控制 codebase 下的多个模型,但真正在执行程序的时候只会运行其中一个模型,以Conv2d和MultiheadAttention为例,我们首先要面对的一个麻烦事就在于,需要将这两个模型可控制的所有参数都记录到 config 中:
import torch.nn as nn
from yacs.config import CfgNode as CN
# 创建一个 config node, 并且罗列我们需要控制的参数
cfg = CN()
# 创建一个子 config node 来控制 Conv2d 所需要的参数
cfg.CONV = CN()
cfg.CONV.in_channels = 16
cfg.CONV.out_channels = 16
cfg.CONV.kernel_size = 3
cfg.CONV.stride = 1
cfg.CONV.padding = 1
# 创建一个子 config node 来控制 MultiheadAttention 参数
cfg.ATTN = CN()
cfg.ATTN.num_heads = 8
cfg.ATTN.embed_dim = 256
# 创建一个参数来控制我们需要创建的模型
cfg.MODEL = "conv"
# 构建一个 build_model()函数来根据 config 返回我们需要的模型
def build_model(cfg):
if cfg.MODEL == "conv":
model = nn.Conv2d(
in_channels=cfg.CONV.in_channels,
out_channels=cfg.CONV.out_channels,
kernel_size=cfg.CONV.kernel_size,
stride=cfg.CONV.stride,
padding=cfg.CONV.padding,
)
elif cfg.MODEL == "attn":
model = nn.MultiheadAttention(
embed_dim=cfg.ATTN.embed_dim,
num_heads=cfg.ATTN.num_heads
)
else:
raise NotImplementedError("only implement conv and attn")
return model
model = build_model(cfg)
print(model)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
并且如果不给到用户足够多的提示,例如cfg.MODEL这个参数,用户很可能无法知道我们的模型库里具体有多少个模型,并且应该如何调用,给用户带来了更多的困扰。用LazyConfig可以很直接地避免这些问题。如果用户需要构建一个 conv 的 config,或者是 attn 的 config,可以按照以下的操作:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 conv config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 通过 LazyCall 构建一个 attn config 对象
attn_config = LazyCall(nn.MultiheadAttention)(embed_dim=256, num_heads=8)
# 根据我们的需要实例化对应的模型
model = instantiate(attn_config)
并且LazyCall在语法上更加贴合原生的 python 语法的使用,即 import 需要的对象 -> 创建对应的 config -> 在需要的时候实例化, 整体上更像是一个即插即用的 config 插件,用户可以对这整个过程中的每一步做更加灵活精细且直观地控制。但是 LazyConfig 本身在某种程度上过于 flexible,缺少了 yacs 中frozen()与defrost()的保护机制,所以在使用的过程中也需要注意,尽可能避免在一些小细节的地方做了不必要的修改。
这篇文章只能算是一个引子,简单对比了两个配置系统的基本使用与理解。其实其背后的设计思想都各有好处,具体还是需要大家在使用的过程中去感受。我们后续会搭配更多的文章详细介绍一些更高级的用法。这里再对文章的内容作一个简单的对比与总结:
YACS 与 LazyConfig 在功能与灵活性上的比较:
配置系统需要具有的基本功能: 学习了解一些基本的使用可以很直观地帮助到大家高效地做实验。
https://detectron2.readthedocs.io/en/latest/tutorials/lazyconfigs.html
https://detectron2.readthedocs.io/en/latest/tutorials/configs.html
为了帮助大家方便阅读,以下是本文内容的索引,满满的干货,期待你能用心看完。
一、引言
二、介绍
三、YACS 基本用法
3.1、如何通过 YACS 创建 config
3.2、Congfig System 的核心需求:增删查改
3.3、如何合理地更新和保护定义好的 Config
3.4、YACS 小结
四、LazyConfig 基本用法
4.1、LazyConfig 样例介绍
4.2、什么是 LazyConfig 中的”lazy”?
4.3、LazyConfig:在保证基本功能的前提下更加灵活
五、LazyConfig 与 YACS 的简单对比
六、总结
七、参考文献
近期 IDEA-CVR 团队基于 detectron2 开发面向 transformer-based 目标检测 codebase —— detrex,目前已经集成了从 DETR 到 DINO 的一系列目标检测模型,detrex 希望提供一个更易用的平台方便模型的调用与对比,并且在已复现的模型上均与原始模型性能持平或者更好。为了帮助大家更好地上手 detectron2 以及 detrex,我们会陆续推出新的 Blog 将我们在开发过程中的经验与思考分享给大家。这篇是关于 codebase 基本组件“配置系统(Config System)”的选择与思考,希望对读者有所帮助。
detrex 项目地址: https://github.com/IDEA-Research/detrex
配置系统(Config System)是 open-source library 的一个很重要的组件。一个好的配置系统给用户的使用体验带来的好处是很明显的:
所以作为一个 open-source codebase,首先需要明确自身需要采用哪套 Config System,这是一切的基础。而yacs和LazyConfig是 detectron2 使用过的两套配置系统。这篇文章将针对这两套配置系统进行相关的介绍,并探讨这两套系统背后对应的一些开源思想。文章不会从特别细枝末节的功能开始讲解,而是从一些常见的使用场景出发,希望让读者读起来轻松一些。有些必要的功能可以在后续使用时接触到了再进行了解。文章会给出一些简单的 example,帮助大家理解与感受 detectron2 下第二代 LazyConfig 为用户带来的便捷性。
大家可能已经注意到,detectron2 的最新版本下已经用了最新的 LazyConfig 完全替代了之前的 YACS 版本的.yaml格式的 config,但是为了兼容早期的 baseline,并没有完全删除 YACS 的使用,呈现出了兼容的状态,我们基于 detectron2 开发的 detrex,完全采用了 LazyConfig 的配置系统,在新的这套配置下,一个完整的训练代码只有百行不到,并且 config 的可读性相对于之前的 YACS 来说有了本质的提升,可以极大地减少用户阅读代码的负担。这篇 blog 表达的内容有限,只是作为第一篇 blog 让大家更好的理解 LazyConfig 以及 YACS 的区别与好处,并且帮助大家更好地上手 detectron2 与 detrex。后续还会有更多的 blog 对 detrex 和 detectron2 的设计做更多的介绍。
yacs是 4 年前 rbg 大佬团队开发的用于Detectron, maskrcnn-benchmark以及早期Detectron2的一个轻量化配置系统,其使用与可读性较好的.yaml文件有着紧密的联系。在讨论一个 config system 的时候,我们首先可以了解一下这个 config 的基本格式。
3.1、如何通过 YACS 创建 config
所有的配置在yacs下都可以通过CfgNode这个类来定义,如果我们希望得到下面这样.yaml排版格式的 config 配置:
MODEL:
BACKBONE: R50
NORM: BN
NAME: Test
我们可以看到 config 最外层总共有两个参数,一个MODEL一个NAME, 并且 MODEL 下还有两个子节点BACKBONE以及NORM, 那么我们用yacs便可以轻松地创建出这样层级关系的 config:
from yacs.config import CfgNode as CN
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
cfg.MODEL = CN()
cfg.MODEL.BACKBONE = "R50"
cfg.MODEL.NORM = "BN"
当我们print(cfg), 就可以得到刚刚想要的 config 格式了:
MODEL:
BACKBONE: R50
NORM: BN
NAME: Test
3.2、Config System 的核心需求: 增删查改
配置系统的一个基本需求当然是让用户可以很方便地对其参数进行访问与更新。 在 yacs 中,我们可以很直观地对这些配置系统进行相应的增删查改。依旧以之前我们创建的 config 为例:
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
cfg.MODEL = CN()
cfg.MODEL.BACKBONE = "R50"
cfg.MODEL.NORM = "BN"
现在我们定义了一组我们代码仓库需要的配置,我们可以对其进行以下几个基本操作:
# 1. 访问某个参数
print(cfg.NAME)
>>> Test
# 2. 修改某个参数
cfg.NAME = "Update"
print(cfg.NAME)
>>> Update
# 3. 新增一个参数
cfg.NEW_PARAM = "New"
print(cfg.NEW_PARAM)
>>> New
删除一个参数倒是没有特别的方法,目前看来只能从定义的部分直接删除,但是其提供了一个cfg.clear()接口可以将所有的配置清空,一般很少使用。
3.3、如何合理地更新和保护定义好的 Config
我们了解完了一个 config 配置的定义和基本操作(增删查改)后,随之而来的问题便是,面对这么一个灵活的配置系统,应该如何有效地避免我们在代码的某处不小心对其进行了修改,从而导致实验出错。yacs提供了几个接口帮助我们尽可能地避免这个问题。
CfgNode.clone(): 返回一份复制的 config,对其进行修改不会影响你最初定义的 config 内容:
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
# 将 cfg 中的内容完整地复制给 new_cfg
new_cfg = cfg.clone()
print(new_cfg.NAME)
>>> Test
# 对 new_cfg 进行修改, 不会影响原 cfg 中的参数
new_cfg.NAME = "New"
print(new_cfg.NAME)
>>> New
print(cfg.NAME)
>>> Test
.clone()方法,一方面可以作为对原来 config 的一种保护,另一方面也是快速创建一个新的 config 的方式。
CfgNode.freeze()与CfgNode.defrost(): 作为一个开关, 可以保护定义好的 params 不可被修改对于配置参数的保护,yacs提供了另一种方法,即可以通过freeze()方法冻结整个配置系统,使其在 freeze 之后无法进行任何修改,这也保证了整个过程中我们的配置不会有任何的改动。
from yacs.config import CfgNode as CN
cfg = CN()
cfg.NAME = "Test"
# 保护 cfg 下定义的所有参数不可被修改
cfg.freeze()
cfg.NAME = "New"
>>> AttributeError: Attempted to set NAME to New, but CfgNode is immutable
# 可以通过 defrost 让超参重新变得可修改
cfg.defrost()
cfg.NAME = "New"
print(cfg.NAME)
>>> New
3.4、YACS 小结
yacs作为一个极其 lightweight 的 config system,可以说是麻雀虽小五脏俱全。其中一些基本的应用如果熟悉了,有助于帮助到大家平时的实验:
虽然yacs的功能很全面了,但还是难免会存在一些问题。detectron2的早期设计,旨在尽可能把整套训练所涉及到的参数都通过 config system 控制,但是yacs在本身的语法上有所限制,并且 detectron2 最初将所有的基本 config 都定义到了 detectron2/config/default.py 下,并在训练过程中会将所有的 config 都 dump 下来,虽然带有一部分的注释,但是在可读性上依旧不是很好,存在一部分臃肿冗余的参数。
在最近的 detectron2 更新中,引入了一种 LazyConfig 机制,全面替换了之前的yacs config system。在基本功能都有所保证的情况下,LazyConfig 相比于之前的yacs具有了更简洁、更准确、更高效的特性。detectron2 在结构设计上不断能有新的简洁的设计,在代码设计上不断地做减法,让人很敬佩。接下来我们简单介绍一下 LazyConfig 的使用,帮助新接触 detectron2 的用户熟悉一下这套配置系统。
4.1、LazyConfig 样例介绍
我们首先通过一个例子入手,直观地比较一下两种 config 方式的区别,假设我们现在的需求是,需要一个简单的卷积层,并且需要可以灵活地控制其中的参数,例如stride, kernel_size, padding等,我们对比一下两套配置方案是如何完成这样的事情:
YACS Config System:
在 YACS 下,需要在 config 中指定所有我们需要调整的参数,这意味着我们的 config 中如果面对需要调整的新的参数,就必须新增一项内容:
import torch.nn as nn
from yacs.config import CfgNode as CN
# 创建一个 config node, 并且罗列我们需要控制的参数
cfg = CN()
cfg.in_channels = 16
cfg.out_channels = 16
cfg.kernel_size = 3
cfg.stride = 1
cfg.padding = 1
# 通过 cfg 传入超参, 实例化一个卷积层
conv_layer = nn.Conv2d(
in_channels=cfg.in_channels,
out_channels=cfg.out_channels,
kernel_size=cfg.kernel_size,
stride=cfg.stride,
padding=cfg.padding,
)
# 打印 cfg
print(cfg)
>>> in_channels: 16
kernel_size: 3
out_channels: 16
padding: 1
stride: 1
# 打印 conv_layer
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
LazyConfig System:
在 LazyConfig System 下,以上一个 10 行代码才能完成的事情,只需要 2 行即可:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 通过 instantiate 实例化
conv_layer = instantiate(conv_config)
# 打印 conv_layer
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
4.2、什么是 LazyConfig 中的 “lazy” ?
LazyConfig 中的核心思想是 Lazy,Lazy 表示一种延迟状态。这里的表述可能会让人云里雾里。我们结合上一小节的 example,对 lazyconfig 中的核心思想做进一步解释,我们可以看到在上文创建卷积层的这个 example 中,我们新接触到了两个函数,LazyCall以及instantiate, 这也是 LazyConfig 中的核心用法,我们和直接通过nn.Conv2d创建一层卷积层进行一下简单的对比:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
conv_layer = instantiate(conv_config)
# 直接通过 nn.Conv2d 创建 conv_layer
conv_layer = nn.Conv2d(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
用最通俗的语言表达就是:LazyCall 将实例化这个过程拆分成了两个步骤,将一步即可实例化的过程拆分成了两个状态,也就是所谓的”延迟”:
1.通过LazyCall将需要实例化的对象包裹一下,传入对应的参数
2.通过instantiate来进行实例化
我们可以打印一下使用LazyCall包裹的对象,观察一下具体的内容:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
print(conv_config)
>>> {'in_channels': 16, 'out_channels': 16, 'kernel_size': 3, 'stride': 1, 'padding': 1, '_target_': <class 'torch.nn.modules.conv.Conv2d'>}
我们可以观察到 LazyCall 返回给我们一个类似字典的数据结构。当然其不是真正的 dict,我们去打印 type 可以发现是一个omegaconf.dictconfig.DictConfig对象。我们暂时可以不需要去了解,感兴趣的小伙伴可以去看看omegaconf这个 repo。这个DictConfig对象包含了几个 key:
那么所谓的Lazy顾名思义,在我们真正实例化这个类之前,它将一直以DictConfig对象的形式存在。也就是说我们不马上去实例化这个对象,在需要的时候再调用instantiate函数进行实例化即可。这种形式给我们带来了几个好处:
4.3、LazyConfig:在保证基本功能的前提下更加灵活
在解释完什么是 Lazy 之后,我们直接进入最基本的使用,让我们看看 LazyConfig 是如何满足一个配置系统的基本需求的:
config 的修改:
如果我们需要得到一个卷积核大小为 5 的卷积层:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 修改 kernel_size
conv_config.kernel_size = 5
# 实例化这个对象, 得到 kernel_size=5 的卷积层
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(5, 5), stride=(1, 1), padding=(1, 1))
config 的增加:
如果我们需要将这个卷积替换为分组卷积,意味着需要新指定一个groups参数:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 新增一个 groups 参数, 用来控制分组卷积的组数
conv_config.groups = 16
# 实例化这个对象, 得到 groups=16 的分组卷积
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), groups=16)
LazyCall 的 flexible 也会带来一个问题,当你新增一个不属于需要实例化的那个类的参数的时候,会报相应的错误,LazyCall 不存在超参检查的机制,需要用户对传入的参数足够了解:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 新增了一个不属于 nn.Conv2d 的参数并且实例化
conv_config.embed_dim = 16
conv_layer = instantiate(conv_config)
会提示以下错误:
Error when instantiating torch.nn.modules.conv.Conv2d!
TypeError: __init__() got an unexpected keyword argument 'embed_dim'
config 的删除:
如果我们不需要传入 padding 这个参数,使用默认值,但是我们原有的 config 中已经指定了的话,那么我们可以在实例化之前 del 这个参数,而在 yacs 中需要删除原始 config 中对应的字段。
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 删除 padding 这个参数
del conv_config.padding
# 实例化这个对象
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1))
直接替换我们实例化的对象:
更有趣的功能是,如果我们不需要 Conv2d 了,需要一个一维卷积(Conv1d),我们可以直接修改 _target_ 参数:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 修改 target 参数, 将实例化的对象改为 Conv1D
conv_config._target_ = nn.Conv1D
# 实例化这个对象, 得到 groups=16 的分组卷积
conv_layer = instantiate(conv_config)
print(conv_layer)
>>> Conv1d(16, 16, kernel_size=(3,), stride=(1,), padding=(1,))
非常神奇地发现我们实例化的对象从 Conv2d 变成了 Conv1d。
在上面的内容中,我们简单介绍了yacs和LazyConfig的基本使用。我们可以很直观地看出来,很多时候我们在使用 YACS 作为 config system,构建整套 codebase 需要的组件时,我们的 config 会变得越发臃肿且可读性变差。假设我们需要控制 codebase 下的多个模型,但真正在执行程序的时候只会运行其中一个模型,以Conv2d和MultiheadAttention为例,我们首先要面对的一个麻烦事就在于,需要将这两个模型可控制的所有参数都记录到 config 中:
import torch.nn as nn
from yacs.config import CfgNode as CN
# 创建一个 config node, 并且罗列我们需要控制的参数
cfg = CN()
# 创建一个子 config node 来控制 Conv2d 所需要的参数
cfg.CONV = CN()
cfg.CONV.in_channels = 16
cfg.CONV.out_channels = 16
cfg.CONV.kernel_size = 3
cfg.CONV.stride = 1
cfg.CONV.padding = 1
# 创建一个子 config node 来控制 MultiheadAttention 参数
cfg.ATTN = CN()
cfg.ATTN.num_heads = 8
cfg.ATTN.embed_dim = 256
# 创建一个参数来控制我们需要创建的模型
cfg.MODEL = "conv"
# 构建一个 build_model()函数来根据 config 返回我们需要的模型
def build_model(cfg):
if cfg.MODEL == "conv":
model = nn.Conv2d(
in_channels=cfg.CONV.in_channels,
out_channels=cfg.CONV.out_channels,
kernel_size=cfg.CONV.kernel_size,
stride=cfg.CONV.stride,
padding=cfg.CONV.padding,
)
elif cfg.MODEL == "attn":
model = nn.MultiheadAttention(
embed_dim=cfg.ATTN.embed_dim,
num_heads=cfg.ATTN.num_heads
)
else:
raise NotImplementedError("only implement conv and attn")
return model
model = build_model(cfg)
print(model)
>>> Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
并且如果不给到用户足够多的提示,例如cfg.MODEL这个参数,用户很可能无法知道我们的模型库里具体有多少个模型,并且应该如何调用,给用户带来了更多的困扰。用LazyConfig可以很直接地避免这些问题。如果用户需要构建一个 conv 的 config,或者是 attn 的 config,可以按照以下的操作:
import torch.nn as nn
from detectron2.config import LazyCall, instantiate
# 通过 LazyCall 创建一个 conv config 对象
conv_config = LazyCall(nn.Conv2d)(in_channels=16, out_channels=16, kernel_size=3, stride=1, padding=1)
# 通过 LazyCall 构建一个 attn config 对象
attn_config = LazyCall(nn.MultiheadAttention)(embed_dim=256, num_heads=8)
# 根据我们的需要实例化对应的模型
model = instantiate(attn_config)
并且LazyCall在语法上更加贴合原生的 python 语法的使用,即 import 需要的对象 -> 创建对应的 config -> 在需要的时候实例化, 整体上更像是一个即插即用的 config 插件,用户可以对这整个过程中的每一步做更加灵活精细且直观地控制。但是 LazyConfig 本身在某种程度上过于 flexible,缺少了 yacs 中frozen()与defrost()的保护机制,所以在使用的过程中也需要注意,尽可能避免在一些小细节的地方做了不必要的修改。
这篇文章只能算是一个引子,简单对比了两个配置系统的基本使用与理解。其实其背后的设计思想都各有好处,具体还是需要大家在使用的过程中去感受。我们后续会搭配更多的文章详细介绍一些更高级的用法。这里再对文章的内容作一个简单的对比与总结:
YACS 与 LazyConfig 在功能与灵活性上的比较:
配置系统需要具有的基本功能: 学习了解一些基本的使用可以很直观地帮助到大家高效地做实验。
https://detectron2.readthedocs.io/en/latest/tutorials/lazyconfigs.html
https://detectron2.readthedocs.io/en/latest/tutorials/configs.html