Transformers 文档

如何将模型添加到🤗 Transformers?

如何将模型添加到🤗 Transformers?

🤗 Transformers 库通常能够通过社区贡献者提供新模型。但这可能是一个具有挑战性的项目,需要对 🤗 Transformers 库和要实现的模型有深入的了解。在 Hugging Face,我们正在努力让更多的社区成员能够积极添加模型,并为此编写了本指南,以引导您完成添加 PyTorch 模型的过程(请确保您已安装 PyTorch)。

在此过程中,您将:

  • 深入了解开源最佳实践
  • 理解最流行的深度学习库之一背后的设计原则
  • 学习如何高效测试大型模型
  • 学习如何集成Python工具,如blackruffmake fix-copies,以确保代码的整洁和可读性

Hugging Face 团队成员将随时为您提供帮助,因此您永远不会孤单。🤗 ❤️

要开始使用,请为您希望在🤗 Transformers中看到的模型打开一个新模型添加问题。如果您对贡献特定模型没有特别的要求,可以通过新模型标签进行筛选,查看是否有任何未认领的模型请求并开始工作。

一旦你打开了一个新的模型请求,第一步就是熟悉🤗 Transformers,如果你还没有熟悉的话!

🤗 Transformers 的概述

首先,你应该对🤗 Transformers有一个大致的了解。🤗 Transformers是一个非常固执己见的库,因此有可能你不同意该库的一些哲学或设计选择。然而,根据我们的经验,我们发现该库的基本设计选择和哲学对于高效扩展🤗 Transformers至关重要,同时将维护成本保持在合理水平。

为了更好地理解这个库,一个好的起点是阅读我们的理念文档。由于我们的工作方式,我们尝试将一些选择应用于所有模型:

  • 组合通常优于抽象
  • 如果重复代码能显著提高模型的可读性或可访问性,那么它并不总是坏事
  • 模型文件尽可能自包含,这样当你阅读特定模型的代码时,理想情况下你只需要查看相应的 modeling_....py 文件。

我们认为,库的代码不仅仅是提供产品的手段,例如使用BERT进行推理的能力,而且也是我们想要改进的产品本身。因此,在添加模型时,用户不仅是使用你模型的人,还包括所有阅读、尝试理解并可能调整你代码的人。

考虑到这一点,让我们更深入地了解一般库的设计。

模型概述

要成功添加模型,重要的是要理解模型与其配置之间的交互, PreTrainedModel,以及PretrainedConfig。为了示例目的,我们将 将要添加到🤗 Transformers的模型称为BrandNewBert

让我们来看一下:

正如你所见,我们在🤗 Transformers中确实使用了继承,但我们将抽象层次保持在绝对最低水平。库中的任何模型都不会超过两个抽象层次。BrandNewBertModel继承自BrandNewBertPreTrainedModel,而后者又继承自PreTrainedModel,仅此而已。作为一般规则,我们希望确保新模型仅依赖于PreTrainedModel。自动提供给每个新模型的重要功能包括from_pretrained()save_pretrained(),这些功能用于序列化和反序列化。所有其他重要功能,例如BrandNewBertModel.forward,应完全在新的modeling_brand_new_bert.py脚本中定义。接下来,我们希望确保具有特定头层的模型,例如BrandNewBertForMaskedLM,不继承自BrandNewBertModel,而是使用BrandNewBertModel作为可以在其前向传递中调用的组件,以保持较低的抽象层次。每个新模型都需要一个配置类,称为BrandNewBertConfig。此配置始终作为属性存储在PreTrainedModel中,因此可以通过config属性访问所有继承自BrandNewBertPreTrainedModel的类:

model = BrandNewBertModel.from_pretrained("brandy/brand_new_bert")
model.config  # model has access to its config

与模型类似,配置继承了基本的序列化和反序列化功能,这些功能来自 PretrainedConfig。请注意,配置和模型总是被序列化为两种 不同的格式——模型序列化为pytorch_model.bin文件,配置序列化为config.json文件。调用 模型的save_pretrained()将自动调用 配置的save_pretrained(),以便同时保存模型和配置。

代码风格

在编写新模型时,请记住Transformers是一个有主见的库,我们对于代码应该如何编写有一些自己的独特之处 :-)

  1. 你的模型的前向传播应该完全写在建模文件中,同时完全独立于库中的其他模型。如果你想从另一个模型中重用某个块,复制代码并在顶部粘贴一个# Copied from注释(参见这里以获取一个很好的示例,以及那里以获取更多关于Copied from的文档)。
  2. 代码应该完全可理解,即使对于非母语为英语的人也是如此。这意味着你应该选择描述性的变量名并避免缩写。例如,activationact 更可取。除非是 for 循环中的索引,否则强烈不建议使用单字母变量名。
  3. 更普遍地,我们更喜欢长而明确的代码,而不是短而神奇的代码。
  4. 避免在PyTorch中子类化nn.Sequential,而是子类化nn.Module并编写前向传递,这样任何使用你代码的人都可以通过添加打印语句或断点来快速调试。
  5. 您的函数签名应进行类型注释。对于其余部分,良好的变量名称比类型注释更具可读性和可理解性。

分词器概述

