面向自然语言处理的迁移学习(三)(2)https://developer.aliyun.com/article/1522491
8.1.3 应用于填空和下一句预测任务
我们在这一节的练习中使用了上一小节的文章。让我们立即开始编写一个用于填写空白的流程,使用以下代码:
from transformers import pipeline fill_mask = pipeline("fill-mask",model="bert-base-cased",tokenizer="bert-base-cased")
注意,在这里我们使用的是 BERT BASE 模型。这些任务对任何 BERT 模型的训练来说都是基本的,所以这是一个合理的选择,不需要特殊的微调模型。初始化适当的流程后,我们现在可以将它应用于上一小节中文章的第一句话。我们通过用适当的掩码标记[MASK]
来删除“cases”这个词,并使用以下代码向模型提供已省略的词进行预测:
fill_mask("A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer [MASK] by the start of June")
输出显示,最高的是“deaths”,这是一个可能合理的完成。即使剩下的建议也可以在不同的情境下起作用!
[{'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June [SEP]', 'score': 0.19625532627105713, 'token': 6209}, {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer executions by the start of June [SEP]', 'score': 0.11479416489601135, 'token': 26107}, {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer victims by the start of June [SEP]', 'score': 0.0846652239561081, 'token': 5256}, {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer masks by the start of June [SEP]', 'score': 0.0419488325715065, 'token': 17944}, {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer arrests by the start of June [SEP]', 'score': 0.02742016687989235, 'token': 19189}]
我们鼓励你尝试从各种句子中省略各种单词,以确信这几乎总是非常有效的。在节省篇幅的情况下,我们的附带笔记本会为几个更多的句子做到这一点,但我们不在这里打印这些结果。
然后我们继续进行下一个句子预测(NSP)任务。在写作本文时,此任务尚未包含在 pipelines API 中。因此,我们将直接使用 transformers API,这也将让您更加熟悉它。我们首先需要确保已安装 transformers 3.0.0 以上的版本,因为该任务仅在该阶段的库中包含。我们使用以下代码实现这一点;在写作本文时,Kaggle 默认安装了较早的版本:
!pip install transformers==3.0.1 # upgrade transformers for NSP
升级版本后,我们可以使用以下代码加载一个 NSP-specific BERT:
from transformers import BertTokenizer, BertForNextSentencePrediction ❶ import torch from torch.nn.functional import softmax ❷ tokenizer = BertTokenizer.from_pretrained('bert-base-cased') model = BertForNextSentencePrediction.from_pretrained('bert-base-cased') model.eval() ❸
❶ NSP-specific BERT
❷ 计算原始输出的最终概率
❸ PyTorch 模型默认是可训练的。为了更便宜的推断和可执行重复性,将其设置为“eval”模式,如此处所示。通过 model.train()将其设置回“train”模式。对于 TensorFlow 模型不适用!
作为健全性检查,首先我们要确定第一句和第二句是否从模型的角度来看是合理的完成。我们使用以下代码进行检查:
prompt = "A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June." next_sentence = "Containment policies had a large impact on the number of COVID-19 cases and deaths, directly by reducing transmission rates and indirectly by constraining people’s behavior." encoding = tokenizer.encode(prompt, next_sentence, return_tensors='pt') logits = model(encoding)[0] ❶ probs = softmax(logits) ❷ print("Probabilities: [not plausible, plausible]") print(probs)
❶ 输出是一个元组;第一项描述了我们追求的两个句子之间的关系。
❷ 从原始数字计算概率
注意代码中的术语logits
。这是 softmax 函数的原始输入。通过 softmax 将logits
传递,可以得到概率。代码的输出确认找到了正确的关系,如下所示:
Probabilities: [not plausible, plausible] tensor([[0.1725, 0.8275]], grad_fn=<SoftmaxBackward>)
现在,让我们将第二个句子替换为一个有点随机的“Cats are independent.” 这将产生以下结果:
Probabilities: [not plausible, plausible] tensor([0.7666, 0.2334], grad_fn=<SoftmaxBackward>)
看起来一切都如预期的那样工作!
现在,你应该已经非常清楚 BERT 在训练中解决哪些任务了。需要注意的是,本章我们还没有将 BERT 调整到任何新域或任务特定的数据上进行微调。这是有意为之的,以帮助你在没有任何干扰的情况下了解模型架构。在下一节中,我们会演示如何进行微调,通过进行跨语言迁移学习实验。对于我们已经介绍过的所有其他任务,都可以采用类似的迁移学习方式进行,通过完成下一节练习,您将有很好的发挥空间去自己实践。
8.2 基于多语言 BERT(mBERT)的跨语言学习
在本节中,我们将进行本书中第二个整体和第一个主要的跨语言实验。更具体地说,我们正在进行一个迁移学习实验,该实验涉及从多语言 BERT 模型中转移知识到其原始训练中不包含的语言。与之前一样,我们在实验中使用的语言将是 Twi 语,这是一种被认为是“低资源”的语言,因为缺乏多种任务的高质量训练数据。
多语言 BERT(mBERT)本质上是指应用前一节中所描述的 BERT,并将其应用于约 100 个连接在一起的语言维基百科⁷ 语料库。最初的语言集合是前 100 大维基百科,现已扩展到前 104 种语言。该语言集合不包括 Twi,但包括一些非洲语言,如斯瓦希里语和约鲁巴语。由于各种语言语料库的大小差异很大,因此会应用一种“指数平滑”过程来对高资源语言(如英语)进行欠采样,对低资源语言(如约鲁巴语)进行过采样。与之前一样,使用了 WordPiece 分词。对于我们而言,它足以提醒你,这种分词过程是子词级别的,正如我们在之前的章节中所看到的。唯一的例外是中文、日文的汉字和韩文汉字,它们通过在每个字符周围加上空格的方式被转换为有效的字符分词。此外,为了在精度和模型效率之间做出权衡选择,mBERT 作者消除了重音词汇。
我们可以直观地认为,一个在 100 多种语言上训练的 BERT 模型包含了可以转移到原始训练集中未包含的语言的知识。简单来说,这样的模型很可能会学习到所有语言中共同的特征。这种共同特征的一个简单例子是单词和动词-名词关系的概念。如果我们将提出的实验框架设定为多任务学习问题,正如我们在第四章中讨论的那样,我们期望对以前未见过的新场景的泛化性能得到改善。在本节中,我们将基本证明这一点。我们首先使用预训练的分词器将 mBERT 转移到单语 Twi 数据上。然后,我们通过从头开始训练相同的 mBERT/BERT 架构以及训练适当的分词器来重复实验。比较这两个实验将允许我们定性地评估多语言转移的有效性。我们为此目的使用 JW300 数据集的 Twi 子集⁸。
本节的练习对于你的技能集具有超越多语言转移的影响。这个练习将教会你如何从头开始训练你自己的分词器和基于 transformer 的模型。它还将演示如何将一个检查点转移到这样一个模型的新领域/语言数据。之前的章节和一点冒险/想象力将为你提供基于 transformer 的迁移学习超能力,无论是用于领域自适应、跨语言转移还是多任务学习。
在接下来的小节中,我们简要概述了 JW300 数据集,然后是执行跨语言转移和从头开始训练的小节。
8.2.1 JW300 数据集简介
JW300 数据集是一个面向低资源语言的广泛覆盖的平行语料库。正如之前提到的,它是一个可能具有偏见的样本,由耶和华见证人翻译的宗教文本组成。然而,对于许多低资源语言研究而言,它是一个起点,通常是唯一可用的平行数据的开放来源。然而,重要的是要记住这种偏见,并在这个语料库上进行任何训练时配备第二阶段,该阶段可以将第一阶段的模型转移到一个更少偏见和更具代表性的语言和/或任务样本。
尽管它本质上是一个平行语料库,但我们只需要 Twi 数据的单语语料库进行我们的实验。Python 包 opustools-pkg 可以用于获取给定语言对的平行语料库。为了让您的工作更容易,我们已经为英语-Twi 语对进行了这项工作,并将其托管在 Kaggle 上。⁹要为其他低资源语言重复我们的实验,您需要稍微调整一下opustools-pkg并获取一个等价的语料库(如果您这样做,请与社区分享)。我们只使用平行语料库的 Twi 部分进行我们的实验,并忽略英语部分。
让我们继续将 mBERT 转移到单语低资源语言语料库。
8.2.2 将 mBERT 转移到单语 Twi 数据与预训练的标记器
首先要做的是初始化一个 BERT 标记器到来自 mBERT 模型中的预训练检查点。这次我们使用的是大小写版本,如下代码所示:
from transformers import BertTokenizerFast ❶ tokenizer = BertTokenizerFast.from_pretrained("bert-base-multilingual-cased")❷
❶ 这只是 BertTokenizer 的一个更快的版本,你可以用这个替代它。
❷ 使用了预训练的 mBERT 标记器
准备好了标记器后,让我们按以下方法将 mBERT 检查点加载到 BERT 遮蔽语言模型中,并显示参数数量:
from transformers import BertForMaskedLM ❶ model = BertForMaskedLM.from_pretrained("bert-base-multilingual-cased") ❷ print("Number of parameters in mBERT model:") print(model.num_parameters())
❶ 使用了遮蔽语言建模
❷ 初始化到了 mBERT 检查点
输出表明模型有 1.786 亿个参数。
接下来,我们使用 transformers 附带的方便的 LineByLineTextDataset
方法,使用单语 Twi 文本的标记器来构建数据集,如下所示:
from transformers import LineByLineTextDataset dataset = LineByLineTextDataset( tokenizer=tokenizer, file_path="../input/jw300entw/jw300.en-tw.tw", block_size=128) ❶
❶ 指示一次读取多少行
如下代码所示,接下来我们需要定义一个“data collator” —— 一个帮助方法,通过一批样本数据行(长度为block_size
)创建一个特殊对象。 这个特殊对象适用于 PyTorch 进行神经网络训练:
from transformers import DataCollatorForLanguageModeling data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15) ❶
❶ 使用了遮蔽语言建模,并以 0.15 的概率遮蔽单词
在这里,我们使用了遮蔽语言建模,就像前一节所描述的一样。在我们的输入数据中,有 15% 的单词被随机遮蔽,模型在训练期间被要求对它们进行预测。
定义标准的训练参数,比如输出目录和训练批量大小,如下所示:
from transformers import TrainingArguments training_args = TrainingArguments( output_dir="twimbert", overwrite_output_dir=True, num_train_epochs=1, per_gpu_train_batch_size=16, save_total_limit=1, )
然后使用先前定义的数据集和数据收集器定义一个“训练器”来进行数据上的一个训练周期。注意,数据包含了超过 600,000 行,因此一次遍历所有数据是相当大量的训练!
trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=dataset, prediction_loss_only=True)
训练并计算训练时间,如下所示:
import time start = time.time() trainer.train() end = time.time() print("Number of seconds for training:") print((end-start))
模型在所示的超参数下大约需要三个小时才能完成一个周期,并且损失大约为 0.77。
按如下进行模型保存:
trainer.save_model("twimbert")
最后,我们从语料库中取出以下句子 —— “Eyi de ɔhaw kɛse baa sukuu hɔ” —— 它的翻译是 “这在学校中提出了一个大问题。” 我们遮蔽了一个单词,sukuu(在 Twi 中意思是“学校”),然后应用 pipelines API 来预测遗漏的单词,如下所示:
from transformers import pipeline fill_mask = pipeline( ❶ "fill-mask", model="twimbert", tokenizer=tokenizer) print(fill_mask("Eyi de ɔhaw kɛse baa [MASK] hɔ.")) ❷
❶ 定义了填空管道
❷ 预测被遮蔽的标记
这将产生如下输出:
[{'sequence': '[CLS] Eyi de ɔhaw kɛse baa me hɔ. [SEP]', 'score': 0.13256989419460297, 'token': 10911}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Israel hɔ. [SEP]', 'score': 0.06816119700670242, 'token': 12991}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa ne hɔ. [SEP]', 'score': 0.06106790155172348, 'token': 10554}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Europa hɔ. [SEP]', 'score': 0.05116277188062668, 'token': 11313}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Eden hɔ. [SEP]', 'score': 0.033920999616384506, 'token': 35409}]
你立刻就能看到结果中的宗教偏见。“以色列”和“伊甸园”被提议为前五个完成之一。话虽如此,它们算是比较有说服力的完成 —— 因为它们都是名词。总的来说,表现可能还算不错。
如果你不会说这种语言,不用担心。在下一节中,我们将从头开始训练 BERT,并将损失值与我们在这里获得的值进行比较,以确认我们刚刚执行的转移学习实验的功效。我们希望您能尝试在其他您感兴趣的低资源语言上尝试这里概述的步骤。
8.2.3 在单语 Twi 数据上从零开始训练的 mBERT 和分词器
要从头开始训练 BERT,我们首先需要训练一个分词器。我们可以使用下一节代码中的代码初始化、训练和保存自己的分词器到磁盘。
代码清单 8.1 从头初始化、训练和保存我们自己的 Twi 分词器
from tokenizers import BertWordPieceTokenizer paths = ['../input/jw300entw/jw300.en-tw.tw'] tokenizer = BertWordPieceTokenizer() ❶ tokenizer.train( ❷ paths, vocab_size=10000, min_frequency=2, show_progress=True, special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"], ❸ limit_alphabet=1000, wordpieces_prefix="##") !mkdir twibert ❹ tokenizer.save("twibert")
❶ 初始化分词器
❷ 自定义训练,并进行训练
❸ 标准 BERT 特殊标记
❹ 将分词器保存到磁盘
要从刚刚保存的分词器中加载分词器,我们只需要执行以下操作:
from transformers import BertTokenizerFast tokenizer = BertTokenizerFast.from_pretrained("twibert", max_len=512) ❶
❶ 使用我们刚刚训练的语言特定的分词器,max_len=512,以保持与上一小节一致
请注意,我们使用最大序列长度为 512,以保持与上一小节一致——这也是预训练的 mBERT 使用的长度。还要注意,保存分词器将在指定文件夹中创建词汇文件 vocab.txt 文件。
从这里开始,我们只需初始化一个全新的 BERT 模型来进行掩码语言建模,如下所示:
from transformers import BertForMaskedLM, BertConfig model = BertForMaskedLM(BertConfig()) ❶
❶ 不要初始化为预训练的;创建一个全新的。
否则,步骤与上一小节相同,我们不在此处重复代码。重复相同的步骤在一个时代后大约 1.5 小时产生大约 2.8 的损失,并在两个时代后的大约 3 小时产生 2.5 的损失。这显然不如前一小节的 0.77 损失值好,证实了在那种情况下转移学习的功效。请注意,这次实验每个时代的时间较短,因为我们构建的分词器完全专注于 Twi,因此其词汇量比 104 种语言的预训练 mBERT 词汇表小。
去吧,改变未来!
摘要
- transformer 架构使用自注意力机制来构建文本的双向上下文以理解文本。这使得它最近在 NLP 中成为主要的语言模型。
- transformer 允许序列中的标记独立于彼此进行处理。这比按顺序处理标记的 bi-LSTM 实现了更高的可并行性。
- transformer 是翻译应用的一个不错选择。
- BERT 是一种基于 transformer 的架构,对于其他任务,如分类,是一个不错的选择。
- BERT 可以同时在多种语言上进行训练,生成多语言模型 mBERT。该模型捕获的知识可转移到原本未包含在训练中的语言。
- A. Vaswani 等人,“注意力就是一切”,NeurIPS (2017)。
- A. Radford 等人,“通过生成式预训练改善语言理解”,arXiv (2018)。
- M. E. Peters et al., “BERT: Pre-Training of Deep Bidirectional Transformers for Language Understanding,” Proc. of NAACL-HLT (2019): 4171-86.
github.com/google-research/bert/blob/master/multilingual.md
www.weforum.org/agenda/2020/07/口罩命令和其他封锁政策减少了在美国的 COVID-19 传播
.- P. Rajpurkar et al., “SQuAD: 100,000+ Questions for Machine Comprehension of Text,” arXiv (2016).
github.com/google-research/bert/blob/master/multilingual.md
opus.nlpl.eu/JW300.php
- [
www.kaggle.com/azunre/jw300entw
](https://www.kaggle.com/azunre/jw300entw括
- 实施判别微调和逐步解冻等策略。
- 在教师和学生BERT 模型之间执行知识蒸馏
在本章和下一章中,我们将介绍迄今为止已涵盖的深度NLP迁移学习建模架构的一些适应策略。换句话说,鉴于预训练架构如 ELMo、BERT 或 GPT,我们如何更有效地进行迁移学习?我们可以在这里采取几种效率措施。我们选择关注参数效率,即目标是在减少性能的同时产生尽可能少的参数模型。这样做的目的是使模型更小、更容易存储,从而更容易在智能手机设备上部署。另外,智能适应策略可能需要在某些困难的迁移情况下达到可接受的性能水平。
在第六章中,我们描述了 ULMFiT¹方法,即通用语言模型微调。该方法引入了判别微调和逐步解冻的概念。简而言之,逐步解冻逐渐增加网络中解冻或微调的子层的数量。另一方面,判别微调为网络中的每一层指定了可变的学习率,从而实现更有效的迁移。我们在第六章的代码中没有实施这些方法,因为作为适应策略,我们认为它们最适合放在本章中。在本章中,我们使用 ULMFiT 作者编写的fast.ai库来演示预训练的循环神经网络(RNN)语言模型的概念。
一些模型压缩方法通常被应用于大型神经网络以减小其大小。一些著名的方法包括权重修剪和量化。在这里,我们将重点关注适应策略,即NLP领域最近备受关注的知识蒸馏。该过程本质上试图使用显著较小的学生模型模拟来自较大的教师模型的输出。特别是,我们使用变压器库中的 DistilBERT²方法的实现来演示通过这种方法可以将 BERT 的大小减半以上。
让我们从下一节开始 ULMFiT。
9.1 逐步解冻和判别微调
在本节中,我们将在代码中实现 ULMFiT 方法,将语言模型适应于新的数据领域和任务。我们首先在第六章的最后讨论了这种方法的概念,因为从历史上看,它首先是在递归神经网络(RNNs)的背景下引入的。然而,我们将实际的编码练习推迟到现在,以强调在其核心,ULMFiT 是一组与架构无关的适应技术。这意味着它们也可以应用于基于 transformer 的模型。然而,为了与源材料保持一致,我们在 RNN-based 语言模型的背景下进行练习编码。我们将编码练习集中在我们在第六章中看到的假新闻检测示例上。
作为提醒,辨别微调指定网络中每一层的可变学习率。此外,学习率在学习过程中不是恒定的。相反,它们是倾斜三角形的——在开始时线性增加到一定程度,然后线性衰减。换句话说,这意味着快速增加学习率,直到达到最大速率,然后以较慢的速度减小。这个概念在图 6.8 中有所说明,我们在这里为了您的方便重复了它。
图 6.8(从第六章复制)建议的倾斜三角形 ULMFiT 学习率时间表,对于总迭代次数为 10,000 的情况。学习率在总迭代次数的 10%(即 1,000)上线性增加,最高达 0.01,然后线性减少到 0。
请注意,图中标有“最大学习率”的点在我们的情况下会有所不同(不是 0.01)。迭代的总数也将与图中显示的 10,000 次不同。这个时间表会产生更有效的转移和更具一般性的模型。
渐进解冻,另一方面,逐渐增加网络的子层的数量解冻,这样可以减少过拟合,同时也会产生更有效的转移和更具一般性的模型。所有这些技术在第六章的最后一节中都有详细讨论,可能在着手本节其余部分之前,简要回顾该讨论会有益处。
我们将在这里使用第 5.2 节的说明性示例——事实核查示例。回想一下,这个数据集包含超过 40,000 篇文章,分为两类:“假”和“真”。真实文章是从 reuters.com,一个声誉良好的新闻网站收集来的。另一方面,假文章则是从 PolitiFact 标记为不可靠的各种来源收集来的。在第 6.2 节,我们在预训练的 ELMo 模型导出的特征向量上训练了一个二元分类器。这个分类器预测一篇给定的文章是真实的(1)还是假的(0)。使用由每个类别的 1,000 篇文章组成的数据集,获得了 98%+ 的准确率。在这里,我们将看看是否可以通过 ULMFiT 方法取得更好的效果。
在本节中,我们将该方法分为两个小节。第一个小节涉及在目标任务数据上微调预训练语言模型的第一阶段 ULMFiT。斜三角形学习率在这里发挥作用,以及分层微调的概念。一些数据预处理和模型架构讨论也自然地融入到这个第一个小节中。第二个小节涵盖了第二阶段,涉及在目标任务数据上微调目标任务分类器——它位于微调语言模型之上——的阶段。逐步解冻程序的有效性由此得到证明。
请注意,本节中呈现的代码采用 fast.ai 版本 1 语法编写。选择这样做的原因是该库的第 2 版更改了输入数据的处理方式,提供了将其分割为训练集和验证集的内部函数,而不是允许您自己指定。为了与我们在前几章中的工作保持一致,在那里我们自己分割了数据,我们在这里坚持使用版本 1。我们还在 Kaggle 笔记本中提供了等效的 fast.ai 版本 2 语法代码³,您应该运行并与此处呈现的版本 1 代码进行比较。最后,请注意,版本 1 的文档托管在 fastai1.fast.ai/
,而版本 2 的文档托管在 docs.fast.ai/
。
9.1.1 预训练语言模型微调
第 5.2 节已经描述了我们需要对事实核查示例数据集进行的初始数据预处理步骤。特别地,我们对文章文本数据进行了洗牌,并将其加载到 NumPy 数组train_x
和test_x
中。我们还构建了相应的标签 NumPy 数组train_y
和test_y
,其中包含每篇文章是否为真实的信息,当文章为真时标记为 1,否则为 0。如同第 5.2 节一样,保持 1,000 个样本和测试/验证比例为 30%,得到的训练数组——train_x
,train_y
——长度为 1,400,测试数组——test_x
,test_y
——长度为 600。
我们需要做的第一件事是准备 fast.ai 库所期望的数据形式。其中一种数据格式是一个两列的 Pandas DataFrame,第一列包含标签,第二列包含数据。我们可以相应地构建训练和测试/验证数据框,如下所示:
train_df = pd.DataFrame(data=[train_y,train_x]).T test_df = pd.DataFrame(data=[test_y,test_x]).T
这些数据框应该分别有 1,400 行和 600 行,每个都对应于相应数据样本中的每篇文章,并且在继续之前,最好用通常的.shape
命令检查一下,如下所示:
train_df.shape test_df.shape
预期输出分别为(1400, 2)
和(600, 2)
。
fast.ai 中的数据使用TextLMDataBunch
类进行消耗,这些实例可以使用我们刚刚准备的 DataFrame 格式构建,使用以下命令:
data_lm = TextLMDataBunch.from_df(train_df = train_df, valid_df = test_df, path = "")
另一方面,fast.ai 中的数据由一个特定于任务的分类器使用TextClasDataBunch
类进行消耗。我们构建此类的一个实例,准备进入下一小节,使用以下类似的命令从我们的数据框中:
data_clas = TextClasDataBunch.from_df(path = "", train_df = train_df, valid_df = test_df, vocab=data_lm.train_ds.vocab)
现在我们准备在目标数据上微调我们的语言模型!为此,我们需要使用以下命令创建language_model_learner
fast.ai 类的一个实例:
learn = language_model_learner(data_lm, AWD_LSTM, drop_mult=0.3) ❶
❶ 使用 30%的权重丢失率初始化预训练的权重丢失 LSTM。这是在 WikiText-103 基准数据集上预训练的。
这里,AWD_LSTM
代表ASGD 权重丢失 LSTM。⁴这只是通常的 LSTM 架构,其中一些权重已被随机丢弃,就像通常的 dropout 层对神经网络激活所做的那样,与权重相反。这是最类似于 fast.ai 库中原始 ULMFiT 论文中所做的架构选择。此外,如果您检查上一个命令的执行日志,您应该能够确认它还从在 WikiText-103 基准数据集上训练的检查点加载预训练权重。⁶这个数据集,官方称为“WikiText 长期依赖语言建模数据集”,是一组由人类判断为“好”的维基百科文章。这是一个很好的、干净的无监督数据来源,已被许多自然语言处理论文用于基准测试。
现在我们已经加载了一个模型实例和一些预训练权重,我们将尝试确定用于微调语言模型的最佳或最优学习率。fast.ai 中一个称为lr_find
的巧妙实用方法可以自动为我们完成这项工作。它会迭代一系列学习率,并检测结果损失与学习率曲线上损失函数下降最快的点。等价地,这是损失梯度最小的地方。⁷我们可以使用我们的语言模型学习器learn
快速进行如下操作:
learn.lr_find() ❶ learn.recorder.plot(suggestion=True) ❷
❶ 寻找最佳/最优学习率
❷ 绘制它
结果的损失与学习率曲线,突出显示了最佳率,如图 9.1 所示。
图 9.1 fast.ai 库用于语言模型微调步骤中的最佳学习率找寻过程的结果,用于虚假新闻检测示例。通过多次迭代不同的学习率,并选择在曲线上损失下降最快的点作为最佳学习率。
我们可以通过以下命令程序化地检索这个学习率,并显示它:
rate = learn.recorder.min_grad_lr ❶ print(rate) ❷
❶ 检索最佳率
❷ 显示它
在我们执行代码时,返回的最佳学习率约为 4.0e-2。
找到最佳学习率后,我们现在可以使用下面的命令对我们的预训练的权重丢弃 LSTM 模型进行微调,使用 fit_one_cycle
fast.ai 命令如下所示:
learn.fit_one_cycle(1, rate) ❶
❶ 这个命令在底层使用了斜三角形学习率。它以 epochs 的数量和期望的最大学习率作为输入。
执行命令,在单个 Kaggle GPU 上进行大约 26 秒的微调,得到了 0.334 的准确度。
获得了基准值后,我们想要找出是否差异化微调能够带来改善。我们首先通过使用 unfreeze
命令解冻所有层,然后使用 slice
方法指定学习率范围的上限和下限。这个命令将最接近输出的层的最大学习率设置为上限,并通过除以一个常数因子几何地减少每个后续层的最大学习率到下限。下面展示了执行这个操作的确切代码:
learn.unfreeze() ❶ learn.fit_one_cycle(1, slice(rate/100,rate)) ❷
❶ 确保所有层都解冻以进行微调
❷ 在最终层中的最佳率和比该最佳率小两个数量级的值之间以几何方式变化
从代码可以看出,我们任意选择了将学习率从最大最优值变化到比该值小两个数量级的值。这个调度背后的直觉是,随后的层包含的信息更为一般化,与任务无关,因此它应该比最接近输出的层从这个特定目标数据集中学到的更少。
执行所提出的差异化微调代码,得到了一个准确度分数为 0.353,明显优于我们在没有使用它时得到的 0.334 的值。使用以下命令保存微调后的语言模型以供以后使用:
learn.save_encoder('fine-tuned_language_model')
通过斜三角形学习率和差异化微调调整了我们的预训练语言模型后,让我们看看我们能得到多好的目标任务分类器——也就是虚假新闻检测器。我们在下一小节对微调后的语言模型之上微调一个分类器。
9.1.2 目标任务分类器微调
请回想在前一小节中,我们创建了一个用于目标任务分类器的数据消费对象。我们将这个变量称为data_clas
。作为微调我们的目标任务分类器的下一步,我们需要实例化一个分类器学习器的实例,方法恰当地命名为text_classifier_learner
,在 fast.ai 中。下面的代码完成了这一步:
learn = text_classifier_learner(data_clas, AWD_LSTM, drop_mult=0.3) ❶ learn.load_encoder('fine-tuned_language_model') ❷
❶ 实例化目标任务分类器学习的一个实例。使用我们微调过的语言模型相同的设置,因此我们可以无问题地加载。
❷ 载入我们微调过的语言模型
作为下一步,我们再次使用实用的 fast.ai 方法lr_find
来找到最佳学习率,使用以下代码:
learn.lr_find() ❶ learn.recorder.plot(suggestion=True) ❷
❶ 寻找最佳速率
❷ 绘制它
执行该代码得到的是图 9.2 中显示的损失与学习率曲线。
图 9.2 从 fast.ai 库获取目标任务分类器微调步骤中用于找到最佳学习率的结果的过程。通过几个学习率进行迭代,并选择最佳学习率,即在曲线上损失下降最快的点。
我们看到最佳速率约为 7e-4。我们使用倾斜三角形学习率,通过以下代码对分类器学习器进行一轮训练:
rate = learn.recorder.min_grad_lr ❶ learn.fit_one_cycle(1, rate) ❷
❶ 提取最佳的最大学习率
❷ 使用确定的最大学习率在倾斜三角形学习率计划中微调目标任务分类器
执行该代码得到的准确率约为 99.5%。这已经比我们在第六章(第 6.2 节)通过在 ELMo 嵌入之上训练分类器得到的 98%+的结果更好了。我们还能做些什么来进一步提高它呢?
幸运的是,我们还有一个底牌:渐进式解冻。再次提醒,这是当我们仅解冻一层,微调它,解冻一个额外的较低层,微调它,并重复此过程一定次数时。ULMFiT 的作者发现,在目标任务分类器阶段应用此方法显着改善了结果。举个简单的例子,要执行此过程直到 2 层深度,我们需要以下代码:
depth = 2 ❶ for i in range(1,depth+1): ❷ learn.freeze_to(-i) ❸ learn.fit_one_cycle(1, rate) ❹
❶ 我们仅执行渐进式解冻,直到解冻两个层为止。
❷ 逐渐解冻更多层,首先一个,然后两个,每次使用倾斜三角形学习率进行一轮训练
❸ 此命令解冻了顶部 i 层。
❹ 执行一次倾斜三角形学习率,如已经介绍的
请注意,命令 learn.freeze_to``(-i)
冻结前 i
层对于本次操作至关重要。在我们对虚假新闻检测示例上执行代码时,我们发现在第一步中准确性达到了 99.8%,当解冻了前两层时,准确性达到了惊人的 100%。这些结果充分说明了自己,似乎表明 ULMFiT 方法是一套非常有用的技术。请注意,如果有必要,我们可以继续解冻更深层次的层次——第 3 层,第 4 层等等。
奇妙的事情!看来在我们适应新场景时,聪明地调整模型可以带来显著的好处!在接下来的章节中,我们将介绍另一种实现这一点的方法——知识蒸馏。
9.2 知识蒸馏
知识蒸馏是一种神经网络压缩方法,旨在教授一个较小的学生模型大型教师模型所包含的知识。这种方法近年来在 NLP 社区中变得流行,本质上是试图通过学生来模仿教师的输出。此方法也与模型无关——教师和学生可以是基于变压器的、基于循环神经网络的或其他结构,并且彼此之间可以完全不同。
在 NLP 领域中,对此方法的最初应用是由于对双向 LSTM(bi-LSTMs)的表示能力与基于变压器的架构之间的比较的疑问。⁸ 作者想要知道单个 bi-LSTM 层是否能够捕捉到 BERT 的多少信息。令人惊讶的是,研究人员发现,在某些情况下,预训练的基于变压器的语言模型的参数数量可以减少 100 倍,推理时间可以减少 15 倍,同时不损失标准性能指标。这是一个巨大的尺寸和时间上的减少,可以决定这些方法是否可以实际部署!知识蒸馏的过程在图 9.3 中简要概述。
图 9.3 是知识蒸馏的一般过程的示意图。教师模型产生的“软”标签被用于通过蒸馏损失鼓励学生模型表现出类似的行为。同时,学生损失被训练成与通过学生损失的标准地面真实情况行为类似。
如图所示,传统上,教师产生的标签被用于计算“软”标签,通过与学生的输出进行比较来确定蒸馏损失。这种损失促使学生模型跟踪教师模型的输出。此外,学生还通过学生损失同时学习“硬”的真实标签。我们将通过 Hugging Face 的 transformers 库来快速展示如何使用这个想法实现。
已经提出了几种架构来减小预训练的 NLP 语言模型的尺寸,包括 TinyBERT ⁹ 和 DistilBERT. ¹⁰ 我们选择专注于 DistilBERT,因为它在 transformers 库中已经准备就绪。 DistilBERT 是由 Hugging Face 开发的,这是与编写 transformers 库相同的团队。 与以前一样,我们对这个主题的覆盖并不意味着是全面的,而是举例说明。 在像这样快速发展的领域中保持进一步开发和文献的更新仍然很重要。 我们希望这里所呈现的内容能让您做到这一点。
DistilBERT 研究的目标是特别生成 BERT 模型的较小版本。 学生架构被选择为与 BERT 相同-在第 7 和第八章中描述的堆叠变压器编码器。 学生的层数减少了一半,只有六层的模型。 这是大部分尺寸节省的地方。 作者发现在这种框架中,内部隐藏维度的变化对效率几乎没有影响,因此,在教师和学生之间都是相似的。 过程的一个重要部分是将学生初始化到适当的一组权重,从中收敛会相对较快。 因为教师和学生的所有层的尺寸都是相似的,作者可以简单地使用对应层中的预训练教师权重来初始化学生,并发现这样做效果良好。
作者对 GLUE 等基准进行了广泛的实验证明,我们将在下一章中看到,并在 SQuAD 上进行了实验证明。 他们发现,由结果产生的 DistilBERT 模型在 GLUE 基准上的性能保持了 BERT 教师模型的 97% ,但参数个数只有教师的 40%. 它在 CPU 上的推理时间也快了 60%,而在 iPhone 等移动设备上快了 71%。 如您所见,这是一项明显的改进。
执行实际蒸馏的脚本可在官方 transformers 存储库中找到。¹¹ 要训练自己的 DistilBERT 模型,你需要创建一个每行一个文本样本的文件,并执行该页面提供的一系列命令,这些命令准备数据并蒸馏模型。因为作者已经提供了各种检查点可供直接加载——所有检查点都列在了该页面上——而我们的重点是迁移学习,我们在这里不重复从头开始训练的步骤。相反,我们使用了一个类似于我们在第八章中用于跨语言迁移学习实验的 mBERT 检查点。这样可以直接比较使用蒸馏架构与原始 mBERT 的性能和好处,同时还教会你如何开始在自己的项目中使用这个架构。这也为你提供了另一个机会,即在自定义语料库上微调预训练的基于 transformer 的模型——直接修改具有不同架构、预训练检查点和自定义数据集的代码应该适用于你自己的用例。
更具体地说,我们将重复我们在第 8.2.2 节中进行的实验,即通过在来自 JW300 数据集的语料库上进行微调,将 mBERT 中包含的知识转移到单语 Twi 场景中。我们执行使用检查点中包含的预训练 tokenizer 的实验变体,而不是从头开始训练一个新的,为了简单起见。
9.2.1 使用预训练 tokenizer 将 DistilmBERT 转移到单语 Twi 数据
在本小节中,我们的目标是从一个在超过 100 种语言上训练过的模型中生成一个用于加纳语 Twi 的 DistilBERT 模型,不包括 Twi 在内。BERT 的多语言等效版本称为 mBERT;因此,DistilBERT 的多语言等效版本可预见地称为 DistilmBERT。这个 DistilmBERT 模型直接类比于我们在第八章中实验过的 mBERT 模型。我们当时发现,即使 Twi 没有包含在原始训练中,从这个检查点开始是有益的。在这里,我们基本上会复制相同的步骤序列,将每个 mBERT 实例替换为 DistilmBERT。这样可以直接比较两者,并因此直观地了解知识蒸馏的好处,同时学习如何在自己的项目中使用 DistilBERT。与之前一样,我们会在 JW300 数据集的单语 Twi 子集上对模型进行微调。¹²
我们首先初始化一个 DistilBERT tokenizer,使用 DistilmBERT 模型的预训练检查点。这次我们使用 cased 版本,如下所示:
from transformers import DistilBertTokenizerFast ❶ tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert-base-multilingual-cased") ❷
❶ 这只是 DistilBertTokenizer 的一个更快的版本,你可以用它来代替。
❷ 使用预训练的 DistilmBERT tokenizer
准备好 tokenizer 后,将 DistilmBERT 检查点加载到 DistilBERT 掩码语言模型中,并按照以下方式显示参数的数量:
from transformers import DistilBertForMaskedLM ❶ model = DistilBertForMaskedLM.from_pretrained("distilbert-base-multilingual-cased") ❷ print("Number of parameters in DistilmBERT model:") print(model.num_parameters())
❶ 使用掩码语言建模
❷ 初始化为 mBERT 检查点
输出表明,与我们在第八章中发现的 BERT 模型的 178.6 百万个参数相比,该模型具有 1.355 亿个参数。 因此,DistilBERT 模型的大小仅为等效 BERT 模型的 76%。
接下来,使用 transformers 中方便的 LineByLineTextDataset
方法从单语 Twi 文本构建数据集,具体方法如下所示:
from transformers import LineByLineTextDataset dataset = LineByLineTextDataset( tokenizer=tokenizer, file_path="../input/jw300entw/jw300.en-tw.tw", ❶ block_size=128) ❷
❶ 我们在第 8.2.1 节中介绍的英语到 Twi JW300 数据集
❷ 一次读取多少行
随后,按照下面的代码片段中所示的方式定义“数据集整理器”——这是一个帮助程序,它将一批样本数据行(长度为 block_size
)创建成一个特殊对象,这个特殊对象可以被 PyTorch 用于神经网络训练:
from transformers import DataCollatorForLanguageModeling data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15) ❶
❶ 使用掩码语言建模,并掩码单词的概率为 0.15
在这里,我们使用了掩码语言建模的方法——将我们输入数据中的 15% 的单词随机掩码,要求模型在训练过程中进行预测。
接下来,按照以下方式定义标准的训练参数,例如输出目录(我们选择为 twidistilmbert
)和训练批次大小:
from transformers import TrainingArguments training_args = TrainingArguments( output_dir="twidistilmbert", overwrite_output_dir=True, num_train_epochs=1, per_gpu_train_batch_size=16, save_total_limit=1, )
然后,使用已定义的数据集和数据整理器定义“训练器”,并在数据上进行一个训练时代,具体方法如下。请记住,Twi 数据包含超过 600,000 行,因此在所有数据上进行一遍训练是相当费力的!
trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=dataset, prediction_loss_only=True)
最后,按照以下方式进行训练并计算训练所需的时间:
import time start = time.time() trainer.train() end = time.time() print("Number of seconds for training:") print((end-start))
一如既往地,一定要保存模型:
trainer.save_model("twidistilmbert")
我们发现,与第八章中等效教师完成每个时代所需的 3 小时相比,该模型花费了大约 2 小时和 15 分钟完成该时代。 因此,学生的训练时间只有老师的 75%。 显著提高!
此外,损失函数的值达到了约 0.81,而 mBERT 的等效模型在第八章中的损失为约 0.77。就绝对值而言,性能差异可以粗略地量化为大约 5%——我们看到 DistilBERT 达到了 BERT 性能的 95%。 这非常接近 DistilBERT 作者在论文中报告的基准数字 97%。
最后一步,从语料库中取出以下句子:“Eyi de ɔhaw kɛse baa sukuu h*ɔ。” 掩盖一个单词,sukuu(在 Twi 中表示“学校”),然后将管道 API 应用于以下预测所删除的单词:
from transformers import pipeline fill_mask = pipeline( ❶ "fill-mask", model="twidistilmbert", tokenizer=tokenizer) print(fill_mask("Eyi de ɔhaw kɛse baa [MASK] hɔ.")) ❷
❶ 定义了填空管道
❷ 预测掩码标记
这会产生以下输出:
[{'sequence': '[CLS] Eyi de ɔhaw kɛse baa fie hɔ. [SEP]', 'score': 0.31311026215553284, 'token': 29959}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa me hɔ. [SEP]', 'score': 0.09322386980056763, 'token': 10911}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa ne hɔ. [SEP]', 'score': 0.05879712104797363, 'token': 10554}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa too hɔ. [SEP]', 'score': 0.052420321851968765, 'token': 16683}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa no hɔ. [SEP]', 'score': 0.04025224596261978, 'token': 10192}]
这确实是可信的完成。值得注意的是,我们在第 8.2.2 节中看到的结果中的宗教偏见似乎已经在模型中得到了缓解。像“以色列”和“伊甸园”这样的完成,在第 8.2.2 节的 mBERT 等价模型中建议,现在已经不再存在了。这可以通过两者之间参数数量的显著差异来解释。由于这一点,DistilBERT 不太可能过拟合,而 BERT 则更有可能这样做。
现在你知道如何在自己的项目中使用 DistilBERT 了!我们再次强调,你刚刚进行的练习教会了你如何在自定义语料库上微调预训练的基于 transformer 的模型——只需修改代码以应用于你自己的用例,包括不同的架构、预训练检查点和自定义数据集。
在下一章的第一节中,我们将有机会在英语中再次微调一个基于 transformer 的模型,这次是在自定义语料库上进行!我们将讨论 ALBERT 架构背后的适应性思想——一种轻量级的 BERT——并将其微调到来自 Multi-Domain Sentiment Dataset 的一些评论中。回想一下,在第四章我们玩过这个数据集。这是亚马逊 25 个产品类别的评论数据集,我们将重点关注书评,就像第四章一样。
第十章:ALBERT,适配器和多任务适配策略
本章介绍
- 对嵌入因子分解和层间参数共享进行应用
- 在多个任务上对 BERT 系列模型进行微调
- 将迁移学习实验分成多个步骤
- 对 BERT 系列模型应用适配器
在上一章中,我们开始介绍了到目前为止我们所涵盖的深度 NLP 迁移学习建模架构的一些适配策略。换句话说,给定一个预训练的架构,如 ELMo、BERT 或 GPT,如何更有效地进行迁移学习?我们涵盖了 ULMFiT 方法背后的两个关键思想,即区分性微调和逐渐解冻的概念。
我们在本章中将要讨论的第一个适配策略围绕着两个目标展开,旨在创建更有利于具有更大词汇量和更长输入长度的基于 transformer 的语言模型。第一个想法实质上涉及巧妙的因子分解,或者将更大的权重矩阵分解为两个较小的矩阵,使您可以增加一个的维度而不影响另一个的维度。第二个想法涉及在所有层之间共享参数。这两个策略是 ALBERT 方法的基础,即 A Lite BERT。我们使用 transformers 库中的实现来获得这种方法的一些实际经验。
在第四章中,我们介绍了多任务学习的概念,即模型被训练为同时执行多种任务。由此产生的模型通常对新场景更具泛化能力,并且可能导致更好的迁移效果。毫不奇怪,这个想法在预训练的 NLP 语言模型的适配策略的背景下再次出现。当面临转移场景时,没有足够的训练数据来微调给定任务时,为什么不在多个任务上进行微调呢?讨论这个想法为介绍(GLUE)数据集提供了一个很好的机会:一个包含了几个代表人类语言推理任务的数据集。这些任务包括检测句子之间的相似性、问题之间的相似性、释义、情感分析和问答。我们展示了如何利用 transformers 库快速进行多任务微调使用这个数据集。这个练习还演示了如何在一个来自这些重要问题类别的自定义数据集上类似地微调 BERT 系列模型。
在第四章中,我们还讨论了领域自适应,在那里我们发现源域和目标域的相似性对于迁移学习的有效性起着至关重要的作用。更大的相似性通常意味着更容易的迁移学习过程。当源和目标过于不相似时,你可能会发现在一个步骤中执行该过程是不可能的。在这种情况下,可以使用“顺序适应”的概念将整体所需的转移分解成更简单、更易管理的步骤。例如,一个语言工具在西非和东非之间无法转移,但可以先在西非和中非之间成功转移,然后在中非和东非之间转移成功。在本章中,我们将“填空”目标预训练 BERT 顺序适应到一个低资源句子相似度检测场景中,首先适应到一个数据丰富的问题相似度场景。
我们将探讨的最终适应策略是使用所谓的适应模块或适配器。这些是预训练神经网络层之间只有少量参数的新引入模块。对于新任务微调这个修改后的模型只需要训练这几个额外的参数。原始网络的权重保持不变。通常情况下,当每个任务只增加 3-4% 的额外参数时,与微调整个模型相比,性能几乎没有损失。这些适配器也是模块化的,并且很容易在研究人员之间共享。
10.1 嵌入因子分解和跨层参数共享
我们在本节讨论的适应策略围绕着两个想法,旨在创建具有更大词汇表和更长最大输入长度的基于 transformer 的语言模型。第一个想法基本上涉及将一个更大的权重矩阵巧妙地分解为两个较小的矩阵,使得其中一个可以在不影响另一个维度的情况下增加维度。第二个想法涉及在所有层之间共享参数。这两种策略是 ALBERT 方法的基础。我们再次使用 transformers 库中的实现来获取一些与该方法有关的实际经验。这既可以让你对所获得的改进有所了解,也可以让你有能力在自己的项目中使用它。我们将使用第四章中的 Multi-Domain Sentiment Dataset 中的亚马逊图书评论作为我们这次实验的自定义语料库。这将使您能够进一步体验在自定义语料库上微调预训练的基于 transformer 的语言模型,这次是用英语!
第一个策略,即嵌入因子分解,受到了观察的启发,即在 BERT 中,输入嵌入的大小与其隐藏层的维度密切相关。分词器为每个标记创建一个 one-hot 编码的向量——该向量在与标记对应的维度上等于 1,在其他维度上等于 0。这个 one-hot 编码向量的维度等于词汇表的大小,V。输入嵌入可以被看作是一个维度为V乘以E的矩阵,将 one-hot 编码的向量乘以它并投影到大小为E的维度中。在早期的模型(如 BERT)中,这等于隐藏层的维度H,因此这个投影直接发生在隐藏层中。
这意味着当隐藏层的大小增加时,输入嵌入的维度也必须增加,这可能非常低效。另一方面,ALBERT 的作者观察到,输入嵌入的作用是学习上下文无关的表示,而隐藏层的作用是学习上下文相关的表示——这是一个更难的问题。受此启发,他们提出将单一输入嵌入矩阵分成两个矩阵:一个是V乘以E,另一个是E乘以H,允许H和E完全独立。换句话说,one-hot 编码的向量可以首先投影到较小尺寸的中间嵌入中,然后再馈送到隐藏层。即使隐藏层的尺寸很大或需要扩展,这也使得输入嵌入可以具有显着较小的尺寸。仅此设计决策就导致将投影 one-hot 嵌入向量到隐藏层的矩阵/矩阵的尺寸减少了 80%。
第二个策略,即跨层参数共享,与我们在第四章中讨论的软参数共享多任务学习场景相关。在学习过程中,通过对它们施加适当的约束,鼓励所有层之间的相应权重彼此相似。这起到了正则化的效果,通过减少可用自由度的数量来降低过拟合的风险。这两种技术的结合使得作者能够构建出在当时(2020 年 2 月)超越了 GLUE 和 SQuAD 记录性能的预训练语言模型。与 BERT 相比,在参数大小上实现了约 90%的减少,而性能只有轻微的下降(在 SQuAD 上不到 1%)。
再次,因为多种检查点可用于直接加载,我们不在此重复从头开始的训练步骤,因为我们的重点是迁移学习。相反,我们使用类似于我们在前一章和第八章中用于我们的跨语言迁移学习实验的“基础”BERT 检查点。这使我们能够直接比较使用这种架构与原始 BERT 的性能和效益,并教你如何开始在自己的项目中使用这种架构。
10.1.1 在 MDSD 书评上对预训练的 ALBERT 进行微调
我们准备数据的步骤与第 4.4 节中的步骤相同,我们在此不再重复。这些步骤也在本书附带的 Kaggle 笔记本中重复出现。我们从列表 4.6 生成的变量data
开始。假设与第 4.4 节相同的超参数设置,这是一个由 2,000 本书评文本组成的 NumPy 数组。
使用以下代码将这个 NumPy 数组写入 Pandas 到文件中:
import pandas as pd train_df = pd.DataFrame(data=data) train_df.to_csv("albert_dataset.csv")
我们首先初始化一个 Albert 分词器,使用基本 ALBERT 模型中的预训练检查点,如下所示。我们使用版本 2 是因为它是目前可用的最新版本。你可以在 Hugging Face 网站上随时找到所有可用的 ALBERT 模型列表。⁶
from transformers import AlbertTokenizer ❶ tokenizer = AlbertTokenizer.from_pretrained("albert-base-v2") ❷
❶ 加载 ALBERT 分词器
❷ 使用预训练的 ALBERT 分词器
准备好分词器后,将基础 ALBERT 检查点加载到 ALBERT 遮盖语言模型中,并显示参数数量如下:
from transformers import AlbertForMaskedLM ❶ model = AlbertForMaskedLM.from_pretrained("albert-base-v2") ❷ print("Number of parameters in ALBERT model:") print(model.num_parameters())
❶ 使用遮盖语言建模
❷ 初始化到 ALBERT 检查点
输出表明模型有 1180 万个参数——与第八章的 BERT 的 178.6 万个参数和直接 BERT 的 135.5 万个参数相比,这是一个巨大的缩小。事实上,这是与 BERT 模型相比的 15 倍缩小。哇!
然后,像之前一样,使用 transformers 中提供的方便的LineByLineTextDataset
方法,使用单语 Twi 文本中的分词器构建数据集,如下所示:
from transformers import LineByLineTextDataset dataset = LineByLineTextDataset( tokenizer=tokenizer, file_path="albert_dataset.csv", block_size=128) ❶
❶ 每次读取多少行
定义一个“数据收集器”——一个帮助方法,将一个样本数据行批量(block_size
长度)创建成一个特殊对象——如下所示。这个特殊对象可以被 PyTorch 用于神经网络训练:
from transformers import DataCollatorForLanguageModeling data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15) ❶
❶ 使用遮盖语言建模,并用 0.15 的概率遮盖单词
在这里,我们使用了以 15%的概率对我们的输入数据进行随机遮盖的遮盖语言建模,并要求模型在训练过程中对它们进行预测。
定义标准的训练参数,如输出目录和训练批量大小,如下代码片段所示。注意,这一次我们训练 10 次,因为数据集比前一章中使用的超过 60,000 个单语 Twi 样本要小得多:
from transformers import Trainer, TrainingArguments training_args = TrainingArguments( output_dir="albert", overwrite_output_dir=True, num_train_epochs=10, per_gpu_train_batch_size=16, save_total_limit=1, )
然后,使用之前定义的数据集和整理器来定义一个“训练器”,以跨数据进行一个训练 epoch,如下所示:
trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=dataset, prediction_loss_only=True, )
按照以下步骤训练并计时训练时间:
import time start = time.time() trainer.train() end = time.time() print("Number of seconds for training:") print((end-start))
在这个小数据集上,10 个 epochs 大约只需约五分钟就能完成训练。损失值达到约 1。
按以下方式保存模型:
trainer.save_model("albert_fine-tuned")
最后,让我们按照以下步骤应用管道 API 来预测虚构书评中的遮蔽词:
from transformers import pipeline fill_mask = pipeline( ❶ "fill-mask", model="albert_fine-tuned", tokenizer=tokenizer ) print(fill_mask("The author fails to [MASK] the plot.")) ❷
❶ 定义填空管道
❷ 预测遮蔽的标记
这产生了以下非常合理的输出:
[{'sequence': '[CLS] the author fails to describe the plot.[SEP]', 'score': 0.07632581889629364, 'token': 4996}, {'sequence': '[CLS] the author fails to appreciate the plot.[SEP]', 'score': 0.03849967569112778, 'token': 8831}, {'sequence': '[CLS] the author fails to anticipate the plot.[SEP]', 'score': 0.03471902385354042, 'token': 27967}, {'sequence': '[CLS] the author fails to demonstrate the plot.[SEP]', 'score': 0.03338927403092384, 'token': 10847}, {'sequence': '[CLS] the author fails to identify the plot.[SEP]', 'score': 0.032832834869623184, 'token': 5808}]
到目前为止,您可能已经观察到,我们在此处对自定义书评语料库对 ALBERT 进行微调的步骤序列与我们在上一章中使用 DistilBERT 的步骤序列非常相似。这一系列步骤反过来又与我们在第八章中使用的 mBERT 的步骤序列非常相似。我们再次强调,这个配方可以用作 transformers 中几乎任何其他架构的蓝图。虽然我们无法提供在每种可能的应用类型上微调的示例,但这个配方应该可以推广,或者至少作为许多用例的良好起点。例如,考虑一种情况,您想要教 GPT-2 以某种选择的风格写作。只需复制我们在这里使用的相同代码,将数据集路径指向您选择的写作风格的语料库,并将标记器和模型引用从 AlbertTokenizer
/ AlbertForMaskedLM
更改为 GPT2Tokenizer
/ GPT2LMHeadModel
。
需要注意的一点是,所有 PyTorch transformers 模型默认情况下都会解冻所有层进行训练。要冻结所有层,您可以执行以下代码片段:
for param in model.albert.parameters(): param.requires_grad = False
您可以使用类似的代码片段仅冻结一些参数。
在下一节中,我们将讨论多任务微调,我们将有另一个机会来看看这些类型模型的微调,这次是针对各种任务。
面向自然语言处理的迁移学习(三)(4)https://developer.aliyun.com/article/1522493