跳转至

CHANfiG: Easier Configuration

摘要

一年之前,我们分享了 CHANfiG 在过去一年的开发经历

不久之前,我们刚发布了 v0.0.105。在这15个版本中我们添加了 77 次提交。

虽然还有不少更新,但我们已经进入了一个接近稳定的状态。

今天,我还想跟大家分享一下这一年当中我们都做了一些什么。

回退

CHANfiG 的最初是一个配置文件仓库(尽管他现在越来越像一个 dict 仓库了),我们没有忘记这个初心。在 v0.0.92 中,我们为 NestedDict.get 方法添加了一个 fallback 参数,来支持回退访问。

一个最常见的配置文件需求就是回退。比如说 dropout,一个模型可能会有几百个 dropout,但我们有时可能只想改变某个特定模块中的 dropout,有时却想改变所有的 dropout。在这种情况下,我们可以使用 fallback 参数来实现这个需求。如果在获得某一个特定的 dropout 时失败,那么我们会向上回退,直到找到一个可用的 dropout。如下例所示,ffndropout 为 0.2,而 attn 没有定义 dropout,因此 attndropout 会回退到 0。

YAML
1
2
3
4
5
6
dropout: 0
backbone:
  attn:
    num_heads: 8
  ffn:
    dropout: 0.2

由于我们的代码一开始就会将循环展开,因此这一操作几乎不需要占用任何额外时间。

插入

为了表征不同成员之间的关系,我们曾在 v0.0.34 中引入了 Variable。为了进一步丰富功能,我们在 v0.0.59 中引入了 postboot

然而,这些无法解决一个问题:我们该如何在 yaml 或者 json 文件中来表示一个变量等于另一个变量?换句话说,我们该如何让配置文件知道某些变量应该被构建成一个 Variable

在 v0.0.92 当中,我们引入了 interpolate 方法来执行变量插值。如下例所示,ffn.hidden_dim 会被插值为 512

YAML
1
2
3
hidden_dim: 128
ffn:
  hidden_dim: ${hidden_dim} * 4

当然,这还有一些问题。由于 Variable 是勤奋的,因此在保存时,Variable 会被展开为其值。我们仍在评估这一问题的影响,并希望在未来的版本中解决这一问题。

数据类

PEP 557 提出了数据类的概念,相关 API 在 Python 3.7 中被正式引入。

7年之后的今天,数据类已经开始获得应用。比如 🤗 Transformers 就广泛使用了数据类来作为配置。

让 CHANfiG 符合数据类的标准是我们长期以来的计划。在 v0.0.97 版本中,我们第一次引入了 configclass 来作为 dataclass 的替代。

然而,由于最近一年始终很忙的缘故,我并没有能仔细深入阅读 PEP 557。因此其实并没有领会到数据类的精髓。configclass 的实现其实是一个很大的失败。

一直以来,困扰着 CHANfiG 的一个重要问题是类的属性。CHANfiG 的许多类都需要一些额外的属性来实现一些功能,比如说 FlatDict 有一个额外的 indent 来控制打印时的缩进,NestedDict 则有 separator 来控制键的分隔符(是的,如果设置 separator_ 的话,那等价于 dict.a.b.c 的访问将会是 dict['a_b_c'])。Config 更是高度依赖于属性来配置 Parser。但问题是,定义类的属性会和 PEP 577 产生冲突。

由此会产生一些我们无法解决的问题。比如说我们无法区分类中定义的变量到底是一个成员(dict member)还是一个属性(配置)。因此当你在类中定义一个变量时,他会同时成为一个属性和一个成员。这使我非常苦恼。因此,在很长的一段时间内,getattr方法是无法访问父类的属性的。此外,为了避免将属性复制到字典当中,我们在 configclass 中会判断当前类是否是一个 chanfig 的类,并在此中断。因此,子类本质上是无法定义额外的类属性的。

我们一直没能找到一个良好的解决方式,直到我最近开始阅读 PEP 577。PEP 577引入了 __annotations__ 来存储类型注解。在一个数据类当中,没有类型注解的属性是不会被认为是数据类的一部分的。 根据这个特性,我们现在会在构建时检查类的变量是否有类型注解。有类型注解的变量将被视作成员,没有的将被视作属性。这样,我们终于能够分清类的属性和成员了!由此,getattr 方法也终于可以顺着 __mro__ 来访问所有父类的属性了。

类型注解

在去年,我们引入了 validate 来检查成员是否符合类型注解。这很好,但是还可以更好。最近的 CHANfiG 中我们进一步增强了类型注解的作用。在赋值时,CHANfiG将会尝试将类型转换为类型注解定义的类型(事实上,这已经超越了类型注解的初始目的,但我们认为这是有益的)。因此,如果你的 dict.a 的类型注解是一个 float,在传入一个 int 时,现代 CHANfiG 将不会报错,而是将他转换为 float。我们将继续观察这个特性的影响。

我们希望这个改变将有良性的影响,并在未来将他引入 DefaultDictNestedDict 中 – 那时,除了设置 default_factory 之外,你还可以通过设置类型注解来精确的控制某个成员的类型。

未来

随着越来越多功能的加入,CHANfiG的性能也越来越差。虽然我们仍然是 ml_collections 的 4 倍和 EasyDict 的 2 倍,但是相比 Python dict,我们已经慢了许多,尤其是在构建 NestedDict 时。便捷化的代价是巨大的。我们可能需要将部分代码 C 化以提高性能。

此外,Variable 的进一步更新也迫在眉睫。我们在开发一个类似于 FieldReferenceField 来支持懒加载。同时对 yaml 进行一定扩充。这将是一个很大的更新。


癸卯年处暑

于柯士甸道西1号

chanfig-2024.pdf