还没准备好 :-( 这个部分很快就会添加!

逐步指南:如何将模型添加到 🤗 Transformers

每个人都有不同的偏好来移植模型,因此查看其他贡献者如何将模型移植到Hugging Face的总结对你非常有帮助。以下是一些社区博客文章的列表,介绍了如何移植模型:

  1. 移植GPT2模型Thomas
  2. 移植WMT19 MT模型Stas

根据经验,我们可以告诉您,在添加模型时最重要的注意事项是:

  • 不要重复造轮子!你为新🤗 Transformers模型添加的大部分代码在🤗 Transformers中已经存在。花些时间找到可以复制的类似、已经存在的模型和分词器。greprg是你的朋友。请注意,你的模型的分词器可能基于一个模型实现,而你的模型的建模代码基于另一个模型实现。例如,FSMT的建模代码基于BART,而FSMT的分词器代码基于XLM。
  • 这更像是一个工程挑战,而不是科学挑战。你应该花更多时间创建一个高效的调试环境,而不是试图理解论文中模型的所有理论方面。
  • 当你遇到困难时,寻求帮助!模型是🤗 Transformers的核心组件,因此我们在Hugging Face非常乐意在每一步帮助你添加你的模型。如果你发现自己没有进展,请不要犹豫,立即寻求帮助。

在以下内容中,我们尝试为您提供一个通用的方法,这是我们在将模型移植到🤗 Transformers时发现最有用的。

以下列表是添加模型所需完成的所有事项的摘要,您可以将其用作待办事项列表:

☐ (可选)理解了模型的理论方面
☐ 准备好了 🤗 Transformers 开发环境
☐ 设置了原始仓库的调试环境
☐ 创建了脚本,成功使用原始仓库和检查点运行了 forward() 传递
☐ 成功将模型骨架添加到 🤗 Transformers
☐ 成功将原始检查点转换为 🤗 Transformers 检查点
☐ 成功在 🤗 Transformers 中运行了 forward() 传递,输出与原始检查点相同
☐ 完成了 🤗 Transformers 中的模型测试
☐ 成功在 🤗 Transformers 中添加了分词器
☐ 运行了端到端集成测试
☐ 完成了文档
☐ 将模型权重上传到 Hub
☐ 提交了拉取请求
☐ (可选)添加了一个演示笔记本

首先,我们通常建议从对BrandNewBert有一个良好的理论理解开始。然而,如果你更喜欢在工作中理解模型的理论方面,那么直接深入BrandNewBert的代码库也是完全可以的。如果你的工程技能比理论技能更强,如果你难以理解BrandNewBert的论文,或者如果你只是更喜欢编程而不是阅读科学论文,那么这个选项可能更适合你。

1. (可选)BrandNewBert的理论方面

你应该花些时间阅读BrandNewBert的论文,如果存在这样的描述性工作。论文中可能有大段内容难以理解。如果是这种情况,没关系——不用担心!目标不是深入理解论文的理论,而是提取必要的信息,以便在🤗 Transformers中有效地重新实现模型。也就是说,你不必在理论方面花费太多时间,而应专注于实际方面,即:

  • brand_new_bert 是什么类型的模型?是类似 BERT 的仅编码器模型?还是类似 GPT2 的仅解码器模型?或者是类似 BART 的编码器-解码器模型?如果你不熟悉这些模型之间的区别,请查看 model_summary
  • brand_new_bert的应用有哪些?文本分类?文本生成?Seq2Seq任务,例如,摘要生成?
  • 该模型的新颖特性是什么,使其与BERT/GPT-2/BART不同?
  • 现有的🤗 Transformers模型中,哪一个与brand_new_bert最相似?
  • 使用了哪种类型的分词器?是句子片段分词器吗?还是词片段分词器?它与BERT或BART使用的分词器相同吗?

在你觉得对模型的架构有了一个很好的概览之后,你可能想向Hugging Face团队提出你可能有的任何问题。这可能包括关于模型架构、其注意力层等问题。我们将非常乐意帮助你。

2. 接下来准备你的环境

  1. 通过点击仓库页面上的‘Fork’按钮来fork仓库。这将在你的GitHub用户账户下创建一个代码副本。

  2. 将你的 transformers 分支克隆到本地磁盘,并将基础仓库添加为远程仓库:

    git clone https://github.com/[your Github handle]/transformers.git
    cd transformers
    git remote add upstream https://github.com/huggingface/transformers.git
  3. 设置一个开发环境,例如通过运行以下命令:

    python -m venv .env
    source .env/bin/activate
    pip install -e ".[dev]"

    根据您的操作系统,以及Transformers的可选依赖项数量不断增加,您可能会在使用此命令时遇到失败。如果发生这种情况,请确保安装您正在使用的深度学习框架(PyTorch、TensorFlow和/或Flax),然后执行以下操作:

    pip install -e ".[quality]"

    这对于大多数用例来说应该足够了。然后您可以返回到父目录

    cd ..
  4. 我们建议将brand_new_bert的PyTorch版本添加到Transformers中。要安装PyTorch,请按照https://pytorch.org/get-started/locally/上的说明进行操作。

    注意: 你不需要安装CUDA。让新模型在CPU上运行就足够了。

  5. 要移植brand_new_bert,您还需要访问其原始仓库:

    git clone https://github.com/org_that_created_brand_new_bert_org/brand_new_bert.git
    cd brand_new_bert
    pip install -e .

现在你已经设置了一个开发环境,将brand_new_bert移植到🤗 Transformers。

3.-4. 使用原始仓库运行预训练检查点

最初,您将在原始的brand_new_bert仓库上工作。通常,原始实现非常“学术化”。这意味着文档可能不足,代码可能难以理解。但这正是您重新实现brand_new_bert的动力所在。在Hugging Face,我们的主要目标之一是让人们站在巨人的肩膀上,这在这里非常适用,即采用一个可工作的模型并重写它,使其尽可能易于访问、用户友好和美观。这是将模型重新实现到🤗 Transformers中的首要动机——试图使复杂的新NLP技术对每个人都易于访问。

你应该从深入研究原始仓库开始。

成功运行原始仓库中的官方预训练模型通常是最困难的一步。 根据我们的经验,花一些时间熟悉原始代码库非常重要。你需要 弄清楚以下几点:

  • 在哪里可以找到预训练的权重?
  • 如何将预训练权重加载到相应的模型中?
  • 如何独立于模型运行分词器?
  • 追踪一次前向传递,以便了解简单前向传递所需的类和函数。通常,你只需要重新实现这些函数。
  • 能够定位模型的重要组件:模型的类在哪里?是否有模型的子类,例如 EncoderModel, DecoderModel?自注意力层在哪里?是否有多个不同的注意力层,例如 自注意力, 交叉注意力…?
  • 如何在仓库的原始环境中调试模型?你是否必须添加print语句,可以使用像ipdb这样的交互式调试器,还是应该使用像PyCharm这样的高效IDE来调试模型?

在开始移植过程之前,能够高效地调试原始仓库中的代码非常重要!此外,请记住你正在使用一个开源库,所以不要犹豫,可以在原始仓库中提出问题,甚至提交拉取请求。这个仓库的维护者很可能会非常高兴有人查看他们的代码!

此时,选择哪种调试环境和策略来调试原始模型完全取决于您。我们强烈建议不要设置昂贵的GPU环境,而是在开始深入研究原始仓库时以及在开始编写模型的🤗 Transformers实现时,仅在CPU上工作。只有在模型已经成功移植到🤗 Transformers之后,才应该验证模型在GPU上是否也能按预期工作。

通常,运行原始模型有两种可能的调试环境

Jupyter笔记本的优势在于它们允许逐单元执行,这有助于更好地将逻辑组件分开,并且由于可以存储中间结果,调试周期更快。此外,笔记本通常更容易与其他贡献者共享,如果您想向Hugging Face团队寻求帮助,这可能非常有用。如果您熟悉Jupyter笔记本,我们强烈建议您使用它们。

Jupyter笔记本的明显缺点是,如果你不习惯使用它们,你将不得不花费一些时间来适应新的编程环境,并且你可能无法再使用你熟悉的调试工具,比如ipdb

对于每个代码库,一个好的第一步通常是加载一个小型预训练检查点,并能够使用一个虚拟的整数向量作为输入ID来重现单个前向传递。这样的脚本可能如下所示(伪代码):

model = BrandNewBertModel.load_pretrained_checkpoint("/path/to/checkpoint/")
input_ids = [0, 4, 5, 2, 3, 7, 9]  # vector of input ids
original_output = model.predict(input_ids)

接下来,关于调试策略,通常有几种可供选择:

  • 将原始模型分解为许多可测试的小组件,并对每个组件进行前向传递以进行验证
  • 将原始模型仅分解为原始的tokenizer和原始的model,对它们运行前向传递,并使用中间打印语句或断点进行验证

再次,选择哪种策略取决于你。通常,根据原始代码库的不同,一种或另一种策略可能更有优势。

如果原始代码库允许你将模型分解为更小的子组件,例如如果原始代码库可以轻松地在急切模式下运行,通常值得付出努力这样做。在开始时选择更困难的道路有一些重要的优势:

  • 在后期将原始模型与Hugging Face实现进行比较时,您可以自动验证每个组件的对应部分是否与🤗 Transformers实现匹配,而不是依赖通过打印语句进行视觉比较
  • 它可以为你提供一些方法,将移植模型的大问题分解为仅移植单个组件的小问题,从而更好地组织你的工作
  • 将模型分离成逻辑上有意义的组件将帮助您更好地了解模型的设计,从而更好地理解模型
  • 在后期,这些逐个组件的测试有助于确保在您继续更改代码时不会发生回归

Lysandre’s 对ELECTRA的集成检查 提供了一个很好的示例,展示了如何做到这一点。

然而,如果原始代码库非常复杂,或者只允许中间组件在编译模式下运行,那么将模型分离成更小的可测试子组件可能会非常耗时,甚至是不可能的。一个很好的例子是T5的MeshTensorFlow库,它非常复杂,并且没有提供简单的方法将模型分解为其子组件。对于这样的库,通常依赖于验证打印语句。

无论你选择哪种策略,推荐的程序通常是相同的,你应该首先调试起始层,最后调试结束层。

建议您按照以下顺序通过打印语句或子组件函数检索以下层的输出:

  1. 检索传递给模型的输入ID
  2. 检索词嵌入
  3. 检索第一个Transformer层的输入
  4. 检索第一个Transformer层的输出
  5. 检索以下 n - 1 个 Transformer 层的输出
  6. 检索整个BrandNewBert模型的输出

输入ID应该由一组整数组成,例如 input_ids = [0, 4, 4, 3, 2, 4, 1, 7, 19]

以下层的输出通常由多维浮点数组组成,可能看起来像这样:

[[
 [-0.1465, -0.6501,  0.1993,  ...,  0.1451,  0.3430,  0.6024],
 [-0.4417, -0.5920,  0.3450,  ..., -0.3062,  0.6182,  0.7132],
 [-0.5009, -0.7122,  0.4548,  ..., -0.3662,  0.6091,  0.7648],
 ...,
 [-0.5613, -0.6332,  0.4324,  ..., -0.3792,  0.7372,  0.9288],
 [-0.5416, -0.6345,  0.4180,  ..., -0.3564,  0.6992,  0.9191],
 [-0.5334, -0.6403,  0.4271,  ..., -0.3339,  0.6533,  0.8694]]],

我们期望添加到🤗 Transformers的每个模型都通过一些集成测试,这意味着原始模型和🤗 Transformers中的重新实现版本必须在0.001的精度内给出完全相同的输出!由于用不同库编写的完全相同的模型可能会根据库框架给出略有不同的输出,我们接受1e-3(0.001)的误差容忍度。如果模型给出几乎相同的输出是不够的,它们必须几乎完全相同。因此,你肯定会多次比较🤗 Transformers版本的中间输出与brand_new_bert原始实现的中间输出,在这种情况下,原始仓库的高效调试环境绝对重要。以下是一些建议,以使你的调试环境尽可能高效。

  • 找到调试中间结果的最佳方法。原始仓库是用PyTorch编写的吗?那么你可能需要花时间编写一个更长的脚本,将原始模型分解为更小的子组件以检索中间值。原始仓库是用TensorFlow 1编写的吗?那么你可能需要依赖TensorFlow的打印操作,如tf.print来输出中间值。原始仓库是用Jax编写的吗?那么在运行前向传播时,确保模型没有被jit编译例如查看此链接
  • 使用你能找到的最小的预训练检查点。检查点越小,你的调试周期就越快。如果你的预训练模型太大,以至于前向传递需要超过10秒,那么这是不高效的。如果只有非常大的检查点可用,可能更有意义的是在新环境中创建一个带有随机初始化权重的虚拟模型,并保存这些权重以便与🤗 Transformers版本的模型进行比较。
  • 确保你在原始仓库中使用最简单的方式调用前向传播。理想情况下,你希望在原始仓库中找到调用一次前向传播的函数,例如通常称为predictevaluateforward__call__的函数。你不希望调试一个多次调用forward的函数,例如生成文本的函数,如autoregressive_samplegenerate
  • 尝试将分词与模型的前向传递分开。如果原始仓库展示了需要输入字符串的示例,那么尝试找出在前向调用中字符串输入被更改为输入ID的位置,并从此点开始。这可能意味着您可能需要自己编写一个小脚本或更改原始代码,以便可以直接输入ID而不是输入字符串。
  • 确保在调试设置中的模型处于训练模式,这通常会导致模型由于多个dropout层而产生随机输出。确保在调试环境中的前向传递是确定性的,以便不使用dropout层。或者,如果旧的和新的实现在同一个框架中,使用transformers.utils.set_seed

以下部分为您提供了关于如何为brand_new_bert执行此操作的更具体细节/提示。

5.-14. 将 BrandNewBert 移植到 🤗 Transformers

接下来,你终于可以开始向🤗 Transformers添加新代码了。进入你🤗 Transformers分叉的克隆:

cd transformers

在特殊情况下,如果您要添加的模型的架构与现有模型的架构完全匹配,您只需按照本节所述添加一个转换脚本。在这种情况下,您可以重复使用现有模型的整个架构。

否则,让我们开始生成一个新模型。我们建议使用以下脚本从现有模型开始添加模型:

transformers-cli add-new-model-like

您将被提示填写一份问卷,以填写模型的基本信息。

在主 huggingface/transformers 仓库上打开一个 Pull Request

在开始调整自动生成的代码之前,现在是时候打开一个“进行中的工作(WIP)”拉取请求,例如“[WIP] 添加 brand_new_bert”,在 🤗 Transformers 中,这样你和 Hugging Face 团队可以一起工作,将模型集成到 🤗 Transformers 中。

你应该执行以下操作:

  1. 从主分支创建一个具有描述性名称的分支

    git checkout -b add_brand_new_bert
  2. 提交自动生成的代码:

    git add .
    git commit
  3. 获取并重新基于当前主分支

    git fetch upstream
    git rebase upstream/main
  4. 使用以下命令将更改推送到您的账户:

    git push -u origin a-descriptive-name-for-my-changes
  5. 一旦您满意,请转到GitHub上您的fork的网页。点击“Pull request”。确保添加Hugging Face团队的一些成员的GitHub句柄作为审阅者,以便Hugging Face团队能够收到未来更改的通知。

  6. 通过点击GitHub拉取请求网页右侧的“转换为草稿”将PR更改为草稿。

在以下过程中,每当你取得一些进展时,不要忘记提交你的工作并将其推送到你的账户,以便在拉取请求中显示。此外,你应该确保通过以下方式不时地更新你的工作与当前的主分支保持一致:

git fetch upstream
git merge upstream/main

一般来说,所有关于模型或你实现的问题都应该在你的PR中提出,并在PR中讨论/解决。这样,当你提交新代码或有问题时,Hugging Face团队总能收到通知。通常,向Hugging Face团队指出你添加的代码是非常有帮助的,这样Hugging Face团队可以高效地理解你的问题或疑问。

为此,您可以转到“已更改的文件”选项卡,查看所有更改,找到您想提问的相关行,然后点击“+”符号添加评论。每当问题或问题得到解决时,您可以点击创建的评论的“解决”按钮。

同样地,Hugging Face 团队在审查您的代码时会开放评论。我们建议您在 GitHub 上的 PR 中提出大多数问题。对于一些对公众不太有用的非常一般的问题,请随时通过 Slack 或电子邮件联系 Hugging Face 团队。

5. 为brand_new_bert调整生成的模型代码

首先,我们将只关注模型本身,而不关心分词器。所有相关的代码应该可以在生成的文件 src/transformers/models/brand_new_bert/modeling_brand_new_bert.pysrc/transformers/models/brand_new_bert/configuration_brand_new_bert.py 中找到。

现在你终于可以开始编码了 :)。生成的代码在 src/transformers/models/brand_new_bert/modeling_brand_new_bert.py 中,如果它是一个仅编码器模型,它将具有与BERT相同的架构,如果它是一个编码器-解码器模型,它将具有与BART相同的架构。此时,你应该提醒自己一开始学到的关于模型理论方面的知识:这个模型与BERT或BART有什么不同?”。实现这些变化通常意味着改变自注意力层、归一化层的顺序等……再次强调,查看Transformers中已有模型的类似架构通常有助于更好地理解如何实现你的模型。

