CHANfiG: Easier Configuration¶
摘要
去年11月底,我们发布了 CHANfiG 的第43个版本。 当时我非常开心的认为这个库已经趋于完善,可以准备进入到1.0了。 于是,我写了一篇博客来分享我在开发中的一些碎碎念
我是多么的愚昧。
快一年后的今天,我们即将发布v0.0.90版本。虽然版本号看上去只多了四十多个,commit数量从当时的85提升到了263,代码行数也从当时的2000翻倍来到了4000。
我们的目标也从发布v1.0调整到了先发布一个v0.1。
今天,我想跟大家分享一下这一年当中我们都做了一些什么。
结构¶
基类¶
一年前的 CHANfiG 使用OrderedDict
来作为基类。然而,OrderedDict
的 key 必须为str
类型。这大大的不好。现代版本的 CHANfiG 包括了Registry
,而有些时候,Regitsry
的 key 需要是一个 Python 对象,比如TorchFuncRegistry
。
此外,CHANfiG 最低支持 Python 3.7,而dict
的顺序在 3.7 及以后的版本中是被保证的。因此,不管从任何角度来讲,CHANfiG 都没有使用OrderedDict
来作为基类的必要。于是在 v0.0.54 中,CHANfiG 的基类被换成了dict
。尽管如此,我们计划在未来的版本中实现一些OrderedDict
的专属功能。
许多配置的值是依赖于其他值的,在 v0.0.34 中我们介绍了Variable
来实现值同步,但他无法表征运算关系。
比如, Transformer 的ffn.hidden_dim
通常是hidden_dim * 4
。
为了应对这种情况,在 v0.0.59 版本中,我们加入post
和boot
。其中post
可以用来执行一些后处理,而boot
则用来调用post
方法。在本例中,我们可以在post
里定义ffn.hidden_dim = hidden_dim * 4
来实现动态计算。
元类¶
在即将到来的 v0.0.90 中,我们更进一步的替换了元类。
在最近版本中被介绍的dataclass
已经吸引了增长的注意力。其中有两个功能我很喜欢:
- 类型注解检查
__post_init__
类型注解检查是一位微软的院友老哥提出的议题。 这允许你在类中通过类型注解来定义变量的类型,并在创建对象之后检查成员是否符合所定义的类型。
__post_init__
则提供了一个post
的替代。不同的是,__post_init__
会在对象初始化之后立即被自动调用(这个行为是无法修改的),而post
则需要手动调用(当然目前在调用parse
之后也会被自动调用)。
为了实现这样的功能,我们替换了默认的元类,在构造的 CHANfiG 对象初始化之后自动调用__post_init__
和validate
来进行后初始化。
类似的元类目前也被用到了丹灵上,用来自动准备模型、优化器和数据加载器。
天知道类型注解检查实现起来有多痛苦。在当前版本当中,访问注解拿到的是类型对象,但在未来版本当中拿到的是字符串……在 3.10 当中可以直接调用各种方法访问,但是我们要前向支持到 3.7……此外,由于我们的NestedDict
和Config
在之前就有些类型注解,这些类型注解也有版本问题。更加蛋疼的是,类型注解检查写的很一般,看错误信息完全看不懂发生了些什么……
Variable¶
我们在过去的一年当中做出了许多努力来让Variable
变得更加普通。
比如f"result: {Variable(1.0):.4f}"
的输出是'result: 1.0000'
,isinstance(Variable(10), int)
也是正确的。相比之下,ml_collections 的FieldReference
只会报告一个错误。
尽管如此,这种伪装只能针对遵循惯例的用法。比如在type(Variable(10)) != int
时就会变得力不从行 – 并不是我们不能,但是我认为伪装应该有一个边界,而这就是边界。
此外,应之前微软老哥的要求,我们让Variable
变得更加像dataclass.field
,支持各种类型检查和值域规定。麻麻再也不担心我传的参数不正确了!
在未来,我们计划让Variable
支持和FieldReference
类似的计算图。
功能¶
替换¶
之前跟 MMEngine 谈合作的时候,他们提到了两个问题:
- 如何在配置文件中引用另一个配置的值。
- 如何在一个配置文件中引用另一个配置文件。
后来由于无可奉告的原因,合作并没有进行下去。因此我也没有继续这方面的工作。 无独有偶,这个问题被微软老哥在另一个议题中再次提到。
于是我们就实现了。
其实也不难,首先做一次深度优先搜索来遍历所有的待插值的变量。 然后对于所有待插值的变量再做一次深度优先搜索来判断其中是否存在环。 之后替换就好了。
值得一提的是有关引用其他配置文件的事情。
我们使用了pyyaml-include
库来实现这个功能。
但是这个库是依据 GNU AGPL v3.0 协议开源的。
我们后期或许会视情况重写这部分功能。
empty¶
为了正确配置参数,理论上我们需要在__init__
的最后调用父类的__init__
来完成初始化。
事实上,有很多问题是在子类没有正确定义(尤其是没有正确调用父类的__init__
)的情况下出现的。
但是,这在 CHANfiG 中被认为是一个 feature:你想不想调用初始化都无所谓。
为此,我们引入了empty
和empty_like
方法来创建空对象,避免在load
、merge
等需要创建新的Dict
时候出现问题。
这导致了一个最近的 bug:在过去的版本中,我们通过empty_like
来创建空对象。
之后,我们发现一些property
在创建空对象之后没有正确的指向新对象的属性。
我花了很久才注意到property
是存储在__dict__
当中的,而因为empty_like
会拷贝__dict__
,所以property
也会被拷贝,因此仍旧指向原来的对象。
我最近也在思考,是否可以有一个更优雅的方式来解决这个问题 – 毕竟在初始化的结尾调用父类的__init__
也有很多缺陷。
其他¶
最近一年修复了很多很多错误,事实上,大多数版本都是只有几行修改的错误修复。
其中,NestedDict
的set
是出错误最多的地方,其次大概是__getattribute__
,merge
第三。
就在开始写这篇文章的几分钟之前,我又发现了一个 bug:
在极其特别的情况下,从一个配置类内部load
另一个配置文件会导致RecursionError
。
这种时候出才感受到测试真的很难,哪怕做到 100% 覆盖,你也永远不知道用户的输入到底是什么类型。
总结¶
道虽迩 不行不至
事虽小 不为不成
其为人也多暇日者 其出入不远矣
chanfig-2023.pdf