ExCore 是一个专为深度学习所设计的配置/注册系统,并且带有一些小工具。
✨ ExCore 支持配置文件的自动补全、类型提示、文档字符串和代码跳转。
ExCore 仍在实验阶段。
English | 中文
ExCore 中的配置系统专为深度学习中的训练(泛指所有相似的部分,如测试、验证等.下同)流程所设计。其核心前提是将配置文件中所要创建的对象分为三类—— 主要、 中间 和 孤立 对象。
主要对象是指在训练中 直接 使用的对象,如模型、优化器等。ExCore会创建并返回这些对象。中间对象是指在训练中 间接 使用的对象,如模型的主干、将要传入优化器的模型参数。孤立对象是指 python 内建对象,会在读取配置文件时直接解析,如 int, string, list, dict 等
ExCore 扩展了 toml 文件的语法,引入了一些特殊的前缀字符 —— !, @, $ 和 '&' 以简化配置文件的定义过程.
我们在ExCore中引入了一些术语:
PrimaryField(主字段):例如 Model、TrainData、TestData 等核心字段。RegistryName(注册表名称):注册表的名称,例如Models、Datasets、Losses等。它可以与PrimaryField相同。ModuleName(模块名称):所有已注册项(类、函数、模块)都称为Module(模块),其名称即为ModuleName。
本配置系统有以下特性
摆脱 `type`
Model:
type: ResNet # <----- ugly type
layers: 50
num_classes: 1为了摆脱type, ExCore 将所有注册的名称都视为 保留字. 主要 模块需要定义为 [PrimaryField.ModuleName]. PrimaryField 是一些预先定义的字段, 如 Model, Optimizer. ModuleName 即为注册的名称。
[Model.FCN]
layers = 50
num_classes = 1消除模块嵌套
TrainData:
type: Cityscapes
dataset_root: data/cityscapes
transforms:
- type: ResizeStepScale
min_scale_factor: 0.5
max_scale_factor: 2.0
scale_step_size: 0.25
- type: RandomPaddingCrop
crop_size: [1024, 512]
- type: Normalize
mode: train
ExCore 使用一些特殊的前缀字符来表明一些参数也是模块。后面会介绍更多前缀.
[TrainData.Cityscapes]
dataset_root = "data/cityscapes"
mode = 'train'
# 使用 `!` 表示这是一个需要实例化的模块。规范来说应该使用引号包裹"!transforms",但是无所谓
!transforms = ["ResizeStepScale", "RandomPaddingCrop", "Normalize"]
# 中间对象的`PrimaryField` 可以被省略
[ResizeStepScale]
min_scale_factor = 0.5
max_scale_factor = 2.0
scale_step_size = 0.25
# 也可以显式地指定
[Transforms.RandomPaddingCrop]
crop_size = [1024, 512]
# 没有参数时甚至可以不定义
# [Normalize]
✨ 配置文件自动补全,类型提示,文档字符串和代码跳转
旧式配置的设计因难以编写(没有自动补全功能)和无法导航到相应的类而饱受诟病。然而语言服务器协议(Language Server Protocol)可用于支持各种代码编辑功能,如自动完成、类型提示和代码导航。通过利用 lsp 和 json_schema,它能够提供自动补全、一些弱类型提示(如果代码注释得很好,如 python 中的标准类型提示,它将实现更多功能)和相应类的文档字符串功能。
ExCore 通过将类名到代码文件位置的映射保存在本地来支持代码跳转的功能。目前只支持neovim, 见 excore.nvim.
配置继承
使用__base__ 从另一个toml文件继承,只有字典会局部更新,其他类型会直接被覆盖。
__base__ = ["xxx.toml", "xxxx.toml"]@复用(共享)模块
ExCore 使用 @ 来标记重复使用的模块,这些模块可以在不同模块之间共享。
# FCN 和 SegNet 将会使用同一个 ResNet 对象
[Model.FCN]
@backbone = "ResNet"
[Model.SegNet]
@backbone = "ResNet"
[ResNet]
layers = 50
in_channel = 3等同于
resnet = ResNet(layers=50, in_channel=3)
FCN(backbone=resnet)
SegNet(backbone=resnet)
# 如果使用"!",那么其等同于
FCN(backbone=ResNet(layers=50, in_channel=3))
SegNet(backbone=ResNet(layers=50, in_channel=3))$ 引用类和跨文件
ExCore 使用 $ 来表示使用类本身而不用实例化
[Model.ResNet]
$block = "BasicBlock"
layers = 50
in_channel = 3等同于
from xxx import ResNet, BasicBlock
ResNet(block=BasicBlock, layers=50, in_channel=3)为了跨文件引用模块,$ 可以用于 PrimaryField 之前,例如:
文件 A:
[Block.BasicBlock]文件 B:
[Block.BottleneckBlock]文件 C:
[Model.ResNet]
!block="$Block"所以我们可以将文件A C 或文件B C结合
__base__ = ["A.toml", "C.toml"]
# or
__base__ = ["B.toml", "C.toml"]& 变量引用
ExCore 使用 & 来引用配置文件最顶层的变量。
size = 224
[TrainData.ImageNet]
&train_size = "size"
!transforms = ['RandomResize', 'Pad']
data_path = 'xxx'
[Transform.Pad]
&pad_size = "size"
[TestData.ImageNet]
!transforms = ['Normalize']
&test_size = "size"
data_path = 'xxx'& 也可以用于参数之中。通常配合参数钩子使用,请参考 finegrained_config。
✨在配置文件中使用python模块
ExCore 中的注册器可以注册一个模块,如:
from excore import Registry
import torch
MODULE = Registry("module")
MODULE.register_module(torch)然后你可以在配置文件中使用 torch
[Model.ResNet]
$activation = "torch.nn.ReLU"
# 或者
$activation = "torch.nn.ReLU()"
# 或者, 注意,这里直接使用eval
$activation = "torch.nn.ReLU(inplace=True)"import torch
from xxx import ResNet
ResNet(torch.nn.ReLU)
# 或者
ResNet(torch.nn.ReLU())
# 或者
ResNet(torch.nn.ReLU(inplace=True))✨参数级别 Hook
ExCore 提供了一个简单方式调用无参的参数Hook。
[Optimizer.AdamW]
@params = "$Model.parameters()"
weight_decay = 0.01如果你想要调用一个类方法或者静态方法。
[Model.XXX]
$backbone = "A.from_pretained()"属性也可以被使用。
[Model.XXX]
!channel = "$Block.out_channel"也可以链式调用。
[Model.XXX]
!channel = "$Block.last_conv.out_channels"这种方式要求你在目标类的上定义相应的方法或属性,并且不能传递参数。因此 ExCore 提供了 ConfigArgumentHook
class ConfigArgumentHook(node, enabled)你需要继承自 ConfigArgumentHook 实现自己的类,例如:
from excore import ConfigArgumentHook
from . import HOOKS
@HOOKS.register()
class BnWeightDecayHook(ConfigArgumentHook):
def __init__(self, node, enabled: bool, bn_weight_decay: bool, weight_decay: float):
super().__init__(node, enabled)
self.bn_weight_decay = bn_weight_decay
self.weight_decay = weight_decay
def hook(self):
model = self.node()
if self.bn_weight_decay:
optim_params = model.parameters()
else:
p_bn = [p for n, p in model.named_parameters() if "bn" in n]
p_non_bn = [p for n, p in model.named_parameters() if "bn" not in n]
optim_params = [
{"params": p_bn, "weight_decay": 0},
{"params": p_non_bn, "weight_decay": self.weight_decay},
]
return optim_params[Optimizer.SGD]
@params = "$Model@BnWeightDecayHook"
lr = 0.05
momentum = 0.9
weight_decay = 0.0001
[ConfigHook.BnWeightDecayHook]
weight_decay = 0.0001
bn_weight_decay = false
enabled = true使用 @ 来调用用户定义的Hook.
实例级别 hook
If the logic of module building are too complicated, instance-level hook may be helpful.
TODO
✨Lazy Config with simple API
LazyConfig 的核心概念是 `Lazy`,它代表一种延迟的状态。在实例化之前,所有参数都会存储在一个特殊的字典中,该字典还包含了目标类/函数是什么。因此,可以很容易地更改模块的任何参数,并控制应该实例化哪个模块,不应该实例化哪个模块。它还用于通过Python语言服务(LSP)解决纯文本配置的缺陷,Python LSP能够提供代码导航、自动补全等功能。
ExCore 实现了一些节点—— ModuleNode、InternNode、ReusedNode、ClassNode、ConfigHookNode、GetAttr 和 VariableReference,以及一个 LazyConfig 来管理所有节点。
ExCore 只提供了两个简单的 API 来构建模块—— load 和 build_all 。
通常情况下,使用以下代码一键创建所有对象:
from excore import config
layz_cfg = config.load('xxx.toml')
module_dict, run_info = config.build_all(layz_cfg)build_all 的结果分别是 Primary 模块和 Isolated 对象。
如果你只想使用某个特定的模块:
from excore import config
layz_cfg = config.load('xxx.toml')
model = layz_cfg.Model() # Model是`PrimaryField`之一
# 或者
model = layz_cfg['Model']()如果你想按照其他逻辑构建模块,你仍然可以使用 LazyConfig 来调整 nodes 的参数和其他事情。
from excore import config
layz_cfg = config.load('xxx.toml')
lazy_cfg.Model << dict(pre_trained='./')
# 或者
lazy_cfg.Model.add(pre_trained='./')
module_dict, run_info = config.build_all(layz_cfg)✨模块参数验证及延迟赋参
在模块初始化和调用之前验证参数,这将节省一些连续的耗时初始化的时间。
可以手动设置任何缺少的参数值,其会被解析为字符串、整数、列表、元组或字典。
使用环境变量 EXCORE_VALIDATE 和 EXCORE_MANUAL_SET 来控制是否进行验证和分配。
配置打印
from excore import config
cfg = config.load_config('xx.toml')
print(cfg)结果:
╒══════════════════════════╤══════════════════════════════════════════════════════════════════════╕
│ size │ 1024 │
├──────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ TrainData.CityScapes │ ╒═════════════╤════════════════════════════════════════════════════╕ │
│ │ │ &train_size │ size │ │
│ │ ├─────────────┼────────────────────────────────────────────────────┤ │
│ │ │ !transforms │ ['RandomResize', 'RandomFlip', 'Normalize', 'Pad'] │ │
│ │ ├─────────────┼────────────────────────────────────────────────────┤ │
│ │ │ data_path │ xxx │ │
│ │ ╘═════════════╧════════════════════════════════════════════════════╛ │
├──────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ Transform.RandomFlip │ ╒══════╤═════╕ │
│ │ │ prob │ 0.5 │ │
│ │ ├──────┼─────┤ │
│ │ │ axis │ 0 │ │
│ │ ╘══════╧═════╛ │
├──────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ Transform.Pad │ ╒═══════════╤══════╕ │
│ │ │ &pad_size │ size │ │
│ │ ╘═══════════╧══════╛ │
├──────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ Normalize.std │ [0.5, 0.5, 0.5] │
├──────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ Normalize.mean │ [0.5, 0.5, 0.5] │
├──────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ TestData.CityScapes │ ╒═════════════╤═══════════════╕ │
│ │ │ !transforms │ ['Normalize'] │ │
│ │ ├─────────────┼───────────────┤ │
│ │ │ &test_size │ size │ │
│ │ ├─────────────┼───────────────┤ │
│ │ │ data_path │ xxx │ │
│ │ ╘═════════════╧═══════════════╛ │
├──────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ Model.FCN │ ╒═══════════╤════════════╕ │
│ │ │ @backbone │ ResNet │ │
│ │ ├───────────┼────────────┤ │
│ │ │ @head │ SimpleHead │ │
│ │ ╘═══════════╧════════════╛ │
...
✨LazyRegistry
为了减少不必要的导入,ExCore 提供了 LazyRegistry,其可以存储类或函数的名称到它们的 “限制名称” (qualname)映射,并且将映射转储到本地。当配置文件解析时,必要的模块才会被导入。
存储额外信息
from excore import Registry
Models = Registry("Model", extra_field="is_backbone")
@Models.register(is_backbone=True)
class ResNet:
pass模块分类和模糊搜索
from excore import Registry
Models = Registry("Model", extra_field="is_backbone")
@Models.register(is_backbone=True)
class ResNet:
pass
@Models.register(is_backbone=True)
class ResNet50:
pass
@Models.register(is_backbone=True)
class ResNet101:
pass
@Models.register(is_backbone=False)
class head:
pass
print(Models.module_table(select_info='is_backbone'))
print(Models.module_table(filter='**Res**'))results:
╒═══════════╤═══════════════╕
│ Model │ is_backbone │
╞═══════════╪═══════════════╡
│ ResNet │ True │
├───────────┼───────────────┤
│ ResNet101 │ True │
├───────────┼───────────────┤
│ ResNet50 │ True │
├───────────┼───────────────┤
│ head │ False │
╘═══════════╧═══════════════╛
╒═══════════╕
│ Model │
╞═══════════╡
│ ResNet │
├───────────┤
│ ResNet101 │
├───────────┤
│ ResNet50 │
╘═══════════╛
一键注册
from torch import optim
from excore import Registry
OPTIM = Registry("Optimizer")
def _get_modules(name: str, module) -> bool:
if name[0].isupper():
return True
return False
OPTIM.match(optim, _get_modules)
print(OPTIM)results:
╒════════════╤════════════════════════════════════╕
│ NAME │ DIR │
╞════════════╪════════════════════════════════════╡
│ Adadelta │ torch.optim.adadelta.Adadelta │
├────────────┼────────────────────────────────────┤
│ Adagrad │ torch.optim.adagrad.Adagrad │
├────────────┼────────────────────────────────────┤
│ Adam │ torch.optim.adam.Adam │
├────────────┼────────────────────────────────────┤
│ AdamW │ torch.optim.adamw.AdamW │
├────────────┼────────────────────────────────────┤
│ SparseAdam │ torch.optim.sparse_adam.SparseAdam │
├────────────┼────────────────────────────────────┤
│ Adamax │ torch.optim.adamax.Adamax │
├────────────┼────────────────────────────────────┤
│ ASGD │ torch.optim.asgd.ASGD │
├────────────┼────────────────────────────────────┤
│ SGD │ torch.optim.sgd.SGD │
├────────────┼────────────────────────────────────┤
│ RAdam │ torch.optim.radam.RAdam │
├────────────┼────────────────────────────────────┤
│ Rprop │ torch.optim.rprop.Rprop │
├────────────┼────────────────────────────────────┤
│ RMSprop │ torch.optim.rmsprop.RMSprop │
├────────────┼────────────────────────────────────┤
│ Optimizer │ torch.optim.optimizer.Optimizer │
├────────────┼────────────────────────────────────┤
│ NAdam │ torch.optim.nadam.NAdam │
├────────────┼────────────────────────────────────┤
│ LBFGS │ torch.optim.lbfgs.LBFGS │
╘════════════╧════════════════════════════════════╛
多合一
可以通过 Registry 来获取所有已定义的注册器,并且可以将它们合并为一个全局注册器。
from excore import Registry
MODEL = Registry.get_registry("Model")
G = Registry.make_global()✨注册 python 模块
Registry 不只能够注册类或者函数,还能注册python模块,如:
from excore import Registry
import torch
MODULE = Registry("module")
MODULE.register_module(torch)可以在配置中使用 torch:
[Model.ResNet]
$activation = "torch.nn.ReLU"
# 或者
!activation = "torch.nn.ReLU"等同于
import torch
from xxx import ResNet
ResNet(torch.nn.ReLU)
# 或者
ResNet(torch.nn.ReLU())路径管理器
通过结构化方式管理目录创建,当作用域内的操作失败时自动清理已创建目录。
from excore.plugins.path_manager import PathManager
with PathManager(
base_path = "./exp",
sub_folders=["folder1", "folder2"],
config_name="config_dir",
instance_name="test1",
remove_if_fail=True,
sub_folder_exist_ok=False,
config_name_first=False,
return_str=True,
) as pm:
folder1_path:str = pm.get("folder1") # 获取文件夹路径
folder2_path:str = pm.get("folder2")
do_sth(folder1_path, folder2_path)
train()生成的目录结构:
exp
├── folder1
│ └── config_dir
│ └── test1
└── folder2
└── config_dir
└── test1
可通过数据类优化使用体验:
from dataclasses import dataclass
from excore.plugins.path_manager import PathManager
@dataclass
class SubPath:
folder1: str = "folder1"
folder2: str = "folder2"
sub_path = SubPath()
with PathManager(
base_path = "./exp",
sub_folders=sub_path,
config_name="config_dir",
instance_name="test1",
remove_if_fail=True,
sub_folder_exist_ok=False,
config_name_first=False,
return_str=True,
) as pm:
folder1_path:str = sub_path.folder1
folder2_path:str = sub_path.folder2
do_sth(folder1_path, folder2_path)
train()✨细粒度配置
参考YOLO风格的配置方式,实现细粒度模型架构配置能力。
首先需要为注册的模块类添加参数传递关系声明:
from excore import Registry
MODEL = Registry("Model", extra_field=["receive", "send"])
MODEL.register_module(nn.Conv2d, receive="in_channels", send="out_channels")
MODEL.register_module(nn.BatchNorm2d, receive="num_features", send="num_features")receive 应为字符串或字符串列表,表示参数传递中需要接收的参数名称。send 同理。
第二步启用细粒度配置功能:
from excore.plugins.finegrained_config import enable_finegrained_config
enable_finegrained_config()第三步使用 * 符号定义细粒度配置,需要三个必须参数:
$class_mapping: 使用的模块类名列表info: 层级配置列表,格式为[重复次数, 模块索引]args: 各层初始化参数列表
[Backbone.FinegrainedModel]
$backbone = "torch.nn.Sequential*FinegrainedConfig"
[FinegrainedConfig]
$class_mapping = ['Conv2d', 'BatchNorm2d']
# [参数来源层, 重复次数, 模块索引]
info = [
[1, 0],
[3, 0],
[1, 1],
[2, 0],
[1, 1],
]
args = [
[3],
[32, 3],
[64, 3],
[128],
[224, 1],
[224],
]$backbone = "torch.nn.Sequential*$ConfigInfo::backbone" 表示使用 torch.nn.Sequential 作为容器包装 FinegrainedConfig 生成的模块。*FinegrainedConfig 表示应用细粒度配置解析器,并从 FinegrainedConfig 字典获取初始化参数。
系统会根据模块类声明的 receive 和 send 参数自动传递关键参数。
最终生成的骨干网络结构如下:
Sequential(
(0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1))
(1): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
(2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
(3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
(4): BatchNorm2d(64, eps=128, momentum=0.1, affine=True, track_running_stats=True)
(5): Conv2d(64, 224, kernel_size=(1, 1), stride=(1, 1))
(6): Conv2d(64, 224, kernel_size=(1, 1), stride=(1, 1))
(7): BatchNorm2d(224, eps=224, momentum=0.1, affine=True, track_running_stats=True)
)更多特性可以参照 Roadmap of ExCore