注意,在这一点上,你不必非常确定你的代码是完全正确或干净的。相反,建议先添加一个不干净的、复制粘贴的原始代码版本到src/transformers/models/brand_new_bert/modeling_brand_new_bert.py,直到你觉得所有必要的代码都已添加。根据我们的经验,快速添加所需代码的第一个版本,并通过转换脚本逐步改进/修正代码要高效得多。此时唯一需要确保的是你可以实例化🤗 Transformers的brand_new_bert实现,以下命令应该可以工作:

from transformers import BrandNewBertModel, BrandNewBertConfig

model = BrandNewBertModel(BrandNewBertConfig())

上述命令将根据BrandNewBertConfig()中定义的默认参数创建一个模型,并使用随机权重,从而确保所有组件的init()方法正常工作。

请注意,所有的随机初始化都应该在您的BrandnewBertPreTrainedModel类的_init_weights方法中进行。它应该根据配置的变量初始化所有叶子模块。以下是BERT的_init_weights方法的示例:

def _init_weights(self, module):
    """Initialize the weights"""
    if isinstance(module, nn.Linear):
        module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
        if module.bias is not None:
            module.bias.data.zero_()
    elif isinstance(module, nn.Embedding):
        module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
        if module.padding_idx is not None:
            module.weight.data[module.padding_idx].zero_()
    elif isinstance(module, nn.LayerNorm):
        module.bias.data.zero_()
        module.weight.data.fill_(1.0)

