如何将模型添加到 🤗 Transformers?
🤗 Transformers 库通常能够通过社区贡献者提供新模型。但这可能是一个具有挑战性的项目,需要深入了解 🤗 Transformers 库和要实现的模型。在 Hugging Face,我们正在努力赋予更多社区成员积极添加模型的能力,为此我们整理了本指南,指导您完成添加 PyTorch 模型的过程(请确保您已 安装 PyTorch)。
在此过程中,您将
- 深入了解开源最佳实践
- 理解最受欢迎的深度学习库之一的设计原则
- 学习如何有效地测试大型模型
- 学习如何集成 Python 实用程序,如
black
、ruff
和make fix-copies
,以确保代码干净且可读
Hugging Face 团队成员将随时为您提供帮助,因此您永远不会孤军奋战。 🤗 ❤️
要开始,请为要添加到 🤗 Transformers 的模型打开一个 新模型添加 问题。如果您对贡献特定模型不太挑剔,您可以按 新模型标签 筛选,看看是否有任何未认领的模型请求,并进行处理。
在您打开新的模型请求后,第一步是熟悉 🤗 Transformers(如果您还不熟悉的话!)。
🤗 Transformers 的总体概述
首先,您应该对 🤗 Transformers 有一个总体的了解。🤗 Transformers 是一个非常有主见的库,因此您可能不同意该库的某些理念或设计选择。然而,根据我们的经验,我们发现该库的基本设计选择和理念对于有效地扩展 🤗 Transformers 同时将维护成本保持在合理水平至关重要。
要更好地理解该库,一个好的起点是阅读 我们理念的文档。由于我们工作方式的结果,我们尝试将一些选择应用于所有模型
- 组合通常优于抽象
- 如果代码复制可以显著提高模型的可读性或可访问性,那么代码复制并不总是坏事
- 模型文件尽可能独立,因此当您阅读特定模型的代码时,理想情况下只需要查看相应的
modeling_....py
文件。
在我们看来,该库的代码不仅仅是提供产品的一种手段,例如 使用 BERT 进行推理的能力,而且也是我们希望改进的产物本身。因此,在添加模型时,用户不仅是将使用您的模型的人,而且是所有将阅读、尝试理解以及可能调整您的代码的人。
考虑到这一点,让我们更深入地了解一下库的总体设计。
模型概述
要成功添加模型,了解模型与其配置、PreTrainedModel 和 PretrainedConfig 之间的交互至关重要。为了示例目的,我们将要添加到 🤗 Transformers 的模型称为 BrandNewBert
。
让我们看一下
如您所见,我们在 🤗 Transformers 中使用了继承,但我们将抽象级别降至最低限度。该库中任何模型的抽象级别都从未超过两级。BrandNewBertModel
继承自 BrandNewBertPreTrainedModel
,而 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 是一个有主见的库,我们在代码编写方式方面有一些自己的怪癖 :-)
- 模型的正向传递应该完全写在模型文件中,同时完全独立于库中的其他模型。如果您想重用另一个模型的块,请复制代码并粘贴它,并在顶部添加
# Copied from
注释(请参阅 此处 获取一个很好的示例,并参阅 此处 获取有关 Copied from 的更多文档)。 - 代码应该是完全可以理解的,即使对于非英语母语人士也是如此。这意味着您应该选择描述性的变量名,并避免使用缩写。例如,
activation
比act
更可取。强烈不建议使用单字母变量名,除非它是在 for 循环中的索引。 - 一般来说,我们更喜欢更长的显式代码,而不是简短的魔法代码。
- 避免在 PyTorch 中对
nn.Sequential
进行子类化,而是子类化nn.Module
并编写正向传递,以便任何使用您代码的人都可以通过添加打印语句或断点来快速调试它。 - 您的函数签名应该进行类型注解。对于其余部分,好的变量名比类型注解更具可读性和可理解性。
分词器概述
还没准备好 :-( 本节将很快添加!
将模型添加到 🤗 Transformers 的分步指南
每个人都有不同的模型移植偏好,因此查看其他贡献者如何将模型移植到 Hugging Face 的总结对你来说非常有用。这里列出了关于如何移植模型的社区博客文章
- 移植 GPT2 模型 由 Thomas 撰写
- 移植 WMT19 MT 模型 由 Stas 撰写
根据经验,我们可以告诉你,在添加模型时最重要的是
- 不要重新造轮子!你为新 🤗 Transformers 模型添加的大部分代码已经在 🤗 Transformers 中的某个地方存在。花一些时间找到类似的、已经存在的模型和分词器,你可以从它们那里复制。 grep 和 rg 是你的朋友。请注意,你的模型的分词器很可能基于一个模型实现,而你的模型的建模代码基于另一个模型实现。例如,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. 接下来准备你的环境
通过点击仓库页面上的“Fork”按钮,将 仓库 分叉。这将在你的 GitHub 用户帐户下创建一个代码副本。
将你的
transformers
分叉克隆到你的本地磁盘,并将基本仓库添加为远程仓库git clone https://github.com/[your Github handle]/transformers.git cd transformers git remote add upstream https://github.com/huggingface/transformers.git
设置开发环境,例如运行以下命令
python -m venv .env source .env/bin/activate pip install -e ".[dev]"
根据你的操作系统,以及由于 Transformers 的可选依赖项数量不断增加,你可能会使用此命令遇到错误。如果出现这种情况,请确保安装你正在使用的深度学习框架(PyTorch、TensorFlow 和/或 Flax),然后执行
pip install -e ".[quality]"
这对于大多数用例来说应该足够了。然后你可以返回到父目录
cd ..
我们建议将 brand_new_bert 的 PyTorch 版本添加到 Transformers。要安装 PyTorch,请按照 https://pytorch.ac.cn/get-started/locally/ 上的说明操作。
注意:你不必安装 CUDA。使新模型在 CPU 上运行就足够了。
要移植 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 笔记本 / Google Colab
- 本地 Python 脚本。
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*,对它们执行一次前向传递,并使用中间打印语句或断点进行验证
同样,选择哪种策略取决于你。通常,取决于原始代码库,其中一种或另一种策略更有优势。
如果原始代码库允许你将模型分解成更小的子组件,例如,如果原始代码库可以轻松地在 eager 模式下运行,那么通常值得付出努力。在开始阶段走更难的路,有一些重要的优势
- 在后期,当比较原始模型和 Hugging Face 的实现时,你可以自动验证每个组件是否与 🤗 Transformers 实现的相应组件匹配,而不是依赖于通过打印语句进行的视觉比较
- 它可以让你有足够的绳索将模型移植到 🤗 Transformers 的大问题分解成只移植单个组件的小问题,从而更好地构建你的工作
- 将模型分离成有逻辑意义的组件将帮助你更好地了解模型的设计,从而更好地理解模型
- 在后期,这些逐个组件的测试有助于确保在继续更改代码时不会出现回归
Lysandre 的 ELECTRA 集成检查给出了一个很好的示例,说明如何做到这一点。
但是,如果原始代码库非常复杂,或者只允许中间组件在编译模式下运行,那么将模型分离成更小的可测试子组件可能需要花费太多时间,甚至不可能。一个很好的例子是 T5 的 MeshTensorFlow 库,它非常复杂,并且没有提供将模型分解成子组件的简单方法。对于此类库,人们通常依赖于验证打印语句。
无论选择哪种策略,推荐的程序通常是相同的,你应该先开始调试起始层,最后调试结束层。
建议按以下顺序检索以下各层的输出,可以通过打印语句或子组件函数检索:
- 检索传递给模型的输入 ID
- 检索词嵌入
- 检索第一个 Transformer 层的输入
- 检索第一个 Transformer 层的输出
- 检索以下 n - 1 个 Transformer 层的输出
- 检索整个 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 编写的吗?那么请确保在执行前向传递时模型 **没有 jitted**,例如,查看 此链接。
- 使用你能找到的最小预训练检查点。检查点越小,你的调试周期就越快。如果你的预训练模型很大,以至于你的前向传递花费的时间超过 10 秒,那么效率就很低。如果只有非常大的检查点可用,那么创建新环境中的虚拟模型(具有随机初始化权重),并保存这些权重以与你的模型的 🤗 Transformers 版本进行比较,可能更有意义。
- 确保你使用的是原始存储库中调用前向传递的最简单方法。理想情况下,你想要找到原始存储库中 **只** 调用一次前向传递的函数,即通常被称为 `predict`、`evaluate`、`forward` 或 `__call__` 的函数。你不想调试调用 `forward` 多次的函数,例如,用于生成文本,如 `autoregressive_sample`、`generate`。
- 尝试将标记化与模型的 *forward* 传递分离。如果原始存储库显示了你需要输入字符串的示例,那么尝试找出在 forward 调用中将字符串输入更改为输入 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
在开始调整自动生成的代码之前,现在是时候在 🤗 Transformers 中打开一个“正在进行的工作(WIP)”拉取请求,例如,“[WIP] 添加 *brand_new_bert*”,以便你和 Hugging Face 团队可以并肩合作将模型集成到 🤗 Transformers 中。
你应该执行以下操作
从你的主分支创建具有描述性名称的分支
git checkout -b add_brand_new_bert
提交自动生成的代码
git add . git commit
获取并重新基线到当前主分支
git fetch upstream git rebase upstream/main
使用以下命令将更改推送到你的帐户
git push -u origin a-descriptive-name-for-my-changes
一旦你满意,请转到你在 GitHub 上的分支的网页。点击“Pull Request”。确保添加一些 Hugging Face 团队成员的 GitHub 句柄作为审阅者,以便 Hugging Face 团队在将来发生更改时得到通知。
通过单击 GitHub 拉取请求网页右侧的“转换为草稿”,将 PR 更改为草稿。
以下内容中,每当你取得一些进展时,请不要忘记提交你的工作并将其推送到你的帐户,以便它显示在拉取请求中。此外,你应该确保定期使用以下命令更新你的工作,以使用当前的主分支
git fetch upstream git merge upstream/main
一般来说,你可能对模型或你的实现提出的所有问题都应该在你的 PR 中提出,并在 PR 中讨论/解决。这样,Hugging Face 团队就会始终在你在提交新代码或你有问题时收到通知。通常,向 Hugging Face 团队指明你添加的代码非常有用,这样 Hugging Face 团队就能有效地理解你的问题或疑问。
为此,你可以转到“已更改文件”选项卡,在那里你可以看到你所有的更改,转到你想提出问题的行,然后单击“+”符号添加评论。每当解决一个问题或问题时,你都可以单击创建的评论的“解决”按钮。
同样,Hugging Face 团队会在审阅你的代码时打开评论。我们建议在 GitHub 上你的 PR 上提出大多数问题。对于一些对公众没有太大用处的非常普遍的问题,请随时通过 Slack 或电子邮件 ping Hugging Face 团队。
5. 调整为 brand_new_bert 生成的模型代码
首先,我们只关注模型本身,不关心分词器。所有相关代码应在生成的文件 src/transformers/models/brand_new_bert/modeling_brand_new_bert.py
和 src/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
中,直到您觉得所有必要的代码都已添加。根据我们的经验,快速添加第一个所需代码版本,然后使用下一节中描述的转换脚本迭代地改进/更正代码,效率更高。此时唯一需要运行的是,您可以实例化 brand_new_bert 的 🤗 Transformers 实现,即以下命令应该可以运行
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
标志在内部用于确保我们只初始化一个子模块一次。通过将其设置为 module.project_q
和 module.project_hid
的 True
,我们确保我们所做的自定义初始化不会在以后被覆盖,_init_weights
函数不会应用于它们。
6. 编写转换脚本
接下来,您应该编写一个转换脚本,让您将用于在原始存储库中调试 brand_new_bert 的检查点转换为与您刚刚创建的 brand_new_bert 的 🤗 Transformers 实现兼容的检查点。不建议从头编写转换脚本,而是查看 🤗 Transformers 中已存在的转换脚本,寻找一个用于转换用与 brand_new_bert 相同的框架编写的类似模型的脚本。通常,复制已存在的转换脚本并略微调整以适合您的用例就足够了。如果您难以找到合适的转换脚本,请随时向 Hugging Face 团队寻求帮助。
- 如果您要将模型从 TensorFlow 移植到 PyTorch,那么 BERT 的转换脚本 这里 可能是一个不错的起点。
- 如果您要将模型从 PyTorch 移植到 PyTorch,那么 BART 的转换脚本 这里 可能是一个不错的起点。
在下面,我们将快速解释 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)
现在我们可以创建该模型定义的实例,它将用随机权重填充所有权重:dense
、intermediate
、layer_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 实现的脚本和原始实现产生相同的值。接下来,您应该确保输出值也相同。这是添加新模型中最困难的部分之一。导致输出不相同的常见错误是
- 某些层未添加,即未添加激活层,或遗漏了残差连接。
- 词嵌入矩阵未绑定。
- 使用了错误的位置嵌入,因为原始实现使用了偏移。
- 在正向传递期间应用了丢弃。要解决此问题,请确保model.training 为 False 并且在正向传递期间没有错误地激活任何丢弃层,即将self.training 传递给 PyTorch 的函数式丢弃
解决此问题的最佳方法通常是并排查看原始实现和 🤗 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
的模板文件,你应该填写该文件。使用你模型的用户通常会在使用模型之前先查看此页面。因此,文档必须易于理解且简洁。为社区添加一些 *提示* 以展示模型的使用方式非常有用。关于文档字符串,请随时联系 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 社区的重大贡献。你的代码和移植的预训练模型肯定会被数百甚至数千个开发人员和研究人员使用。你应该为自己的工作感到自豪,并与社区分享你的成就。
你又创建了一个对社区中每个人来说都超级容易访问的模型!🤯
< > 更新 在 GitHub 上