当你阅读这些文字时,一个奇迹正在发生:这页上的涂鸦正在在你的大脑皮层中转化为单词、概念和情感。我的 2021 年 11 月的思想现在已成功侵入你的大脑。如果它们设法引起你的注意并在这个严酷而竞争激烈的环境中存活足够长的时间,它们可能有机会再次通过你与他人分享这些思想而繁殖。由于语言,思想已经变得空气中传播并且高度传染的大脑细菌——而且没有疫苗即将出现。
幸运的是,大多数大脑细菌是无害的,而一些则非常有用。事实上,人类的大脑细菌构成了我们最宝贵的两个财富:知识和文化。就像我们没有健康的肠道细菌就无法正常消化一样,没有健康的大脑细菌,我们也无法正常思考。你的大部分想法实际上并不是你自己的:它们在感染你之前在许多其他大脑中产生、生长和演变。因此,如果我们想要构建智能机器,我们将需要找到一种方法来感染它们。
好消息是,在过去几年中,另一个奇迹已经在发生:深度学习的几项突破为强大的语言模型奠定了基础。由于你正在阅读这本书,你可能已经看到了一些令人惊讶的这些语言模型的演示,比如 GPT-3,它可以根据“一只青蛙遇到了一只鳄鱼”这样的简短提示来写一个完整的故事。虽然它还不是莎士比亚,但有时很难相信这些文本是由人工神经网络写的。事实上,GitHub 的 Copilot 系统正在帮助我写这些文字:你永远不会知道我真正写了多少。
这一革命远不止于文本生成。它涵盖了自然语言处理(NLP)的整个领域,从文本分类到摘要、翻译、问答、聊天机器人、自然语言理解(NLU)等等。无论何处有语言、语音或文本,都有 NLP 的应用。你已经可以向你的手机询问明天的天气,或者与虚拟帮助台助手聊天以解决问题,或者从似乎真正理解你的查询的搜索引擎中获得有意义的结果。但是这项技术是如此新颖,以至于最好的可能还在未来。
像大多数科学进步一样,NLP 领域的这一最新革命也是建立在数百位无名英雄的辛勤工作之上的。但是它的成功有三个关键因素是显而易见的:
在大多数项目中,你可能无法访问一个庞大的数据集来从头开始训练模型。幸运的是,通常可以下载一个在通用数据集上预训练的模型:然后你所需要做的就是在自己的(小得多的)数据集上进行微调。自 2010 年代初以来,在图像处理领域,预训练已经成为主流,但在自然语言处理(NLP)领域,它仅限于无上下文的词嵌入(即,个别词的密集向量表示)。例如,“bear”这个词在“teddy bear”和“to bear”中具有相同的预训练嵌入。然后,在 2018 年,几篇论文提出了完整的语言模型,可以在各种 NLP 任务中进行预训练和微调;这彻底改变了游戏规则。
模型中心,如 Hugging Face 的,也是一个改变游戏规则的存在。在早期,预训练模型随处可见,所以很难找到你需要的东西。墨菲定律保证了 PyTorch 用户只能找到 TensorFlow 模型,反之亦然。而且,当你找到一个模型时,弄清楚如何对其进行微调并不总是容易的。这就是 Hugging Face 的 Transformers 库发挥作用的地方:它是开源的,支持 TensorFlow 和 PyTorch,而且可以轻松地从 Hugging Face Hub 下载最先进的预训练模型,为你的任务配置它,对你的数据集进行微调,并进行评估。该库的使用量正在迅速增长:2021 年第四季度,它被超过五千家组织使用,并且每月使用pip安装超过四百万次。此外,该库及其生态系统正在扩展到 NLP 之外:图像处理模型也可用。您还可以从 Hub 下载大量数据集来训练或评估您的模型。
那么,你还能要求什么呢?嗯,就是这本书!它是由 Hugging Face 的开源开发人员撰写的,包括 Transformers 库的创造者!——这一点可以看出:你将在这些页面中找到的信息的广度和深度令人震惊。它涵盖了从 Transformer 架构本身到 Transformers 库及其周围整个生态系统的一切。我特别欣赏这种实践方法:你可以在 Jupyter 笔记本中跟着做,所有的代码示例都直截了当,易于理解。作者在训练非常大的 Transformer 模型方面拥有丰富的经验,并提供了大量的技巧和窍门,以确保一切都能高效运行。最后但同样重要的是,他们的写作风格直接而生动:读起来就像一部小说。
简而言之,我非常喜欢这本书,我相信你也会喜欢。任何对使用最先进的语言处理功能构建产品感兴趣的人都需要阅读它。它充满了所有正确的大脑细菌!
Aurélien Géron
2021 年 11 月,新西兰奥克兰
那么,是什么让 Transformer 几乎一夜之间改变了这个领域?就像许多伟大的科学突破一样,它是几个想法的综合,比如注意力、迁移学习和扩展神经网络,它们当时正在研究界中酝酿。
以下资源为本书涵盖的主题提供了良好的基础。我们假设您的技术知识大致在它们的水平上:
使用 Scikit-Learn 和 TensorFlow 进行实践性机器学习,作者 Aurélien Géron(O'Reilly)
使用 fastai 和 PyTorch 进行深度学习,作者 Jeremy Howard 和 Sylvain Gugger(O'Reilly)
使用 PyTorch 进行自然语言处理,作者 Delip Rao 和 Brian McMahan(O'Reilly)
本书的目标是让您能够构建自己的语言应用程序。为此,它侧重于实际用例,并只在必要时深入理论。本书的风格是实践性的,我们强烈建议您通过运行代码示例来进行实验。
本书涵盖了 NLP 中 Transformer 的所有主要应用,每一章(有少数例外)都专门致力于一个任务,结合一个现实的用例和数据集。每一章还介绍了一些额外的概念。以下是我们将要涵盖的任务和主题的高级概述:
第一章,你好,Transformer,介绍了 Transformer 并将其置于上下文中。它还介绍了 Hugging Face 生态系统。
第二章,“文本分类”,侧重于情感分析的任务(一种常见的文本分类问题),并介绍了TrainerAPI。
第三章,“Transformer 解剖”,更深入地探讨了 Transformer 架构,为接下来的章节做准备。
第四章,“多语言命名实体识别”,专注于在多种语言文本中识别实体的任务(一个标记分类问题)。
第五章,“文本生成”,探讨了 Transformer 模型生成文本的能力,并介绍了解码策略和度量标准。
第六章,“摘要”,深入探讨了文本摘要的复杂序列到序列任务,并探索了用于此任务的度量标准。
第八章,“使 Transformer 在生产中更高效”,侧重于模型性能。我们将研究意图检测的任务(一种序列分类问题),并探索知识蒸馏、量化和修剪等技术。
第九章,“处理少量或没有标签的数据”,探讨了在缺乏大量标记数据的情况下改善模型性能的方法。我们将构建一个 GitHub 问题标记器,并探索零样本分类和数据增强等技术。
第十章,“从头开始训练 Transformer”,向您展示了如何从头开始构建和训练一个用于自动完成 Python 源代码的模型。我们将研究数据集流和大规模训练,并构建我们自己的分词器。
第十一章,“未来方向”,探讨了 Transformer 面临的挑战,以及这一领域的研究正在进行的一些令人兴奋的新方向。
有了这些工具,您几乎可以解决任何自然语言处理挑战!
由于本书的实践性方法,我们强烈建议您在阅读每一章时运行代码示例。由于我们涉及 Transformer,您需要访问一台配备 NVIDIA GPU 的计算机来训练这些模型。幸运的是,有几种免费的在线选项可供您使用,包括:
我们大部分章节都是使用 NVIDIA Tesla P100 GPU 开发的,它们有 16GB 的内存。一些免费平台提供的 GPU 内存较少,因此在训练模型时可能需要减少批处理大小。
本书使用以下排版约定:
斜体
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
常量宽度
用于程序清单,以及在段落中引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
常量宽度粗体
显示用户应该按照字面意思输入的命令或其他文本。
常量宽度斜体
显示应由用户提供的值或由上下文确定的值替换的文本。
这个元素表示提示或建议。
这个元素表示一般说明。
这个元素表示警告或注意。
感谢 Janine,在这漫长的一年里,有许多深夜和忙碌的周末,你的耐心和鼓励支持。
我首先要感谢 Lewis 和 Leandro 提出了这本书的想法,并强烈推动以如此美丽和易于访问的格式出版。我还要感谢所有 Hugging Face 团队,他们相信 AI 是一个社区努力的使命,以及整个 NLP/AI 社区,他们与我们一起构建和使用本书中描述的库和研究。
我们所建立的不仅仅是重要的,我们所走过的旅程才是真正重要的,我们有幸能够与成千上万的社区成员和像你们今天一样的读者一起走这条路。衷心感谢你们所有人。
¹ NLP 研究人员倾向于以Sesame Street中的角色命名他们的创作。我们将在第一章中解释所有这些首字母缩略词的含义。
2017 年,谷歌的研究人员发表了一篇关于序列建模的新型神经网络架构的论文。这种被称为Transformer的架构在机器翻译任务中优于循环神经网络(RNN),无论是翻译质量还是训练成本。
与此同时,一种名为 ULMFiT 的有效迁移学习方法表明,对非常大和多样化的语料库进行长短期记忆(LSTM)网络训练可以产生具有很少标记数据的最先进文本分类器。
这些进步是当今两个最著名的 Transformer 的催化剂:生成式预训练 Transformer(GPT)和来自 Transformer 的双向编码器表示(BERT)。通过将 Transformer 架构与无监督学习相结合,这些模型消除了需要从头开始训练特定任务的架构,并在 NLP 几乎每个基准测试中取得了显著的突破。自 GPT 和 BERT 发布以来,出现了一系列 Transformer 模型;最突出的条目的时间表如图 1-1 所示。
但我们正在走得太快了。要理解 Transformer 的新颖之处,我们首先需要解释:
编码器-解码器框架
注意机制
迁移学习
在本章中,我们将介绍支持 transformer 广泛应用的核心概念,参观一些它们擅长的任务,并最后看一下 Hugging Face 工具和库的生态系统。
让我们首先探讨编码器-解码器框架和在 transformer 崛起之前的架构。
在 transformer 之前,像 LSTM 这样的循环架构是自然语言处理中的最新技术。这些架构在网络连接中包含反馈循环,允许信息从一个步骤传播到另一个步骤,使它们非常适合对文本等序列数据进行建模。如图 1-2 左侧所示,RNN 接收一些输入(可以是单词或字符),通过网络传递,并输出一个称为隐藏状态的向量。同时,模型通过反馈循环向自身反馈一些信息,然后在下一步中使用。如果我们像图 1-2 右侧所示“展开”循环,就可以更清楚地看到这一点:RNN 在每一步中传递其状态的信息给序列中的下一个操作。这使得 RNN 能够跟踪先前步骤的信息,并将其用于输出预测。
RNN 在机器翻译系统的发展中发挥了重要作用,其目标是将一种语言中的单词序列映射到另一种语言。这种任务通常使用编码器-解码器或序列到序列架构,适用于输入和输出都是任意长度序列的情况。编码器的工作是将输入序列的信息编码成通常称为最后隐藏状态的数值表示。然后将该状态传递给解码器,解码器生成输出序列。
一般来说,编码器和解码器组件可以是任何能够建模序列的神经网络架构。这在图 1-3 中对一对 RNNs 进行了说明,其中英语句子“Transformers are great!”被编码为一个隐藏状态向量,然后解码以生成德语翻译“Transformer sind grossartig!”输入单词依次通过编码器,输出单词从上到下逐个生成。
尽管其简洁而优雅,这种架构的一个弱点是编码器的最终隐藏状态创建了一个信息瓶颈:它必须代表整个输入序列的含义,因为这是解码器在生成输出时所能访问的全部内容。这对于长序列尤其具有挑战性,因为序列开头的信息可能在压缩到单一固定表示的过程中丢失。
幸运的是,通过允许解码器访问编码器的所有隐藏状态,可以摆脱这一瓶颈。这一般机制称为注意力,⁶,它是许多现代神经网络架构的关键组成部分。了解注意力是如何为 RNNs 开发的将使我们能够更好地理解 Transformer 架构的主要构建模块之一。让我们深入了解一下。
注意力的主要思想是,编码器不是为输入序列产生单个隐藏状态,而是在每一步输出一个解码器可以访问的隐藏状态。然而,同时使用所有状态会为解码器创建一个巨大的输入,因此需要一些机制来优先使用哪些状态。这就是注意力的作用:它允许解码器在每个解码时间步为每个编码器状态分配不同数量的权重或“注意力”。这个过程在图 1-4 中进行了说明,显示了注意力在预测输出序列的第三个标记时的作用。
通过关注每个时间步最相关的输入标记,这些基于注意力的模型能够学习生成翻译中的单词与源句中的单词之间的非平凡对齐。例如,图 1-5 可视化了英语到法语翻译模型的注意权重,其中每个像素表示一个权重。该图显示了解码器如何能够正确对齐两种语言中顺序不同的单词“zone”和“Area”。
尽管注意力使得翻译质量大大提高,但使用循环模型作为编码器和解码器仍然存在一个主要缺点:计算是固有的顺序性的,无法在输入序列上并行化。
使用 transformer 引入了一种新的建模范式:完全放弃循环,而是完全依赖一种称为自注意力的特殊形式的注意力。我们将在第三章中更详细地介绍自注意力,但基本思想是允许注意力作用于神经网络同一层中的所有状态。这在图 1-6 中显示,编码器和解码器都有自己的自注意机制,其输出被馈送到前馈神经网络(FF NNs)。这种架构可以比循环模型快得多地训练,并为自然语言处理中的许多最新突破铺平了道路。
在原始 Transformer 论文中,翻译模型是从头开始在各种语言的大量句对语料库上进行训练的。然而,在 NLP 的许多实际应用中,我们无法获得大量标记文本数据来训练我们的模型。要启动 transformer 革命,还缺少最后一块拼图:迁移学习。
如今,在计算机视觉中,通常使用迁移学习来训练卷积神经网络(如 ResNet)进行一个任务的训练,然后在新任务上对其进行调整或微调。这允许网络利用从原始任务中学到的知识。在架构上,这涉及将模型分为主体和头部,其中头部是一个特定任务的网络。在训练过程中,主体的权重学习源域的广泛特征,并使用这些权重来初始化新任务的新模型。⁷与传统监督学习相比,这种方法通常能够在各种下游任务上更有效地训练高质量的模型,并且需要更少的标记数据。图 1-7 显示了这两种方法的比较。
尽管迁移学习已成为计算机视觉中的标准方法,但多年来,对于自然语言处理的类似预训练过程并不清楚。因此,NLP 应用通常需要大量标记数据才能实现高性能。即使如此,其性能也无法与视觉领域所实现的性能相媲美。
在 2017 年和 2018 年,几个研究小组提出了新的方法,最终使得迁移学习在自然语言处理中起作用。这始于 OpenAI 研究人员的洞察,他们通过使用从无监督预训练中提取的特征,在情感分类任务上取得了强大的性能。⁸接着是 ULMFiT,它引入了一个通用框架,用于调整预训练的 LSTM 模型以适应各种任务。⁹
如图 1-8 所示,ULMFiT 包括三个主要步骤:
预训练
初始的训练目标非常简单:根据先前的单词预测下一个单词。这个任务被称为语言建模。这种方法的优雅之处在于不需要标记的数据,并且可以利用来自维基百科等来源的大量可用文本。¹⁰
领域适应
一旦语言模型在大规模语料库上预训练,下一步就是将其适应于领域语料库(例如,从维基百科到 IMDb 电影评论的语料库,如图 1-8 所示)。这个阶段仍然使用语言建模,但现在模型必须预测目标语料库中的下一个单词。
微调
通过引入 NLP 中的预训练和迁移学习的可行框架,ULMFiT 提供了使 Transformer 起飞的缺失环节。2018 年,发布了两个将自注意力与迁移学习相结合的 Transformer:
GPT
仅使用 Transformer 架构的解码器部分,以及 ULMFiT 相同的语言建模方法。GPT 是在 BookCorpus 上预训练的,¹¹其中包括来自各种流派的 7000 本未发表的书籍,包括冒险、奇幻和浪漫。
BERT
使用 Transformer 架构的编码器部分,以及一种特殊形式的语言建模称为掩码语言建模。掩码语言建模的目标是预测文本中随机掩码的单词。例如,给定一个句子“我看着我的[MASK],看到[MASK]迟到了。”模型需要预测由[MASK]表示的掩码单词的最可能的候选项。BERT 是在 BookCorpus 和英文维基百科上预训练的。
GPT 和 BERT 在各种 NLP 基准测试中树立了新的技术水平,并开启了 Transformer 时代。
将新的机器学习架构应用于新任务可能是一个复杂的过程,通常涉及以下步骤:
在代码中实现模型架构,通常基于 PyTorch 或 TensorFlow。
从服务器加载预训练的权重(如果可用)。
预处理输入,将其通过模型,并应用一些特定于任务的后处理。
实现数据加载器,并定义损失函数和优化器来训练模型。
每个步骤都需要为每个模型和任务编写自定义逻辑。传统上(但并非总是如此!),当研究小组发布新文章时,他们通常会连同模型权重一起发布代码。然而,这些代码很少是标准化的,通常需要数天的工程来适应新的用例。
每个 NLP 任务都始于一段文本,比如以下关于某个在线订单的虚构客户反馈:
现在我们有了我们的管道,让我们生成一些预测!每个管道都将一个文本字符串(或字符串列表)作为输入,并返回一系列预测。每个预测都是一个 Python 字典,所以我们可以使用 Pandas 将它们漂亮地显示为DataFrame:
在这种情况下,模型非常确信文本具有消极情绪,这是有道理的,因为我们正在处理一个愤怒客户的投诉!请注意,对于情感分析任务,管道只返回POSITIVE或NEGATIVE标签中的一个,因为另一个可以通过计算1-score来推断。
现在让我们来看另一个常见的任务,识别文本中的命名实体。
预测客户反馈的情绪是一个很好的第一步,但通常您想知道反馈是否是关于特定的项目或服务。在自然语言处理中,像产品、地点和人这样的现实世界对象被称为命名实体,从文本中提取它们被称为命名实体识别(NER)。我们可以通过加载相应的管道并将我们的客户评论提供给它来应用 NER:
您可以看到管道检测到了所有实体,并为每个实体分配了类别,例如ORG(组织)、LOC(位置)或PER(人)。在这里,我们使用了aggregation_strategy参数根据模型的预测对单词进行分组。例如,实体“奥普蒂默斯·普莱姆”由两个单词组成,但被分配了一个单一的类别:MISC(杂项)。分数告诉我们模型对其识别的实体有多自信。我们可以看到它对“欺诈者”和“Megatron”的第一次出现的识别最不自信,它们都未能被分组为单个实体。
看到上一个表格中word列中的奇怪的井号符号(#)了吗?这些是模型的分词器产生的,它将单词分割成称为标记的原子单位。您将在第二章中学习有关标记化的所有内容。
提取文本中的所有命名实体是不错的,但有时我们想提出更有针对性的问题。这就是我们可以使用问答的地方。
在问答中,我们向模型提供了一段文本,称为上下文,以及一个我们想要提取答案的问题。然后模型返回对应于答案的文本范围。让我们看看当我们针对客户反馈提出具体问题时会得到什么:
我们可以看到,除了答案之外,管道还返回了start和end整数,这些整数对应于找到答案跨度的字符索引(就像 NER 标记一样)。我们将在第七章中调查几种问答的变体,但这种特定的称为抽取式问答,因为答案直接从文本中提取。
文本摘要的目标是将长文本作为输入,并生成一个包含所有相关事实的简短版本。这比以前的任务要复杂得多,因为它要求模型生成连贯的文本。现在应该是一个熟悉的模式,我们可以像下面这样实例化一个摘要管道:
这个摘要还不错!虽然原始文本的部分被复制,但模型能够捕捉到问题的本质,并正确识别“大黄蜂”(出现在末尾)是投诉的作者。在这个例子中,您还可以看到我们传递了一些关键字参数,如max_length和clean_up_tokenization_spaces给管道;这些允许我们在运行时调整输出。
但是当您收到一种您不懂的语言的反馈时会发生什么?您可以使用谷歌翻译,或者您可以使用自己的转换器为您翻译!
与摘要类似,翻译是一个输出生成文本的任务。让我们使用翻译管道将英文文本翻译成德文:
同样,该模型产生了一个非常好的翻译,正确使用了德语的正式代词,如“Ihrem”和“Sie”。在这里,我们还展示了如何覆盖管道中的默认模型,以选择最适合您应用的模型——您可以在 Hugging Face Hub 上找到成千上万种语言对的模型。在我们退后一步,看看整个 Hugging Face 生态系统之前,让我们再看一个应用。
假设您希望能够通过访问自动完成功能更快地回复客户反馈。使用文本生成模型,您可以这样做:
好吧,也许我们不想使用这个完成来安抚大黄蜂,但您大致明白了。
现在您已经看到了一些转换器模型的很酷的应用,您可能想知道训练是在哪里进行的。本章中使用的所有模型都是公开可用的,并且已经针对手头的任务进行了微调。然而,一般来说,您可能希望在自己的数据上微调模型,在接下来的章节中,您将学习如何做到这一点。
正如前面所述,迁移学习是推动转换器成功的关键因素之一,因为它使得可以重用预训练模型来处理新任务。因此,能够快速加载预训练模型并进行实验至关重要。
Hugging Face Hub 托管了超过 20,000 个免费可用的模型。如图 1-10 所示,有任务、框架、数据集等过滤器,旨在帮助您浏览 Hub 并快速找到有前途的候选模型。正如我们在管道中看到的那样,在您的代码中加载一个有前途的模型实际上只是一行代码的距离。这使得尝试各种模型变得简单,并且让您可以专注于项目的领域特定部分。
除了模型权重,Hub 还托管数据集和用于计算指标的脚本,这些脚本可以让您重现已发布的结果或利用额外的数据进行应用。
Hub 还提供模型和数据集 卡片,以记录模型和数据集的内容,并帮助您对是否适合您做出明智的决定。Hub 最酷的功能之一是,您可以通过各种特定任务的交互式小部件直接尝试任何模型,如图 1-11 所示。
让我们继续我们的旅程与 Tokenizers。
PyTorch 和 TensorFlow 也提供了自己的中心,并且值得检查,如果 Hugging Face Hub 上没有特定的模型或数据集。
在本章中我们看到的每个管道示例背后都有一个标记化步骤,将原始文本分割成称为标记的较小部分。我们将在第二章中详细介绍这是如何工作的,但现在理解标记可能是单词、单词的部分,或者只是标点符号等字符就足够了。Transformer 模型是在这些标记的数值表示上进行训练的,因此正确进行这一步对整个 NLP 项目非常重要!
Tokenizers 提供许多标记化策略,并且由于其 Rust 后端,它在标记化文本方面非常快速。它还负责所有的预处理和后处理步骤,例如规范化输入和将模型输出转换为所需的格式。使用 Tokenizers,我们可以像加载预训练模型权重一样加载标记器。
我们需要一个数据集和指标来训练和评估模型,所以让我们来看看负责这方面的数据集。
加载、处理和存储数据集可能是一个繁琐的过程,特别是当数据集变得太大而无法容纳在您的笔记本电脑的 RAM 中时。此外,通常需要实现各种脚本来下载数据并将其转换为标准格式。
Datasets 通过为数千个数据集提供标准接口来简化这个过程。它还提供智能缓存(这样您就不必每次运行代码时都重新进行预处理),并通过利用一种称为内存映射的特殊机制来避免 RAM 限制,该机制将文件的内容存储在虚拟内存中,并使多个进程更有效地修改文件。该库还与流行的框架如 Pandas 和 NumPy 兼容,因此您无需离开您喜爱的数据整理工具的舒适区。
然而,如果您无法可靠地衡量性能,拥有一个好的数据集和强大的模型是毫无价值的。不幸的是,经典的 NLP 指标有许多不同的实现,可能会略有不同,并导致误导性的结果。通过提供许多指标的脚本,Datasets 有助于使实验更具再现性,结果更值得信赖。
有了 Transformers、Tokenizers 和 Datasets 库,我们有了训练自己的 Transformer 模型所需的一切!然而,正如我们将在第十章中看到的那样,有些情况下我们需要对训练循环进行细粒度控制。这就是生态系统中最后一个库发挥作用的地方:Accelerate。
如果您曾经不得不在 PyTorch 中编写自己的训练脚本,那么当尝试将在笔记本电脑上运行的代码移植到组织的集群上运行时,可能会遇到一些头痛。Accelerate 为您的正常训练循环添加了一层抽象,负责处理训练基础设施所需的所有自定义逻辑。这实际上通过简化必要时的基础设施更改来加速您的工作流程。
这总结了 Hugging Face 开源生态系统的核心组件。但在结束本章之前,让我们看一看在尝试在现实世界中部署 Transformer 时会遇到的一些常见挑战。
在本章中,我们已经对可以使用 Transformer 模型解决的各种自然语言处理任务有了一瞥。阅读媒体头条时,有时会觉得它们的能力是无限的。然而,尽管它们很有用,Transformer 远非灵丹妙药。以下是与它们相关的一些挑战,我们将在整本书中探讨:
语言
自然语言处理研究主要以英语为主导。还有一些其他语言的模型,但很难找到稀有或低资源语言的预训练模型。在第四章中,我们将探讨多语言 Transformer 及其进行零-shot 跨语言转移的能力。
数据可用性
尽管我们可以使用迁移学习丨大大减少模型需要的标记训练数据量,但与人类执行任务所需的量相比,仍然是很多。解决标记数据很少或没有的情况是第九章的主题。
处理长文档
自注意力在段落长的文本上效果非常好,但当我们转向整个文档等更长的文本时,成本就会变得非常高。我们将在第十一章中讨论缓解这一问题的方法。
不透明性
与其他深度学习模型一样,Transformer 在很大程度上是不透明的。很难或不可能解开模型为何做出某种预测的“原因”。当这些模型被部署用于做出关键决策时,这是一个特别困难的挑战。我们将在第二章和第四章中探讨一些探究 Transformer 模型错误的方法。
偏见
Transformer 模型主要是在互联网文本数据上进行预训练的。这会将数据中存在的所有偏见都印刻到模型中。确保这些偏见既不是种族主义的、性别歧视的,也不是更糟糕的,是一项具有挑战性的任务。我们将在第十章中更详细地讨论其中一些问题。
尽管令人生畏,许多这些挑战是可以克服的。除了特定提到的章节外,我们将在接下来的几乎每一章中涉及这些主题。
希望到目前为止,您已经对如何开始训练和将这些多才多艺的模型集成到您自己的应用程序中感到兴奋!您在本章中已经看到,只需几行代码,您就可以使用最先进的模型进行分类、命名实体识别、问答、翻译和摘要,但这实际上只是“冰山一角”。
在接下来的章节中,您将学习如何将 Transformer 适应于各种用例,比如构建文本分类器,或者用于生产的轻量级模型,甚至从头开始训练语言模型。我们将采取实践方法,这意味着对于每个涵盖的概念,都会有相应的代码,您可以在 Google Colab 或您自己的 GPU 机器上运行。
现在我们已经掌握了 Transformer 背后的基本概念,是时候开始动手进行我们的第一个应用了:文本分类。这是下一章的主题!
权重是神经网络的可学习参数。
这对于英语而言更为真实,而对于世界上大多数语言来说,获取大规模的数字化文本语料库可能会很困难。找到弥合这一差距的方法是自然语言处理研究和活动的一个活跃领域。
在第二章中,我们看到了微调和评估 Transformer 所需的内容。现在让我们来看看它们在内部是如何工作的。在本章中,我们将探索 Transformer 模型的主要构建模块以及如何使用 PyTorch 实现它们。我们还将提供如何在 TensorFlow 中进行相同操作的指导。我们首先将专注于构建注意力机制,然后添加必要的部分来使 Transformer 编码器工作。我们还将简要介绍编码器和解码器模块之间的架构差异。通过本章结束时,您将能够自己实现一个简单的 Transformer 模型!
本章还介绍了 Transformer 的分类法,以帮助您了解近年来出现的各种模型。在深入代码之前,让我们先来看一下开启 Transformer 革命的原始架构的概述。
正如我们在第一章中所看到的,原始 Transformer 基于“编码器-解码器”架构,这种架构被广泛用于诸如机器翻译之类的任务,其中一个词序列被翻译成另一种语言。这种架构由两个组件组成:
编码器
将输入的令牌序列转换为嵌入向量序列,通常称为“隐藏状态”或“上下文”
解码器
使用编码器的隐藏状态迭代地生成一个令牌序列的输出,每次生成一个令牌
如图 3-1 所示,编码器和解码器本身由几个构建模块组成。
我们很快将详细查看每个组件,但我们已经可以在图 3-1 中看到一些特征,这些特征表征了 Transformer 架构:
输入文本被标记化,并使用我们在第二章中遇到的技术转换为“令牌嵌入”。由于注意力机制不知道令牌的相对位置,我们需要一种方法将一些关于令牌位置的信息注入到输入中,以建模文本的顺序性质。因此,令牌嵌入与包含每个令牌的位置信息的“位置嵌入”相结合。
编码器由一堆“编码器层”或“块”组成,类似于在计算机视觉中堆叠卷积层。解码器也是如此,它有自己的一堆“解码器层”。
编码器的输出被馈送到每个解码器层,然后解码器生成一个最可能的下一个令牌序列的预测。这一步的输出然后被反馈到解码器中以生成下一个令牌,依此类推,直到达到特殊的序列结束(EOS)令牌。在图 3-1 的示例中,想象一下解码器已经预测了“Die”和“Zeit”。现在它将这两个作为输入以及所有编码器的输出来预测下一个令牌,“fliegt”。在下一步中,解码器获得“fliegt”作为额外的输入。我们重复这个过程,直到解码器预测出 EOS 令牌或者达到最大长度。
Transformer 架构最初是为机器翻译等序列到序列任务设计的,但编码器和解码器块很快被改编为独立的模型。虽然有数百种不同的 transformer 模型,但它们大多属于以下三种类型之一:
仅编码器
这些模型将文本输入序列转换为丰富的数值表示,非常适合文本分类或命名实体识别等任务。BERT 及其变种,如 RoBERTa 和 DistilBERT,属于这类架构。在这种架构中,对于给定标记的表示取决于左侧(标记之前)和右侧(标记之后)的上下文。这经常被称为双向注意力。
仅解码器
给定一个文本提示,比如“谢谢午餐,我吃了一个...”,这些模型将通过迭代预测最有可能的下一个词来自动完成序列。GPT 模型系列属于这一类。在这种架构中,对于给定标记的表示仅取决于左侧上下文。这经常被称为因果或自回归注意力。
编码器-解码器
这些模型用于对一个文本序列到另一个文本序列的复杂映射进行建模;它们适用于机器翻译和摘要任务。除了我们已经看到的 Transformer 架构,它结合了编码器和解码器,BART 和 T5 模型也属于这一类。
实际上,解码器-仅模型与仅编码器模型的应用区别有些模糊。例如,像 GPT 系列中的解码器-仅模型可以被用于传统上被认为是序列到序列任务的翻译等任务。同样,像 BERT 这样的仅编码器模型可以应用于通常与编码器-解码器或仅解码器模型相关的摘要任务。¹
现在您已经对 Transformer 架构有了高层次的理解,让我们更仔细地看看编码器的内部工作。
正如我们之前看到的,transformer 的编码器由许多相邻堆叠的编码器层组成。如图 3-2 所示,每个编码器层接收一系列嵌入,并通过以下子层进行处理:
多头自注意力层
应用于每个输入嵌入的全连接前馈层
每个编码器层的输出嵌入与输入的大小相同,我们很快就会看到编码器堆栈的主要作用是“更新”输入嵌入,以产生编码一些上下文信息的表示。例如,如果“keynote”或“phone”这样的词靠近“apple”,那么“apple”这个词将被更新为更“公司化”而不是更“水果化”。
每个子层也使用跳跃连接和层归一化,这是训练深度神经网络的标准技巧。但要真正理解 transformer 的工作原理,我们必须深入了解。让我们从最重要的构建模块开始:自注意力层。
正如我们在第一章中讨论的那样,注意力是一种机制,它允许神经网络为序列中的每个元素分配不同数量的权重或“注意力”。对于文本序列,元素是标记嵌入,就像我们在第二章中遇到的那样,其中每个标记都被映射到某个固定维度的向量。例如,在 BERT 中,每个标记表示为一个 768 维向量。“自注意力”中的“自”指的是这些权重是针对同一集合中的所有隐藏状态计算的,例如,编码器的所有隐藏状态。相比之下,与循环模型相关的注意力机制涉及计算每个编码器隐藏状态对于给定解码时间步的解码器隐藏状态的相关性。
自注意力的主要思想是,我们可以使用整个序列来计算每个嵌入的加权平均,而不是为每个标记使用固定的嵌入。另一种表述方法是,给定标记嵌入序列x 1 , ... , x n,自注意力会产生一个新嵌入序列x 1 ' , ... , x n ',其中每个x i '都是所有x j的线性组合:
x i ' = ∑ j=1 n w ji x j
系数w ji被称为注意力权重,并且被归一化,使得∑ j w ji = 1。要了解为什么对标记嵌入进行平均可能是一个好主意,请考虑当你看到“flies”这个词时会想到什么。你可能会想到讨厌的昆虫,但如果你得到更多的上下文,比如“time flies like an arrow”,那么你会意识到“flies”是指动词。同样,我们可以通过以不同的比例组合所有标记嵌入,也许通过给“time”和“arrow”的标记嵌入分配更大的权重w ji,来创建一个包含这个上下文的“flies”的表示。以这种方式生成的嵌入称为上下文化嵌入,并且早于像 ELMo 这样的语言模型中的 transformers 的发明。2 显示了该过程的图表,我们在其中说明了如何通过自注意力,根据上下文,可以生成“flies”的两种不同表示。
现在让我们看看如何计算注意力权重。
有几种实现自注意力层的方法,但最常见的是来自 Transformer 架构的“缩放点积注意力”。实现这种机制需要四个主要步骤:
将每个标记嵌入投影到称为查询、键和值的三个向量中。
计算注意力分数。我们使用相似性函数来确定查询和键向量之间的关系程度。正如其名称所示,缩放点积注意力的相似性函数是点积,通过嵌入的矩阵乘法进行高效计算。相似的查询和键将具有较大的点积,而那些没有共同之处的将几乎没有重叠。这一步的输出被称为注意力分数,对于具有 n 个输入标记的序列,有一个相应的 n×n 的注意力分数矩阵。
计算注意力权重。点积通常会产生任意大的数,这可能会使训练过程不稳定。为了处理这个问题,首先将注意力分数乘以一个缩放因子来归一化它们的方差,然后通过 softmax 进行归一化,以确保所有列的值总和为 1。得到的 n×n 矩阵现在包含了所有的注意力权重,wji。
更新标记嵌入。一旦计算出注意力权重,我们将它们乘以值向量 v1,直到 vn,以获得嵌入的更新表示 xi'。
我们可以使用一个称为“BertViz for Jupyter”的巧妙库来可视化注意力权重的计算。这个库提供了几个函数,可以用来可视化 Transformer 模型中注意力的不同方面。为了可视化注意力权重,我们可以使用neuron_view模块,它跟踪权重的计算过程,以展示查询和键向量是如何组合产生最终权重的。由于 BertViz 需要访问模型的注意力层,我们将使用 BertViz 的模型类来实例化我们的 BERT 检查点,然后使用show()函数来为特定的编码器层和注意力头生成交互式可视化。请注意,您需要点击左侧的“+”来激活注意力可视化:
从可视化中,我们可以看到查询和键向量的值被表示为垂直条带,其中每个条带的强度对应于其大小。连接线的权重根据标记之间的注意力而加权,我们可以看到“flies”的查询向量与“arrow”的键向量有最强的重叠。
让我们通过实现计算缩放点积注意力的操作图来更详细地了解这个过程,如图 3-4 所示。
在本章中,我们将使用 PyTorch 来实现 Transformer 架构,但 TensorFlow 中的步骤是类似的。我们提供了两个框架中最重要函数的映射,详见表 3-1。
表 3-1。本章中使用的 PyTorch 和 TensorFlow(Keras)类和方法
我们需要做的第一件事是对文本进行标记化,因此让我们使用我们的标记器提取输入 ID:
请注意,此时的标记嵌入与它们的上下文无关。这意味着在上一个示例中的“flies”等同义词(拼写相同但含义不同的单词)具有相同的表示。随后的注意力层的作用将是混合这些标记嵌入,以消除歧义并使用上下文的内容来形成每个标记的表示。
现在我们有了查找表,我们可以通过输入 ID 生成嵌入:
这给我们提供了一个形状为[batch_size, seq_len, hidden_dim]的张量,就像我们在第二章中看到的一样。我们将推迟位置编码,因此下一步是创建查询、键和值向量,并使用点积作为相似性函数来计算注意力分数:
这创建了一个每个批次样本的5 × 5注意力分数矩阵。我们将在后面看到,查询、键和值向量是通过将独立的权重矩阵W Q,K,V应用于嵌入来生成的,但现在为了简单起见,我们将它们保持相等。在缩放的点积注意力中,点积被嵌入向量的大小缩放,以便在训练过程中不会得到太多的大数,这可能会导致我们接下来将应用的 softmax 饱和。
现在让我们应用 softmax:
最后一步是将注意力权重乘以值:
就是这样——我们已经完成了实现简化形式的自注意力的所有步骤!请注意,整个过程只是两次矩阵乘法和一个 softmax,因此你可以将“自注意力”看作是一种花哨的平均形式。
让我们将这些步骤封装成一个我们以后可以使用的函数:
我们的注意机制使用相等的查询和键向量将为上下文中相同的单词分配非常大的分数,特别是对于当前单词本身:查询与自身的点积始终为 1。但实际上,一个单词的含义更多地受到上下文中的补充单词的影响,而不是相同的单词,例如,“flies”的含义更好地通过“time”和“arrow”的信息来定义,而不是通过另一个“flies”的提及。我们如何促进这种行为呢?
让模型通过使用三个不同的线性投影为一个标记的查询、键和值创建不同的向量集。
在我们简单的例子中,我们只是使用嵌入“原样”来计算注意力分数和权重,但这远非全部。实际上,自注意力层对每个嵌入应用三个独立的线性变换,以生成查询、键和值向量。这些变换将嵌入投影到不同的空间,并且每个投影都携带其自己的可学习参数集,这使得自注意力层能够关注序列的不同语义方面。
同时拥有多个线性投影集也被证明是有益的,每个代表一个所谓的注意头。结果的多头注意力层在图 3-5 中有所说明。但为什么我们需要多个注意头呢?原因是一个头的 softmax 倾向于主要关注相似性的某个方面。拥有多个头允许模型同时关注多个方面。例如,一个头可以关注主谓交互,而另一个可以找到附近的形容词。显然,我们不会手工将这些关系编码到模型中,它们完全是从数据中学习到的。如果你熟悉计算机视觉模型,你可能会看到它与卷积神经网络中的滤波器的相似之处,其中一个滤波器可以负责检测脸部,另一个可以在图像中找到车轮。
让我们首先编写一个单个注意力头来实现这一层:
在这里,我们初始化了三个独立的线性层,它们对嵌入向量进行矩阵乘法,以产生形状为[batch_size, seq_len, head_dim]的张量,其中head_dim是我们投影到的维度的数量。虽然head_dim不一定要小于标记的嵌入维度(embed_dim),但实际上它被选择为embed_dim的倍数,以便每个头部的计算是恒定的。例如,BERT 有 12 个注意力头,因此每个头的维度是768 / 12 = 64。
现在我们有了一个单独的注意力头,我们可以将每个头的输出连接起来,以实现完整的多头注意力层:
注意,注意力头的连接输出也通过最终的线性层,以产生适合下游前馈网络的形状为[batch_size, seq_len, hidden_dim]的输出张量。为了确认,让我们看看多头注意力层是否产生了我们输入的预期形状。我们在初始化MultiHeadAttention模块时传递了之前从预训练 BERT 模型加载的配置。这确保我们使用与 BERT 相同的设置:
它起作用了!在注意力这一部分结束时,让我们再次使用 BertViz 来可视化单词“flies”两种不同用法的注意力。在这里,我们可以使用 BertViz 的head_view()函数,通过计算预训练检查点的注意力,并指示句子边界的位置:
在这个例子中,输入由两个句子组成,而[CLS]和[SEP]标记是 BERT 的分词器中的特殊标记,我们在第二章中遇到过。从可视化中我们可以看到的一件事是,注意力权重在属于同一句子的单词之间最强,这表明 BERT 可以知道它应该关注同一句子中的单词。然而,对于单词“flies”,我们可以看到 BERT 已经确定了第一个句子中“arrow”是重要的,第二个句子中是“fruit”和“banana”。这些注意力权重使模型能够区分“flies”作为动词或名词,取决于它出现的上下文!
现在我们已经涵盖了注意力,让我们来看看如何实现编码器层中缺失的位置逐层前馈网络。
我们现在已经拥有了创建一个完整的 transformer 编码器层的所有要素!唯一剩下的决定是在哪里放置跳过连接和层归一化。让我们看看这如何影响模型架构。
正如前面提到的,Transformer 架构使用了层归一化和跳过连接。前者将批处理中的每个输入归一化为零均值和单位方差。跳过连接将一个张量传递到模型的下一层而不进行处理,并将其添加到处理过的张量中。在将层归一化放置在 transformer 的编码器或解码器层中时,文献中采用了两种主要选择:
后层归一化
这是 Transformer 论文中使用的安排;它将层归一化放置在跳过连接之间。这种安排很难从头开始训练,因为梯度可能会发散。因此,你经常会看到一个称为“学习率预热”的概念,在训练过程中学习率会逐渐从一个小值增加到某个最大值。
层前归一化
这是文献中最常见的安排;它将层归一化放置在跳过连接的范围内。这在训练过程中往往更加稳定,通常不需要任何学习率预热。
两种安排的区别在图 3-6 中有所说明。
我们将使用第二种安排,因此我们可以简单地将我们的构建模块连接在一起:
现在让我们用我们的输入嵌入来测试一下:
我们现在已经从头开始实现了我们的第一个 transformer 编码器层!然而,我们设置编码器层的方式有一个问题:它们对令牌的位置是完全不变的。由于多头注意力层实际上是一种花哨的加权和,令牌位置的信息会丢失。⁴
幸运的是,有一个简单的技巧可以使用位置嵌入来纳入位置信息。让我们看看。
位置嵌入基于一个简单但非常有效的思想:用一个与位置相关的值模式增强令牌嵌入,这些值排列在一个向量中。如果该模式对每个位置都是特征性的,那么每个堆栈中的注意力头和前馈层可以学习将位置信息纳入它们的转换中。
有几种方法可以实现这一点,其中最流行的方法之一是使用可学习的模式,特别是当预训练数据集足够大时。这与令牌嵌入的方式完全相同,但是使用位置索引而不是令牌 ID 作为输入。通过这种方式,在预训练期间学习到了一种有效的编码令牌位置的方式。
让我们创建一个自定义的Embeddings模块,它结合了一个令牌嵌入层,将input_ids投影到一个稠密的隐藏状态,以及一个位置嵌入,对position_ids做同样的事情。最终的嵌入就是这两种嵌入的和:
我们看到嵌入层现在为每个令牌创建了一个单一的稠密嵌入。
虽然可学习的位置嵌入易于实现并被广泛使用,但也有一些替代方案:
绝对位置表示
Transformer 模型可以使用由调制正弦和余弦信号组成的静态模式来编码标记的位置。当没有大量数据可用时,这种方法特别有效。
相对位置表示
尽管绝对位置很重要,但可以说在计算嵌入时,周围的标记最重要。相对位置表示遵循这种直觉,并对标记之间的相对位置进行编码。这不能仅通过在开始时引入一个新的相对嵌入层来设置,因为相对嵌入会根据我们从序列的哪个位置进行关注而为每个标记更改。相反,注意力机制本身被修改,增加了考虑标记之间相对位置的额外项。DeBERTa 等模型使用这样的表示。⁵
现在让我们将所有这些组合起来,通过将嵌入与编码器层组合来构建完整的 Transformer 编码器:
让我们检查编码器的输出形状:
我们可以看到我们为批处理中的每个标记获得一个隐藏状态。这种输出格式使得架构非常灵活,我们可以轻松地将其调整为各种应用,比如预测掩码语言建模中的缺失标记,或者在问答中预测答案的起始和结束位置。在接下来的部分中,我们将看到如何构建一个类似于我们在第二章中使用的分类器。
在初始化模型之前,我们需要定义我们想要预测多少个类别:
这正是我们一直在寻找的。对于批处理中的每个示例,我们得到输出中每个类别的非归一化 logits。这对应于我们在第二章中使用的 BERT 模型,用于检测推文中的情绪。
这结束了我们对编码器的分析,以及我们如何将其与特定任务的头部结合起来。现在让我们把注意力(双关语!)转向解码器。
如图 3-7 所示,解码器和编码器之间的主要区别在于解码器有两个注意力子层:
掩码多头自注意力层
编码器-解码器注意力层
对编码器堆栈的输出键和值向量执行多头注意力,其中解码器的中间表示充当查询。⁶ 这样,编码器-解码器注意力层学习如何关联来自两个不同序列的标记,比如两种不同的语言。解码器在每个块中都可以访问编码器的键和值。
让我们看一下我们需要对自注意力层进行的修改,以包含掩码,并将编码器-解码器注意力层的实现作为一个作业问题留下。掩码自注意力的技巧是引入一个掩码矩阵,在下对角线上为 1,在上方为 0:
通过将上限值设置为负无穷大,我们保证了一旦我们对分数进行 softmax 计算,注意力权重都将为零,因为e -∞ = 0(回想一下,softmax 计算的是归一化指数)。我们可以通过对我们在本章早些时候实现的缩放点积注意力函数进行小的修改,轻松地包含这种掩码行为:
我们在这里给了你很多技术信息,但现在你应该对 Transformer 架构的每个部分是如何工作有了很好的理解。在我们继续构建比文本分类更高级的任务模型之前,让我们稍微回顾一下,看看不同 transformer 模型的景观以及它们之间的关系。
正如你在本章中看到的,transformer 模型有三种主要的架构:编码器、解码器和编码器-解码器。早期 transformer 模型的初步成功引发了模型开发的寒武纪爆发,研究人员在不同大小和性质的各种数据集上构建模型,使用新的预训练目标,并调整架构以进一步提高性能。尽管模型的种类仍在快速增长,但它们仍然可以分为这三类。
在本节中,我们将简要介绍每个类别中最重要的 transformer 模型。让我们从看一下 transformer 家族谱开始。
基于 Transformer 架构的第一个仅编码器模型是 BERT。在发布时,它在流行的 GLUE 基准测试中表现优异,⁷该测试衡量了自然语言理解(NLU)在多个不同难度的任务中的表现。随后,BERT 的预训练目标和架构已经被调整以进一步提高性能。仅编码器模型仍然在 NLU 任务(如文本分类、命名实体识别和问答)的研究和行业中占主导地位。让我们简要地看一下 BERT 模型及其变种:
BERT
BERT 在预训练时具有两个目标:预测文本中的屏蔽标记,以及确定一个文本段是否可能跟随另一个文本段。前者任务称为屏蔽语言建模(MLM),后者称为下一个句子预测(NSP)。
DistilBERT
尽管 BERT 取得了很好的结果,但其规模使得在需要低延迟的环境中部署变得棘手。通过在预训练期间使用一种称为知识蒸馏的技术,DistilBERT 在使用 40%更少的内存和速度提高 60%的情况下,实现了 BERT 性能的 97%。您可以在第八章中找到有关知识蒸馏的更多细节。
RoBERTa
在发布 BERT 后的一项研究发现,通过修改预训练方案可以进一步提高其性能。RoBERTa 在更大的批次上进行更长时间的训练,并且放弃了 NSP 任务。这些改变显著提高了其性能,与原始 BERT 模型相比。
XLM
在跨语言语言模型(XLM)的工作中探索了构建多语言模型的几个预训练目标,包括来自 GPT 类模型的自回归语言建模和来自 BERT 的 MLM。此外,XLM 预训练论文的作者介绍了翻译语言建模(TLM),这是对多语言输入的 MLM 的扩展。通过对这些预训练任务进行实验,他们在几个多语言 NLU 基准测试以及翻译任务上取得了最先进的结果。
XLM-RoBERTa
在 XLM 和 RoBERTa 的工作之后,XLM-RoBERTa 或 XLM-R 模型通过大规模扩展训练数据,进一步推动了多语言预训练的发展。其开发者利用 Common Crawl 语料库创建了一个包含 2.5TB 文本的数据集,然后在该数据集上进行了 MLM 编码器的训练。由于数据集只包含没有平行文本(即翻译)的数据,因此 XLM 的 TLM 目标被取消。这种方法在低资源语言上明显优于 XLM 和多语言 BERT 变体。
ALBERT
ALBERT 模型引入了三个改变,使得编码器架构更加高效。首先,它将标记嵌入维度与隐藏维度解耦,从而允许嵌入维度较小,从而节省参数,特别是当词汇量变大时。其次,所有层共享相同的参数,这进一步减少了有效参数的数量。最后,NSP 目标被替换为句子排序预测:模型需要预测两个连续句子的顺序是否交换,而不是预测它们是否完全属于一起。这些改变使得可以使用更少的参数训练更大的模型,并在 NLU 任务上取得更优越的性能。
ELECTRA
标准 MLM 预训练目标的一个局限性是,在每个训练步骤中,只有被屏蔽的标记的表示会被更新,而其他输入标记不会被更新。为了解决这个问题,ELECTRA 采用了两模型方法:第一个模型(通常较小)类似于标准的屏蔽语言模型,预测被屏蔽的标记。然后,称为鉴别器的第二个模型被要求预测第一个模型输出的标记中哪些是最初被屏蔽的。因此,鉴别器需要对每个标记进行二元分类,这使得训练效率提高了 30 倍。对于下游任务,鉴别器像标准的 BERT 模型一样进行微调。
DeBERTa
DeBERTa 模型引入了两个架构变化。首先,每个标记表示为两个向量:一个用于内容,另一个用于相对位置。通过将标记的内容与它们的相对位置分离,自注意力层可以更好地建模附近标记对的依赖关系。另一方面,单词的绝对位置也很重要,特别是对于解码。因此,在标记解码头的 softmax 层之前添加了绝对位置嵌入。DeBERTa 是第一个(作为集合)在 SuperGLUE 基准上击败人类基线的模型,这是 GLUE 的更难版本,由几个子任务组成,用于衡量 NLU 性能。
现在我们已经强调了一些主要的仅编码器架构,让我们来看一下仅解码器模型。
Transformer 解码器模型的进展在很大程度上是由 OpenAI 带头的。这些模型在预测序列中的下一个单词方面表现出色,因此主要用于文本生成任务。它们的进展得益于使用更大的数据集,并将语言模型扩展到越来越大的规模。让我们来看看这些迷人生成模型的演变:
GPT
GPT 的引入将 NLP 中的两个关键思想结合在一起:新颖而高效的 Transformer 解码器架构和迁移学习。在这种设置下,模型通过基于先前单词预测下一个单词来进行预训练。该模型在 BookCorpus 上进行了训练,并在分类等下游任务上取得了很好的结果。
GPT-2
受到简单且可扩展的预训练方法成功的启发,原始模型和训练集被扩大,产生了 GPT-2。这个模型能够生成连贯文本的长序列。由于可能被滥用的担忧,该模型是分阶段发布的,先发布较小的模型,后来再发布完整的模型。
CTRL
像 GPT-2 这样的模型可以继续输入序列(也称为提示)。然而,用户对生成序列的风格几乎没有控制。条件 Transformer 语言(CTRL)模型通过在序列开头添加“控制标记”来解决这个问题。这些标记允许控制生成文本的风格,从而实现多样化的生成。
GPT-3
GPT-Neo/GPT-J-6B
transformers 树的最终分支是编码器-解码器模型。让我们来看一下。
尽管使用单个编码器或解码器堆栈构建模型已经很普遍,但 Transformer 架构的编码器-解码器变体有许多新颖的应用,涵盖了 NLU 和 NLG 领域:
T5
T5 模型通过将所有 NLU 和 NLG 任务统一转换为文本到文本任务。²³ 所有任务都被构建为序列到序列任务,采用编码器-解码器架构是自然的。例如,对于文本分类问题,这意味着文本被用作编码器输入,解码器必须生成标签作为普通文本,而不是类别。我们将在第六章中更详细地讨论这个问题。T5 架构使用了原始的 Transformer 架构。使用大型抓取的 C4 数据集,该模型通过将所有任务转换为文本到文本任务,进行了掩码语言建模以及 SuperGLUE 任务的预训练。具有 110 亿参数的最大模型在几个基准测试中取得了最先进的结果。
BART
BART 将 BERT 和 GPT 的预训练程序结合到编码器-解码器架构中。²⁴ 输入序列经历了几种可能的转换,从简单的屏蔽到句子排列、标记删除和文档旋转。这些修改后的输入通过编码器,解码器必须重建原始文本。这使得模型更加灵活,因为可以将其用于 NLU 和 NLG 任务,并且在两者上都实现了最先进的性能。
M2M-100
传统上,翻译模型是为一种语言对和翻译方向构建的。自然地,这无法扩展到许多语言,而且可能存在语言对之间的共享知识,可以用于罕见语言之间的翻译。M2M-100 是第一个可以在 100 种语言之间进行翻译的翻译模型。²⁵ 这允许在罕见和代表性不足的语言之间进行高质量的翻译。该模型使用前缀标记(类似于特殊的[CLS]标记)来指示源语言和目标语言。
BigBird
Transformer 模型的一个主要限制是最大上下文大小,这是由于注意机制的二次内存需求。BigBird 通过使用一种稀疏形式的注意力来解决这个问题,从而实现了线性扩展。²⁶ 这允许将大多数 BERT 模型中的 512 个标记的上下文急剧扩展到 BigBird 中的 4,096 个标记。这在需要保留长依赖性的情况下特别有用,比如在文本摘要中。
在本章中,我们从 Transformer 架构的核心开始,深入研究了自注意力,随后添加了构建 Transformer 编码器模型所需的所有必要部分。我们为标记和位置信息添加了嵌入层,为了补充注意力头,我们添加了一个前馈层,最后我们为模型主体添加了一个分类头来进行预测。我们还研究了 Transformer 架构的解码器部分,并总结了本章中最重要的模型架构。
现在您对基本原理有了更好的理解,让我们超越简单的分类,构建一个多语言命名实体识别模型。
M.E. Peters 等人,《深度上下文化的词表示》,(2017 年)。
A. Vaswani 等人,《Attention Is All You Need》,(2017 年)。
更高级的术语是,自注意力和前馈层被称为置换等变 - 如果输入被置换,那么层的相应输出将以完全相同的方式被置换。
通过结合绝对和相对位置表示的思想,旋转位置嵌入在许多任务上取得了出色的结果。GPT-Neo 是具有旋转位置嵌入的模型的一个例子。
请注意,与自注意力层不同,编码器-解码器注意力中的关键和查询向量可以具有不同的长度。这是因为编码器和解码器的输入通常涉及不同长度的序列。因此,该层中的注意力分数矩阵是矩形的,而不是正方形的。
A. Wang 等人,《GLUE: A Multi-Task Benchmark and Analysis Platform for Natural Language Understanding》,(2018 年)。
J. Devlin 等人,《BERT: Pre-Training of Deep Bidirectional Transformers for Language Understanding》,(2018 年)。
V. Sanh 等人,《DistilBERT, a Distilled Version of BERT: Smaller, Faster, Cheaper and Lighter》,(2019 年)。
Y. Liu 等人,《RoBERTa: A Robustly Optimized BERT Pretraining Approach》,(2019 年)。
G. Lample 和 A. Conneau,《跨语言语言模型预训练》,(2019 年)。
A. Conneau 等人,《规模化的无监督跨语言表示学习》,(2019 年)。
Z. Lan 等人,《ALBERT: A Lite BERT for Self-Supervised Learning of Language Representations》,(2019 年)。
K. Clark 等人,《ELECTRA: Pre-Training Text Encoders as Discriminators Rather Than Generators》,(2020 年)。
P. He 等人,《DeBERTa: Decoding-Enhanced BERT with Disentangled Attention》,(2020 年)。
A. Wang 等人,《SuperGLUE: A Stickier Benchmark for General-Purpose Language Understanding Systems》,(2019 年)。
A. Radford 等人,《通过生成预训练改进语言理解》,OpenAI(2018 年)。
A. Radford 等人,《语言模型是无监督多任务学习者》,OpenAI(2019 年)。
N.S. Keskar 等人,《CTRL: A Conditional Transformer Language Model for Controllable Generation》,(2019 年)。
J. Kaplan 等人,《神经语言模型的缩放定律》,(2020 年)。
T. Brown 等人,《语言模型是少样本学习者》,(2020 年)。
到目前为止,在这本书中,我们已经应用 transformers 来解决英语语料库上的 NLP 任务 - 但是当你的文档是用希腊语、斯瓦希里语或克林贡语写的时候,你该怎么办呢?一种方法是在 Hugging Face Hub 上搜索合适的预训练语言模型,并在手头的任务上对其进行微调。然而,这些预训练模型往往只存在于像德语、俄语或普通话这样的“高资源”语言中,这些语言有大量的网络文本可用于预训练。另一个常见的挑战是当你的语料库是多语言的时候:在生产中维护多个单语模型对你或你的工程团队来说都不是什么乐趣。
幸运的是,有一类多语言 transformers 可以拯救我们。像 BERT 一样,这些模型使用掩码语言建模作为预训练目标,但它们是在一百多种语言的文本上联合训练的。通过在许多语言的大型语料库上进行预训练,这些多语言 transformers 实现了零-shot 跨语言转移。这意味着对一个语言进行微调的模型可以应用于其他语言,而无需进一步的训练!这也使得这些模型非常适合“代码切换”,即说话者在单一对话的上下文中交替使用两种或多种语言或方言。
在本章中,我们将探讨如何对一种名为 XLM-RoBERTa 的单一 transformer 模型(在第三章介绍)进行微调,以执行跨多种语言的命名实体识别(NER)。正如我们在第一章中看到的,NER 是一种常见的 NLP 任务,用于识别文本中的人物、组织或地点等实体。这些实体可以用于各种应用,例如从公司文件中获取见解,增强搜索引擎的质量,或者仅仅是从语料库中构建结构化数据库。
在本章中,让我们假设我们想为一个位于瑞士的客户执行 NER,那里有四种官方语言(英语通常作为它们之间的桥梁)。让我们首先找到一个适合这个问题的多语言语料库。
零-shot 转移或零-shot 学习通常指的是在一个标签集上训练模型,然后在另一个标签集上对其进行评估的任务。在 transformers 的上下文中,零-shot 学习也可能指的是像 GPT-3 这样的语言模型在一个甚至没有进行微调的下游任务上进行评估。
在本章中,我们将使用跨语言 TRansfer 多语言编码器(XTREME)基准的子集,称为 WikiANN 或 PAN-X。² 这个数据集包括许多语言的维基百科文章,包括瑞士四种最常用的语言:德语(62.9%)、法语(22.9%)、意大利语(8.4%)和英语(5.9%)。每篇文章都以“内外开始”(IOB2)格式标注了LOC(位置)、PER(人物)和ORG(组织)标签。在这种格式中,B-前缀表示实体的开始,属于同一实体的连续标记被赋予I-前缀。O标签表示该标记不属于任何实体。例如,以下句子:
Jeff Dean 是 Google 在加利福尼亚的计算机科学家
将以 IOB2 格式标记,如 Table 4-1 所示。
表 4-1。一个带有命名实体注释的序列示例
要加载 XTREME 中的 PAN-X 子集之一,我们需要知道要传递给 load_dataset() 函数的 数据集配置。每当处理具有多个领域的数据集时,可以使用 get_dataset_config_names() 函数查找可用的子集:
哇,这有很多配置!让我们缩小搜索范围,只查找以“PAN”开头的配置:
为了创建一个真实的瑞士语料库,我们将根据 PAN-X 中各语言的口语比例抽样德语(de)、法语(fr)、意大利语(it)和英语(en)语料库。这将创建一个语言不平衡的情况,这在现实世界的数据集中非常常见,因为在少数语言中获取标记示例可能会很昂贵,因为缺乏精通该语言的领域专家。这种不平衡的数据集将模拟在多语言应用程序中工作时的常见情况,我们将看到如何构建一个适用于所有语言的模型。
为了跟踪每种语言,让我们创建一个 Python defaultdict,将语言代码存储为键,DatasetDict 类型的 PAN-X 语料库存储为值:
按设计,我们在德语中有比其他所有语言加起来更多的示例,因此我们将其用作从中执行零-shot 跨语言转移到法语、意大利语和英语的起点。让我们检查德语语料库中的一个示例:
与我们之前遇到的 Dataset 对象一样,我们示例的键对应于 Arrow 表的列名,而值表示每列中的条目。特别是,我们看到 ner_tags 列对应于将每个实体映射到类 ID。这对人眼来说有点神秘,所以让我们创建一个新列,其中包含熟悉的 LOC、PER 和 ORG 标签。为此,首先要注意的是我们的 Dataset 对象具有一个 features 属性,该属性指定了与每列关联的基础数据类型:
Sequence 类指定该字段包含一系列特征,对于 ner_tags,这对应于一系列 ClassLabel 特征。让我们从训练集中挑选出这个特征:
现在我们已经将标签转换为人类可读的格式,让我们看看训练集中第一个示例中的标记和标签是如何对齐的:
LOC标签的存在是有意义的,因为句子“2,000 Einwohnern an der Danziger Bucht in der polnischen Woiwodschaft Pommern”在英语中的意思是“2,000 inhabitants at the Gdansk Bay in the Polish voivodeship of Pomerania”,而 Gdansk Bay 是波罗的海的一个海湾,“voivodeship”对应于波兰的一个州。
作为对标签是否存在异常不平衡的快速检查,让我们计算每个实体在每个拆分中的频率:
看起来很好 - PER、LOC和ORG频率的分布在每个拆分中大致相同,因此验证和测试集应该能够很好地衡量我们的 NER 标记器的泛化能力。接下来,让我们看一下一些流行的多语言 Transformer 以及它们如何适应我们的 NER 任务。
多语言 Transformer 涉及与其单语对应物相似的架构和训练程序,唯一的区别在于用于预训练的语料库包含许多语言的文档。这种方法的一个显著特点是,尽管没有接收到区分语言的明确信息,但由此产生的语言表示能够很好地跨语言进行泛化,适用于各种下游任务。在某些情况下,这种跨语言转移的能力可以产生与单语模型竞争的结果,从而避免了需要为每种语言训练一个模型的需求!
en
在英语训练数据上进行微调,然后在每种语言的测试集上进行评估。
each
在单语测试数据上进行微调和评估,以衡量每种语言的性能。
all
在所有训练数据上进行微调,以便在每种语言的测试集上进行评估。
我们将采用类似的评估策略来进行我们的 NER 任务,但首先我们需要选择一个模型来评估。最早的多语言 Transformer 之一是 mBERT,它使用与 BERT 相同的架构和预训练目标,但将许多语言的维基百科文章添加到预训练语料库中。从那时起,mBERT 已经被 XLM-RoBERTa(或简称 XLM-R)取代,因此这是我们将在本章中考虑的模型。
正如我们在第三章中看到的,XLM-R 仅使用 MLM 作为 100 种语言的预训练目标,但与其前身相比,其预训练语料库的规模巨大:每种语言的维基百科转储和来自网络的 2.5 terabytes的 Common Crawl 数据。这个语料库的规模比早期模型使用的语料库大几个数量级,并为缅甸语和斯瓦希里语等低资源语言提供了显著的信号增强,因为这些语言只有少量的维基百科文章。
模型名称中的 RoBERTa 指的是预训练方法与单语 RoBERTa 模型相同。RoBERTa 的开发人员在几个方面改进了 BERT,特别是通过完全删除下一个句子预测任务。³ XLM-R 还放弃了 XLM 中使用的语言嵌入,并使用 SentencePiece 直接对原始文本进行标记化。⁴ 除了其多语言性质之外,XLM-R 和 RoBERTa 之间的一个显著差异是各自词汇表的大小:25 万个标记与 5.5 万个标记!
XLM-R 是多语言 NLU 任务的一个很好的选择。在下一节中,我们将探讨它如何能够高效地在许多语言中进行标记化。
XLM-R 使用了一个名为 SentencePiece 的分词器,而不是使用 WordPiece 分词器,该分词器是在所有一百种语言的原始文本上进行训练的。为了了解 SentencePiece 与 WordPiece 的比较,让我们以通常的方式使用 nlpt_pin01 Transformers 加载 BERT 和 XLM-R 分词器:
通过对一小段文本进行编码,我们还可以检索每个模型在预训练期间使用的特殊标记:
在这里,我们看到 XLM-R 使用了<s>和<\s>来表示序列的开始和结束,而不是 BERT 用于句子分类任务的[CLS]和[SEP]标记。这些标记是在标记化的最后阶段添加的,我们将在下面看到。
到目前为止,我们把分词看作是一个将字符串转换为我们可以通过模型传递的整数的单个操作。这并不完全准确,如果我们仔细看一下,我们会发现它实际上是一个完整的处理管道,通常包括四个步骤,如图 4-1 所示。
让我们仔细看看每个处理步骤,并用示例句子“Jack Sparrow loves New York!”来说明它们的效果:
规范化
预分词
这一步将文本分割成较小的对象,这些对象给出了训练结束时你的标记的上限。一个好的思考方式是,预分词器将把你的文本分割成“单词”,你的最终标记将是这些单词的一部分。对于允许这样做的语言(英语、德语和许多印欧语言),字符串通常可以根据空格和标点符号分割成单词。例如,这一步可能会将我们的["jack", "sparrow", "loves", "new", "york", "!"]转换成这些单词。然后,这些单词更容易在管道的下一步中使用字节对编码(BPE)或 Unigram 算法分割成子词。然而,将文本分割成“单词”并不总是一个微不足道和确定性的操作,甚至不是一个有意义的操作。例如,在中文、日文或韩文等语言中,将符号分组成像印欧语言单词那样的语义单元可能是一个非确定性的操作,有几个同样有效的分组。在这种情况下,最好不要对文本进行预分词,而是使用一个特定于语言的库进行预分词。
分词器模型
一旦输入文本被规范化和 pretokenized,分词器会在单词上应用一个子词分割模型。这是流程中需要在你的语料库上进行训练(或者如果你使用的是预训练分词器,则已经进行了训练)的部分。模型的作用是将单词分割成子词,以减少词汇量的大小,并尝试减少词汇表外标记的数量。存在几种子词分割算法,包括 BPE、Unigram 和 WordPiece。例如,我们的运行示例在分词器模型应用后可能看起来像[jack, spa, rrow, loves, new, york, !]。请注意,此时我们不再有一个字符串列表,而是一个整数列表(输入 ID);为了保持示例的说明性,我们保留了单词,但删除了引号以表示转换。
后处理
这是分词流程的最后一步,在这一步中,可以对标记列表应用一些额外的转换,例如在输入标记索引序列的开头或结尾添加特殊标记。例如,BERT 风格的分词器会添加分类和分隔符标记:[CLS, jack, spa, rrow, loves, new, york, !, SEP]。然后,这个序列(请记住,这将是一个整数序列,而不是你在这里看到的标记)可以被馈送到模型中。
回到我们对 XLM-R 和 BERT 的比较,我们现在明白了 SentencePiece 在后处理步骤中添加了<s>和<\s>,而不是[CLS]和[SEP](作为惯例,我们将在图形说明中继续使用[CLS]和[SEP])。让我们回到 SentencePiece 分词器,看看它有什么特别之处。
SentencePiece 分词器基于一种称为 Unigram 的子词分割类型,并将每个输入文本编码为 Unicode 字符序列。这个特性对于多语言语料库特别有用,因为它允许 SentencePiece 对重音、标点和许多语言(比如日语)没有空格字符这一事实保持不可知。SentencePiece 的另一个特点是将空格分配给 Unicode 符号 U+2581,或者称为▁字符,也叫做下四分之一块字符。这使得 SentencePiece 能够在不依赖于特定语言的 pretokenizers 的情况下,对序列进行去标记化处理。例如,在前一节的例子中,我们可以看到 WordPiece 丢失了“York”和“!”之间没有空格的信息。相比之下,SentencePiece 保留了标记化文本中的空格,因此我们可以无歧义地将其转换回原始文本。
现在我们了解了 SentencePiece 的工作原理,让我们看看如何将我们的简单示例编码成适合 NER 的形式。首先要做的是加载带有标记分类头的预训练模型。但我们不会直接从 nlpt_pin01 Transformers 中加载这个头,而是自己构建它!通过深入研究 nlpt_pin01 Transformers API,我们可以用几个简单的步骤来实现这一点。
在第二章中,我们看到对于文本分类,BERT 使用特殊的[CLS]标记来表示整个文本序列。然后,这个表示被馈送到一个全连接或密集层,以输出所有离散标签值的分布,如图 4-2 所示。
BERT 和其他仅编码器的 Transformer 在 NER 方面采取了类似的方法,只是每个单独的输入标记的表示被馈送到相同的全连接层,以输出标记的实体。因此,NER 经常被构建为标记分类任务。该过程看起来像图 4-3 中的图表。
Transformer 编码器的命名实体识别架构。宽线性层显示相同的线性层应用于所有隐藏状态。
到目前为止,一切顺利,但在标记分类任务中,我们应该如何处理子词?例如,图 4-3 中的第一个名字“Christa”被标记为子词“Chr”和“##ista”,那么应该分配B-PER标签给哪一个(或哪些)呢?
在 BERT 论文中,作者将这个标签分配给第一个子词(在我们的例子中是“Chr”),并忽略后面的子词(“##ista”)。这是我们将在这里采用的约定,我们将用IGN表示被忽略的子词。我们稍后可以很容易地将第一个子词的预测标签传播到后续子词中的后处理步骤。我们也可以选择包括“##ista”子词的表示,通过分配一个B-LOC标签的副本,但这违反了 IOB2 格式。
Transformer 围绕着每种架构和任务都有专门的类进行组织。与不同任务相关的模型类根据<ModelName>For<Task>约定命名,或者在使用AutoModel类时为AutoModelFor<Task>。
接下来我们将看到,这种主体和头部的分离使我们能够为任何任务构建自定义头部,并将其直接安装在预训练模型的顶部。
要开始,我们需要一个数据结构来表示我们的 XLM-R NER 标记器。作为第一个猜测,我们将需要一个配置对象来初始化模型和一个 forward() 函数来生成输出。让我们继续构建我们的 XLM-R 标记分类的类:
config_class 确保在初始化新模型时使用标准的 XLM-R 设置。如果要更改默认参数,可以通过覆盖配置中的默认设置来实现。使用 super() 方法调用 RobertaPreTrainedModel 类的初始化函数。这个抽象类处理预训练权重的初始化或加载。然后我们加载我们的模型主体,即 RobertaModel,并用自己的分类头扩展它,包括一个 dropout 和一个标准的前馈层。请注意,我们设置 add_pooling_layer=False 以确保返回所有隐藏状态,而不仅仅是与 [CLS] 标记相关联的隐藏状态。最后,我们通过调用从 RobertaPreTrainedModel 继承的 init_weights() 方法来初始化所有权重,这将加载模型主体的预训练权重并随机初始化我们的标记分类头的权重。
唯一剩下的事情就是定义模型在前向传递中应该做什么,使用 forward() 方法。在前向传递期间,数据首先通过模型主体进行馈送。有许多输入变量,但我们现在只需要 input_ids 和 attention_mask。然后,模型主体输出的隐藏状态通过 dropout 和分类层进行馈送。如果我们在前向传递中还提供标签,我们可以直接计算损失。如果有注意力掩码,我们需要做一些额外的工作,以确保我们只计算未掩码标记的损失。最后,我们将所有输出封装在一个 TokenClassifierOutput 对象中,这样我们就可以从前几章中熟悉的命名元组中访问元素。
现在我们准备加载我们的标记分类模型。除了模型名称之外,我们还需要提供一些额外的信息,包括我们将用于标记每个实体的标签以及每个标签与 ID 之间的映射,反之亦然。所有这些信息都可以从我们的tags变量中派生出来,作为一个ClassLabel对象,它具有一个我们可以用来派生映射的names属性:
现在,我们可以像往常一样使用from_pretrained()函数加载模型权重,还可以使用额外的config参数。请注意,我们没有在我们的自定义模型类中实现加载预训练权重;我们通过从RobertaPreTrainedModel继承来免费获得这一点:
为了快速检查我们是否正确初始化了标记器和模型,让我们在我们已知实体的小序列上测试预测:
正如你在这里看到的,起始<s>和结束</s>标记分别被赋予了 ID 0 和 2。
最后,我们需要将输入传递给模型,并通过取 argmax 来提取预测,以获得每个标记最有可能的类:
在这里,我们看到 logits 的形状为[batch_size, num_tokens, num_tags],每个标记都被赋予了七个可能的 NER 标记中的一个 logit。通过枚举序列,我们可以快速看到预训练模型的预测:
毫不奇怪,我们的具有随机权重的标记分类层还有很多需要改进的地方;让我们在一些带标签的数据上进行微调,使其变得更好!在这样做之前,让我们把前面的步骤封装成一个辅助函数,以备后用:
在我们训练模型之前,我们还需要对输入进行标记化处理并准备标签。我们接下来会做这个。
其中examples相当于Dataset的一个切片,例如panx_de['train'][:10]。由于 XLM-R 标记器返回模型输入的输入 ID,我们只需要用注意力掩码和编码关于每个 NER 标记与每个标记相关联的哪个标记的信息的标签 ID 来增加这些信息。
接下来,我们对每个单词进行标记化,并使用is_split_into_words参数告诉标记器我们的输入序列已经被分成了单词:
在这个例子中,我们可以看到标记器将“Einwohnern”分成了两个子词,“▁Einwohner”和“n”。由于我们遵循的约定是只有“▁Einwohner”应该与B-LOC标签相关联,我们需要一种方法来屏蔽第一个子词之后的子词表示。幸运的是,tokenized_input是一个包含word_ids()函数的类,可以帮助我们实现这一点:
在这个例子中,我们可以看到word_ids已经将每个子词映射到words序列中的相应索引,因此第一个子词“▁2.000”被分配索引 0,而“▁Einwohner”和“n”被分配索引 1(因为“Einwohnern”是words中的第二个单词)。我们还可以看到像<s>和<\s>这样的特殊标记被映射为None。让我们将-100 设置为这些特殊标记和训练过程中希望屏蔽的子词的标签:
这就是全部!我们可以清楚地看到标签 ID 与标记对齐,所以让我们通过定义一个包装所有逻辑的单个函数,将其扩展到整个数据集:
现在我们已经有了编码每个拆分所需的所有要素,让我们编写一个可以迭代的函数:
通过将此函数应用于DatasetDict对象,我们可以得到每个拆分的编码Dataset对象。让我们使用这个来对我们的德语语料库进行编码:
现在我们有了一个模型和一个数据集,我们需要定义一个性能指标。
正如我们所看到的,seqeval期望预测和标签作为列表的列表,每个列表对应于我们的验证或测试集中的单个示例。为了在训练过程中集成这些指标,我们需要一个函数,可以获取模型的输出并将其转换为seqeval所期望的列表。以下方法可以确保我们忽略与后续子词相关联的标签 ID:
有了性能指标,我们可以开始实际训练模型了。
在每个时代结束时,我们评估模型对验证集的预测,调整权重衰减,并将save_steps设置为一个较大的数字,以禁用检查点并加快训练速度。
我们还需要告诉Trainer如何在验证集上计算指标,因此我们可以使用之前定义的align_predictions()函数来提取seqeval计算F[1]-score 所需格式的预测和标签:
填充标签是必要的,因为与文本分类任务不同,标签也是序列。这里的一个重要细节是,标签序列用值-100 进行填充,正如我们所见,这个值会被 PyTorch 损失函数忽略。
在本章的过程中,我们将训练几个模型,因此我们将通过创建model_init()方法来避免为每个Trainer初始化一个新模型。该方法加载一个未经训练的模型,并在train()调用开始时调用:
现在我们可以将所有这些信息与编码的数据集一起传递给Trainer:
然后按照以下方式运行训练循环,并将最终模型推送到 Hub:
这些 F1 分数对于 NER 模型来说相当不错。为了确认我们的模型按预期工作,让我们在我们简单示例的德语翻译上进行测试:
这很有效!但我们不应该对基于单个示例的性能过于自信。相反,我们应该对模型的错误进行适当和彻底的调查。在下一节中,我们将探讨如何在 NER 任务中进行这样的调查。
在我们深入探讨 XLM-R 的多语言方面之前,让我们花一分钟来调查我们模型的错误。正如我们在第二章中看到的,对模型进行彻底的错误分析是训练和调试 Transformer(以及机器学习模型一般)最重要的方面之一。有几种失败模式,其中模型看起来表现良好,而实际上它存在一些严重的缺陷。训练可能失败的例子包括:
我们可能会意外地屏蔽太多的标记,也会屏蔽一些标签,以获得真正有希望的损失下降。
compute_metrics()函数可能存在一个高估真实性能的错误。
我们可能会将零类或O实体包括在 NER 中作为正常类别,这将严重扭曲准确性和F[1]-分数,因为它是绝大多数类别。
当模型的表现远低于预期时,查看错误可能会提供有用的见解,并揭示很难仅通过查看代码就能发现的错误。即使模型表现良好,并且代码中没有错误,错误分析仍然是了解模型优势和劣势的有用工具。这些都是我们在将模型部署到生产环境时需要牢记的方面。
对于我们的分析,我们将再次使用我们手头上最强大的工具之一,即查看损失最大的验证示例。我们可以重复使用我们构建的函数的大部分内容来分析第二章中的序列分类模型,但现在我们将计算样本序列中每个标记的损失。
让我们定义一个可以应用于验证集的方法:
现在我们可以使用map()将这个函数应用到整个验证集,并将所有数据加载到DataFrame中进行进一步分析:
标记和标签仍然使用它们的 ID 进行编码,因此让我们将标记和标签映射回字符串,以便更容易阅读结果。对于带有标签-100 的填充标记,我们分配一个特殊的标签IGN,以便稍后过滤它们。我们还通过将它们截断到输入的长度来消除loss和predicted_label字段中的所有填充:
有了这样的数据,我们现在可以按输入标记对其进行分组,并聚合每个标记的损失、计数、均值和总和。最后,我们按损失的总和对聚合数据进行排序,并查看验证集中累积损失最多的标记:
我们可以在这个列表中观察到几种模式:
空格标记具有最高的总损失,这并不奇怪,因为它也是列表中最常见的标记。然而,它的平均损失要低得多。这意味着模型不会在对其进行分类时遇到困难。
像“in”、“von”、“der”和“und”这样的词出现相对频繁。它们经常与命名实体一起出现,有时也是它们的一部分,这解释了为什么模型可能会混淆它们。
括号、斜杠和单词开头的大写字母很少见,但平均损失相对较高。我们将进一步调查它们。
我们还可以对标签 ID 进行分组,并查看每个类别的损失:
我们看到B-ORG的平均损失最高,这意味着确定组织的开始对我们的模型构成了挑战。
我们可以通过绘制标记分类的混淆矩阵来进一步分解这一点,在那里我们看到组织的开始经常与随后的I-ORG标记混淆:
从图中我们可以看出,我们的模型往往最容易混淆B-ORG和I-ORG实体。否则,它在分类其余实体方面表现相当不错,这可以从混淆矩阵近似对角线的性质中清楚地看出。
现在我们已经在标记级别上检查了错误,让我们继续看一下损失较大的序列。为了进行这种计算,我们将重新审视我们的“未爆炸”的DataFrame,并通过对每个标记的损失求和来计算总损失。为此,让我们首先编写一个帮助我们显示带有标签和损失的标记序列的函数:
显然,这些样本的标签出现了问题;例如,联合国和中非共和国分别被标记为一个人!与此同时,第一个例子中的“8. Juli”被标记为一个组织。原来 PAN-X 数据集的注释是通过自动化过程生成的。这样的注释通常被称为“银标准”(与人工生成的注释的“金标准”相对),并且并不奇怪,自动化方法未能产生合理的标签。事实上,这种失败模式并不是自动方法的特有现象;即使在人类仔细注释数据时,当注释者的注意力分散或者他们简单地误解句子时,也会出现错误。
我们早些时候注意到的另一件事是,括号和斜杠的损失相对较高。让我们看一些带有开括号的序列的例子:
通常情况下,我们不会将括号及其内容包括在命名实体的一部分,但这似乎是自动提取标注文档的方式。在其他例子中,括号中包含地理位置的说明。虽然这确实也是一个位置,但我们可能希望在注释中将其与原始位置断开。这个数据集由不同语言的维基百科文章组成,文章标题通常包含括号中的某种解释。例如,在第一个例子中,括号中的文本表明哈马是一个“Unternehmen”,或者在英语中是公司。当我们推出模型时,了解这些重要细节是很重要的,因为它们可能对模型所属的整个流水线的下游性能产生影响。
通过相对简单的分析,我们已经确定了我们的模型和数据集的一些弱点。在实际用例中,我们将在这一步上进行迭代,清理数据集,重新训练模型,并分析新的错误,直到我们对性能感到满意。
在这里,我们分析了单一语言的错误,但我们也对跨语言的性能感兴趣。在下一节中,我们将进行一些实验,看看 XLM-R 中的跨语言转移效果如何。
现在我们已经在德语上对 XLM-R 进行了微调,我们可以通过Trainer的predict()方法评估它对其他语言的转移能力。由于我们计划评估多种语言,让我们创建一个简单的函数来为我们执行这些操作:
我们可以使用这个函数来检查测试集的性能,并在dict中跟踪我们的分数:
这对于 NER 任务来说是相当不错的结果。我们的指标大约在 85%左右,我们可以看到模型似乎在ORG实体上遇到了最大的困难,可能是因为这些在训练数据中最不常见,而且 XLM-R 的词汇表中许多组织名称都很少见。其他语言呢?为了热身,让我们看看我们在德语上进行微调的模型在法语上的表现如何:
不错!尽管两种语言的名称和组织都是相同的,但模型成功地正确标记了“Kalifornien”的法语翻译。接下来,让我们通过编写一个简单的函数来对整个法语测试集上的德语模型的表现进行量化,该函数对数据集进行编码并生成分类报告:
尽管我们在微观平均指标上看到了约 15 个点的下降,但请记住,我们的模型没有看到任何一个标记的法语示例!一般来说,性能下降的大小与语言之间的“距离”有关。尽管德语和法语被归为印欧语系,但它们从技术上属于不同的语系:分别是日耳曼语和罗曼语。
接下来,让我们评估意大利语的性能。由于意大利语也是一种罗曼语言,我们期望得到与法语相似的结果:
事实上,我们的期望得到了F[1]-分数的证实。最后,让我们检查英语的性能,英语属于日耳曼语系:
令人惊讶的是,尽管我们可能直觉地认为德语与英语更相似,但我们的模型在英语上表现得最差。在对德语进行微调并进行零-shot 转移到法语和英语之后,接下来让我们考虑何时直接在目标语言上进行微调是有意义的。
到目前为止,我们已经看到,在德语语料库上微调 XLM-R 可以获得约 85%的F[1]-分数,在任何额外的训练的情况下,该模型能够在我们语料库中的其他语言上取得适度的性能。问题是,这些结果有多好,它们与在单语语料库上微调的 XLM-R 模型相比如何?
在本节中,我们将通过在不断增加大小的训练集上微调 XLM-R 来探讨这个问题。通过这种方式跟踪性能,我们可以确定零-shot 跨语言转移何时更优越,这在实践中对于指导是否收集更多标记数据的决策可能是有用的。
为简单起见,我们将保持与在德语语料库上进行微调运行相同的超参数,只是我们将调整TrainingArguments的logging_steps参数,以考虑训练集大小的变化。我们可以将所有这些封装在一个简单的函数中,该函数接受与单语语料库对应的DatasetDict对象,通过num_samples对其进行下采样,并在该样本上对 XLM-R 进行微调,以返回最佳时期的指标:
就像我们在德语语料库上进行微调一样,我们还需要将法语语料库编码为输入 ID、注意掩码和标签 ID:
接下来,让我们通过在一个包含 250 个示例的小训练集上运行该函数来检查我们的函数是否有效:
我们可以看到,仅有 250 个示例时,在法语上进行微调的性能远远低于从德语进行零-shot 转移。现在让我们将训练集大小增加到 500、1,000、2,000 和 4,000 个示例,以了解性能的增加情况:
我们可以通过绘制测试集上的F[1]-分数作为不断增加的训练集大小的函数来比较在法语样本上微调与从德语进行零-shot 跨语言转移的性能:
从图中我们可以看到,零-shot 转移在大约 750 个训练示例之前仍然具有竞争力,之后在法语上进行微调达到了与我们在德语上进行微调时获得的类似性能水平。尽管如此,这个结果也不容忽视!根据我们的经验,即使是让领域专家标记数百个文档也可能成本高昂,特别是对于 NER,其中标记过程是细粒度且耗时的。
我们可以尝试一种最终的技术来评估多语言学习:一次在多种语言上进行微调!让我们看看我们可以如何做到这一点。
对于训练,我们将再次使用前几节中相同的超参数,因此我们只需更新训练器中的日志步骤、模型和数据集:
让我们看看模型在每种语言的测试集上的表现:
它在法语拆分上的表现比以前好得多,与德语测试集的表现相匹配。有趣的是,它在意大利语和英语拆分上的表现也提高了大约 10 个百分点!因此,即使在另一种语言中添加训练数据,也会提高模型在未知语言上的表现。
让我们通过比较分别在每种语言上进行微调和在所有语料库上进行多语言学习的性能来完成我们的分析。由于我们已经在德语语料库上进行了微调,我们可以使用我们的train_on_subset()函数在剩余的语言上进行微调,其中num_samples等于训练集中的示例数:
现在我们已经在每种语言的语料库上进行了微调,下一步是将所有拆分合并在一起,创建一个包含所有四种语言的多语言语料库。与之前的德语和法语分析一样,我们可以使用concatenate_splits()函数在我们在上一步生成的语料库列表上执行此步骤:
现在我们有了我们的多语言语料库,我们可以使用训练器运行熟悉的步骤:
最后一步是从训练器在每种语言的测试集上生成预测。这将让我们了解多语言学习的实际效果如何。我们将在我们的f1_scores字典中收集F[1]-分数,然后创建一个DataFrame,总结我们多语言实验的主要结果:
从这些结果中,我们可以得出一些一般性的结论:
多语言学习可以显著提高性能,特别是如果跨语言转移的低资源语言属于相似的语言家族。在我们的实验中,我们可以看到德语、法语和意大利语在all类别中实现了类似的性能,这表明这些语言彼此之间更相似,而不是与英语相似。
作为一般策略,专注于语言家族内部的跨语言转移是一个好主意,特别是在处理日语等不同脚本的情况下。
在本章中,我们已经将许多经过精细调整的模型推送到了 Hub。虽然我们可以使用pipeline()函数在本地机器上与它们交互,但 Hub 提供了适合这种工作流程的小部件。例如,我们在图 4-5 中展示了一个示例,用于我们的transformersbook/xlm-roberta-base-finetuned-panx-all检查点,可以看到它在识别德语文本的所有实体方面做得很好。
到目前为止,我们已经研究了两个任务:序列分类和标记分类。这两者都属于自然语言理解的范畴,其中文本被合成为预测。在下一章中,我们将首次研究文本生成,其中模型的输入和输出都是文本。