如果你需要为某些模块进行特殊初始化,你可以有更多的自定义方案。例如,在Wav2Vec2ForPreTraining中,最后两个线性层需要使用常规的PyTorch nn.Linear初始化,但所有其他层应使用上述的初始化。代码如下:

def _init_weights(self, module):
    """Initialize the weights"""
    if isinstance(module, Wav2Vec2ForPreTraining):
        module.project_hid.reset_parameters()
        module.project_q.reset_parameters()
        module.project_hid._is_hf_initialized = True
        module.project_q._is_hf_initialized = True
    elif isinstance(module, nn.Linear):
        module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
        if module.bias is not None:
            module.bias.data.zero_()

_is_hf_initialized 标志在内部用于确保我们只初始化子模块一次。通过将其设置为 True 对于 module.project_qmodule.project_hid,我们确保我们进行的自定义初始化不会在以后被覆盖, _init_weights 函数将不会应用于它们。

6. 编写一个转换脚本

接下来,你应该编写一个转换脚本,让你能够将用于调试brand_new_bert的检查点从原始仓库转换为与你刚刚创建的🤗 Transformers实现兼容的检查点。不建议从头开始编写转换脚本,而是建议查看🤗 Transformers中已经存在的转换脚本,寻找一个用于转换与brand_new_bert使用相同框架编写的类似模型的脚本。通常,复制一个已经存在的转换脚本并稍作调整以适应你的用例就足够了。不要犹豫,向Hugging Face团队寻求帮助,让他们为你指向一个类似的已经存在的转换脚本。

  • 如果您正在将模型从TensorFlow移植到PyTorch,一个好的起点可能是BERT的转换脚本这里
  • 如果您正在将模型从PyTorch移植到PyTorch,一个好的起点可能是BART的转换脚本 here

