将fairseq wmt19翻译系统移植到transformers

将fairseq wmt19翻译系统移植到transformers

Stas Bekman的客座博客文章

本文试图记录如何将fairseq wmt19翻译系统移植到transformers

我正在寻找一些有趣的项目来开展工作,Sam Shleifer建议我负责移植一个高质量的翻译器。

我阅读了一篇简短的论文《Facebook FAIR的WMT19新闻翻译任务提交》,描述了原始系统,决定尝试一下。

起初,我不知道如何处理这个复杂的项目,Sam帮助我将其分解为较小的任务,这对我非常有帮助。

在移植过程中,我选择使用预训练的en-ru/ru-en模型,因为我会这两种语言。要是使用de-en/en-de对会更困难,因为我不会说德语,而在移植过程的高级阶段,通过阅读和理解输出来评估翻译质量可以节省很多时间。

此外,当我进行初始移植时,我完全不知道de-en/en-de模型使用了合并的词汇表,而前者使用了两个不同大小的独立词汇表。因此,一旦我完成了支持两个独立词汇表的更复杂的工作,合并词汇表就变得非常简单。

让我们作弊

第一步当然是作弊。为什么要付出大努力呢,小努力就可以了。所以我写了一个简短的笔记本,只用几行代码提供了一个fairseq的代理,并模拟了transformers的API。

如果只需要基本的翻译,那么这就足够了。但是,当然,我们希望能够完全移植,所以在取得这个小胜利之后,我开始着手更困难的事情。

准备工作

为了本文的目的,让我们假设我们在~/porting下工作,因此让我们创建这个目录:

mkdir ~/porting
cd ~/porting

我们需要安装一些东西来进行这项工作:

# 安装fairseq
git clone https://github.com/pytorch/fairseq
cd fairseq
pip install -e .
# 在fairseq下安装mosesdecoder
git clone https://github.com/moses-smt/mosesdecoder
# 在fairseq下安装fastBPE
git clone [email protected]:glample/fastBPE.git
cd fastBPE; g++ -std=c++11 -pthread -O3 fastBPE/main.cc -IfastBPE -o fast; cd -
cd -

# 安装transformers
git clone https://github.com/huggingface/transformers/
pip install -e .[dev]

文件

简要概述一下,需要创建和编写以下文件:

  • src/transformers/configuration_fsmt.py – 一个简短的配置类。
  • src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py – 一个复杂的转换脚本。
  • src/transformers/modeling_fsmt.py – 实现模型架构的地方。
  • src/transformers/tokenization_fsmt.py – 一个分词器代码。
  • tests/test_modeling_fsmt.py – 模型测试。
  • tests/test_tokenization_fsmt.py – 分词器测试。
  • docs/source/model_doc/fsmt.rst – 一个文档文件。

还有其他需要修改的文件,我们将在最后讨论这些文件。

转换

移植过程中最重要的部分之一是创建一个脚本,该脚本将获取模型的原始开发者提供的所有可用源数据,包括带有预训练权重的检查点、模型和训练配置、字典和分词器支持文件,并将它们转换为transformers支持的新一组模型文件。您可以在这里找到最终的转换脚本:src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py

我开始这个过程是通过复制一个已有的转换脚本 src/transformers/convert_bart_original_pytorch_checkpoint_to_pytorch.py ,然后将其中大部分内容清空,随着迁移过程的进行逐步添加了一些部分。

在开发过程中,我对所有的代码都进行了本地转换模型文件的测试,只有在一切准备就绪之后,我才将文件上传到 🤗 s3 并继续在线版本的测试。

fairseq 模型和其支持文件

让我们首先看一下使用 fairseq 预训练模型时获得的数据。

我们将使用方便的 torch.hub API,它非常容易部署提交到该 hub 的模型:

import torch
torch.hub.load('pytorch/fairseq', 'transformer.wmt19.en-ru', checkpoint_file='model4.pt', 
               tokenizer='moses', bpe='fastbpe')

这段代码下载了预训练模型及其支持文件。我在 pytorch hub 上 fairseq 对应页面找到了这些信息。

为了查看下载文件的内容,我们首先要找到 ~/.cache 下的正确文件夹。

ls -1 ~/.cache/torch/hub/pytorch_fairseq/

显示:

15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9
15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9.json

如果你之前使用过 hub 的其他模型,可能会有多个条目。

为了以后方便引用那个难以理解的缓存文件夹名称,我们可以创建一个符号链接:

ln -s /code/data/cache/torch/hub/pytorch_fairseq/15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9 \
~/porting/pytorch_fairseq_model

注意:当你自己尝试时,路径可能会有所不同,因为模型的哈希值可能会改变。你可以在 ~/.cache/torch/hub/pytorch_fairseq/ 下找到正确的路径。

如果我们查看该文件夹的内容:

ls -l ~/porting/pytorch_fairseq_model/
total 13646584
-rw-rw-r-- 1 stas stas     532048 Sep  8 21:29 bpecodes
-rw-rw-r-- 1 stas stas     351706 Sep  8 21:29 dict.en.txt
-rw-rw-r-- 1 stas stas     515506 Sep  8 21:29 dict.ru.txt
-rw-rw-r-- 1 stas stas 3493170533 Sep  8 21:28 model1.pt
-rw-rw-r-- 1 stas stas 3493170532 Sep  8 21:28 model2.pt
-rw-rw-r-- 1 stas stas 3493170374 Sep  8 21:28 model3.pt
-rw-rw-r-- 1 stas stas 3493170386 Sep  8 21:29 model4.pt

我们有:

  1. model*.pt – 4 个检查点(带有预训练权重的 pytorch state_dict,以及其他各种内容)
  2. dict.*.txt – 源字典和目标字典
  3. bpecodes – 分词器使用的特殊映射文件

我们将在以下章节中调查每个文件。

翻译系统的工作原理

这里是关于计算机如何进行文本翻译的一个非常简要的介绍。

计算机无法阅读文本,只能处理数字。所以在处理文本时,我们必须将一个或多个字母映射为数字,并将其传递给计算机程序。当程序完成后,它也会返回数字,我们需要将其转换回文本。

让我们以俄语和英语中的两个句子开始,并为每个单词分配一个唯一的编号:

я  люблю следовательно я  существую
10 11    12            10 13

I  love therefore I  am
20 21   22        20 23

