重复你自己*

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

🤗 Transformers 设计理念

“别重复你自己”"Don't repeat yourself"),或称 DRY,是软件开发中一个众所周知的原则。该原则源自于代码设计领域最受欢迎的书籍之一《程序员修炼之道》。这个原则传达的信息简单明了:不要重写已经存在于别处的逻辑。这能确保代码保持同步,使其更易于维护且更加健壮。对该逻辑模式的任何更改都将统一影响其所有依赖项。

乍一看,Hugging Face Transformers 库的设计似乎与 DRY 原则背道而驰。注意力机制的代码被或多或少地复制了 50 多次到不同的模型文件中。有时,整个 BERT 模型的代码也会被复制到其他模型文件中。我们经常强制要求新的模型贡献,即使它们与现有模型几乎完全相同——除了一个微小的逻辑调整——也要复制所有现有代码。我们为什么要这样做?难道我们只是太懒或忙不过来,无法将所有逻辑部分集中到一个地方吗?

不,我们并非懒惰——不将 DRY 设计原则应用于 Transformers 库是一个非常审慎的决定。相反,我们决定采用一种不同的设计原则,我们称之为 单一模型文件 策略。单一模型文件 策略规定,模型前向传播所需的所有代码都位于且仅位于一个文件中——称为模型文件。如果读者想了解 BERT 在推理时如何工作,她应该只需要查看 BERT 的 modeling_bert.py 文件。我们通常会拒绝任何将不同模型的相同子组件抽象到一个新的中心化位置的尝试。我们不希望有一个包含所有可能注意力机制的 attention_layer.py 文件。那么,我们为什么要这样做呢?

简而言之,原因是:

  • 1. Transformers 是由开源社区构建,并为开源社区服务的。
  • 2. 我们的产品是模型,我们的客户是阅读或调整模型代码的用户。
  • 3. 机器学习领域的发展速度极快。
  • 4. 机器学习模型是静态的。

1. 由开源社区构建,并为开源社区服务

Transformers 的构建旨在积极鼓励外部贡献。贡献通常是 bug 修复或新模型贡献。如果在某个模型文件中发现了一个 bug,我们希望让发现者尽可能容易地修复它。没有什么比修复一个 bug 后发现它导致了其他 100 个模型的失败更令人沮丧的了。

因为模型代码独立于所有其他模型,所以对于只了解自己正在使用的那个模型的人来说,修复它相当容易。同样,如果只需添加一个新模型文件,添加新的建模代码和审查相应的 PR 也会更容易。贡献者不必费心去思考如何在不破坏现有模型的情况下向中心化的注意力机制添加新功能。审查者可以轻松验证所有现有模型都没有被破坏。

2. 建模代码是我们的产品

我们假设,Transformers 库的大量用户不仅会阅读文档,还会查看实际的建模代码并可能对其进行修改。这一假设得到了 Transformers 库被 fork 超过 10,000 次以及 Transformers 论文被引用超过一千次的佐证。因此,让首次阅读 Transformers 建模代码的人能够轻松理解并可能进行修改至关重要。在一个单一的建模文件中按顺序提供所有必要的逻辑组件,极大地有助于提高可读性和适应性。此外,我们非常注重合理的变量/方法命名,并倾向于使用表达力强/可读性高的代码,而非字符高效的代码。

3. 机器学习正以前所未有的速度发展

机器学习领域,尤其是神经网络的研究,发展速度极快。一年前还是最先进的模型,今天可能就已经过时了。我们不知道一年后哪种注意力机制、位置嵌入或架构会是最好的。因此,我们无法定义适用于所有模型的标准逻辑模式。

举个例子,两年前,人们可能会将 BERT 的自注意力层定义为所有 Transformers 模型使用的标准注意力层。从逻辑上讲,一个“标准”的注意力函数可以被移到一个中心的 attention.py 文件中。但随后出现了在每个注意力层中添加相对位置嵌入的注意力层(T5),多种不同形式的分块注意力(Reformer, Longformer, BigBird),以及用于位置和词嵌入的独立注意力机制(DeBERTa),等等……每一次我们都不得不问自己,是否应该调整“标准”的注意力函数,或者是否最好在 attention.py 中添加一个新的注意力函数。但那时我们该如何命名呢?attention_with_positional_embd, reformer_attention, deberta_attention

给机器学习模型的逻辑组件起通用名称是危险的,因为人们对这个组件所代表的含义的看法可能会很快改变或过时。例如,分块注意力是指 GPTNeo、Reformer 还是 BigBird 的分块注意力?注意力层是自注意力层、交叉注意力层,还是两者都包括?然而,如果我们用模型的名称来命名注意力层,我们就应该直接将注意力函数放在相应的建模文件中。

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

Transformers 库是不同研究团队创建的机器学习模型的统一且精炼的集合。每个机器学习模型通常都附有一篇论文及其官方 GitHub 仓库。一旦一个机器学习模型被发布,之后就很少再被修改或改变。

相反,研究团队倾向于在先前模型的基础上发布新模型,但很少对已发布的代码进行重大更改。这是在决定 Transformers 库的设计原则时的一个重要认识。这意味着一旦一个模型架构被添加到 Transformers,模型的基本组件就不会再改变了。Bug 经常被发现和修复,方法和变量可能会被重命名,模型的输出或输入格式可能会有轻微改变,但模型的核心组件不会再改变。因此,对 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 类 的这一行代码强制整个类与 DeBERTa 的类 相同,除了前缀 DeBERTav2。通过这种方式,复制机制使得建模代码非常容易理解,同时显著降低了维护成本。如果一个前身模型的函数中的某些代码被更改,而其后继模型的函数引用了它,那么会有工具自动修正后继模型的函数。

缺点

显然,单一文件策略也有其缺点,我们想在这里快速提及其中两个。

Transformers 的一个主要目标是为所有模型提供统一的推理和训练 API,以便用户可以在其设置中快速切换不同的模型。然而,如果建模文件不允许使用抽象的逻辑模式,那么确保跨模型 API 的统一性就会困难得多。我们通过运行 大量 测试(在撰写这篇博文时,每天大约运行 20,000 个测试)来解决这个问题,以确保模型遵循一致的 API。在这种情况下,单一文件策略要求我们在审查模型和测试添加时非常严格。

其次,有很多研究仅仅针对机器学习模型的一个组件。例如,研究团队会研究新形式的注意力机制,这些机制可以应用于所有现有的预训练模型,就像在 《用 Performers 重新思考注意力》 中所做的那样。我们应该如何将这类研究整合到 Transformers 库中呢?这确实是个问题。我们应该修改所有现有的模型吗?这与上面写的第 3 点和第 4 点相悖。我们应该添加 100 多个以 Performer... 为前缀的新建模文件吗?这似乎很荒谬。在这种情况下,遗憾的是没有好的解决方案,我们选择不将该论文整合到 Transformers 中。如果该论文获得了更大的关注度并包含了强大的预训练检查点,我们可能会添加一些最重要模型的新建模文件,例如 modeling_performer_bert.py

结论

总而言之,在 🤗 Hugging Face,我们坚信 单一文件策略 是 Transformers 正确的编码理念。

您怎么看?如果您读到了这里,我们非常想听听您的意见!如果您想留言,请访问相应的论坛帖子 这里

社区

注册登录 发表评论