接下来,我们将快速解释PyTorch模型如何存储层权重并定义层名称。在PyTorch中,层的名称由您赋予该层的类属性名称定义。让我们在PyTorch中定义一个名为SimpleModel的虚拟模型,如下所示:

from torch import nn


class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.dense = nn.Linear(10, 10)
        self.intermediate = nn.Linear(10, 10)
        self.layer_norm = nn.LayerNorm(10)

现在我们可以创建这个模型定义的实例,它将填充所有权重:denseintermediatelayer_norm 使用随机权重。我们可以打印模型以查看其架构

model = SimpleModel()

print(model)

这将打印出以下内容:

SimpleModel(
  (dense): Linear(in_features=10, out_features=10, bias=True)
  (intermediate): Linear(in_features=10, out_features=10, bias=True)
  (layer_norm): LayerNorm((10,), eps=1e-05, elementwise_affine=True)
)

我们可以看到,层的名称是由PyTorch中类属性的名称定义的。你可以打印出特定层的权重值:

print(model.dense.weight.data)

查看权重是否被随机初始化

tensor([[-0.0818,  0.2207, -0.0749, -0.0030,  0.0045, -0.1569, -0.1598,  0.0212,
         -0.2077,  0.2157],
        [ 0.1044,  0.0201,  0.0990,  0.2482,  0.3116,  0.2509,  0.2866, -0.2190,
          0.2166, -0.0212],
        [-0.2000,  0.1107, -0.1999, -0.3119,  0.1559,  0.0993,  0.1776, -0.1950,
         -0.1023, -0.0447],
        [-0.0888, -0.1092,  0.2281,  0.0336,  0.1817, -0.0115,  0.2096,  0.1415,
         -0.1876, -0.2467],
        [ 0.2208, -0.2352, -0.1426, -0.2636, -0.2889, -0.2061, -0.2849, -0.0465,
          0.2577,  0.0402],
        [ 0.1502,  0.2465,  0.2566,  0.0693,  0.2352, -0.0530,  0.1859, -0.0604,
          0.2132,  0.1680],
        [ 0.1733, -0.2407, -0.1721,  0.1484,  0.0358, -0.0633, -0.0721, -0.0090,
          0.2707, -0.2509],
        [-0.1173,  0.1561,  0.2945,  0.0595, -0.1996,  0.2988, -0.0802,  0.0407,
          0.1829, -0.1568],
        [-0.1164, -0.2228, -0.0403,  0.0428,  0.1339,  0.0047,  0.1967,  0.2923,
          0.0333, -0.0536],
        [-0.1492, -0.1616,  0.1057,  0.1950, -0.2807, -0.2710, -0.1586,  0.0739,
          0.2220,  0.2358]]).