以10开头的数字将俄语单词映射到唯一的数字。以20开头的数字对英语单词也是如此。即使你不会说俄语,你仍然可以看到单词я(意思是“我”)在句子中重复了两次,并且它获得了与之关联的相同编号10。对于I(20)也是如此,它也重复了两次。

翻译系统的工作分为以下几个阶段:

1. [я люблю следовательно я существую] # 将句子标记为单词
2. [10 11 12 10 13]                    # 在输入字典中查找单词并转换为编号
3. [黑盒子]                             # 机器学习系统的魔力
4. [20 21 22 20 23]                    # 在输出字典中查找数字并转换为文本
5. [I love therefore I am]             # 将标记转换回句子

如果将第一步和最后一步合并,我们就得到了3个阶段:

  1. 编码输入:将输入文本分成标记,创建这些标记的词典(vocab),并将每个标记重新映射为该词典中的唯一编号。
  2. 生成翻译:将输入编号输入到预训练的机器学习模型中,该模型预测最佳翻译,并返回输出编号。
  3. 解码输出:将输出编号查找在目标语言词典中,将其转换回文本,最后将转换后的标记合并为翻译的句子。

第二阶段可能返回一个或多个可能的翻译结果。在后一种情况下,调用者可以选择最适合的结果。在本文中,我将提到波束搜索算法,这是搜索多个可能结果的一种方式。波束的大小指的是返回的结果数量。

如果只请求一个结果,模型将选择具有最高可能性的那个。如果请求多个结果,它将按概率对这些结果进行排序返回。

请注意,这个想法适用于大多数自然语言处理任务,不仅仅是翻译。

标记化

早期的系统将句子标记为单词和标点符号。但由于许多语言有数十万个单词,使用庞大的词汇表非常耗费计算资源,并且任务完成所需的时间也大大增加,因此这种方法是非常繁重的。

截至2020年,有很多不同的标记化方法,但最近的大多数方法都基于子词标记化——即不是将输入文本分解为单词,而是使用某种训练方法将输入文本分解为词段和字母,以获得最优的标记化。

让我们看看这种方法如何帮助减少内存和计算需求。如果我们有一个包含6个常见单词(go,going,speak,speaking,sleep,sleeping)的输入词汇表,使用单词级别的标记化,我们最终得到6个标记。然而,如果我们将其分解为:go,go-ing,speak,speak-ing等,则我们的词汇表中只有4个标记:go,speak,sleep,ing。这个简单的改变使性能提高了33%!除此之外,子词标记器不使用语法规则,而是在大量文本输入上进行训练以找到这样的拆分。在这个例子中,我使用了一个简单的语法规则,因为它容易理解。

这种方法的另一个重要优点是处理不在词汇表中的输入文本单词。例如,假设我们的系统遇到了单词grokking(*),它在词汇表中找不到。如果我们将其拆分为’grokk’-‘ing’,那么机器学习模型可能不知道如何处理单词的第一部分,但它获得一个有用的信息,即’ing’表示持续时态,因此它将能够产生更好的翻译。在这种情况下,标记器会将未知部分拆分为它所知道的部分,最坏的情况下将它们减少为单个字母。

  • 脚注:在1961年,罗伯特·A·海因莱因在《异乡人》中创造了“grok”一词:用直觉或同理心理解(某事)。

现代分词方法比简单的词分词法更优越的细微差别还有很多,但这超出了本文的范围。大多数这些系统在分词方面非常复杂,与刚才演示的简单示例相比,它们的原理是类似的。

分词器移植

第一步是移植分词器的编码器部分,将文本转换为ID。解码器部分直到最后才会需要。

fairseq的分词器工作原理

让我们了解一下fairseq的分词器是如何工作的。

fairseq (*) 使用Byte Pair Encoding(BPE)算法进行分词。

  • 脚注:从现在开始,当我提到fairseq时,我指的是这个特定的模型实现 – fairseq项目本身有几十个不同模型的实现。

让我们看看BPE是怎么做的:

import torch
sentence = "Machine Learning is great"
checkpoint_file='model4.pt'
model = torch.hub.load('pytorch/fairseq', 'transformer.wmt19.en-ru', checkpoint_file=checkpoint_file, tokenizer='moses', bpe='fastbpe')

# 逐步进行编码
tokens = model.tokenize(sentence)
print("tokenize ", tokens)

bpe = model.apply_bpe(tokens)
print("apply_bpe: ", bpe)

bin = model.binarize(bpe)
print("binarize: ", len(bin), bin)

# 与model.encode进行比较 - 应该得到相同的输出
expected = model.encode(sentence)
print("encode:   ", len(expected), expected)

输出结果如下:

