不要 重复自己*

发表于 2022年4月5日
在 GitHub 上更新
如何为现代机器学习设计开源库

🤗 Transformers 设计理念

“不要重复自己 (Don’t Repeat Yourself)” ,或 DRY,是广为人知的软件开发原则。该原则出自《程序员修炼之道: 从小工到专家》 (英文名为 The pragmatic programmer),这是代码设计领域迄今为止阅读量最大的一本书。该原则言简意赅,即: 重用而不要重写其他地方已有的逻辑。这可以确保代码保持同步,使其更易于维护且更健壮。该做法使得对公共代码逻辑的任何更改都会统一地影响所有依赖该公共代码逻辑的代码。

乍一看,Hugging Face transformers 库的设计与 DRY 原则背道而驰。注意力机制的代码被复制到不同的模型文件里不下 50 次。有时整个 BERT 模型的代码都会被复制到其他模型文件中。贡献者在添加新模型时,如果新模型用到了现有的某个模型,我们经常强制要求他们把该现有模型的所有代码复制到新模型代码中,连一个小小的逻辑调整也不例外。我们为什么要这么做?是因为我们太懒抑或是因为我们无力承担将所有公共逻辑集中到一个地方所带来的工作量?

不,我们并不懒 —— 不在 transformers 库中使用 DRY 原则是有意之举。我们决定采用一种与 DRY 不同的设计原则,我们称之为 单模型文件 策略 (single model file policy)。 单一模型文件 策略要求,任何模型的所有代码都只应该放在一个文件中,这个文件就是该模型自己的模型文件。如果读者想了解 BERT 如何是进行推理的,他/她只需要阅读 BERT 的 modeling_bert.py 文件即可。通常情况下,我们拒绝任何将不同模型的相同子模块抽象并集中到一个新文件中的尝试。我们不想要一个包含所有可能的注意力机制的 attention_layer.py

我们为何作出这样的设计呢?我们将原因概括如下:

  • 1. Transformers 生于开源,服务开源
  • 2. 我们的产品是模型,我们的客户是那些阅读或修改模型代码的用户。
  • 3. 机器学习领域发展极其迅速。
  • 4. 机器学习模型是静态的。

1. 生于开源,服务开源

Transformers 积极鼓励来自外部的贡献。贡献一般有错误修复和新模型添加两类。如果有人发现了某个模型文件中的错误,我们希望他/她很容易就能修复它。没有什么比修复了一个 bug 却发现它导致了其他模型上的 100 个 bug 更令人沮丧的了。

因为每个模型代码相互独立,所以对于只了解他/她正在用的那个模型的人来说,修复它会轻松很多。同样,如果只添加一个新的模型文件,添加新的模型代码以及 review 相应的 PR 会更容易。贡献者不必弄清楚如何在不破坏现有模型的情况下向公共的注意力机制代码添加新功能,代码评审者也缺省地知道这个 PR 不会破坏任何一个现有模型。

2. 模型代码即产品

我们假设 transformers 库的很多用户不仅会阅读文档,而且会查看实际模型代码并有可能对其进行修改。鉴于 transformers 库被 fork 了 1 万多次,我们的 transformers 论文被引用了 1 千多次,这个假设应该是站得住脚的。

因此,最重要的是让第一次阅读 transformers 模型代码的人能够轻松理解并修改它。在单个模型文件中囊括该模型的所有必要逻辑组件有助于提高可读性和可修改性。处于同样的目的,我们也非常关注变量及方法命名的合理性,我们更喜欢表达力强/可读性强的代码,而不盲目追求短代码。

3. 机器学习正以惊人的速度发展

机器学习领域,尤其是神经网络领域的研究发展非常迅速。一年前最先进的模型今天可能已经过时了。我们甚至不知道明年会流行哪一种注意力机制、位置嵌入或架构。因此,我们无法定义适用于所有模型的标准模板。

例如,两年前,人们可能将 BERT 的自注意力层定义为所有 transformer 模型的标准注意力层。从逻辑上讲,“标准”注意力函数可以移到一个集中性的 attention.py 文件中。但是随后出现了在每层中添加相对位置嵌入的注意力层 (如 T5),多种不同形式的分块注意力层 (Reformer,Longformer,BigBird),以及将位置嵌入和词嵌入分离的注意力机制 (DeBERTa) …… 每当发生这类事情时,我们都不得不问自己是否应该调整“标准”注意力函数,还是说向 attention.py 添加一个新的注意力函数更好。但如果要添加新的注意力函数,我们该如何命名呢? attention_with_positional_embdreformer_attention 还有 deberta_attention

给机器学习模型的组件起通用的名字是危险的,因为关于名字意义的解释可能会很快改变或过时。例如,分块注意力指的是 GPTNeo 的分块注意力,还是 Reformer 的分块注意力,抑或是 BigBird 的分块注意力?注意层是自注意层、交叉注意层,还是两者都包含?如果我们最终决定用模型名称来命名注意力层,我们何不直接把这个注意力函数放在相应的模型文件中?