在转换脚本中,您应该用检查点中相应层的精确权重填充那些随机初始化的权重。例如

# retrieve matching layer weights, e.g. by
# recursive algorithm
layer_name = "dense"
pretrained_weight = array_of_dense_layer

model_pointer = getattr(model, "dense")

model_pointer.weight.data = torch.from_numpy(pretrained_weight)

在此过程中,您必须验证您的PyTorch模型中每个随机初始化的权重及其对应的预训练检查点权重在形状和名称上完全匹配。为此,有必要为形状添加断言语句并打印出检查点权重的名称。例如,您应该添加如下语句:

assert (
    model_pointer.weight.shape == pretrained_weight.shape
), f"Pointer shape of random weight {model_pointer.shape} and array shape of checkpoint weight {pretrained_weight.shape} mismatched"

此外,您还应该打印出两个权重的名称以确保它们匹配,例如

logger.info(f"Initialize PyTorch weight {layer_name} from {pretrained_weight.name}")

如果形状或名称不匹配,您可能将错误的检查点权重分配给了🤗 Transformers实现中随机初始化的层。

形状不正确很可能是由于BrandNewBertConfig()中的配置参数设置不正确,这些参数与您要转换的检查点所使用的参数不完全匹配。然而,也可能是PyTorch对某一层的实现要求事先对权重进行转置。

最后,您还应该检查所有必需的权重是否已初始化,并打印出所有未用于初始化的检查点权重,以确保模型正确转换。转换尝试因形状声明错误或名称分配错误而失败是完全正常的。这很可能是因为您在BrandNewBertConfig()中使用了错误的参数,🤗 Transformers实现中的架构有误,🤗 Transformers实现中某个组件的init()函数中存在错误,或者您需要转置其中一个检查点权重。

此步骤应与前一步骤迭代,直到检查点的所有权重都正确加载到Transformers模型中。将检查点正确加载到🤗 Transformers实现后,您可以将模型保存到您选择的文件夹/path/to/converted/checkpoint/folder中,该文件夹应包含pytorch_model.bin文件和config.json文件:

model.save_pretrained("/path/to/converted/checkpoint/folder")

7. 实现前向传播

成功将预训练权重加载到🤗 Transformers实现中后,您现在应该确保前向传播正确实现。在熟悉原始仓库中,您已经创建了一个脚本,使用原始仓库运行模型的前向传播。现在您应该编写一个类似的脚本,使用🤗 Transformers实现而不是原始实现。它应该如下所示:

model = BrandNewBertModel.from_pretrained("/path/to/converted/checkpoint/folder")
input_ids = [0, 4, 4, 3, 2, 4, 1, 7, 19]
output = model(input_ids).last_hidden_states

很可能🤗 Transformers的实现和原始模型实现不会在第一次就给出完全相同的输出,或者前向传递会抛出错误。不要失望——这是预料之中的!首先,你应该确保前向传递不会抛出任何错误。经常发生的情况是使用了错误的维度导致维度不匹配错误,或者使用了错误的数据类型对象,例如 torch.long而不是torch.float32。如果你无法解决某些错误,不要犹豫,向Hugging Face团队寻求帮助。

确保🤗 Transformers实现正确工作的最后一部分是确保输出在1e-3的精度下是等效的。首先,您应该确保输出形状是相同的, outputs.shape 应该为🤗 Transformers实现的脚本和原始实现产生相同的值。接下来,您应该确保输出值也是相同的。这是添加新模型时最困难的部分之一。输出不相同的常见错误原因包括:

  • 一些层没有被添加,例如一个激活层没有被添加,或者残差连接被遗忘了
  • 词嵌入矩阵未绑定
  • 使用了错误的位置嵌入,因为原始实现使用了偏移量
  • 在前向传播过程中应用了Dropout。要解决这个问题,请确保model.training为False,并且在前向传播过程中没有错误地激活任何Dropout层,self.training传递给PyTorch的功能性Dropout

解决问题的最佳方法通常是并排查看原始实现的前向传播和🤗 Transformers实现的前向传播,并检查是否存在任何差异。理想情况下,您应该调试/打印出两个实现的前向传播的中间输出,以找到网络中🤗 Transformers实现显示与原始实现不同输出的确切位置。首先,确保两个脚本中的硬编码input_ids是相同的。接下来,验证input_ids的第一次转换(通常是词嵌入)的输出是否相同。然后逐步检查到网络的最后一层。在某个时刻,您会注意到两个实现之间的差异,这应该指向🤗 Transformers实现中的错误。根据我们的经验,一个简单而有效的方法是在原始实现和🤗 Transformers实现中分别添加许多打印语句,在网络中的相同位置,并逐步删除显示中间表示相同值的打印语句。