('tokenize ', 'Machine Learning is great')
('apply_bpe: ', 'Mach@@ ine Lear@@ ning is great')
('binarize: ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))
('encode:   ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))

你可以看到model.encode实际上是tokenize+apply_bpe+binarize的过程 – 因此我们得到了相同的输出。

具体步骤如下:

  1. tokenize:通常会转义撇号等进行预处理,在这个例子中,它只是返回输入句子而没有任何更改
  2. apply_bpe:BPE根据由分词器提供的<bpecodes文件将输入分割成单词和子词 – 我们得到6个BPE块
  3. binarize:这个步骤将前一步的BPE块简单地映射到词汇表中对应的ID

你可以参考这个笔记本来了解更多细节。

现在是时候查看<bpecodes文件的内容了。以下是文件的前部分内容:

$ head -15 ~/porting/pytorch_fairseq_model/bpecodes
e n</w> 1423551864
e r 1300703664
e r</w> 1142368899
i n 1130674201
c h 933581741
a n 845658658
t h 811639783
e n 780050874
u n 661783167
s t 592856434
e i 579569900
a r 494774817
a l 444331573
o r 439176406
th e</w> 432025210
[...]

该文件的前面几个条目包括非常频繁的短1字母序列。如我们将在下一刻看到的,文件的底部包括最常见的多字母子词甚至完整的长单词。

特殊的标记</w>表示单词的结束。因此,在上面引用的几行中,我们可以找到:

e n</w> 1423551864
e r</w> 1142368899
th e</w> 432025210

如果第二列不包含</w>,则表示该段落出现在单词中间而不是结尾。

最后一列声明了在训练过程中遇到该BPE代码的次数。 bpecodes文件按照这一列进行排序 – 因此最常见的BPE代码位于顶部。

通过查看计数,我们现在知道当这个分词器进行训练时,它遇到了1,423,551,864个以en结尾的单词,1,142,368,899个以er结尾的单词和432,025,210个以the结尾的单词。对于后者,最有可能表示实际的单词the,但也可能包括像latheloathetithe等单词。

这些巨大的数字还告诉我们,这个分词器是在大量的文本上进行训练的!

如果我们看一下同一文件的底部:

$ tail -10 ~/porting/pytorch_fairseq_model/bpecodes
4 x 109019
F ische</w> 109018
sal aries</w> 109012
e kt 108978
ver gewal 108978
Sten cils</w> 108977
Freiwilli ge</w> 108969
doub les</w> 108965
po ckets</w> 108953
Gö tz</w> 108943

我们可以看到仍然有一些复杂的子词组合,例如出现了109,012次的sal aries!因此,它在bpecodes映射文件中有自己的专用条目。

apply_bpe是如何工作的?它通过在bpecodes映射文件中查找各种字母的组合,并在找到最长匹配的条目时使用它。

回到我们的例子,我们看到它将Machine拆分为:Mach@@ + ine – 让我们检查一下:

$ grep -i ^mach  ~/porting/pytorch_fairseq_model/bpecodes
mach ine</w> 463985
Mach t 376252
Mach ines</w> 374223
mach ines</w> 214050
Mach th 119438

你可以看到它有mach ine</w>。我们在里面找不到Mach ine – 所以它必须在处理大小写不匹配时使用小写查找。

现在让我们检查一下:Lear@@ + ning

$ grep -i ^lear  ~/porting/pytorch_fairseq_model/bpecodes
lear n</w> 675290
lear ned</w> 505087
lear ning</w> 417623

我们发现lear ning</w>在其中(同样,大小写不同)。

思考更多,大小写对于分词可能并不重要,只要字典中有Mach / Learmach / lear的唯一条目,在其中每种情况都得到覆盖就很关键。

希望现在你能明白这是如何工作的。

有一个令人困惑的事情是,如果你记得apply_bpe的输出是:

('apply_bpe: ', 6, ['Mach@@', 'ine', 'Lear@@', 'ning', 'is', 'great'])

而不是使用</w>来标记单词的结尾,它保持原样,但是用@@来标记不是结尾的单词。可能是因为fairseq使用了fastBPE实现,所以它是这样做的。我不得不修改这一点以适应transformers的实现,因为它不使用fastBPE

最后要检查的是BPE代码到词汇ID的重映射。重申一遍,我们有:

('apply_bpe: ', 'Mach@@ ine Lear@@ ning is great')
('binarize: ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))

2 – 最后一个标记ID是一个eos(流结束)标记。它用于向模型指示输入的结束。

然后Mach@@被重映射为10217ine被重映射为1419

让我们检查一下词典文件是否一致:

$ grep ^Mach@@ ~/porting/pytorch_fairseq_model/dict.en.txt
Mach@@ 6410
$ grep "^ine " ~/porting/pytorch_fairseq_model/dict.en.txt
ine 88376

等一下 – 这些不是我们在binarize之后得到的ID,应该是相应的102171419

经过一些调查发现,词汇文件中的ID并不是模型使用的ID,内部加载词汇文件后会将其重新映射为新的ID。幸运的是,我不需要弄清楚具体是如何做的。相反,我只需使用fairseq.data.dictionary.Dictionary.load加载字典(*),它执行了所有的重新映射,然后保存了最终的字典。通过使用调试器逐步执行fairseq代码,我发现了Dictionary类。

  • 脚注:我越是在移植模型和数据集上工作,就越意识到让原始代码为我工作,而不是试图复制它,这是一个巨大的时间节省器,最重要的是该代码已经经过测试 – 很容易忽略某些东西,并在后面发现大问题!毕竟,最终只有由此转换代码生成的数据将被transformers及其最终用户使用。

这是转换脚本的相关部分:

from fairseq.data.dictionary import Dictionary
def rewrite_dict_keys(d):
    # (1) 删除单词分隔符
    # (2) 在未分隔单词的地方添加单词结尾符号,
    # 例如:d = {'le@@': 5, 'tt@@': 6, 'er': 7} => {'le': 5, 'tt': 6, 'er</w>': 7}
    d2 = dict((re.sub(r"@@$", "", k), v) if k.endswith("@@") else (re.sub(r"$", "</w>", k), v) for k, v in d.items())
    keep_keys = "<s> <pad> </s> <unk>".split()
    # 恢复特殊标记
    for k in keep_keys:
        del d2[f"{k}</w>"]
        d2[k] = d[k]  # 恢复
    return d2

src_dict_file = os.path.join(fsmt_folder_path, f"dict.{src_lang}.txt")
src_dict = Dictionary.load(src_dict_file)
src_vocab = rewrite_dict_keys(src_dict.indices)
src_vocab_size = len(src_vocab)
src_vocab_file = os.path.join(pytorch_dump_folder_path, "vocab-src.json")
print(f"生成 {src_vocab_file}")
with open(src_vocab_file, "w", encoding="utf-8") as f:
    f.write(json.dumps(src_vocab, ensure_ascii=False, indent=json_indent))
# 目标词典也是一样的,这里省略了引用
# 我们还必须保存`bpecodes`,在transformers世界中称为`merges.txt`

运行转换脚本后,让我们检查转换后的词典:

$ grep '"Mach"' /code/huggingface/transformers-fair-wmt/data/wmt19-en-ru/vocab-src.json
  "Mach": 10217,
$ grep '"ine</w>":' /code/huggingface/transformers-fair-wmt/data/wmt19-en-ru/vocab-src.json
  "ine</w>": 1419,

transformers 版本的词汇文件中,我们有正确的 id。

正如您所看到的,我还必须重写词汇表以匹配 transformers 的 BPE 实现。我们需要更改:

['Mach@@', 'ine', 'Lear@@', 'ning', 'is', 'great']

为:

['Mach', 'ine</w>', 'Lear', 'ning</w>', 'is</w>', 'great</w>']

除了最后一个片段之外,我们不再标记单词的片段,而是标记最后一个片段或单词。您可以轻松地在两种编码风格之间切换。

这成功地完成了模型文件的第一部分的移植。您可以在这里查看代码的最终版本。

如果您想深入了解,还有更多细节可在这个笔记本中找到。

将分词器的编码器移植到 transformers

transformers 无法依赖于 fastBPE,因为后者需要 C 编译器,但幸运的是,有人已经在 tokenization_xlm.py 中实现了相同功能的 Python 版本。

所以我只是将它复制到 src/transformers/tokenization_fsmt.py 并重命名类名:

cp tokenization_xlm.py tokenization_fsmt.py
perl -pi -e 's|XLM|FSMT|ig; s|xlm|fsmt|g;' tokenization_fsmt.py

只做了很少的更改,我就有了一个可用的分词器的编码器部分。有很多代码与我需要支持的语言无关,所以我删除了那些代码。

由于我需要两个不同的词汇表,而不是一个在这里的分词器和其他地方都需要改变代码来支持两者。所以,例如,我必须覆盖超类的方法:

    def get_vocab(self) -> Dict[str, int]:
        return self.get_src_vocab()

    @property
    def vocab_size(self) -> int:
        return self.src_vocab_size

由于 fairseq 没有使用 bos(流的开头)标记,我还必须更改代码以不包括这些标记 (*):

-            return bos + token_ids_0 + sep
-        return bos + token_ids_0 + sep + token_ids_1 + sep
+            return token_ids_0 + sep
+        return token_ids_0 + sep + token_ids_1 + sep
  • 脚注:这是 diff(1) 的输出,它显示两个代码块之间的差异 – 以 - 开头的行表示删除的内容,以 + 开头的行表示添加的内容。

fairseq 也会转义字符并进行激进的破折号拆分,所以我还必须更改:

-        [...].tokenize(text, return_str=False, escape=False)
+        [...].tokenize(text, return_str=False, escape=True, aggressive_dash_splits=True)

如果您正在跟随并想要查看我对原始 tokenization_xlm.py 所做的所有更改,可以执行:

cp tokenization_xlm.py tokenization_orig.py
perl -pi -e 's|XLM|FSMT|g; s|xlm|fsmt|g;' tokenization_orig.py
diff -u tokenization_orig.py tokenization_fsmt.py  | less

只需确保在发布 fsmt 时检查存储库,因为自那时以来,这两个文件可能已经发生了分歧。

最后的阶段是运行一堆输入,并确保移植的分词器生成与原始分词器相同的标识符。您可以在这个 notebook 中看到这是如何完成的,我在尝试弄清楚如何使输出匹配时一直在重复运行它。

大多数移植过程都是这样进行的,我会取一个小功能,用 fairseq 的方式运行它,得到输出,然后用 transformers 代码做同样的操作,尝试使输出匹配-调整代码直到输出匹配,然后尝试不同类型的输入,确保它产生相同的输出,依此类推,直到所有输入产生匹配的输出为止。

移植核心翻译功能

在移植分词器相对快速成功之后(显然,主要是由于大部分代码已经存在),下一个阶段变得更加复杂。这就是 generate() 函数,它接受输入标识符,运行它们通过模型并返回输出标识符。

我必须将其分解为多个子任务。我必须

  1. 移植模型权重。
  2. 使 generate() 适用于单个 beam(即只返回一个结果)。
  3. 然后是多个 beam(即返回多个结果)。

我首先研究了哪些现有架构最接近我的需求。最接近的是 BART,所以我继续做了以下操作:

cp modeling_bart.py modeling_fsmt.py
perl -pi -e 's|Bart|FSMT|ig; s|bart|fsmt|g;' modeling_fsmt.py

这是我需要调整以适应 fairseq 提供的模型权重的起点。

移植权重和配置

我做的第一件事是查看公开共享的检查点中有什么。这个 notebook 显示了我在那里做了什么。

我发现里面有4个检查点。我不知道该怎么办,所以我从简单的工作开始,只使用第一个检查点。后来我发现 fairseq 使用了全部4个检查点的集合来获取最佳预测结果,而 transformers 目前不支持该功能。当移植完成并且我能够测量性能得分时,我发现 model4.pt 检查点提供了最佳得分。但在移植过程中,性能并不是很重要。由于我只使用一个检查点,当我比较输出时,关键是 fairseq 也只使用同一个检查点。

为了实现这一点,我使用了稍微不同的 fairseq API:

from fairseq import hub_utils
#checkpoint_file = 'model1.pt:model2.pt:model3.pt:model4.pt'
checkpoint_file = 'model1.pt'
model_name_or_path = 'transformer.wmt19.ru-en'
data_name_or_path = '.'
cls = fairseq.model_parallel.models.transformer.ModelParallelTransformerModel
models = cls.hub_models()
kwargs = {'bpe': 'fastbpe', 'tokenizer': 'moses'}
ru2en = hub_utils.from_pretrained(
            model_name_or_path,
            checkpoint_file,
            data_name_or_path,
            archive_map=models,
            **kwargs
        )

首先我查看了模型:

print(ru2en["models"][0])

TransformerModel(
  (encoder): TransformerEncoder(
    (dropout_module): FairseqDropout()
    (embed_tokens): Embedding(31232, 1024, padding_idx=1)
    (embed_positions): SinusoidalPositionalEmbedding()
    (layers): ModuleList(
      (0): TransformerEncoderLayer(
        (self_attn): MultiheadAttention(
          (dropout_module): FairseqDropout()
          (k_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (v_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (q_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (out_proj): Linear(in_features=1024, out_features=1024, bias=True)
        )
      [...]
# 完整的输出在笔记本中

这个看起来非常类似于BART的架构,只是在一些层上有一些细微的差异 – 有些层被添加了,有些层被删除了。所以这是个好消息,因为我不需要重新发明轮子,只需要微调一个运行良好的设计。

请注意,在上面的代码示例中,我没有使用torch.load()来加载state_dict。这是我最初做的,结果非常令人困惑 – 我缺少self_attn.(k|q|v)_proj的权重,而是只有一个单独的self_attn.in_proj。当我尝试使用fairseq API加载模型时,它修复了这些问题 – 显然那个模型是旧的,并且使用了旧的架构,其中有一个权重集用于k/q/v,而较新的架构将它们分开。当fairseq加载这个旧模型时,它会重写权重以匹配现代架构。

我还使用这个笔记本来直观地比较state_dict。在那个笔记本中,您还将看到fairseqlast_optimizer_state中获取了2.2GB的数据,我们可以安全地忽略它,并且有一个三倍更小的最终模型大小。

在转换脚本中,我还需要删除一些我不打算使用的state_dict键,例如model.encoder.versionmodel.model和其他一些键。

接下来我们来看一下配置参数:

args = dict(vars(ru2en["args"]))
pprint(args)

 'activation_dropout': 0.0,
 'activation_fn': 'relu',
 'adam_betas': '(0.9, 0.98)',
 'adam_eps': 1e-08,
 'adaptive_input': False,
 'adaptive_softmax_cutoff': None,
 'adaptive_softmax_dropout': 0,
 'arch': 'transformer_wmt_en_de_big',
 'attention_dropout': 0.1,
 'bpe': 'fastbpe',
 [...完整输出在笔记本中...]

好的,我们将复制这些配置用于配置模型。我不得不重命名一些参数名,因为transformers使用了不同的名称来对应的配置设置。所以配置的重新映射如下:

    model_conf = {
        "architectures": ["FSMTForConditionalGeneration"],
        "model_type": "fsmt",
        "activation_dropout": args["activation_dropout"],
        "activation_function": "relu",
        "attention_dropout": args["attention_dropout"],
        "d_model": args["decoder_embed_dim"],
        "dropout": args["dropout"],
        "init_std": 0.02,
        "max_position_embeddings": args["max_source_positions"],
        "num_hidden_layers": args["encoder_layers"],
        "src_vocab_size": src_vocab_size,
        "tgt_vocab_size": tgt_vocab_size,
        "langs": [src_lang, tgt_lang],
        [...]
        "bos_token_id": 0,
        "pad_token_id": 1,
        "eos_token_id": 2,
        "is_encoder_decoder": True,
        "scale_embedding": not args["no_scale_embedding"],
        "tie_word_embeddings": args["share_all_embeddings"],
    }

现在只需要将配置保存到config.json中,并创建一个新的state_dict转储到pytorch.dump中:

    print(f"生成{fsmt_tokenizer_config_file}")
    with open(fsmt_tokenizer_config_file, "w", encoding="utf-8") as f:
        f.write(json.dumps(tokenizer_conf, ensure_ascii=False, indent=json_indent))
    [...]
    print(f"生成{pytorch_weights_dump_path}")
    torch.save(model_state_dict, pytorch_weights_dump_path)

我们已经将配置和模型的state_dict移植完成 – 太好了!

您可以在这里找到最终的转换代码。

移植架构代码

现在我们已经移植了模型权重和模型配置,我们只需要调整从modeling_bart.py中复制的代码以匹配fairseq的功能。

第一步是将一个句子进行编码,然后将其输入到generate函数中 – 对于fairseqtransformers来说。

在尝试了几次失败的尝试后(*) – 我很快意识到,使用print作为调试方法无法让我取得任何进展,基本的pdb调试器也一样。为了提高效率,并能够监视多个变量并进行代码评估,我需要一个严肃的可视化调试器。我花了一天时间尝试了各种Python调试器,只有当我尝试使用pycharm时,我意识到这是我需要的工具。这是我第一次使用pycharm,但我很快就弄明白了如何使用它,因为它非常直观。

  • 注释:该模型生成的是俄语中的’nononono’ – 这真是公平而滑稽!

随着时间的推移,我在pycharm中发现了一个很棒的功能,它允许我根据功能对断点进行分组,我可以根据需要打开和关闭整个组。例如,在这里,我关闭了与波束搜索相关的断点,并打开了解码器相关的断点:

现在我已经使用这个调试器来移植FSMT,我知道如果要使用pdb完成同样的工作,可能需要花费我更多的时间 – 我甚至可能已经放弃了。

我从两个脚本开始:

  • fseq-translate
  • fsmt-translate

(首先没有decode部分)

同时运行两个脚本,在每一边的调试器中逐步执行,并比较相关变量的值 – 直到找到第一个分歧点。然后我研究了代码,在modeling_fsmt.py内进行了调整,重新启动了调试器,快速跳到分歧点并重新检查输出。这个循环重复了多次,直到输出匹配。

我首先对简单的无波束搜索进行了这个过程,一旦输出完全匹配,我就用更复杂的波束搜索重复了一遍。在这里,例如,我发现fairseq使用的是early_stopping=True的等价物,而transformers默认为False。当启用早停时,只要有与波束大小相同的候选者,就停止搜索新的候选者,而当禁用时,算法只有在找不到比已有候选者更高概率的候选者时才停止搜索。《fairseq》的论文提到使用了一个巨大的波束大小为50,这弥补了使用早停的缺点。

Tokenizer解码器移植

一旦我让移植后的generate函数产生了与fairseqgenerate函数非常相似的结果,接下来我需要完成最后一个阶段,将输出解码为可读文本。这样我就可以用肉眼进行快速比较和翻译质量的评估 – 这是我在输出id上做不到的。

与编码过程类似,这个过程是反过来进行的。

具体步骤如下:

  1. 将输出id转换为文本字符串
  2. 移除BPE编码
  3. 解标记化 – 处理转义字符等

在进行了一些调试后,我不得不改变原来在tokenization_xlm.py中处理BPE的方法,并将输出通过moses解标记器。

     def convert_tokens_to_string(self, tokens):
         """ 将一系列的标记(字符串)转换为单个字符串。 """
-        out_string = "".join(tokens).replace("</w>", " ").strip()
-        return out_string
+        # 删除BPE
+        tokens = [t.replace(" ", "").replace("</w>", " ") for t in tokens]
+        tokens = "".join(tokens).split()
+        # 解标记化
+        text = self.moses_detokenize(tokens, self.tgt_lang)
+        return text

一切都很好。

上传模型到S3

一旦转换脚本完全将所有所需的文件转移到 transformers ,我将模型上传到了我的🤗 S3账户:

cd data
transformers-cli upload -y wmt19-ru-en
transformers-cli upload -y wmt19-en-ru
transformers-cli upload -y wmt19-de-en
transformers-cli upload -y wmt19-en-de
cd -

在测试期间,我使用了我的🤗 S3账户,一旦我的带有完整更改的PR准备好合并,我在PR中要求将模型移动到 facebook 组织账户,因为这些模型属于那里。

有几次我只需要更新配置文件,我不想重新上传大模型,所以我编写了这个小脚本来生成正确的上传命令,否则输入命令太长以至于容易出错:

perl -le 'for $f (@ARGV) { print qq[transformers-cli upload -y $_/$f --filename $_/$f] \
for map { "wmt19-$_" } ("en-ru", "ru-en", "de-en", "en-de")}' \
vocab-src.json vocab-tgt.json tokenizer_config.json config.json
# 根据需要添加/删除文件

例如,如果我只需要更新所有的 config.json 文件,上面的脚本将给我一个方便的复制粘贴:

transformers-cli upload -y wmt19-en-ru/config.json --filename wmt19-en-ru/config.json
transformers-cli upload -y wmt19-ru-en/config.json --filename wmt19-ru-en/config.json
transformers-cli upload -y wmt19-de-en/config.json --filename wmt19-de-en/config.json
transformers-cli upload -y wmt19-en-de/config.json --filename wmt19-en-de/config.json

上传完成后,可以通过以下方式访问这些模型(*):

tokenizer = FSMTTokenizer.from_pretrained("stas/wmt19-en-ru")
  • 注: stas 是我在 https://huggingface.co 的用户名。

在进行这次上传之前,我必须使用模型文件夹的本地路径,例如:

tokenizer = FSMTTokenizer.from_pretrained("/code/huggingface/transformers-fair-wmt/data/wmt19-en-ru")

重要提示:如果您更新了模型文件并重新上传它们,您必须知道由于CDN缓存的原因,上传的模型在上传后可能在最多24小时内不可用 – 即旧的缓存模型将被提供。因此,要更快开始使用新模型,唯一的方法是:

  1. 将其下载到本地路径,并将该路径作为传递给 from_pretrained() 的参数。
  2. 或在接下来的24小时内使用:from_pretrained(..., use_cdn=False) – 单次操作不足以生效。

AutoConfig、AutoTokenizer等

我需要做的另一个更改是将新转移的模型插入到自动化的 transformers 系统中。这主要用于模型网站,以加载模型配置、分词器和主要类,而无需提供任何特定的类名。例如,在 FSMT 的情况下,可以这样做:

from transformers import AutoTokenizer, AutoModelWithLMHead
mname = "facebook/wmt19-en-ru"
tokenizer = AutoTokenizer.from_pretrained(mname)
model = AutoModelWithLMHead.from_pretrained(mname)

有3个*auto*文件用于启用映射:

-rw-rw-r-- 1 stas stas 16K Sep 23 13:53 src/transformers/configuration_auto.py
-rw-rw-r-- 1 stas stas 65K Sep 23 13:53 src/transformers/modeling_auto.py
-rw-rw-r-- 1 stas stas 13K Sep 23 13:53 src/transformers/tokenization_auto.py

然后是pipeline,它完全隐藏了所有的NLP复杂性,为终端用户提供了一个非常简单的API,只需选择一个模型并将其用于所需的任务。例如,以下是使用pipeline执行摘要任务的方法:

summarizer = pipeline("summarization", model="t5-base", tokenizer="t5-base")
summary = summarizer("这里是一些长文档", min_length=5, max_length=20)
print(summary)

翻译pipeline目前正在进展中,关注此文档的更新以了解何时支持翻译(目前只支持少数特定的模型和语言)。

最后,还有src/transformers/__init__.py需要进行编辑,以便可以进行如下操作:

from transformers import FSMTTokenizer, FSMTForConditionalGeneration

而不是:

from transformers.tokenization_fsmt import FSMTTokenizer
from transformers.modeling_fsmt import FSMTForConditionalGeneration

两种写法都可以。

为了找到我需要插入FSMT的所有位置,我模仿了BartConfigBartForConditionalGenerationBartTokenizer。我只是使用grep查找了有这些内容的文件,并为FSMTConfigFSMTForConditionalGenerationFSMTTokenizer插入了相应的条目。

$ egrep -l "(BartConfig|BartForConditionalGeneration|BartTokenizer)" src/transformers/*.py \
| egrep -v "(marian|bart|pegasus|rag|fsmt)"
src/transformers/configuration_auto.py
src/transformers/generation_utils.py
src/transformers/__init__.py
src/transformers/modeling_auto.py
src/transformers/pipelines.py
src/transformers/tokenization_auto.py

grep搜索中,我排除了也包含这些类的文件。

手动测试

到目前为止,我主要使用自己的脚本进行测试。

一旦翻译器正常工作,我将翻转的ru-en模型进行了转换,然后编写了两个改写脚本:

  • fseq-paraphrase
  • fsmt-paraphrase

这两个脚本接受源语言的句子,将其翻译为另一种语言,然后将翻译结果再次翻译回原语言。由于不同语言表达相似内容的方式不同,这个过程通常会产生改写的结果。

通过这些脚本的帮助,我发现了更多的分词器问题,通过调试器逐步执行,并使fsmt脚本产生与fairseq版本相同的结果。

目前,无beam搜索产生的结果大部分相同,但beam搜索仍然存在一些差异。为了识别特殊情况,我编写了一个fsmt-port-validate.py脚本,该脚本使用sacrebleu测试数据作为输入,并将该数据分别通过fairseqtransformers进行翻译,仅报告不匹配的结果。通过观察模式,我能够快速识别出一些问题,并修复了这些问题。

移植其他模型

接下来我开始移植en-dede-en模型。

我很惊讶地发现它们并不是以相同的方式构建的。每个都有一个合并的字典,所以一开始我感到很沮丧,因为我以为我现在又要做一个巨大的改变来支持这个。但是,我不需要做任何改变,因为合并的字典可以无需任何改变地适应进去。我只是使用了两个相同的字典 – 一个作为源,一个作为目标。

我编写了另一个脚本来测试所有移植模型的基本功能:fsmt-test-all.py。

测试覆盖率

这一步非常重要 – 我需要为移植模型准备一套广泛的测试。

transformers的测试套件中,大多数涉及大型模型的测试都被标记为@slow,这些测试通常不能在CI(持续集成)上正常运行,因为它们太慢了。所以我还需要创建一个非常小的模型,它具有与正常预训练模型相同的结构,但它必须非常小,并且可以具有随机权重。这个小模型可以用来测试移植功能。但是它不能用于质量测试,因为它只有少量权重,因此无法真正训练出任何实用的功能。fsmt-make-tiny-model.py创建了这样一个小模型。生成的模型及其所有的字典和配置文件的大小只有3MB。我使用s3transformers-cli upload将其上传到了云端,现在我可以在测试套件中使用它了。

与代码一样,我首先复制了tests/test_modeling_bart.py并将其转换为使用FSMT,然后调整它使其与新模型配合工作。

然后我将我用于手动测试的一些脚本转换为单元测试 – 这很容易。

transformers有一套庞大的通用测试,每个模型都要通过这些测试 – 我不得不进行一些调整以使这些测试适用于FSMT(主要是为了适应两个字典的设置),并且我不得不覆盖一些测试,这些测试由于模型的独特性无法运行,以便跳过它们。可以在这里看到结果。

我添加了一个执行轻量级BLEU评估的测试 – 对于每个模型,我只使用了8个文本输入,并测量了它们的BLEU得分。这是测试和生成数据的脚本。

SinusoidalPositionalEmbedding

fairseq使用了与transformers稍微不同的SinusoidalPositionalEmbedding实现。最初,我复制了fairseq的实现。但是,当尝试让测试套件工作时,我无法通过torchscript测试。因为SinusoidalPositionalEmbedding被设计为不是state_dict的一部分,也不会与模型权重一起保存 – 这个类生成的所有权重都是确定性的,不会被训练。fairseq使用了一个技巧,通过不将其权重作为参数或缓冲区,然后在forward调用之前将权重切换到正确的设备,使其在不透明地工作。但是torchscript对此不太适应,因为它要求所有权重在第一次forward调用之前都在正确的设备上。

我不得不重新编写实现,将其转换为正常的nn.Embedding子类,并添加功能,以便在save_pretrained()时不保存这些权重,并且在from_pretrained()加载state_dict时,如果找不到这些权重,不会报错。

评估

我知道移植模型在我对大量文本进行手动测试时表现得相当好,但我不知道移植模型相对于原始模型的表现如何。所以现在是时候进行评估了。

对于翻译任务,BLEU分数被用作评估指标。transformers有一个名为run_eval.py的脚本来执行评估。

这是ru-en对的评估。

export PAIR=ru-en
export MODEL=facebook/wmt19-$PAIR
export DATA_DIR=data/$PAIR
export SAVE_DIR=data/$PAIR
export BS=64
export NUM_BEAMS=5
export LENGTH_PENALTY=1.1
mkdir -p $DATA_DIR
sacrebleu -t wmt19 -l $PAIR --echo src > $DATA_DIR/val.source
sacrebleu -t wmt19 -l $PAIR --echo ref > $DATA_DIR/val.target
PYTHONPATH="src:examples/seq2seq" python examples/seq2seq/run_eval.py $MODEL \
$DATA_DIR/val.source $SAVE_DIR/test_translations.txt --reference_path $DATA_DIR/val.target \
--score_path $SAVE_DIR/test_bleu.json --bs $BS --task translation --num_beams $NUM_BEAMS \
--length_penalty $LENGTH_PENALTY --info $MODEL --dump-args

运行这段代码需要几分钟的时间,返回结果如下:

{'bleu': 39.0498, 'n_obs': 2000, 'runtime': 184, 'seconds_per_sample': 0.092, 
'num_beams': 5, 'length_penalty': 1.1, 'info': 'ru-en'}

可以看到BLEU分数为39.0498,使用sacrebleuwmt19数据集的2000个测试输入进行评估。

请记住,我无法使用模型合集,所以下一步需要找到表现最佳的检查点。为此,我编写了一个脚本fsmt-bleu-eval-each-chkpt.py,它将每个检查点转换,并运行评估脚本并报告最佳检查点。结果表明,model4.pt是4个可用检查点中表现最佳的。

我的BLEU分数与原始论文中的分数不一致,所以接下来我需要确保我们使用相同的数据和工具进行比较。通过在fairseq问题中提问,我得到了fairseq开发人员用于获取BLEU分数的代码-你可以在这里找到。但遗憾的是,他们的方法使用了未公开的重新排序方法。此外,他们在解标记化之前评估输出,而不是真实的输出,这显然得分更高。总之-我们的评分方式不同(*)。

  • 脚注:一篇名为《在报告BLEU分数时呼吁明确》的论文邀请开发人员开始使用相同的方法来计算指标(tldr:使用sacrebleu)。

目前,这个移植模型在BLEU分数上略逊于原始模型,因为没有使用模型合集,但在使用相同的测量方法之前,很难确定确切的差异。

移植新模型

在这里上传了4个fairseq模型后,有人建议移植3个wmt16和2个wmt19的AllenAI模型(Jungo Kasai等人)。移植过程非常顺利,因为我只需要弄清楚如何将所有源文件放在一起,因为它们分散在几个不相关的存档中。一旦完成这个步骤,转换就可以顺利进行。

移植后我发现的唯一问题是获得的BLEU分数比原始分数低。这些模型的创建者Jungo Kasai非常乐于建议使用自定义超参数length_penalty=0.6,一旦我使用这个参数,结果就好多了。

这个发现促使我编写了一个新脚本:run_eval_search.py,可以用来搜索各种超参数以获得最佳的BLEU分数。以下是使用示例:

# search space
export PAIR=ru-en
export DATA_DIR=data/$PAIR
export SAVE_DIR=data/$PAIR
export BS=32
mkdir -p $DATA_DIR
sacrebleu -t wmt19 -l $PAIR --echo src > $DATA_DIR/val.source
sacrebleu -t wmt19 -l $PAIR --echo ref > $DATA_DIR/val.target
PYTHONPATH="src:examples/seq2seq" python examples/seq2seq/run_eval_search.py stas/wmt19-$PAIR \
$DATA_DIR/val.source $SAVE_DIR/test_translations.txt --reference_path $DATA_DIR/val.target \
--score_path $SAVE_DIR/test_bleu.json --bs $BS --task translation \
--search="num_beams=5:8:11:15 length_penalty=0.6:0.7:0.8:0.9:1.0:1.1 early_stopping=true:false"

在这里,它搜索了所有可能的num_beamslength_penaltyearly_stopping的组合。

执行完毕后,它报告:

bleu  | num_beams | length_penalty | early_stopping
----- | --------- | -------------- | --------------
39.20 |        15 |            1.1 |              0
39.13 |        11 |            1.1 |              0
39.05 |         5 |            1.1 |              0
39.05 |         8 |            1.1 |              0
39.03 |        15 |            1.0 |              0
39.00 |        11 |            1.0 |              0
38.93 |         8 |            1.0 |              0
38.92 |        15 |            1.1 |              1
[...]

可以看出,在transformers的情况下,early_stopping=False的表现更好(fairseq使用等效的early_stopping=True)。

因此,对于这5个新模型,我使用了这个脚本来找到最佳的默认参数,并在转换模型时使用了这些参数。用户仍然可以在调用generate()时覆盖这些参数,但为什么不提供最佳默认值呢。

您可以在这里找到这5个转换过的AllenAI模型。

更多脚本

由于每个转换组的模型都有其独特之处,我为每个模型制作了专用脚本,以便将来可以轻松重新构建或创建新的转换脚本。您可以在这里找到所有的转换、评估和其他脚本。

模型卡片

另一个重要的事情是,仅仅将模型转换并提供给其他人是不够的。还需要提供有关如何使用它的信息,超参数的细微差别,数据集的来源,评估指标等等。这一切都可以通过创建模型卡片来完成,它只是一个包含一些元数据的README.md文件,这些元数据由模型网站使用,然后是所有可以共享的有用信息。

例如,我们来看一下facebook/wmt19-en-ru的模型卡片。这是它的顶部:

---
language: 
- en
- ru
thumbnail:
tags:
- translation
- wmt19
- facebook
license: apache-2.0
datasets:
- wmt19
metrics:
- bleu
---

# FSMT

## Model description

This is a ported version of 
[...]

正如您所见,我们定义了语言、标签、许可证、数据集和指标。关于如何编写这些的完整指南可以在模型共享和上传处找到。剩下的是描述模型及其细微差别的markdown文档。您还可以通过推理小部件直接从模型页面尝试这些模型。例如,对于英译俄翻译:https://huggingface.co/facebook/wmt19-en-ru?text=My+name+is+Diego+and+I+live+in+Moscow。

文档

最后,需要添加文档。

幸运的是,大部分文档都是从模块文件中的docstring自动生成的。

和之前一样,我复制了docs/source/model_doc/bart.rst并将其调整为FSMT。当它准备好后,我通过在docs/source/index.rst中添加fsmt条目来链接到它。

我使用了:

make docs

来测试新添加的文档是否正确构建。运行该目标后,我需要检查的文件是docs/_build/html/model_doc/fsmt.html – 我只需在浏览器中加载它并验证它是否正确呈现。

这是最终的源文档docs/source/model_doc/fsmt.rst及其呈现版本。

PR时间到了

当我觉得我的工作相当完整时,我准备提交我的PR。

由于这项工作涉及许多git提交,我想要一个干净的PR,所以我使用了以下技术将所有的提交合并到一个新分支中。这样如果我以后想要访问其中任何一个提交,所有的初始提交都会保持在原位。

我正在开发的分支名叫fair-wmt,我将要提交PR的新分支名叫fair-wmt-clean,所以我做了以下操作:

git checkout master
git checkout -b fair-wmt-clean
git merge --squash fair-wmt
git commit -m "Ready for PR"
git push origin fair-wmt-clean

然后我去github上基于fair-wmt-clean分支提交了这个PR。

这个过程进行了两个星期的多次反馈和修改,以及更多的类似循环。最终一切都令人满意,PR被合并了。

在这个过程中,我发现了一些问题,不断添加新的测试,改进文档等等,所以花费的时间是值得的。

之后,我又针对一些特性进行了改进和重写,添加了各种构建脚本、模型卡片等等,提交了几个更多的PR。

由于我移植的模型属于facebookallenai组织,我不得不请求Sam将那些模型文件从我在s3上的账户转移到相应的组织下。

结束语

  • 虽然我不能将模型集合作为transformers不支持它,但好处是最终的facebook/wmt19-*模型的下载大小只有1.1GB,而不是原来的13GB。由于原始模型中包含了保存在模型中的优化器状态,所以为了只是下载模型以原样进行文本翻译的人来说,它增加了将近9GB(4×2.2GB)的无用负担。

  • 虽然一开始移植工作看起来非常具有挑战性,因为我不了解transformersfairseq的内部机制,但回顾起来,它并不那么困难。这主要是因为我在各个部分的transformers中已经有了大部分组件可用 – 我只需要找到我需要的部分,大部分是从其他模型中大量借鉴,然后调整它们以满足我的需求。这对于代码和测试都是如此。让我们重新表达一下 – 移植工作是困难的 – 但如果我不得不从头开始编写所有内容,那就更难了。而且找到合适的部分并不容易。

感谢

  • 在整个过程中,Sam Shleifer作为我的导师对我非常有帮助,不仅在技术上给予支持,而且在我遇到困难时鼓励和激励我。

  • 在PR合并过程中,除了Sam之外,Lysandre Debut和Sylvain Gugger通过他们的见解和建议也为我做出了很大贡献,我将这些融入了代码库。

  • 我感谢所有为transformers代码库做出贡献的人,他们为我的工作铺平了道路。

注意事项

在Jupyter Notebook中自动打印全部内容

我的jupyter notebook已经配置为自动打印所有表达式,所以我不需要显式地print()它们。默认行为是仅打印每个单元格的最后一个表达式。因此,如果您阅读我的笔记本输出,它们可能与您自己运行时不同,除非您有相同的设置。

您可以通过将以下内容添加到~/.ipython/profile_default/ipython_config.py(如果您没有则创建一个)来启用jupyter notebook中的打印全部功能:

c = get_config()
# 在交互式模式下运行所有节点
c.InteractiveShell.ast_node_interactivity = "all"
# 恢复到原始行为
# c.InteractiveShell.ast_node_interactivity = "last_expr"

并重新启动您的Jupyter笔记本服务器。

为了确保在您阅读这篇文章时,不管它是何时写的,所有的链接都能正常工作,这些链接都是指向代码的特定SHA版本,而不是最新版本。这样做是为了确保即使文件被重命名或删除,您仍然能找到代码与本文相关。如果您想确保查看的是代码的最新版本,请将链接中的哈希码替换为master。例如,一个链接:

https://github.com/huggingface/transformers/blob/129fdae04033fe4adfe013b734deaec6ec34ae2e/src/transformers/modeling_fsmt.py

变成:

https://github.com/huggingface/transformers/blob/master/src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py

感谢您的阅读!