跳转至

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 版本中,我们加入postboot。其中post可以用来执行一些后处理,而boot则用来调用post方法。在本例中,我们可以在post里定义ffn.hidden_dim = hidden_dim * 4来实现动态计算。

元类

在即将到来的 v0.0.90 中,我们更进一步的替换了元类。 在最近版本中被介绍的dataclass已经吸引了增长的注意力。其中有两个功能我很喜欢:

  1. 类型注解检查
  2. __post_init__

类型注解检查是一位微软的院友老哥提出的议题。 这允许你在类中通过类型注解来定义变量的类型,并在创建对象之后检查成员是否符合所定义的类型。

__post_init__则提供了一个post的替代。不同的是,__post_init__会在对象初始化之后立即被自动调用(这个行为是无法修改的),而post则需要手动调用(当然目前在调用parse之后也会被自动调用)。

为了实现这样的功能,我们替换了默认的元类,在构造的 CHANfiG 对象初始化之后自动调用__post_init__validate来进行后初始化。 类似的元类目前也被用到了丹灵上,用来自动准备模型、优化器和数据加载器。

天知道类型注解检查实现起来有多痛苦。在当前版本当中,访问注解拿到的是类型对象,但在未来版本当中拿到的是字符串……在 3.10 当中可以直接调用各种方法访问,但是我们要前向支持到 3.7……此外,由于我们的NestedDictConfig在之前就有些类型注解,这些类型注解也有版本问题。更加蛋疼的是,类型注解检查写的很一般,看错误信息完全看不懂发生了些什么……

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 谈合作的时候,他们提到了两个问题:

  1. 如何在配置文件中引用另一个配置的值。
  2. 如何在一个配置文件中引用另一个配置文件。

后来由于无可奉告的原因,合作并没有进行下去。因此我也没有继续这方面的工作。 无独有偶,这个问题被微软老哥在另一个议题中再次提到。

于是我们就实现了。

其实也不难,首先做一次深度优先搜索来遍历所有的待插值的变量。 然后对于所有待插值的变量再做一次深度优先搜索来判断其中是否存在环。 之后替换就好了。

值得一提的是有关引用其他配置文件的事情。 我们使用了pyyaml-include库来实现这个功能。 但是这个库是依据 GNU AGPL v3.0 协议开源的。 我们后期或许会视情况重写这部分功能。

empty

为了正确配置参数,理论上我们需要在__init__的最后调用父类的__init__来完成初始化。 事实上,有很多问题是在子类没有正确定义(尤其是没有正确调用父类的__init__)的情况下出现的。 但是,这在 CHANfiG 中被认为是一个 feature:你想不想调用初始化都无所谓。

为此,我们引入了emptyempty_like方法来创建空对象,避免在loadmerge等需要创建新的Dict时候出现问题。

这导致了一个最近的 bug:在过去的版本中,我们通过empty_like来创建空对象。 之后,我们发现一些property在创建空对象之后没有正确的指向新对象的属性。 我花了很久才注意到property是存储在__dict__当中的,而因为empty_like会拷贝__dict__,所以property也会被拷贝,因此仍旧指向原来的对象。

我最近也在思考,是否可以有一个更优雅的方式来解决这个问题 – 毕竟在初始化的结尾调用父类的__init__也有很多缺陷。

其他

最近一年修复了很多很多错误,事实上,大多数版本都是只有几行修改的错误修复。 其中,NestedDictset是出错误最多的地方,其次大概是__getattribute__merge第三。

就在开始写这篇文章的几分钟之前,我又发现了一个 bug: 在极其特别的情况下,从一个配置类内部load另一个配置文件会导致RecursionError

这种时候出才感受到测试真的很难,哪怕做到 100% 覆盖,你也永远不知道用户的输入到底是什么类型。

总结

道虽迩 不行不至

事虽小 不为不成

其为人也多暇日者 其出入不远矣