当你确信两个实现产生相同的输出时,使用torch.allclose(original_output, output, atol=1e-3)验证输出,你已经完成了最困难的部分!恭喜 - 剩下的工作应该轻而易举 😊。

8. 添加所有必要的模型测试

此时,您已成功添加了一个新模型。然而,该模型可能尚未完全符合所需的设计。为了确保实现与🤗 Transformers完全兼容,所有常见测试都应通过。Cookiecutter应该已经自动为您的模型添加了一个测试文件,可能位于tests/models/brand_new_bert/test_modeling_brand_new_bert.py下。运行此测试文件以验证所有常见测试是否通过:

pytest tests/models/brand_new_bert/test_modeling_brand_new_bert.py

在修复了所有常见测试之后,现在确保你所做的所有优秀工作都得到了充分测试是至关重要的,这样

  • a) 社区可以通过查看brand_new_bert的具体测试轻松理解您的工作
  • b) 未来对模型的更改不会破坏模型的任何重要功能。

首先,应该添加集成测试。这些集成测试基本上与您之前用于实现模型到🤗 Transformers的调试脚本相同。这些模型测试的模板已经由Cookiecutter添加,称为BrandNewBertModelIntegrationTests,只需要由您填写。为了确保这些测试通过,请运行

RUN_SLOW=1 pytest -sv tests/models/brand_new_bert/test_modeling_brand_new_bert.py::BrandNewBertModelIntegrationTests

如果您使用的是Windows,您应该将RUN_SLOW=1替换为SET RUN_SLOW=1

其次,所有brand_new_bert特有的功能应在BrandNewBertModelTester/BrandNewBertModelTest下的单独测试中进行额外测试。这部分经常被遗忘,但在两个方面非常有用:

  • 它通过展示brand_new_bert的特殊功能应该如何工作,帮助将你在模型添加过程中获得的知识传递给社区。
  • 未来的贡献者可以通过运行这些特殊测试快速测试模型的更改。

9. 实现分词器

接下来,我们应该添加brand_new_bert的分词器。通常,分词器等同于或非常类似于🤗 Transformers中已经存在的分词器。

找到/提取原始的分词器文件并将其加载到🤗 Transformers的分词器实现中非常重要。

为了确保分词器正常工作,建议首先在原始仓库中创建一个脚本,该脚本输入一个字符串并返回input_ids。它可能看起来像这样(伪代码):

input_str = "This is a long example input string containing special characters .$?-, numbers 2872 234 12 and words."
model = BrandNewBertModel.load_pretrained_checkpoint("/path/to/checkpoint/")
input_ids = model.tokenize(input_str)

你可能需要再次深入查看原始仓库以找到正确的分词器函数,甚至可能需要对原始仓库的克隆进行更改,以便仅输出input_ids。在编写了一个使用原始仓库的功能性分词脚本后,应该创建一个类似的🤗 Transformers脚本。它应该看起来像这样:

from transformers import BrandNewBertTokenizer

input_str = "This is a long example input string containing special characters .$?-, numbers 2872 234 12 and words."

tokenizer = BrandNewBertTokenizer.from_pretrained("/path/to/tokenizer/folder/")

input_ids = tokenizer(input_str).input_ids

input_ids产生相同的值时,作为最后一步,还应添加一个分词器测试文件。

类似于brand_new_bert的建模测试文件,brand_new_bert的分词测试文件应包含一些硬编码的集成测试。

10. 运行端到端集成测试

添加了分词器后,您还应该在🤗 Transformers中添加一些端到端的集成测试,使用模型和分词器在tests/models/brand_new_bert/test_modeling_brand_new_bert.py中进行测试。这样的测试应该在一个有意义的文本到文本样本上展示🤗 Transformers实现是否按预期工作。有意义的文本到文本样本可以包括例如源到目标翻译对、文章到摘要对、问题到答案对等。如果移植的检查点中没有在下游任务上进行微调,那么仅依赖模型测试就足够了。在确保模型完全功能的最后一步中,建议您还在GPU上运行所有测试。可能会发生您忘记将一些.to(self.device)语句添加到模型的内部张量中,这样的测试会显示错误。如果您没有访问GPU的权限,Hugging Face团队可以为您运行这些测试。

11. 添加文档字符串

现在,brand_new_bert 的所有必要功能都已添加 - 你几乎完成了!唯一剩下的是添加一个漂亮的文档字符串和文档页面。Cookiecutter 应该已经添加了一个名为 docs/source/model_doc/brand_new_bert.md 的模板文件,你需要填写它。用户在使用你的模型之前通常会先查看这个页面。因此,文档必须易于理解且简洁。为社区添加一些 Tips 来展示如何使用模型是非常有用的。如果有关于文档字符串的问题,不要犹豫,随时联系 Hugging Face 团队。

接下来,确保添加到 src/transformers/models/brand_new_bert/modeling_brand_new_bert.py 的文档字符串是正确的,并包含所有必要的输入和输出。我们有一个详细的指南,关于如何编写文档和我们的文档字符串格式 这里。提醒自己,文档应该至少像 🤗 Transformers 中的代码一样小心对待,因为文档通常是社区与模型的第一个接触点。