4. 机器学习模型是静态的

Transformers 库是不同研究团队创建的统一且完善的机器学习模型的集合。每个机器学习模型通常都对应一篇论文及其官方 GitHub 存储库。机器学习模型一旦发布,后面就很少会对其进行调整或更改。

相反,研究团队倾向于发布基于之前模型构建的新模型,而很少对已发布的代码进行重大更改。在决定 transformers 库的设计原则时,这是一个重要的认知。这意味着一旦将模型架构添加到 transformers 中,模型的基本组件就不会再改变。有可能会发现并修复一些错误,有可能会重命名方法或变量,也有可能对模型的输出或输入格式进行微调,但一般不会改动模型的核心组件。因此,对 transformers 中的所有模型进行大的全局性改动的需求大大减少,这使得每个逻辑模块只存在一次这件事情变得不那么重要,因为我们很少改动它。

第二个认知是模型之间 存在双向依赖。新发布的模型可能依赖于现存模型,但很明显,现存模型在逻辑上并不依赖于其前面的模型。例如,T5 部分建立在 BERT 之上,因此 T5 的模型代码在逻辑上可能依赖于 BERT 的模型代码,但 BERT 在逻辑上绝不可能依赖于 T5。因此,重构 BERT 的注意力功能以使其满足 T5 的要求这件事在逻辑上不合理 —— 阅读 BERT 的注意力层代码的人不需要对 T5 有任何了解。同样,这也促使我们不要将注意力层等组件集中到所有模型都可以访问的公共模块中。

另一方面,新模型的代码在逻辑上可能对其前面的模型有一定的依赖性。例如,DeBERTa-v2 的代码确实在某种程度上依赖于 DeBERTa 的代码。通过确保 DeBERTa-v2 的模型代码与 DeBERTa 的保持同步,可以显著提高可维护性。理论上来讲,修复 DeBERTa 中的 bug 的同时也应该修复 DeBERTa-v2 中的相同 bug。我们如何在确保新模型与其依赖的模型保持同步的同时维持 单模型文件 策略?

现在,我们解释一下为什么我们在 “重复自己” 之后加上星号$ {}^{\textbf{*}} $。我们不会无脑复制粘贴现有模型的相应代码,即使看上去我们好像就是这么做的。 Transformers 的核心维护者之一 Sylvain Gugger 发现了一种既尊重 单文件策略 又将可维护性成本控制在一定范围内的好机制。该机制,我们暂且称其为 “复制机制” ,允许我们使用 #Copied from <predecessor_model>.<function> 语句标记某些逻辑组件 (如注意力层函数),从而强制被标记的当前代码与 <predecessor_model><function> 相同。例如,DeBERTa-v2 类 里的这行代码强制整个 DebertaV2Layer 类除了类名前缀 DeBERTav2 之外须与 DebertaLayer 类 相同。如此可以看到,复制机制使模型代码非常容易理解,同时又显著减少了维护成本。如果有人改动了某个模型的某个函数,则我们可以使用一个自动化工具来更正依赖于这个模型的这个函数的所有其他模型的相应代码。

缺点

显然,单文件策略也有缺点,我们在这里简单提两个。

Transformers 的一个主要目标是为所有模型的推理和训练提供统一的 API,以便用户可以在不同模型之间快速切换。但是,如果不允许模型文件使用抽象这一设计模式,则确保跨模型的统一 API 会困难得多。我们通过运行 大量 测试 (截至本文撰写时,每天需要运行大约 2 万次测试) 来解决这个问题,以确保模型遵循一致的 API。在这种情况下,单文件策略要求我们在评审新模型和新测例时非常严格。

其次,有很多研究仅针对机器学习模型的单个组件。 例如 ,有研究团队会致力于研究一种适用于所有现有预训练模型的注意力机制的新形式,如 Rethinking Attention with Performers 一文所做的。我们应该如何将此类研究纳入 transformers 库?确实不好弄。我们应该改变所有现有模型吗?这将违背上文中的第 3 点和第 4 点。还是我们应该添加 100 多个新的模型文件,每个文件都以 Performer... 为前缀?这也很荒谬。遗憾的是,对此类情况我们还没有好的解决方案,我们只能选择不将该论文的成果集成到 transformers 中。等这篇论文获得更多关注并有了性能强大的预训练 checkpoint,我们可能会为其中最重要的模型添加一个新的模型文件,例如目前我们已有 modeling_performer_bert.py

总结

总而言之,在 🤗 Hugging Face,我们坚信 单文件策略 是适合 transformers 的代码设计理念。

你的想法如何?我们很想听听你的意见!如果你有话要说,欢迎到这个 帖子 下留言。