使用过 OpenMMLab 旗下开源软件,如 mmdet、mmseg 的读者们,一定知道在这些软件中,我们通过配置文件来定义深度学习任务的方方面面,比如模型结构、训练所使用的优化器、数据集等。
以 Yolo V3 模型结构为例,典型的配置文件是这样的:
model = dict( type='YOLOV3', backbone=dict( type='MobileNetV2', out_indices=(2, 4, 6), ...), neck=dict( type='YOLOV3Neck', num_scales=3, in_channels=[320, 96, 32], out_channels=[96, 96, 96]), ... )
可以看到,配置文件虽然使用了 Python 的语法(也支持 json 和 yaml 格式,这里暂且不表),但并不会像我们直接使用 PyTorch 一样,直接 import 相应的模块和类,然后进行类的实例化,而是使用字典来进行配置,使用 type 字段来标示该组件的类型。
这带来了一个问题,如果我想要引用另外一个代码库中注册的模型,该如何用配置文件的方式来实现呢?让我们从一个实际的例子入手。
1. 在 mmdet 中调用 mmcls 的 backbone
通常,在检测任务中,我们会使用一个主干网络来提取图片的特征。而由于图片特征提取对于各类图像任务是较为通用的需要,因而可以“借用”在分类任务中预训练的主干网络和相应的模型权重。因为分类任务比较简单,故而可以利用庞大的 ImageNet 数据集进行预训练,而在此基础上进一步训练检测网络,既能够提高模型收敛速度,又能够提高精度。
假设现在我们想要使用一个 MMDetection 中没有实现的主干网络进行特征提取,我们当然可以直接在 mmdet 中实现这个主干网络,但如果这个主干网络在分类代码库 MMClassification 中已经实现了,我们是可以直接通过修改配置文件来跨库调用的。比如我们想把上述 Yolo V3 中的主干网络从 MobileNet V2 换成 MobileNet V3,但 mmdet 还没有 Mobilenet V3 的实现,可以使用如下配置:
# 直接继承 yolo v3 的原始配置 _base_ = "./yolov3_mobilenetv2_320_300e_coco.py" # 因为 mmdet 中没有 import mmcls # 因而其中的主干网络并不会被注册到管理器中 # 这里我们需要手动用 custom_imports 来指定额外的导入 # 从而注册 mmcls 中的主干网络 custom_imports=dict(imports='mmcls.models', allow_failed_imports=False) model = dict( backbone=dict( # 使用 "scope.type" 的语法,指定从 mmcls 中寻找需要的模块 type='mmcls.MobileNetV3', # MobileNet V3 的其他设置 arch='large', out_indices=(5, 11, 14), init_cfg=dict( type='Pretrained', checkpoint='mmcls://mobilenet_v3_large'), # 配置文件与继承的配置文件中相同字段的字典,默认会融合 # 这里使用 `_delete_` 来删除继承的配置文件中的其他配置 _delete_=True), # 主干网络发生变化,其他相应的配置也需要改变 neck=dict(in_channels=[160, 112, 40]) )
2. 跨代码库调用机制
在 OpenMMLab 的 cfg 模式和 Registry 机制 一文中,我们简要介绍了关于 config 文件和 Registry 的架构和实现。而在这里,我们将从上文中跨仓库调用中涉及的两个关键点,来更进一步地了解 config 和 Registry 一些高级用法和实现。
custom_imports
在 OpenMMLab 的 cfg 模式和 Registry 机制 中,我们提到过类是在何时被注册到 Registry 中的:
通常, 在 import 相应模块时, 都会过一遍相应的定义被装饰对象的代码, 此时装饰器就已经运行了. 例如, 对于 mmdet 的 AnchorGenerator, SSDAnchorGenerator 这些类, 他们是在何时注册到 ANCHOR_GENERATORS 这个 Registry 类实例中的?
在 train.py 执行 from mmdet.core import DistEvalHook, EvalHook 时, 会调用并执行 mmdet/core/__init__.py 中的 from .anchor import *, 进而会调用并执行 mmdet/core/anchor/__init__.py 中的 from .anchor_generator import (AnchorGenerator, LegacyAnchorGenerator, YOLOAnchorGenerator), 从而完成 AnchorGenerator, SSDAnchorGenerator 这些类的注册。
也就是说只要 import 了相应的模块,模块中所有包含的类都会被注册到对应的 Registry 中。
那么问题来了,这些模块是在何时被 import 的呢?答案很简单,就是在我们代码执行的入口程序 tools/train.py 和 tools/test.py 中。以 MMClassification 中的 tools/train.py 为例:
... from mmcv.runner import get_dist_info, init_dist ... from mmcls.datasets import build_dataset from mmcls.models import build_classifier
通过导入 mmcv.runner 包,完成了 mmcv/runner/__init__.py 中一系列执行器、钩子、优化器等类的注册。通过导入 mmcls.datasets 包,完成了mmcls/datasets/__init__.py 中一系列数据集的注册。通过导入mmcls.models 包,完成了mmcls/models/__init__.py中一系列主干网络、颈部头部函数的注册。
当然,注册不一定仅仅发生在入口程序的最外层,比如在入口程序中没有导入数据处理和增强相关的包,这些类是在执行 build_dataset 时,在 mmcls/datasets/base_dataset.py 中进行的注册。
因此,Registry 的注册其实没有什么魔法,就是单纯地通过在入口程序中导入相应的包,在导入过程中完成的注册。这也就为我们的跨代码库调用带来了第一个问题,入口程序中当然不会导入与自己无关的另外一个库中的包,那怎么注册我们我需要的类呢?修改入口程序当然是一种办法,但 MMCV 中提供了更直接的在配置文件中显式导入自定义包的方法 —— custom_imports。
custom_imports=dict(imports='mmcls.models', allow_failed_imports=False)
只要在配置文件中加入这么一行,MMCV 在解析配置文件时,会自动调用 mmcv.import_modules_from_strings 函数,借助 Python 内置的 importlib 库中的 import_module 函数,进而完成对应的一系列类的注册。
Regsitry 中的 scope
在上文的例子中,我们看到,在跨仓库调用 MMClassification 的主干网络时,使用了一种特殊的写法,也就是 type='mmcls.MobileNetV3',既然我们都已经通过 custom_imports实现了 MMClassification 中主干网络的注册,为什么还要在使用时注明 mmcls. 呢?这就涉及到了 Registry 的 scope 机制。
Scope 机制的引入,是为了解决一个基础的问题,即类型的冲突,MMDetection 注册了一个 MobileNetV2,MMClassification 也注册了一个 MobileNetV2,到底用哪一个呢?MMCV 中给出的要求十分简单,禁止同一个 Regsitry 中注册两个相同名字的类。进一步地,每一个代码库在注册自己的模型时,都会注册到代码库自己的 Regsitry 中,而不是注册到 MMCV 统一的 Registry 中,从而避免与其他代码库产生冲突。
与此同时,虽然每个代码库都是注册到自己的 Registry 中,这些 Registry 却又不是独立的,而是以 MMCV 中某个统一的 Registry 为父 Regsitry,从而形成如图所示的树状结构。
通过 scope 这一机制,我们避免了不同代码库之间重复注册产生的冲突,同时使用 scope 来指定 type 具体在哪一个 Registry 中,也使得跨代码库的调用更加显式和清晰。
文章来源:公众号【OpenMMLab】
2021-11-23 19:52