代码重构

很好,现在你已经为brand_new_bert添加了所有必要的代码。此时,你应该通过运行以下命令来纠正一些潜在的代码风格错误:

make style

并验证您的编码风格是否通过质量检查:

make quality

在🤗 Transformers中还有一些非常严格的设计测试可能仍然会失败,这会在你的拉取请求的测试中显示出来。这通常是因为文档字符串中缺少一些信息或命名不正确。如果你在这里遇到困难,Hugging Face团队肯定会帮助你。

最后,在确保代码正确运行后,重构代码总是一个好主意。所有测试都通过后,现在是再次检查添加的代码并进行一些重构的好时机。

你已经完成了编码部分,恭喜!🎉 你太棒了!😎

12. 将模型上传到模型中心

在这最后一部分,您应该转换并上传所有检查点到模型中心,并为每个上传的模型检查点添加一个模型卡片。您可以通过阅读我们的模型共享和上传页面来熟悉中心的功能。您应该与Hugging Face团队一起决定每个检查点的合适名称,并获取所需的访问权限,以便能够在作者的brand_new_bert组织下上传模型。push_to_hub方法,存在于transformers中的所有模型中,是将您的检查点推送到中心的一种快速有效的方式。下面粘贴了一个小片段:

brand_new_bert.push_to_hub("brand_new_bert")
# Uncomment the following line to push to an organization.
# brand_new_bert.push_to_hub("<organization>/brand_new_bert")

值得花一些时间为每个检查点创建合适的模型卡片。模型卡片应突出显示该特定检查点的具体特征,例如 检查点在哪个数据集上进行了预训练/微调?模型应该用于什么下游任务?并且还应包括一些关于如何正确使用模型的代码。

13. (可选)添加笔记本

添加一个详细展示如何使用brand_new_bert进行推理和/或在下游任务上进行微调的笔记本非常有帮助。这对于合并您的PR不是强制性的,但对社区非常有用。

14. 提交你完成的PR

你现在已经完成了编程,可以进入最后一步,即将你的PR合并到主分支。通常,Hugging Face团队应该已经在这个阶段帮助了你,但值得花一些时间为你完成的PR写一个漂亮的描述,并最终在你的代码中添加注释,如果你想向你的审阅者指出某些设计选择的话。

分享你的工作!!

现在,是时候从社区中获得一些对你工作的认可了!完成一个模型的添加是对Transformers和整个NLP社区的重大贡献。你的代码和移植的预训练模型肯定会被数百甚至数千名开发者和研究人员使用。你应该为自己的工作感到自豪,并与社区分享你的成就。

你已经创建了一个超级容易让社区中的每个人访问的模型!🤯

模型添加及其时间线:模型何时被添加到transformers?

我们的目标是让transformers尽可能早地支持新的模型架构和检查点: 对于某些模型,可能在发布当天(甚至发布时)即可使用,而对于其他模型,可能需要几天或几周的时间。

这种可用性通常取决于模型贡献者,以及社区对该架构的兴奋程度。

我们可以将模型架构的可能性分为四个部分:

  • 第0天集成
  • 同周集成
  • 发布后集成
  • Hub首次发布

让我们深入探讨这些内容,看看我们(transformers团队)如何帮助您贡献您的架构,并使您的架构能够被社区的所有成员轻松使用。

第0天集成

为了使第0天的集成工作,我们通常会希望直接与您紧密合作。为了在您的检查点和发布准备就绪之前保持您的架构私密,我们将在transformers的私有分支中一起工作。

如果你计划进行一个以transformers为主的发布,这是一个很好的选择:我们会提前运行CI,确保文档清晰,并且我们旨在尽可能优化你的模型(提供量化,使用Flash-Attention/SDPA进行优化,优化KV缓存等)。

我们还可以帮助您添加模型,提前审查,并确保transformers API按预期工作!

如果您希望选择这条路径,我们建议您提前联系我们,特别是如果架构特别新颖(至少提前几天,但提前几周将实现最佳的集成)。为了联系我们,请发送邮件至transformers@huggingface.co 🤗。

同周集成

当模型作者没有主动联系时,通常会发生同周集成;但我们看到了社区的强烈需求。

为了指定您希望我们集成的特定模型,我们将把您重定向到我们的 问题跟踪器 ,在那里您可以请求一个特定的模型。

问题上的活动越多,我们整合模型的速度就越快/可能性就越大!

发布后集成

发布后的集成通常发生在没有足够的活动/请求来保证同一周内集成,或者我们缺乏足够的带宽来进行集成时。

我们非常欢迎社区在这些情况下做出贡献;超过一半的库是由Hugging Face外部的贡献者贡献的。如果这对你来说很有趣,我们建议你查看我们的标记为“新模型”的开放问题

我们建议您尝试一个备受需求的模型,因为这将倍增您的贡献影响。 如果这是您的第一次贡献,我们会在那里帮助您 🤗。

Code-on-Hub 发布

最后,transformers 有一个“远程代码”的可能性,其中贡献不是在工具包内进行的,而是在 Hub 上进行的。这对于那些使用 transformers 作为项目骨干,但没有足够带宽直接将模型贡献给 transformers 的团队来说,可能特别有趣。

如果模型非常成功,那么我们很可能会最终将其集成到transformers中 - 因为这提供了更好的文档、CI、维护和优化 - 但这仍然是一个很好的方式,可以在第0天以最小的摩擦使您的模型可访问。

本指南是Hub优先发布的绝佳起点:自定义模型

< > Update on GitHub