Lazy Configs
传统的基于 yacs 的配置系统提供了基本的标准功能。 但是,它并没有为许多新项目提供足够的灵活性。 我们开发了一种替代的、非侵入式的配置系统,可以与detectron2 或任何其他复杂的项目一起使用。
Python Syntax
我们的配置对象仍然是字典。 我们没有使用 Yaml 来定义字典,而是直接在 Python 中创建字典。 这为用户提供了 Yaml 中不存在的以下功能:
- 使用 Python 轻松操作字典(添加和删除)。
- 编写简单的算术或调用简单的函数。
- 使用更多数据类型
- 使用熟悉的 Python 导入语法导入/组合其他配置文件。
可以像这样加载 Python 配置文件:
# config.py:
a = dict(x=1, y=2, z=dict(xx=1))
b = dict(x=3, y=4)
# my_code.py:
from detectron2.config import LazyConfig
cfg = LazyConfig.load("path/to/config.py") # an omegaconf dictionary
assert cfg.a.z.xx == 1
在 LazyConfig.load 之后,cfg 将是一个字典,其中包含在配置文件的全局范围内定义的所有字典。 请注意:
- 在加载过程中,所有字典都会转换为 omegaconf 配置对象。 这允许访问 omegaconf 功能,例如其访问语法和插值。
- config.py 中的绝对导入与常规 Python 中的工作方式相同。
- 相对导入只能从配置文件中导入字典。 它们只是 LazyConfig.load_rel 的语法糖。 他们可以在不需要 init.py 的情况下以相对路径加载 Python 文件。
LazyConfig.save 可以将配置对象保存到 yaml。 请注意,如果配置文件中出现不可序列化的对象(例如 lambdas),这并不总是成功的。 是否牺牲储蓄能力以换取灵活性取决于用户。
Recursive Instantiation
LazyConfig 系统大量使用递归实例化,这是一种使用字典来描述对函数/类的调用的模式。 字典包括:
- 一个“target”键,其中包含可调用的路径,例如“module.submodule.class_name”。
- 表示要传递给可调用对象的参数的其他键。 参数本身可以使用递归实例化来定义。
我们提供了一个帮助函数 LazyCall 来帮助创建这样的字典。 以下代码使用 LazyCall
from detectron2.config import LazyCall as L
from my_app import Trainer, Optimizer
cfg = L(Trainer)(
optimizer=L(Optimizer)(
lr=0.01,
algo="SGD"
)
)
创建一个像这样的字典:
cfg = {
"_target_": "my_app.Trainer",
"optimizer": {
"_target_": "my_app.Optimizer",
"lr": 0.01, "algo": "SGD"
}
}
通过使用这样的字典来表示对象,一个通用的实例化函数可以将它们变成实际的对象,即:
from detectron2.config import instantiate
trainer = instantiate(cfg)
# equivalent to:
# from my_app import Trainer, Optimizer
# trainer = Trainer(optimizer=Optimizer(lr=0.01, algo="SGD"))
这种模式足以描述非常复杂的对象,例如:
from detectron2.config import LazyCall as L
from detectron2.layers import ShapeSpec
from detectron2.modeling.meta_arch import GeneralizedRCNN
from detectron2.modeling.anchor_generator import DefaultAnchorGenerator
from detectron2.modeling.backbone.fpn import LastLevelMaxPool
from detectron2.modeling.backbone import BasicStem, FPN, ResNet
from detectron2.modeling.box_regression import Box2BoxTransform
from detectron2.modeling.matcher import Matcher
from detectron2.modeling.poolers import ROIPooler
from detectron2.modeling.proposal_generator import RPN, StandardRPNHead
from detectron2.modeling.roi_heads import (
StandardROIHeads,
FastRCNNOutputLayers,
MaskRCNNConvUpsampleHead,
FastRCNNConvFCHead,
)
from ..data.constants import constants
model = L(GeneralizedRCNN)(
backbone=L(FPN)(
bottom_up=L(ResNet)(
stem=L(BasicStem)(in_channels=3, out_channels=64, norm="FrozenBN"),
stages=L(ResNet.make_default_stages)(
depth=50,
stride_in_1x1=True,
norm="FrozenBN",
),
out_features=["res2", "res3", "res4", "res5"],
),
in_features="${.bottom_up.out_features}",
out_channels=256,
top_block=L(LastLevelMaxPool)(),
),
proposal_generator=L(RPN)(
in_features=["p2", "p3", "p4", "p5", "p6"],
head=L(StandardRPNHead)(in_channels=256, num_anchors=3),
anchor_generator=L(DefaultAnchorGenerator)(
sizes=[[32], [64], [128], [256], [512]],
aspect_ratios=[0.5, 1.0, 2.0],
strides=[4, 8, 16, 32, 64],
offset=0.0,
),
anchor_matcher=L(Matcher)(
thresholds=[0.3, 0.7], labels=[0, -1, 1], allow_low_quality_matches=True
),
box2box_transform=L(Box2BoxTransform)(weights=[1.0, 1.0, 1.0, 1.0]),
batch_size_per_image=256,
positive_fraction=0.5,
pre_nms_topk=(2000, 1000),
post_nms_topk=(1000, 1000),
nms_thresh=0.7,
),
roi_heads=L(StandardROIHeads)(
num_classes=80,
batch_size_per_image=512,
positive_fraction=0.25,
proposal_matcher=L(Matcher)(
thresholds=[0.5], labels=[0, 1], allow_low_quality_matches=False
),
box_in_features=["p2", "p3", "p4", "p5"],
box_pooler=L(ROIPooler)(
output_size=7,
scales=(1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32),
sampling_ratio=0,
pooler_type="ROIAlignV2",
),
box_head=L(FastRCNNConvFCHead)(
input_shape=ShapeSpec(channels=256, height=7, width=7),
conv_dims=[],
fc_dims=[1024, 1024],
),
box_predictor=L(FastRCNNOutputLayers)(
input_shape=ShapeSpec(channels=1024),
test_score_thresh=0.05,
box2box_transform=L(Box2BoxTransform)(weights=(10, 10, 5, 5)),
num_classes="${..num_classes}",
),
mask_in_features=["p2", "p3", "p4", "p5"],
mask_pooler=L(ROIPooler)(
output_size=14,
scales=(1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32),
sampling_ratio=0,
pooler_type="ROIAlignV2",
),
mask_head=L(MaskRCNNConvUpsampleHead)(
input_shape=ShapeSpec(channels=256, width=14, height=14),
num_classes="${..num_classes}",
conv_dims=[256, 256, 256, 256, 256],
),
),
pixel_mean=constants.imagenet_bgr256_mean,
pixel_std=constants.imagenet_bgr256_std,
input_format="BGR",
)
还有一些对象或逻辑不能简单地用字典来描述,例如重用的对象或方法调用。 它们可能需要一些重构才能使用递归实例化。
Using Model Zoo LazyConfigs
我们使用 LazyConfig 系统在模型动物园中提供了一些配置,例如:
- 公共基线。
- 新的 Mask R-CNN 基线
安装detectron2 后,可以通过模型动物园API model_zoo.get_config 加载它们。
使用这些作为参考,您可以自由地为您自己的项目定义自定义配置结构/字段,只要您的训练脚本可以理解它们。 尽管如此,我们的模型动物园配置仍然遵循一些简单的约定以保持一致性,例如 cfg.model 定义模型对象,cfg.dataloader.{train,test} 定义数据加载器对象,cfg.train 包含键值形式的训练选项。 除了 print(),一个更好的查看配置结构的方法是这样的:
from detectron2.model_zoo import get_config
from detectron2.config import LazyConfig
print(LazyConfig.to_py(get_config("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.py")))
从输出中可以更轻松地找到要更改的相关选项,例如 dataloader.train.total_batch_size 用于批量大小,或 optimizer.lr 用于基本学习率。
我们提供了一个参考训练脚本 tools/lazyconfig_train_net.py,可以训练/评估我们的模型动物园配置。 它还展示了如何支持命令行值覆盖。
为了展示新系统的强大功能和灵活性,我们展示了一个简单的配置文件可以让detectron2 训练来自torchvision 的ImageNet 分类模型,即使detectron2 不包含有关ImageNet 分类的特征。 这可以作为在其他深度学习任务中使用detectron2的参考。
Summary
通过使用递归实例化来创建对象,我们避免了将巨大的配置传递到许多地方,因为 cfg 只传递给实例化。 这有以下好处:
- 它是非侵入性的:要构造的对象是与配置无关的常规 Python 函数/类。 他们甚至可以住在其他图书馆。 例如,{“target”: “torch.nn.Conv2d”, “in_channels”: 10, “out_channels”: 10, “kernel_size”: 1} 定义了一个卷积层。
- 清楚将调用哪些函数/类,以及它们使用哪些参数。
- cfg 不需要预定义的键和结构。 只要它转换为有效代码,它就有效。 这提供了更多的灵活性。
- 您仍然可以将庞大的字典作为参数传递,就像旧方法一样。
递归实例化和 Python 语法是正交的:你可以使用一个而没有另一个。 但是通过将它们放在一起,配置文件看起来很像将要执行的代码:
但是,配置文件只定义了字典,可以通过组合或覆盖轻松地进一步操作。 相应的代码只会在稍后调用实例化时执行。 在某种程度上,在配置文件中,我们正在编写“可编辑代码”,稍后将在需要时“延迟执行”。 这就是为什么我们称这个系统为“LazyConfig”。