网站建设与管理清考作业,如何搭建一个论坛网站,辽宁省建设厅网站,京东商城网站建设方案书一.第一章 欢迎来到transformer的世界
1.解码器-编码器框架 在Transformer出现之前#xff0c;NLP的最新技术是LSTM等循环架构。这些架 构通过在神经网络连接使用反馈循环#xff0c;允许信息从一步传播到另一 步#xff0c;使其成为对文本等序列数据进行建模的理想选择。如…一.第一章 欢迎来到transformer的世界
1.解码器-编码器框架 在Transformer出现之前NLP的最新技术是LSTM等循环架构。这些架 构通过在神经网络连接使用反馈循环允许信息从一步传播到另一 步使其成为对文本等序列数据进行建模的理想选择。如RNN接收一些输入可以是单词或字符将其馈送给神经网络 然后输出一个称为隐藏状态的向量。同时模型通过反馈循环将信息 反馈给自身然后在下一步中使用。 RNN发挥重要作用的一个领域是机器翻译系统其目标是将一种语言中 的单词序列映射到另一种语言。此类任务通常使用编码器-解码器或序 列到序列架构 来解决这些架构非常适合输入和输出都是任意长度 序列的情况。编码器将输入序列的信息编码为通常称为最后隐藏状态 的数字表示。然后编码器将该状态传给解码器解码器生成输出序 列。 通常编码器和解码器组件可以是能够对序列进行建模的任何类型的 神经网络架构。但这种架构的一个弱点是编码器的最终隐藏状态会产 生信息瓶颈它必须表示整个输入序列的含义因为解码器在生成输 出时只能靠它来读取全部内容。从而很难处理长序列因为当序列过 长时在将所有内容压缩为单个固定表示的过程中可能会丢失序列开 头的信息。 幸运的是有一种方法可以摆脱这一瓶颈就是允许解码器访问编码 器的所有隐藏状态。这种通用机制称为注意力 。它是许多现代神经 网络架构中的关键组件。了解整合了注意力机制的RNN能够帮助我们更 好地理解Transformer架构。 2.注意力机制 注意力机制的主要思想是编码器不再像图1-3那样对整个输入序列只 生成一个状态了而是为每个表示都生成一个状态即解码器可以访 问编码器所有隐藏状态。但是同时处理所有状态会给解码器带来巨 大的输入数量因此需要一些机制来按优先级使用状态。这就是注意 力的用武之地它让解码器在每个解码时间步为每个编码器状态分配 不同量的权重或“注意力”。 下面是引入注意力机制之前编码器-解码器结构之间的对比 尽管注意力机制能够带来更好的翻译但在编码器和解码器中使用循 环神经网络模型仍然存在一个重大缺点计算本质上是顺序的不能 跨输入序列并行化。 Transformer引入了一种新的建模范式完全放弃循环改为使用一种 特殊形式的注意力——自注意力允许注意力对神经网络同一层 中的所有状态进行操作。如图1-6所示其中编码器和解码器都有自己 的自注意力机制其输出被馈送到前馈神经网络FF NN。这种架 构的训练速度比循环神经网络模型快得多从而为NLP许多最新突破铺 平了道路。 3.NLP的迁移学习 如今计算机视觉的常见做法是使用迁移学习即在一项任务上训练 像ResNet这样的卷积神经网络然后在新任务上对其进行适配或微 调。迁移学习允许网络利用从原始任务中学到的知识。在架构上这 是指将模型拆分为主体和头其中头是指针对任务的网络。在训练期 间主体学习来源于领域的广泛特征并将学习到的权重用于初始化 新任务的新模型 。与传统的监督学习相比这种方法通常会产生高 质量的模型这些模型在各种下游任务中效果更佳并且使用的标注 数据更少。 在计算机视觉中模型首先在包含数百万张图像的ImageNet https://image-net.org等大规模数据集上进行训练。这个过程称 为预训练其主要目的是向模型传授图像的基本特征例如边或颜 色。然后这些预训练模型可以在下游任务上进行微调例如使用相 对较少的标注示例通常每个类几百个对花种进行分类。微调模型 通常比在相同数量的标注数据上从头开始训练的监督模型具有更高的 准确率。 尽管迁移学习成为计算机视觉的标准方法但多年来人们并不清楚 NLP的类似预训练过程是怎么样的。从而导致NLP应用程序通常需要大 量的标注数据才能实现高性能。即便如此这种性能也无法与视觉领 域的成就相提并论。 紧随其后的是ULMFiT它使用预训练LSTM模型构建了一个 可以适用于各种任务的通用框架 。 如图1-8所示ULMFiT包括以下三个主要步骤 预训练 对于世界上大多数语言获得大量数字化文本语料库可能很困难英 语更是如此。寻找解决这一问题的方法是NLP研究方向的一个活跃领 域。 最初的训练目标非常简单根据前面的单词预测下一个单词。这类任 务称为语言建模所生成的模型称为语言模型。这种方法的优雅之处 在于不需要标注数据并且可以使用来自Wikipedia等来源的大量可用 文本 。 领域适配 在大规模语料库上进行预训练得出语言模型之后下一步就是将其适 配于业务领域语料库例如从Wikipedia迁移到电影评论的IMDb语料 库如图1-8所示。这一阶段仍然使用语言建模方法但现在模型必 须预测目标语料库中的下一个单词。 微调 这一步使用目标任务的分类层对语言模型进行微调例如对图1-8中 的电影评论的情感进行分类 通过引入一个可行的NLP预训练和迁移学习框架ULMFiT填补了令 Transformer起飞的缺失部分。2018年发布了两类将自注意力与迁移 学习相结合的Transformer模型 1GPT 仅使用Transformer架构的解码器部分以及与ULMFiT相同的语言建模 方法。GPT在BookCorpus1上进行预训练该语料库由7000本来自冒 险、幻想和浪漫等各种流派的图书组成。 2BERT 仅使用Transformer架构的编码器部分以及一种称为掩码语言建模的 特殊形式的语言建模。掩码语言建模的目的是预测文本中随机掩码的 单词。如给定一个句子如 “I looked at my[MASK]and saw that[MASK]was late.”该 模型需要预测[MASK]所掩码的单词最有可能的候选者。BERT在 BookCorpus和英语Wikipedia上进行预训练。 但是由于各个研究实验室在不同的框架PyTorch或TensorFlow上发 布模型而这些模型无法兼容于其他框架NLP从业者将这些模型移植 到他们自己的应用程序中经常会遇到困难。于是 Hugging FaceTransformers库https://oreil.ly/Z79jF这个能够 跨50多个架构的统一API应运而生。该库催化了Transformer研究的爆 炸式增长并迅速渗透到NLP从业者中从而可以轻松地将 Transformer模型集成到当今的许多实际应用中。 4.Hugging Face Transformers库提供规范化接口 将新颖的机器学习架构应用于新任务可能是一项复杂的任务通常涉 及以下步骤 1.将模型架构付诸代码实现通常基于PyTorch或TensorFlow。 2.从服务器加载预训练权重如果有的话。 3.预处理输入并传给模型然后应用一些针对具体任务的后处理。 4.实现数据加载器并定义损失函数和优化器来训练模型。 5.Transformer应用概览 5.1文本分类 在本章中我 们将从pipeline开始pipeline把将原始文本转换为微调模型的一组 预测所需的所有步骤都抽象出来。 我们可以通过调用Hugging Face Transformers库的pipeline()函数 并提供我们感兴趣的任务名称来实例化pipeline 本例的预测结果是模型非常确信输入文本具有正面情感 5.2命名实体识别 现在我们能够预测客户反馈的情感但你经常还想知道反馈是否与特 定项目或服务有关。在NLP中产品、地点和人等现实世界的对象称为 命名实体从文本中提取它们称为命名实体识别NER。我们可以通 过加载以下pipeline并将我们的客户评论提供给它来应用NER 你可以看到pipeline检测出所有实体并且还为每个实体分配了一个 类别例如ORG组织、LOC位置或PER人。在这里我们使 用aggregation strategy参数根据模型的预测对单词进行分组。例 如实体“Optimus Prime”由两个单词组成但被分配到一个类 别MISC杂项。score列告诉我们模型对其识别的实体的信心概 率。我们可以看到它对“Decepticons”和第一个“Megatron”最 没有信心概率最低两者都未能作为一个实体分在同一组。 看到上表word列中那些奇怪的哈希符号#了吗这些符号是模型的 词元分析器生成的词元分析器将单词划分成称为词元的原子单元。 我们将在第2章讲述词元分析器的详细信息。 现在我们已经把文本中的所有命名实体提取出来了但有时我们想问 更有针对性的问题。于是我们就需要问答pipeline。 5.3问答 我们通过下面的context参数把问答的上下文即本节开头提到的文 本提供给模型通过question参数把问题提供给模型。模型将返回 回答。现在我们看看当询问客户反馈的相关问题时会得到什么回 答 我们可以看到除了回答之外pipeline还返回回答在上下文中的位 置即start和end就像NER标注一样。我们将在第7章研究好几种 类型的问答但本节这种特殊的类型称为提取式问答因为答案是直 接从文本提取出来的。 5.4 文本摘要 文本摘要的目标是输入一段长文本然后生成包含所有相关事实的简 短版本。这是一项比前面所讲任务复杂得多的任务因为它需要模型 生成连贯的文本。我们通过以下熟悉的模式实例化文本摘要 得出的摘要还不错尽管其只是简单地复制粘贴部分原始文本但该 模型能够捕捉到问题的根源并正确识别“Bumblebee”出现在输入 文本的末尾是投诉者。 5.5 文本翻译 我们看到模型同样生成了一个非常好的翻译正确使用了德语的代 词如“Ihrem”和“Sie”。在以上示例代码中我们还展示了如何 通过model参数指定模型来替换默认模型从而可以为你的应用程序选 择最佳模型你可以在Hugging Face Hub上找到数千种语言对模 型。接下来我们看看最后一个应用然后再开始介绍Hugging Face生 态系统。 假设你希望通过自动完成功能来撰写答复从而更快地回复客户。你 可以按如下方式使用text-generation pipeline 现在我们已经讲述了Transformer模型一些很酷的应用你可能想知道 训练在哪里进行。我们在本章使用的所有模型都是公开可用的并且 已经针对以上任务微调过了。一般来说无论如何你都会想根据自 己的数据微调模型在后面的章节中你将学习如何做到这一点。 但是训练模型只是任何NLP项目的一小部分——能够有效地处理数 据、与同事共享结果以及使你的工作可重复也是项目的关键组成部 分。幸运的是围绕着Hugging Face Transformers库有一个由一整 套工具组成的大型生态系统从而可以支持大部分现代机器学习工作 流程。我们一起看看这个生态系统吧。 5.6 Hugging Face生态系统 以Hugging Face Transformers库为基础Hugging Face已经迅速 发展成一个完整的生态系统它由许多库和工具组成以加速你的NLP 和机器学习项目。Hugging Face生态系统主要由一系列库和Hub组 成如图1-9所示。库提供相关代码而Hub提供预训练的模型权重、 数据集、度量指标脚本等。 除了预训练模型之外Hub还托管了用于度量指标的数据集和脚本使 你可以重现已发布的结果或为应用程序利用其他数据。 Hub还提供Models模型和Datasets数据集卡片用于记录模型 和数据集相关内容以帮助你就它们是否适合你做出明智的决定。Hub 最酷的功能之一是你可以直接通过各种特定于任务的交互式小部件 试用任何模型如图1-11所示。 Hugging Face Tokenizers库 我们在本章看到的每个pipeline示例背后都有一个词元化 tokenization步骤该步骤将原始文本拆分为称为词元的更小部 分。我们将在第2章详细介绍这个过程但现在只需要知道词元可以是 单词、单词的一部分或只是标点符号等字符就足够了。Transformer模 型是在这些词元的数字表示上训练的因此正确执行这一步对于整个 NLP项目非常重要 Hugging Face Datasets库 Hugging Face Datasets库https://oreil.ly/959YT通过为可在 Hubhttps://oreil.ly/Rdhcu上找到的数千个数据集提供标准界面 来简化这个过程。它还提供智能缓存因此你不需要每次运行代码时 都重复预处理工作并通过利用称为内存映射的特殊机制来突破内 存限制该机制将文件的内容存储到虚拟内存中并使多个进程能够 更有效地修改文件。该库还可以与Pandas和NumPy等流行框架进行互操 作因此你不必离开自己喜欢的数据整理工具。 Hugging Face Accelerate库 如果你曾经不得不用PyTorch编写自己的训练脚本那么在尝试将笔记 本电脑上运行的代码移植到组织集群上运行的代码时你可能会遇到 一些麻烦。Hugging Face Accelerate库 https://oreil.ly/iRfDe为常规训练操作增加了一个抽象层以负 责训练基础设施所需的所有逻辑。这可以通过在必要时简化基础架构 的更改来加速工作流。 Transformer的主要挑战 语言 NLP研究以英语为主。有一些支持其他语言的模型但很难找到稀有或 资源少的语言的预训练模型。我们将在第4章探讨多语言Transformer 及其执行零样本学习跨语言迁移的能力。 数据可用性 尽管我们可以通过迁移学习来显著减少模型所需的标注训练数据量 但与人类执行任务所需的量相比依然差很多。我们将在第9章探讨如 何处理几乎没有标注数据可用的场景。 处理长文本 自注意力在段落长度的文本上效果非常好但是在处理整个文档这样 长度的文本时将变得非常昂贵。第11章将讨论缓解这种情况的方 法。 不透明度 与其他深度学习模型一样Transformer在很大程度上是不透明的。人 们很难或不可能解开模型做出某种预测的“原因”。当需要通过这些 模型来做出关键决策时这是一个特别艰巨的挑战。我们将在第2章和 第4章探讨一些探测Transformer模型误差的方法。 二.第二章文本分类
现在想象一下你是一名数据科学家需要构建一个系统可以自动 识别人们在Twitter上对你公司产品表达的情感状态例如愤怒或喜 悦。在本章中我们将使用一种名为DistilBERT 的BERT变体来解决 这个任务。该模型的主要优点是在实现与BERT相当的性能的同时 体积更小、效率更高。这使我们能够在几分钟内训练一个分类器如 果你想训练一个更大的BERT模型则只需更改预训练模型的 checkpoint。checkpoint对应于加载到给定Transformer架构中的权重 集。 1.数据集 为了构建我们的情感检测器我们将使用一篇论文 中提供的数据 集该论文探讨了英语推文所表示的情感。与大多数情感分析数据集 只涉及“正面”和“负面”这两种极性不同的是这个数据集包含六 种基本情感anger愤怒、disgust厌恶、fear恐惧、joy 喜悦、sadness悲伤和surprise惊讶。我们的任务是训练 一个模型将给定推文按其中一种情感进行分类。 我们将使用Hugging Face Datasets库从Hugging Face Hub https://oreil.ly/959YT下载数据。我们可以使用 list_datasets()函数查看Hub上有哪些可用数据集。 我们可以看到每个数据集都有一个名称因此让我们使用 load_dataset()函数加载情感数据集 我们可以看到它类似于Python字典每个键对应不同的数据集分割 以上代码返回一个Dataset类的实例。Dataset对象是 Hugging Face Datasets库的核心数据结构之一在本书中我们将探 索它的许多特性。 在本例中text列的数据类型为字符串而label列是一个特殊的 ClassLabel对象它包含有关类名称及其映射到整数的信息。 如果我的数据集不在Hub上那该怎么办 在本书的大部分示例中我们将使用Hugging Face Hub库下载数据 集。但在许多情况下你会发现自己处理的数据要么存储在你的笔记 本电脑上要么存储在你的组织的远程服务器上。Datasets提供了多 个加载脚本来处理本地和远程数据集。最常见数据格式的示例参见表 2-1。 data_files参数可以为文件路径或者url 从Datasets到DataFrame 尽管Hugging Face Datasets库提供了许多底层的功能供我们切分和 处理数据但我们通常将Dataset对象转换为Pandas DataFrame这 样我们就可以使用高级API来进行可视化这样做将非常方便。 Hugging Face Datasets库提供了set_format()方法该方法允许我 们更改数据集的输出格式以进行转换。请注意它不会改变底层的数 据格式Arrow表并且你可以随时切换到另一种格式。 查看类分布 对于处理文本分类问题检查类的样本分布无论何时都是一个好主 意。相对于类平衡的数据集来说一个类分布不平衡的数据集可能需 要在训练损失和度量指标方面采取不同的处理方法。 我们可以使用Pandas和Matplotlib快速地可视化类分布 我们可以看到数据集严重不平衡joy和sadness类频繁出现大约是 love和surprise类的510倍。处理不平衡数据的方法包括 ●随机对少数类进行过采样oversample。 ●随机对多数类进行欠采样undersample。 ●收集更多来自未被充分表示的类的标注数据。 为了保持本章简单我们不使用以上的任何方法而将使用原始的、 不平衡的类。如果你想了解更多关于这些采样技术的内容我们建议 你查看Imbalanced-learn库https://oreil.ly/5XBhb。需要注意 的是在将数据集分割成训练/测试集之前不要应用采样方法否则会 在两者之间造成大量泄漏 2.将文本转换成词元 像DistilBERT这样的Transformer模型不能接收原始字符串作为输入。 它假定文本已经被词元化并编码为数字向量。词元化 tokenization是指将字符串分解为给模型使用的原子单元的步 骤。词元化的策略有好几种具体哪种最佳通常需要从语料库中学 习。在讨论DistilBERT使用的Tokenizer之前我们先讨论两种极端情 况字符词元化和单词词元化。 1)字符词元化 最简单的词元化方案是按每个字符单独馈送到模型中。在Python中 str对象实际上是一组数据这使我们可以用一行代码快速实现字符词 元化 这是一个很好的开始但我们还没有完成任务。我们的模型希望把每 个字符转换为一个整数有时这个过程被称为数值化 numericalization。一个简单的方法是用一个唯一的整数来编码 每个唯一的词元在这里为字符 我们可以看到我们得到了一个包括了每个字符到一个唯一性整数的 映射即词元分析器的词表。我们现在可以使用token2idx将词元化的 文本转换成一个整数列表 现在每个词元都已映射到唯一的数字标识符因此称为 input_ids。最后一步是将input_ids转换为独热向量onehot vector的二维张量。在机器学习中独热向量常常用于编码分 类数据包括有序和无序数据。 我们可以为每个类别创建一个新列在 该类别为true时分配1否则分配0。在Pandas中可以使用 get_dummies()函数实现这点 以上DataFrame中的每行是一个独热向量一整行只有一个1其他都 是0。现在看看我们的input_ids我们有类似的问题这些元素的 取值之间引入了虚假的顺序关系。因为这个关系是虚假的所以对两 个ID进行加减是一个没意义的操作。 如果我们将input_ids改成独热编码结果就很容易解释“热”的两 个条目表明是相同的两个词元。在PyTorch中我们可以使用 one_hot()函数对input_ids进行独热编码 本例共有38个输入词元我们得到了一个20维的独热向量因为我们 的词表包含了20个唯一字符。 2)单词词元化 与字符词元化相比单词词元化将文本细分为单词并将每个单词映 射到一个整数。单词词元化使模型跳过从字符学习单词的步骤从而 降低训练过程的复杂性。 词表太大是一个问题因为它导致了神经网络需要大量的参数。举例 来说假设词表中有100万个唯一词项并按照大多数NLP架构中的标 准步骤将100万维输入向量压缩到第一层神经网络中的1000维向量。这 样就导致了第一层的权重矩阵将包含100万×1千10亿个权重。这已经 可以与最大的GPT-2模型 看齐了该模型总计拥有大约15亿个参数 3)子词词元化(既可 以保留输入信息又能保留文本结构) 子词词元化背后的基本思想是将字符和单词词元化的优点结合起来。 一方面我们希望将生僻单词拆分成更小的单元以使模型能够处理 复杂单词和拼写错误。另一方面我们希望将常见单词作为唯一实体 保留下来以便我们将输入长度保持在可管理的范围内。子词词元化 以及单词词元化是使用统计规则和算法从预训练语料库中学习 的。 在NLP中常用的子词词元化算法有几种我们先从WordPiece 算法开 始这是BERT和DistilBERT词元分析器使用的算法。了解WordPiece如 何工作最简单的方法是看它的运行过程。 Hugging Face Transformers库提供了一个很方便的AutoTokenizer 类它能令你快速加载与预训练模型相关联的词元分析器——只需要 提供模型在Hub上的ID或本地文件路径然后调用它的 from_pretrained()方法即可。我们先加载DistilBERT的词元分析器 from transformers import AutoTokenizer
model_ckptdistilbert-base-uncased
tokenizer AutoTokenizer.from_pretrained(model_ckpt)AutoTokenizer类是“auto”类的一种https://oreil.ly/h4YPz 其任务是根据checkpoint的名称自动检索模型的配置、预训练权重或 词表。使用以上代码的优点是可以快速切换模型但是你也可以手动 加载特定类。 和字符词元化一样我们可以看到单词映射成input_ids字段中的唯 一整数。 中的唯 一整数。我们将在2.2.4节中讨论attention mask字段的作用。现在 我们有了input_ids我们可以通过使用词元分析器的 convert_ids_to_tokens()方法将它们转换回词元 我们可以观察到三件事情。首先序列的开头和末尾多了一些特殊的 词元[CLS]和[SEP]。这些词元具体因模型而异它们的主要作用是 指示序列的开始和结束。其次词元都小写了这是该checkpoint的 特性。最后我们可以看到tokenizing和NLP都被拆分为两个词元这 是有道理的因为它们不是常用的单词。##前缀中的##izing和##p意 味着前面的字符串不是空白符将带有这个前缀的词元转换回字符串 时应当将其与前一个词元合并。AutoTokenizer类有一个 convert_tokens_to_string()方法可以做到这一点所以让我们将它 应用到我们的词元 对整个数据集进行词元化 我们将使用DatasetDict对象的map()方法来对整个语料库进行词元 化。 1)将我们的样本进行词元化的处理函 数 这个函数将词元分析器应用于一个批量样本。paddingTrue表示以零 填充样本以达到批量中最长样本的长度truncationTrue表示将样 本截断为模型的最大上下文大小。 此外除了将编码后的推文返回为input_ids外词元分析器还返回一 系列attention_mask数组。这是因为我们不希望模型被额外的填充词 元所困惑注意力掩码attention mask允许模型忽略输入的填充 部分。图 3.训练文本分类器
如第1章所述像DistilBERT这样的模型被预训练用于预测文本序列中 的掩码单词。然而这些语言模型不能直接用于文本分类我们需要 稍微修改它们。为了理解需要做哪些修改我们来看一下基于编码器 的模型如DistilBERT的架构如图2-4所示。 在DistilBERT的情况下它在猜测掩码词元。
首先文本会被词元化并表示为称为词元编码的独热向量。词元编码 的维度由词元分析器词表的大小决定通常包括两万到二十万个唯一 性词元。接下来这些词元编码会被转换为词元嵌入即存在于低维 空间中的向量。然后这些词元嵌入会通过编码器块层传递以产生 每个输入词元的隐藏状态。对于语言建模的预训练目标 每个隐藏 状态都被馈送到一个层该层预测掩码输入词元。对于分类任务我 们将语言建模层替换为分类层。
实际上PyTorch在实现中跳过了为词元编码创建独热向量的步骤因 为将矩阵与独热向量相乘等同于从矩阵中选择一列。这可以通过直接 从矩阵中获取词元ID对应的列来完成。当我们使用nn.Embedding类 时我们将在第3章中看到这一点。
我们有以下两种选择来基于Twitter数据集进行模型训练
特征提取
我们将隐藏状态用作特征只需训练分类器而无须修改预训练模 型。
微调
我们对整个模型进行端到端的训练这样还会更新预训练模型的参 数。 接下来我们将讲述基于DistilBERT的以上两种选择以及这两种选择 的权衡取舍。
使用Transformer作为特征提取器
使用Transformer作为特征提取器相当简单。如图2-5所示我们在训 练期间冻结主体的权重并将隐藏状态用作分类器的特征。这种方法 的优点是我们可以快速训练一个小型或浅层模型。这样的模型可以 是神经分类层或不依赖于梯度的方法例如随机森林。这种方法特别 适用于没有GPU的场景因为隐藏状态只需要预计算一次。 加载预训练模型
我们将使用Hugging Face Transformers库中另一个很方便的自动类 AutoModel。与AutoTokenizer类似AutoModel具有 from_pretrained()方法可用于加载预训练模型的权重。现在我们使 用该方法来加载DistilBERT checkpoint 这里我们使用PyTorch来检查GPU是否可用即代码 torch.cuda.is available()然后将PyTorch的nn.Module.to()方 法与模型加载器链接起来即代码todevice。这确保了如果有 GPU模型将在GPU上运行。如果没有模型将在CPU上运行不过这样 可能会慢很多。
AutoModel类将词元编码转换为嵌入向量然后将它们馈送到编码器栈 中以返回隐藏状态。我们看一下如何从语料库中提取这些状态。 提取最终隐藏状态
作为预热我们检索一个字符串的最终隐藏状态。我们需要做的第一 件事是对字符串进行编码并将词元转换为PyTorch张量。可以通过向词 元分析器提供return_tensors”pt”参数来实现。具体如下 我们可以看到生成的张量形状为[batch sizen_tokens]。现在我 们已经将编码作为张量获取最后一步是将它们放置在与模型相同的 设备上并按以下方式传输入
这里我们使用了torch.no_grad()上下文管理器来禁用梯度的自动计 算。这对推理很有用因为它减少了计算的内存占用。根据模型配 置输出可以包含多个对象例如隐藏状态、损失或注意力这些对 象以类似于Python中的namedtuple的形式排列。在我们的示例中模 型输出是一个BaseModelOutput实例并且包含了其属性名我们可以 通过这些属性名来获取其详情。我们看到我们的模型只有一个属 性即last_hidden_state最终隐藏状态
现在我们知道如何针对单个字符串获取最终隐藏状态。我们通过创建 一个新的hidden_state列来对整个数据集执行相同的操作以存储所 有这些向量。就像我们在词元分析器中所做的那样我们将使用 DatasetDict的map()方法一次性提取所有隐藏状态。我们需要做的第 一件事是将先前的步骤封装在一个处理函数中 这个函数和我们之前的逻辑的唯一不同在于最后一步即将最终的隐 藏状态作为NumPy数组放回CPU。当我们使用批量输入时map()方法要 求处理函数返回Python或NumPy对象。 由于我们的模型期望输入张量下一步需要将input_ids和 attention_mask列转换为torch格式具体如下 现在我们已经得到了与每个推文相关联的隐藏状态下一步是基于它 们训练一个分类器。为了做到这一点我们需要一个特征矩阵我们 来看一下。
创建特征矩阵 可视化训练集
由于在768维度中可视化隐藏状态是个艰难的任务因此我们将使用强 大的UMAP算法将向量投影到2D平面上 。由于UMAP在特征缩放到[0 1]区间内时效果最佳因此我们将首先应用一个MinMaxScaler然后 使用umap-learn库的UMAP实现来缩放隐藏状态 训练一个简单的分类器
我们已经看到不同情感的隐藏状态是不同的尽管其中一些情感并 没有明显的界限。现在让我们使用这些隐藏状态来训练一个逻辑回归 模型使用Scikit-learn。训练这样一个简单的模型速度很快而 且不需要GPU 从准确率上看我们的模型似乎只比随机模型稍微好一点但由于我 们处理的是一个不平衡的多分类数据集它实际上会显著地表现更 好。我们可以通过将其与简单基准进行比较来检查我们的模型是否良 好。在Scikit-learn中有一个DummyClassifier可以用于构建具有简 单启发式的分类器例如始终选择多数类或始终选择随机类。在这种 情况下表现最佳的启发式是始终选择最常见的类这会产生约35%的 准确率
因此使用DistilBERT嵌入的简单分类器明显优于我们的基线。我们 可以通过查看分类器的混淆矩阵来进一步研究模型的性能该矩阵告 诉我们真实标注和预测标注之间的关系 这里我们可以看到anger和fear最常与sadness混淆这与我们可视 化嵌入时所观察到的一致。此外love和surprise经常与joy混淆。 接下来我们将探究微调方法这种方法可以带来更好的分类效果。但 是重要的是要注意微调需要更多的计算资源比如GPU而你的组 织可能没有GPU。在这种情况下基于特征的方法可以是传统机器学习 和深度学习之间的一个很好的折中方案。
微调Transformer模型
现在我们探讨如何进行端到端的Transformer模型微调。在使用微调方 法时我们不使用隐藏状态作为固定特征而是如图2-6所示那样进行 训练。这要求分类头是可微的这就是为什么这种方法通常使用神经 网络进行分类。 训练用作分类模型输入的隐藏状态将有助于我们避免使用可能不适合 分类任务的数据的问题。相反初始隐藏状态在训练过程中适配以 降低模型损失并提高其性能。
加载预训练模型 定义性能指标
为了在训练期间监控指标我们需要为Trainer定义一个 compute_metrics()函数。该函数接收一个EvalPrediction对象这是 一个具有predictions和label_ids属性的命名元组并需要返回一 个将每个指标名称映射到其值的字典。对于我们的应用我们将计算 模型的F1分数和准确率 有了数据集和度量指标后在定义Trainer类之前我们只需要处理最 后两件事情 1.登录我们的Hugging Face Hub账户。从而让我们能够将我们的微 调模型推送到Hub上并与社区分享它。 2.定义训练运行的所有超参数。
登录hugging-face
from huggingface_hub import notebook_login
notebook_login()
训练模型
我们将使用TrainingArguments类来定义训练参数。此类存储了大量信 息从而为训练和评估提供细粒度的控制。最重要的参数是 output_dir它是存储训练过程中所有工件的位置。以下是 TrainingArguments的完整示例 我们可以看到我们的模型在验证集上的F1分数约为92%这比基于特征 的方法有了显著的提升 我们可以通过计算混淆矩阵来更详细地查看训练指标。为了可视化混 淆矩阵我们首先需要获取验证集上的预测结果。Trainer类的predict()方法返回了几个有用的对象我们可以用它们进行评估
predict()方法的输出是一个PredictionOutput对象它包含了 predictions和label_ids的数组以及我们传给训练器的度量指标。 我们可以通过以下方式访问验证集上的度量指标 它还包含了每个类别的原始预测值。我们可以使用np.argmax()进行贪 婪解码预测然后会得到预测标注并且结果格式与前面的基于特征 的方法相同以便我们进行比较 我们可以基于这个预测结果再次绘制混淆矩阵 可见与前面的基于特征的方法相比微调方法的结果更接近于理想 的对角线混淆矩阵。love类别仍然经常与joy混淆这点逻辑上也讲得 过去。surprise也经常被错误地识别为joy或者与fear混淆。总体而 言模型的性能似乎非常不错但在我们结束之前让我们深入了解 模型可能会犯的错误的类型。
储存和共享模型 下载自己的模型 使用自己的模型预测
4.本章小结
祝贺你现在你知道了如何训练一个用于分类推文情感的Transformer
模型我们已经讨论了基于特征和微调的两种互补方法并研究了它
们的优势和劣势。
然而这只是使用Transformer模型构建实际应用程序的第一步我们
还有很多工作要做。以下是在NLP旅程中可能遇到的挑战清单
我的老板希望我的模型昨天就上线了
在大多数应用程序中你并不希望你的模型闲置你希望将它用于预
测当模型被推送到Hub时会自动创建一个推理端点可以使用HTTP
请求调用它。如果你想了解更多信息建议查看推理API的文档
https://oreil.ly/XACF5。
我的用户需要更快的预测
我们已经讨论了解决此问题的一种方法使用DistilBERT。在第8章
中我们将深入研究知识蒸馏DistilBERT创建的过程以及其他加
速Transformer模型的技巧。
你的模型是否也可以执行X任务
正如我们在本章中所提到的Transformer模型非常多才多艺。在本书
的其余部分中我们将使用相同的基本架构探索一系列任务例如问
答和命名实体识别。
我的文本不是英语
事实证明Transformer模型能够支持多语言在第4章中我们将使
用它们同时处理多种语言。
我没有标注数据
如果可用的标注数据非常少则可能不能进行微调。在第9章中我们
将探讨一些应对这种情况的技术。
现在我们已经了解了如何训练和共享Transforme。在第3章中我们将
探索如何从头开始实现我们自己的Transformer模型。
第三章.Transformer架构剖析
在第2章中我们了解了对于Transformer模型进行微调和评估需要的 条件。现在我们来看看它们里面是如何工作的。在本章中我们将探 索Transformer模型的主要组成部分以及如何使用PyTorch实现它 们。我们还将提供关于如何在TensorFlow实现相同内容的指导。我们 首先专注于构建注意力机制然后添加必要的部分来使Transformer编 码器起作用。我们还会简要地探讨编码器和解码器模块之间的架构差 异。在本章结束时你将能够自己实现一个简单的Transformer模型
1.Transformer架构
原始Transformer是基于编码器-解码 器架构的该架构广泛用于机器翻译等任务中即将一个单词序列从 一种语言翻译成另一种语言。
该架构由两个组件组成
编码器
将一个词元的输入序列转化为一系列嵌入向量通常被称为隐藏状态 或上下文。
解码器
利用编码器的隐藏状态逐步生成一个词元的输出序列每次生成一 个词元。 而编码器和解码器又由如图3-1所示的几个构建块组成。 ●输入的文本会使用第2章中所介绍的技术进行词元化并转换成词元 嵌入。由于注意力机制不了解词元之间的相对位置因此我们需要一 种方法将词元位置的信息注入输入以便模拟文本的顺序性质。也就 是说词元嵌入会与包含每个词元位置信息的位置嵌入进行组合。
●编码器由一系列编码器层或“块”堆叠而成类似于计算机视觉中 叠加卷积层。解码器也是如此由一系列解码器层堆叠而成。
●编码器的输出被提供给每个解码器层然后解码器生成一个对于序 列中下一个最可能的词元的预测。该步骤的输出随后被反馈回解码器 以生成下一个词元以此类推直到达到特殊的结束序列EOS词 元。在图3-1的例子中假设解码器已经预测了“Die”和“Zeit”。 现在它会把这两个作为输入并作为所有编码器的输出来预测下一 个词元“fliegt”。在下一步中解码器将“fliegt”作为额外的输 入。我们重复这个过程直到解码器预测出EOS词元或我们达到了最大 长度。
Transformer架构最初是为序列到序列的任务如机器翻译而设计 的但编码器和解码器模块很快就被抽出来单独形成模型。虽然 Transformer模型已经有数百种不同的变体但其中大部分属于以下三 种类型之一
纯编码器
这些模型将文本输入序列转换为富数字表示的形式非常适用于文本 分类或命名实体识别等任务。BERT及其变体例如RoBERTa和 DistilBERT属于这类架构。此架构中为给定词元计算的表示取决于 左侧词元之前和右侧词元之后上下文。这通常称为双向注意 力。
纯解码器
针对像“谢谢你的午餐我有一个……”这样的文本提示这类模型 将通过迭代预测最可能的下一个词来自动完成这个序列。GPT模型家族 属于这一类。在这种架构中对于给定词元计算出来的表示仅依赖于 左侧的上下文。这通常称为因果或自回归注意力。
编码器-解码器
这类模型用于对一个文本序列到另一个文本序列的复杂映射进行建 模。它们适用于机器翻译和摘要任务。除了Transformer架构它将编 码器和解码器相结合BART和T5模型也属于这个类。
实际上纯解码器和纯编码器架构的应用之间的区别有些模糊不清。 例如像GPT系列中的纯解码器模型可以被优化用于传统上认为是序列 到序列任务的翻译任务。同样BERT等纯编码器模型也可以应用于通 常与编码器-解码器或纯解码器模型相关的文本摘要任务注 。
2.编码器 自注意力机制
正如我们在第1章中讨论的那样注意力机制是一种神经网络为序列中 的每个元素分配不同权重或“注意力”的机制。对文本序列来说元 素则为我们在第2章遇到的词元嵌入其中每个词元映射为固定维度的 向量。例如在BERT中每个词元表示为一个768维向量。自注意力 中的“自”指的是这些权重是针对同一组隐藏状态计算的例如编码 器的所有隐藏状态。与自注意力相对应的与循环模型相关的注意力 机制则计算每个编码器隐藏状态对于给定解码时间步的解码器隐藏状 态的相关性。
自注意力的主要思想是不是使用固定的嵌入值来表示每个词元而 是使用整个序列来计算每个嵌入值的加权平均值。另一种表述方式是 说给定词元嵌入的序列x…x自注意力产生新的嵌入序列其 中每个是所有x的线性组合 当你看到单词“flies”时会想到什么。也许你会想到令人讨厌的昆 虫但是如果你得到更多的上下文比如 “time flies like an arrow”那么你会意识到“flies”表示 的是动词。同样地我们可以通过以不同的比例结合所有词元嵌入来 创建“flies”的表示形式也许可以给“time”和“arrow”的词元 嵌入分配较大的权重wji。用这种方式生成的嵌入称为上下文嵌入早 在Transformer发明之前就存在了例如ELMo语言模型 。
现在我们看一下注意力权重是如何计算的。
实现自注意力层的方法有好几种但最常见的是那篇著名的 Transformer架构论文 所介绍的缩放点积注意力scaled dotproduct attention。要实现这种机制需要四个主要步骤
1.将每个词元嵌入投影到三个向量中分别称为query、key和value。
2.计算注意力分数。我们使用相似度函数确定query和key向量的相关 程度。顾名思义缩放点积注意力的相似度函数是点积并通过嵌入 的矩阵乘法高效计算。相似的query和key将具有较大的点积而那些 没有相似处的则几乎没有重叠。这一步的输出称为注意力分数在一 个有n个输入词元的序列中将对应着一个n×n的注意力分数矩阵。
3.计算注意力权重。点积在一般情况下有可能会产生任意大的数这 可能会导致训练过程不稳定。为了处理这个问题首先将注意力分数 乘以一个缩放因子来规范化它们的方差然后再通过softmax进行规范 化以确保所有列的值相加之和为1。结果得到一个n×n的矩阵该矩 阵包含了所有的注意力权重wji。
4.更新词嵌入。计算完注意力权重之后我们将它们与值向量v… v相乘最终获得词嵌入表示。 1n
我们可以使用一个很赞的库Jupyter的BertViz https://oreil.ly/eQK3I来可视化以上的注意力权重计算过 程。该库提供了一些可用于可视化Transformer模型的不同方面注意力 的函数。如果想可视化注意力权重我们可以使用neuron_view模块 该模块跟踪权重计算的过程以显示如何将query向量和key向量相结 合以产生最终权重。由于BertViz需要访问模型的注意力层因此我们 将使用BertViz中的模型类来实例化我们的BERT checkpoint然后使 用show()函数为特定的编码器层和注意力头生成交互式可视化。请注 意你需要单击左侧的“”才能激活注意力可视化。 从以上可视化图中我们可以看到query向量和key向量的值表示为一 条条的条带其中每个条带的强度对应于其大小。连线的权重是根据 词元之间的注意力加权的我们可以看到“flies”的query向量与 “arrow”的key向量重叠最强。
在你第一次接触query、key和value向量的概念时可能会觉得这些概 念有点晦涩难懂。这些概念受到信息检索系统的启发但我们可以用 一个简单的类比来解释它们的含义。你可以这样想象你正在超市购 买晚餐所需的所有食材。你有一份食谱食谱里面每个食材可以视为 一个query。然后你会扫描货架通过货架上的标注key以检查 该商品是否与你列表中的食材相匹配相似度函数。如果匹配成 功那么你就从货架上取走这个商品value。 我们将使用PyTorch实现Transformer架构使用TensorFlow实现 Transformer架构的步骤与之类似。两个框架中最重要的函数之间的映 射关系详见表3-1。 1对文本进行词元化因此我们使用词元分析 器提取输入ID 正如我们在第2章所看到的那样句子中的每个词元都被映射到词元分 析器的词表中的唯一ID。为了保持简单我们还通过设置 add_special_tokensFalse来将[CLS]和[SEP]词元排除在外。接下 来我们需要创建一些密集嵌入。这里的密集是指嵌入中的每个条目 都包含一个非零值。
2在PyTorch中我们可以通过使用 torch.nn.Embedding层来实现这一点该层作为每个输入ID的查找 表 在这里我们使用AutoConfig类加载了与bert-baseuncased checkpoint相关联的config.json文件。在 Hugging Face Transformers库中每个checkpoint都被分配一个配 置文件该文件指定了各种超参数例如vocab_size和hidden_size。 在我们的示例中每个输入ID将映射到nn.Embedding中存储的30 522 个嵌入向量之一其中每个向量维度为768。AutoConfig类还存储其他 元数据例如标注名称用于格式化模型的预测。
需要注意的是此时的词元嵌入与它们的上下文是独立的。这意味 着同形异义词拼写相同但意义不同的词如前面例子中的 “flies”“飞行”或“苍蝇”具有相同的表示形式。后续的注 意力层的作用是将这些词元嵌入进行混合以消除歧义并通过其上 下文的内容来丰富每个词元的表示。
3现在我们有了查找表通过输入ID我们可以生成嵌入向量 这给我们提供了一个形状为[batch_sizeseq lenhidden_dim]的 张量就像我们在第2章中看到的一样。这里我们将推迟位置编码因 此下一步是创建query、key和value向量并使用点积作为4相似度函数 来计算注意力分数 这产生了一个5×5矩阵其中包含批量中每个样本的注意力分数。稍 后我们将看到query、key和value向量是通过将独立的权重矩阵WQ应 用到嵌入中生成的但这里为简单起见我们将它们设为相等。在缩 放点积注意力中点积按照嵌入向量的大小进行缩放这样我们在训 练过程中就不会得到太多的大数从而可以避免下一步要应用的 softmax饱和。
5接下来我们应用softmax 6最后将注意力权重与值相乘 7这就是全部了我们已经完成了简化形式的自注意力机制实现的所有 步骤请注意整个过程仅涉及两个矩阵乘法和一个softmax因此你 可以将“自注意力”视为一种花哨的平均形式。 我们把这些步骤封装成一个函数以便以后我们可以重用它 我们的注意力机制在query向量和key向量相等的情况下会给上下文 中相同的单词分配非常高的分数特别是给当前单词本身query向量 与自身的点积总是1。而实际上一个单词的含义将更好地受到上下文 中其他单词的影响而不是同一单词甚至自身。以前面的句子为 例通过结合“time”和“arrow”的信息来定义“flies”的含义 比重复提及“flies”要更好。那么我们如何实现这点
我们可以让模型使用三个不同的线性投影将初始词元向量投影到三个 不同的空间中从而允许模型为query、key和value创建一个不同的向 量集。
多头注意力
前面提到我们将query、key和value视为相等来计算注意力分数和权 重。但在实践中我们会使用自注意力层对每个嵌入应用三个独立的 线性变换以生成query、key和value向量。这些变换对嵌入进行投 影每个投影都带有其自己的可学习参数这使得自注意力层能够专 注于序列的不同语义方面。
同时拥有多组线性变换通常也是有益的每组变换代表一种所谓的 注意力头。多头注意力层如图3-5所示。但是为什么我们需要多个注 意力头原因是一个注意力头的softmax函数往往会集中在相似度的某 一方面。拥有多个头能够让模型同时关注多个方面。例如一个头负 责关注主谓交互而另一个头负责找到附近的形容词。显然我们没 有在模型中手工制作这些关系它们完全是从数据中学习到的。如果 你对计算机视觉模型熟悉你可能会发现其与卷积神经网络中的滤波 器相似其中一个滤波器负责检测人脸而另一个滤波器负责在图像 中找到汽车的车轮。 现在我们来编码实现首先编写一个单独的注意力头的类 这里我们初始化了三个独立的线性层用于对嵌入向量执行矩阵乘 法以生成形状为[batch_sizeseq_lenhead_dim]的张量其中 head_dim是我们要投影的维数数量。尽管head_dim不一定比词元的嵌 入维数embed_dim小但在实践中我们选择head_dim是 embed_dim的倍数以便跨每个头的计算能够保持恒定。例如BERT有 12个注意力头因此每个头的维数为768/1264。 现在我们有了一个单独的注意力头因此我们可以将每个注意力头的 输出串联起来来实现完整的多头注意力层 请注意注意力头连接后的输出也通过最终的线性层进行馈送以生 成形状为[batch_sizeseq_lenhidden_dim]的输出张量以适用于 下游的前馈网络。为了确认我们看看多头注意力层是否产生了我们 输入的预期形状。在初始化MultiHeadAttention模块时我们传递了 之前从预训练的BERT模型中加载的配置。这确保我们使用与BERT相同 的设置 这么做是可行的最后我们再次使用BertViz可视化单词“flies” 的两个不同用法的注意力。这里我们可以使用BertViz的head_view() 函数通过计算预训练checkpoint的注意力并指示句子边界的位置来 显示注意力 这种可视化展示了注意力权重表现为连接正在被更新嵌入的词元 左侧与所有被关注的单词右侧之间的线条。线条的颜色深度 表现了注意力权重的大小深色线条代表值接近于1淡色线条代表值 接近于0。
在这个例子中输入由两个句子组成[CLS]和[SEP]符号是我们在第2 章中遇到的BERT的词元分析器中的特殊符号。从可视化结果中我们可 以看到注意力权重最大的是属于同一句子的单词这表明BERT能够判 断出它应该关注同一句子中的单词。然而对于单词“flies”我们 可以看到BERT已经识别出在第一句中“arrow”是重要的在第二句中 “fruit”和“banana”是重要的。这些注意力权重使模型能够根据它 所处的上下文来区分“flies”到底应该为动词还是名词
至此我们已经讲述完注意力机制了我们来看一下如何实现编码器层 缺失的一部分位置编码前馈神经网络。
前馈层
编码器和解码器中的前馈子层仅是一个简单的两层全连接神经网络 但有一点小小的不同它不会将整个嵌入序列处理为单个向量而是 独立处理每个嵌入。因此该层通常称为位置编码前馈神经网络。有 时候你还会看到它又被称为内核大小为1的1维卷积这种叫法通常来 自具有计算机视觉背景的人例如OpenAI GPT代码库就是这么叫 的。论文中的经验法则是第一层的隐藏尺寸应为嵌入尺寸的四倍 并且最常用的激活函数是GELU。这是大部分容量和记忆发生的地方 也是扩展模型时最经常进行缩放的部分。我们可以将其实现为一个简 单的nn.Module如下所示 需要注意的是像nn.Linear这样的前馈层通常应用于形状为 batch_sizeinput_dim的张量上它将独立地作用于批量维度中 的每个元素。这对于除了最后一个维度之外的任何维度都是正确的 因此当我们将形状为batch_sizeseq_lenhidden_dim的张量传 给该层时该层将独立地应用于批量和序列中的所有词元嵌入这正 是我们想要的。我们可以通过传递注意力输出来测试这一点 现在我们已经拥有了创建完整的Transformer编码器层的所有要素唯 一剩下的部分是决定在哪里放置跳跃连接和层规范化。我们看看这会 如何影响模型架构。
添加层规范化
如前所述Transformer架构使用了层规范化和跳跃连接。前者将批处 理中的每个输入规范化为零均值和单位方差。跳跃连接直接将张量传 给模型的下一层而不做处理只将其添加到处理的张量中。在将层 规范化放置在Transformer的编码器或解码器层中时论文提供了两种 选项1 后置层规范化 这是Transformer论文中使用的一种结构它把层规范化置于跳跃连接 之后。这种结构从头开始训练时会比较棘手因为梯度可能会发散。 因此在训练过程中我们经常会看到一个称为学习率预热的概念其 中学习率在训练期间从一个小值逐渐增加到某个最大值。 2前置层规范化 这是论文中最常见的布局它将层规范化置于跳跃连接之前。这样做 往往在训练期间更加稳定并且通常不需要任何学习率预热。
这两种方式的区别
这里我们将使用第二种方式因此我们可以简单地将我们的基本构件 粘在一起如下所示 现在使用我们的输入嵌入来测试一下 在更高层面的术语中自注意力层和前馈层称为置换等变的——如果 输入被置换那么层的相应输出将以完全相同的方式置换。
我们已经成功地从头开始实现了我们的第一个Transformer编码器层 然而我们设置编码器层的方式存在一个问题它们对于词元的位置 是完全不变的。由于多头注意力层实际上是一种精致的加权和因此 词元位置的信息将丢失 。
幸运的是有一种简单的技巧可以使用位置嵌入来整合位置信息。我 们来看看。
位置嵌入
位置编码基于一个简单但非常有效的想法用一个按向量排列的位置 相关模式来增强词元嵌入。如果该模式对于每个位置都是特定的那 么每个栈中的注意力头和前馈层可以学习将位置信息融合到它们的转 换中。
有几种实现这个目标的方法其中最流行的方法之一是使用可学习的 模式特别是在预训练数据集足够大的情况下。这与仅使用词元嵌入 的方式完全相同但是使用位置索引作为输入而不是词元ID。通过 这种方法在预训练期间可以学习到一种有效的编码词元位置的方 式。
我们创建一个自定义的Embeddings模块它将输入的input_ids投影到 密集的隐藏状态上并结合Position_ids的位置嵌入进行投影。最终 的嵌入层是两个嵌入层的简单求和 我们可以看到嵌入层现在为每个词元创建了一个密集的嵌入。 这种可学习的位置嵌入易于实现并广泛使用除此之外还有其他一 些方法
绝对位置表示
Transformer模型可以使用由调制正弦和余弦信号组成的静态模式来编 码词元的位置。当没有大量数据可用时这种方法尤其有效。
相对位置表示
通过结合绝对和相对位置表示的想法旋转位置嵌入在许多任务上取 得了优秀的结果。GPT-Neo是一个采用旋转位置嵌入的模型的例子。
尽管绝对位置很重要但有观点认为在计算嵌入时周围的词元最 为重要。相对位置表示遵循这种直觉对词元之间的相对位置进行编 码。这不能仅通过在开头引入新的相对嵌入层来设置因为相对嵌入 针对每个词元会因我们对序列的访问位置的不同而不同。对此注意 力机制本身通过添加额外项来考虑词元之间的相对位置。像DeBERTa等 模型就使用这种表示 。
现在我们把所有内容整合起来通过将嵌入与编码器层结合起来构建 完整的Transformer编码器 添加分类头
Transformer模型通常分为与任务无关的主体和与任务相关的头。我们 将在第4章讲述Hugging Face Transformers库的设计模式时会再次 提到这种模式。到目前为止我们所构建的都是主体部分的内容如 果我们想构建一个文本分类器那么我们还需要将分类头附加到该主 体上。每个词元都有一个隐藏状态但我们只需要做出一个预测。有 几种方法可以解决这个问题。一般来说这种模型通常使用第一个词 元来进行预测我们可以附加一个dropout和一个线性层来进行分类预 测。下面的类对现有的编码器进行了扩展以用于序列分类 在初始化模型之前我们需要定义希望预测的类数目 这正是我们一直在寻找的。对于批处理中的每个样本我们会得到输 出中每个类别的非规范化logit值。这类似于我们在第2章中使用的 BERT模型用于检测推文中的情感。
至此我们对编码器以及如何将其与具体任务的头相组合的部分就结束 了。现在我们将注意力双关语转向解码器。
3.解码器
解码器和编码器的主要区别在于解码器有两个注意力子 层 掩码多头自注意力层
确保我们在每个时间步生成的词元只基于过去的输出和当前正在预测 的词元。如果没有这样做那么解码器将能够在训练时通过简单复制 目标翻译来欺骗我们导致训练失败。我们需要对输入进行掩码以 确保任务不是简单复制目标翻译。
编码器-解码器注意力层
请注意与自注意力层不同编码器-解码器注意力中的key和query向 量可能具有不同的长度。这是因为编码器和解码器输入通常涉及长度 不同的序列。因此此层中的注意力得分矩阵是矩形的而不是正方 形的。
对编码器栈的输出key和value向量执行多头注意力并以解码器的中 间表示作为query 。在这种方式中编码器-解码器注意力层学习如 何关联来自两个不同序列例如两种不同语言的词元。在每个块 中解码器都可以访问编码器的key和value。
掩码自注意 力的技巧是引入一个掩码矩阵该矩阵对角线下方的元素为1上方的 元素为0 这里我们使用了PyTorch的tril()函数来创建下三角矩阵。一旦我们有 了这个掩码矩阵我们可以使用Tensor.masked fill()将所有的0替 换为负无穷来防止每个注意力头查看后面的词元 通过将矩阵对角线上方的值设为负无穷可以保证当我们对分数进行 softmax时注意力权重都为0因为e-0回想一下softmax计算的是 规范化指数。我们只需要对本章早先实现的缩放点积注意力函数进 行一点点修改就能轻松地包含这种掩码行为 在开始构建比文本分类更高级的 任务模型之前我们稍微退后一步看一看Transformer模型的各种分 支以及它们之间的关系。
揭秘编码器-解码器注意力机制
这里我们用一个示例看看是否帮助你解开编码器-解码器注意力的神秘 面纱。想象一下你解码器正在班上参加一场考试。你的任务 是根据前面的单词解码器输入来预测下一个单词听起来很简 单但实际上非常难你自己试试预测本书的某一段的下一个单 词。幸运的是你旁边的同学编码器拥有整篇文章。不幸的 是他们是留学生文章是用他们的母语写的。不过聪明如你你还 是想出了一种作弊的方式。你画了一幅小漫画描述了你已经拥有的 文章内容query交给了你的同学。然后他们会尝试找出哪一段文 章与那个描述匹配key并画一幅漫画描述紧随该段文章之后的单 词value然后把这个答案传给你。有了这种系统性的帮助你轻 松地通过了考试。
4.认识Transformer
Transformer的生命树
随着时间的推移三种主要的架构都进行了各自的演进。图3-8展示了 一些最著名的模型及其后代。 Transformer模型总计有超过50种不同的架构因此图3-8未能涵盖所 有架构它只列出了一些重要的里程碑。在本章前面部分我们已经 详细介绍了Transformer模型最初的架构现在我们着重研究一些比较 关键的后代架构我们先从编码器分支开始。 5.本章小结
本章我们从Transformer架构的核心——自注意力机制的深入研究开 始随后逐步添加了所有必要的组成部分来构建Transformer编码器模 型。我们添加了用于词元和位置信息的嵌入层构建了一个前馈层以 补充注意力头最后我们往模型主体添加了一个分类头来进行预 测。我们还研究了Transformer架构的解码器部分并在本章末尾浏览 了Transformer最重要的三种分支和模型架构。
第四章.多语言命名实体识别
1.数据集
本章我们将使用跨语言传输多语言编码器XTREME基准测试的子 集即WikiANN又称PAN-X 。该数据集包括多种语言的维基百科文 章其中包括瑞士使用最广泛的四种语言德语62.8%、法语 22.9%、意大利语8.4%和英语5.9%。每篇文章都用LOC 位置、PER人物和ORG组织并以IOB2格式进行标注 https://oreil.ly/yXMUn。在IOB2这种格式中B-前缀表示实体 的开头位于之后的属于同一实体的连续词元则赋予I-前缀。O标记表 示该词元不属于任何实体。以如下句子为例 要在XTREME中加载PAN-X的一个子集我们需要知道要传给 load_dataset()函数的数据集配置。每当要处理具有多个领域的数据 集时你可以使用get_dataset_config_names()函数来查找可用的子 集 有183个配置太多了我们需要缩小搜索范围我们只寻找以 “PAN”开头的配置 为了创建一个真实的瑞士语语料库我们将从PAN-X中按照前面所述的 口语比例提取德语de、法语fr、意大利语it和英语 en语料库。我们这么做将会造成语言类别不平衡这在现实世界 的数据集中非常普遍其中由于缺乏熟练掌握该语言的领域专家获 取少数民族语言的标注样本可能会很昂贵。这种不平衡的数据集将能 够模拟在多语言应用程序中工作时的常见情况我们将看到如何构建 一个可以处理所有语言的模型。
为了追踪每种语言我们创建一个Python defaultdict将语言代码 作为key并将类型为DatasetDict的PAN-X语料库作为value进行存储 这里我们使用shuffle()方法确保不会意外地偏向某个数据集拆分而 select()方法允许我们根据fracs中的值对每个语料库进行下采样。然 后我们可以通过访问Dataset.num_rows属性来查看训练集中每种语言 的实例数量 按照前面的设计我们在德语方面拥有比其他所有语言加起来都多的 样本所以我们将以德语作为起点向法语、意大利语和英语进行零 样本跨语言迁移。
from collections import defaultdict, Counter
import pandas as pdsplit2freqs defaultdict(Counter)
for split, dataset in panx_de.items():for row in dataset[ner_tags_str]:for tag in row:if tag.startswith(B-):tag_type tag[2:] # 获取 B- 后面的部分split2freqs[split][tag_type] 1# 创建 DataFrame
df pd.DataFrame.from_dict(split2freqs, orientindex)
print(df)2.多语言Transformer
多语言Transformer模型通常以三种不 同的方式进行评估 en 在英语训练数据上进行微调然后在每种语言的测试集上进行评估。 each 针对每种语言的测试数据进行微调和评估以度量每种语言的性能。 all 在所有的训练数据上进行微调然后在每种语言的测试数据集上进行 评估。
我们在本章中将使用XLM-R模型。
3.多语言词元化技术
XLM-R使用一种名为SentencePiece的词元分析器而不是使用 WordPiece词元分析器。SentencePiece词元分析器是基于所有100种语 言的原始文本进行训练的。
SentencePiece词元分析器
SentencePiece词元分析器基于一种称为Unigram的子词分割类型并 将每个输入文本编码为Unicode字符序列。这一特点对于处理多语言语 料库尤其有用因为它能够忽略重音符号、标点符号以及许多语言 如日语中没有空格字符的情况。此外SentencePiece的另一个特 别之处在于它使用Unicode符号U2581或者__字符又称为下四分之 一块字符来表示空格。这使得SentencePiece能够无歧义地反词元化 序列也不需要依赖特定于语言的词元化预处理器。还是以我们前面 的示例为例我们可以看到WordPiece丢失了“York”和“!”之间的 空格信息。而SentencePiece则保留了词元化后的文本中的空格因此 我们可以无歧义地将其转换回原始文本。这一特性尤其适用于处理多 语言文本因为它可以保持原始文本的完整性而不会引入额外的歧 义。
4.命名实体识别中的Transformers 5.自定义Hugging Face Transformers库模型类
我们已经看到当我们从 预训练任务转换到下游任务时我们需要用适合该任务的层替换模型 的最后一层。这个最后一层被称为模型头它是与任务相关的部分
模型的其余部分称为主体包括词元嵌入和Transformer层这些层是 通用的、与任务无关的。这种结构表现在具体的代码实现类中模型 主体在BertModel或GPT2Model之类的类中实现这些类将返回最后一 层隐藏状态。而BertForMaskedLM或 BertForSequence Classification则为与任务相关的模型类它们基 于基础模型然后在隐藏状态之上添加必要的头具体如图4-4所示。 创建用于词元分类的自定义模型
这里我们通过建立一个适用于XLM-R的自定义词元分类头的练习来进行 学习。由于XLM-R使用与RoBERTa相同的模型架构因此我们将使用 RoBERTa作为基本模型然后在此之上增加适用于XLM-R的设置。需要 注意的是本练习出于教学目的旨在向你展示如何为自己的任务构 建自定义模型。在实际工作中对于词元分类 Hugging Face Transformers库已经有一个名为 XLMRobertaForTokenClassification的类可以使用你可以直接导入 该类。
import torch.nn as nn
from transformers import XLMRobertaConfig
from transformers.modeling_outputs import TokenClassifierOutput
from transformers.models.roberta.modeling_roberta import RobertaModel
from transformers.models.roberta.modeling_roberta import RobertaPreTrainedModelclass XLMRobertaForTokenClassification(RobertaPreTrainedModel):config_class XLMRobertaConfigdef __init__(self, config):super().__init__(config)self.num_labels config.num_labels# Load model bodyself.roberta RobertaModel(config, add_pooling_layerFalse)# Set up token classification headself.dropout nn.Dropout(config.hidden_dropout_prob)self.classifier nn.Linear(config.hidden_size, config.num_labels)# Load and initialize weightsself.init_weights()def forward(self, input_idsNone, attention_maskNone, token_type_idsNone, labelsNone, **kwargs):# Use model body to get encoder representationsoutputs self.roberta(input_ids, attention_maskattention_mask, token_type_idstoken_type_ids, **kwargs)# Apply classifier to encoder representationsequence_output self.dropout(outputs[0])logits self.classifier(sequence_output)# Calculate lossesloss Noneif labels is not None:loss_fct nn.CrossEntropyLoss()loss loss_fct(logits.view(-1, self.num_labels), labels.view(-1))# Return model output objectreturn TokenClassifierOutput(lossloss,logitslogits,hidden_statesoutputs.hidden_states,attentionsoutputs.attentions)加载自定义模型
现在我们准备加载词元分类模型。除了模型名称之外我们还需要提 供一些额外的信息包括我们将用于词元每个实体的标记以及每个标 记与ID之间的映射和反向映射。所有这些信息都可以从我们的tags变 量中进行推导tags变量作为一个ClassLabel对象具有一个names属 性我们可以使用它来推导映射
第五章 文本生成
1.生成连贯文本的挑战
正如我们所看到的对于像序列或词元分类这样的特定任务生 成预测是相当简单的。模型产生一些logit我们要么取最大值以获得 预测类要么应用softmax函数以获得每个类的预测概率。相比之下 将模型的概率输出转换为文本需要一种解码方法这在文本生成中引 入了一些独特的挑战
解码是迭代进行的因此涉及的计算量比仅通过模型前向传递的一 次传输入要大得多。
生成的文本的质量和多样性取决于解码方法和相关的超参数的选 择。
2.贪婪搜索解码
1)从huggingface加载gpt2模型
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
device cuda if torch.cuda.is_available()else cpu
model_name gpt2
tokenizer AutoTokenizer.from_pretrained(model_name)
model AutoModelForCausalLM.from_pretrained(model_name).to(device)
2自己编写贪婪算法并使用
import pandas as pd
# 输入文本
input_txt Transformers are the
input_ids tokenizer(input_txt, return_tensorspt)[input_ids].to(cuda)# 设置迭代参数
n_steps 8
choices_per_step 5# 用于存储结果
iterations []# 不需要计算梯度
with torch.no_grad():for step in range(n_steps):iteration dict()iteration[Input] tokenizer.decode(input_ids[0])# 生成模型输出output model(input_idsinput_ids)# 获取最后一个token的logit并应用softmaxnext_token_logits output.logits[0, -1, :]next_token_probs torch.softmax(next_token_logits, dim-1)# 获取具有最高概率的tokenssorted_ids torch.argsort(next_token_probs, dim-1, descendingTrue)# 存储具有最高概率的tokensfor choice_idx in range(choices_per_step):token_id sorted_ids[choice_idx].item()token_prob next_token_probs[token_id].cpu().numpy()token_choice f{tokenizer.decode([token_id])} ({100 * token_prob:.2f}%)iteration[fChoice {choice_idx1}] token_choice# 选择下一个token取最高概率的一个next_token_id sorted_ids[0].item()input_ids torch.cat([input_ids, torch.tensor([[next_token_id]]).to(cuda)], dim-1)iterations.append(iteration)# 转换结果为DataFrame以便于查看
df pd.DataFrame(iterations)
print(df)3将使用 Hugging Face Transformers库内置的generate()函数来探索更复杂 的解码方法。
input_ids tokenizer(input_txt, return_tensorspt)[input_ids].to(device)
output model.generate(input_ids,max_new_tokensn_steps, do_sampleFalse)
print(tokenizer.decode(output[0]))
4重现一开头的 OpenAI的独角兽故事
# 设置输入文本和最大长度
max_length 128
input_txt In a shocking finding, scientist discovered
a herd of unicorns living in a remote, previously unexplored
valley, in the Andes Mountains. Even more surprising to the
researchers was the fact that the unicorns spoke perfect English.\n\n# 将输入文本tokenize并移动到GPU
input_ids tokenizer(input_txt, return_tensorspt)[input_ids].to(cuda)# 使用贪婪搜索生成输出
output_greedy model.generate(input_ids, max_lengthmax_length, do_sampleFalse)# 解码并打印输出
print(tokenizer.decode(output_greedy[0], skip_special_tokensTrue))5)贪婪搜索解码的一个主要缺点
它往 往会产生重复的输出序列最后两段是重复的在新闻文章中这显 然是不可取的。这是贪婪搜索算法的一个常见问题它们可能无法给 出最优解。在解码的上下文中它们可能会错过整体概率更高的词序 列只因为高概率的单词恰好在低概率的单词之前出现。 幸运的是还有更好的方法——束搜索解码这也是一种很流行的方 法。
3.束搜索解码
不同于每次都选择概率最高的词元束搜索会跟踪最有可能的下一个 top-b个词元其中b称为束的数量或部分假设。下一个束是通过考虑 现有集合的所有可能的下一个词元扩展并选择最有可能的b个扩展而选 择的。该过程会一直重复直到达到最大长度或遇到EOS词元然后根 据它们的对数概率对b个束进行排名选择最有可能的序列。束搜索的 示例如图5-4所示。 1)让我们计算并比较贪婪搜索和束搜索生成的文本的对数概率以查看 束搜索是否可以提高总体概率。由于Hugging Face Transformers库 返回给定输入词元的下一个词元的非规范化logit因此我们首先需要 规范化logit以创建序列中每个词元在整个词表的概率分布。然后我 们需要选择仅存在于序列中的词元的概率。实现这些步骤的具体函数 如下
import torch.nn.functional as F
def log_probs_from_logits(logits, labels):logp F.log_softmax(logits,dim-1)logp_label torch.gather(logp,2,labels.unsqueeze(2)).squeeze(-1)return logp_label
以上函数给出了单个词元的对数概率如果要获得序列的总对数概 率我们只需将每个词元的对数概率相加。
def sequence_logprob(model,labels,input_len0):with torch.no_grad():output model(labels)log_probs log_probs_from_logits(output.logits[:,:-1,:],labels[:,1:])seq_log_prob torch.sum(log_probs[:,input_len:])return seq_log_prob.cpu().numpy()
2)我们首先使用这些函数来计算对OpenAI提示使用贪婪解码器的序列对 数概率
logp sequence_logprob(model, output_greedy, input_lenlen(input_ids[0]))
print(tokenizer.decode(output_greedy[0]))
print(f\nlog-prob:{logp:.2f})
3)现在我们来比较一下使用束搜索生成的序列。使用generate()函数并 指定num_beams参数的数量即可激活束搜索。我们选择的束数越多结 果就可能越好。但是生成过程变得更慢因为我们要为每个束生成 并行序列
output_beam model.generate(input_ids,max_lengthmax_length, num_beams5,do_sampleFalse)
logp sequence_logprob(model,output_beam, input_lenlen(input_ids[0]))
print(tokenizer.decode(output_beam[0]))
print(f\nlog-prob:{logp:.2f})
4)我们可以看到使用束搜索相较于简单的贪婪解码得到了更好的对 数概率值数值越高越好。然而我们可以看到束搜索一样存在重 复文本的问题。解决这个问题的一种方式是通过 no_repeat_ngram_size参数施加n-gram惩罚该参数会跟踪已经出现 的n-gram并将下一个词元的概率设置为零从而避免产生先前出现过 的n-gram
output_beam model.generate(input_ids,max_lengthmax_length, num_beams5,do_sampleFalse,no_repeat_ngram_size2)
logp sequence_logprob(model,output_beam, input_lenlen(input_ids[0]))
print(tokenizer.decode(output_beam[0]))
print(f\nlog-prob:{logp:.2f})
4.采样方法
我们可以从温度中得到的主要教训是它允许我 们控制样本的质量但在连贯性低温度和多样性高温度之间 总是存在一个权衡需要根据具体应用场景进行调整。
另一种调整连贯性和多样性之间权衡的方法是截断词汇的分布。这允 许我们自由地使用温度来调整多样性但在一个更有限的范围内排除 在上下文中过于奇怪的词汇即低概率词。有两种主要方法top-k 和核或top-p采样。
5.top-k和核采样
top-k和核top-p采样是使用温度的两种流行的替代或扩展方法。 在这两种情况下基本思想是限制每个时间步可以采样的可能词元数 量。
top-k采样的思想是通过仅从具有最高概率的k个词元中进行采样来避 免低概率选择。这对分布的长尾部分施加了固定的截断并确保我们 只从可能的选择中进行采样。
output_topk model.generate(input_ids,top_k50,max_lengthmax_length,do_sampleTrue)
print(tokenizer.decode(output_topk[0]))一种替代方法是使用动态截断。在核top-p采样中我们不选择固 定的截断值而是设定一个当达到一定概率质量时的截断条件。假设 我们将该值设定为95%。然后我们按概率降序排序所有词元并从列 表顶部逐个添加词元直到所选词元的概率之和达到95%。回到图5- 6p的值定义了概率累积总和图上的水平线我们仅从线下词元中进 行采样。根据输出分布的不同可能只有一个非常可能词元或一 百个等可能词元。讲到这里generate()函数还提供了一个激活 top-p采样的参数。
output_topk model.generate(input_ids,top_k50,max_lengthmax_length,do_sampleTrue,top_p0.9)
print(tokenizer.decode(output_topk[0]))由上可见top-p采样方法也产生了一个连贯的故事这次的新变化是 这些独角兽从澳大利亚迁徙到南美洲。你甚至可以结合这两种采样方 法以兼顾两者的优点。设置top_k50和top_p0.9表示从最多50个 词元中选择概率质量为90%的词元。
6.哪种解码方法最好
不幸的是目前没有一种通用的最佳解码方法。最好的方法将取决于 你生成文本的任务性质。如果你想让你的模型执行像算术或提供特定 问题答案这样的精确任务那么你应该降低温度或使用确定性方法 如将贪婪搜索与束搜索相结合以确保获得最可能的答案。如果你想 让模型生成更长的文本甚至有点创造性那么你应该切换采样方 法增加温度或使用top-k和核采样的混合方法。
7.本章小结 在本章中我们探讨了文本生成这是与我们之前遇到的NLU任务非常 不同的任务。生成文本至少需要每个生成的词元进行一次前向传递 如果使用束搜索则需要更多。这使得文本生成在计算上非常耗费需 要适当的基础设施才能以规模运行文本生成模型。此外一个好的解 码策略可以将模型的输出概率转化为离散的词元从而提高文本的质 量。找到最佳的解码策略需要一些实验和对生成的文本进行主观评 估。 然而在实践中仅凭直觉做出这些决策是不可取的就像其他NLP任务 一样我们应该选择一个反映我们想要解决问题的模型性能度量指 标。毫不奇怪有很多选择在第6章中我们将遇到最常见的选择我 们将介绍如何训练和评估文本摘要模型。
第六章 文本摘要
本章将探讨如何利用Transformer预训练 模型来进行文本摘要。摘要是一种经典的序列到序列seq2seq任 务需要输入文本和目标文本。正如我们在第1章中看到的那样 Transformer模型是一个出色的编码器-解码器架构。
在本章中我们将建立自己的编码器-解码器模型将多人对话压缩成 简明的摘要。但在此之前我们先来看看摘要领域中一个经典数据 集CNN/DailyMail语料库。
1.CNN/DailyMail数据集
from datasets import load_dataset# 加载数据集
dataset load_dataset(cnn_dailymail, 3.0.0)# 打印数据集特征
print(fFeatures: {dataset[train].column_names})数据集有三列article列包含新闻文章highlights列包含摘要id 列是每篇文章的唯一标识。
2.摘要基准
一种常见的新闻文章摘要基准是简单地取文章的前三句话。使用NLTK 的句子词元分析器我们可以轻松实现这样的基准 3.GPT-2生成摘要
from transformers import pipeline, set_seed
import nltk
from nltk.tokenize import sent_tokenize# 设置随机种子
set_seed(42)# 初始化生成文本的 pipeline
pipe pipeline(text-generation, modelgpt2)# 取前2000个样本并连接成一个字符串
sample_text dataset[train][1][article][:2000]# 构建 GPT-2 查询
gpt2_query sample_text \nTL;DR:\n# 使用 GPT-2 生成文本
pipe_out pipe(gpt2_query, max_length512, clean_up_tokenization_spacesTrue)# 将生成的文本切分为句子并存储在 summaries 字典中
summaries {}
summaries[gpt2] \n.join(sent_tokenize(pipe_out[0][generated_text][len(gpt2_query):]))# 打印摘要
print(summaries[gpt2])4.T5生成摘要
pipepipeline(summarization,modelt5-large)
pipe_outpipe(sample_text)
summaries[t5]\n.join(sent_tokenize(pipe_out[0][summary_text]))
5.BART
pipepipeline(summarization,modelfacebook/bart-large-cnn)
pipe_outpipe(sample_text)
summaries[bart]\n.join(sent_tokenize(pipe_out[0][summary_text]))
6.PEGASUS
pipepipeline(summarization,modelgoogle/pegasus-cnn_dailymail)
pipe_outpipe(sample_text)
summaries[pegasus]\n.join(sent_tokenize(pipe_out[0][summary_text]))
7.比较不同的摘要
现在我们已经用四个不同的模型生成了摘要我们来比较一下它们的 结果。请记住其中一个模型根本没有在数据集上进行训练GPT2一个模型在执行其他任务的同时进行了微调T5而另外两个 模型则专门针对这个任务进行了微调BART和PEGASUS。我们来看看 这些模型生成的摘要 理想情况下我们将定义一个指标在一些基准数据集上为所 有模型进行度量并选择性能最佳的那一个。但是如何为文本生成 定义一个指标呢我们所看到的标准指标如准确率、查准率和召回 率都不易用于此任务。对于每个由人类撰写的“黄金标准”摘要 可能有数十个可接受的摘要通过同义词、释义或稍微不同的表达方 式。 接下来我们将探讨一些常见的、用来度量生成文本的质量的指标
8.度量生成文本的质量
两个最常用的用于评估生成文本的度量指标是BLEU和ROUGE。我们看看 它们的定义。
1)BLEU
BLEU的思想很简单 与其看生成文本中有多少个词元与参考文本的 词元完全对齐不如看单词或n-gram。BLEU是基于查准率的度量指 标这意味着当我们比较两个文本时我们计算生成文本中出现在参 考文本中的单词数并将其除以生成文本的长度。 然而这种普通的查准率存在一个问题。假设生成文本只是一遍又一 遍地重复同一个单词并且这个单词也出现在参考文本中。如果它重 复了与参考文本长度相同的次数那么我们就得到了完美的查准率 因此BLEU论文的作者引入了一个轻微的修改只计算一个单词在参 考文本中出现的次数。这里举一个例子假设我们的参考文本为 “the cat is on the mat”生成文本为 “the the the the the the”。
总的来说文本生成领域仍在寻找更好的度量指标克服BLEU等指标 的局限性是当前研究的一个活跃领域。BLEU指标的另一个缺点是它期 望文本已经进行了词元化。如果没有使用完全相同的文本词元化方 法则可能会导致不同的结果。SacreBLEU指标通过内置词元化步骤来 解决这个问题。因此它是基准测试的首选指标。
.通过代码计算生成文本的分 数。
!pip install sacrebleu
from datasets import load_metric
bleu_metricload_metric(sacrebleu)
bleu metric对象是Metric类的一个实例它的工作方式类似于聚合 器你可以使用add()添加单个实例也可以通过add batch()添加整 个批量。在添加完需要评估的所有样本之后调用compute()即可计算 指标。然后会返回一个包含多个值的字典包括每个n-gram的查准 率、长度惩罚以及最终的BLEU分数。现在我们将bleu metric对象应 用于之前的示例
import pandas as pd
import numpy as np
from datasets import load_metric# 加载BLEU度量
bleu_metric load_metric(bleu)# 添加预测和参考
bleu_metric.add(predictionthe the the the the the, reference[the cat is on the mat])# 计算BLEU分数
results bleu_metric.compute(smooth_methodfloor, smooth_value0)# 处理结果
# 将precisions中的每个值四舍五入到小数点后两位
precisions_rounded np.round(results[precisions], 2)# 创建结果的DataFrame
results_df pd.DataFrame.from_dict(results, orientindex, columns[Value])# 显示结果
print(results_df)2ROUGE
9.在CNN/DailyMail数据集上评估PEGASUS
现在充分评估模型的条件都齐全了我们拥有CNN/DailyMail测试集数 据集、评估用的ROUGE指标以及一个摘要模型。我们只需要把这些部 分组合起来。首先我们评估三句话基准模型的性能
def evaluate_summaries_baseline(dataset, metric, column_textarticle, column_summaryhighlights):# 生成摘要summaries [three_sentence_summary(text) for text in dataset[column_text]]# 添加预测和参考metric.add_batch(predictionssummaries, referencesdataset[column_summary])# 计算得分score metric.compute()return score然后我们把该函数应用于数据的一个子集。由于CNN/DailyMail数据集 的测试部分包含大约10 000个样本生成所有这些文章的摘要需要很 多时间。回顾第5章每个生成的词元都需要通过模型进行前向传递。 为每个样本生成100个词元将需要100万次前向传递如果我们使用束 搜索则此数字还需要乘以束的数量。为了让计算更快一些我们将 对测试集进行子采样最终使用1000个样本进行评估。这样我们使用 单个GPU上不到一小时就能完成PEGASUS模型的评估而且得到稳定的 分数估计
import pandas as pd
from datasets import load_metricdef evaluate_summaries_baseline(dataset, metric, column_textarticle, column_summaryhighlights):# 生成摘要summaries [three_sentence_summary(text) for text in dataset[column_text]]# 添加预测和参考metric.add_batch(predictionssummaries, referencesdataset[column_summary])# 计算得分score metric.compute()return score# 加载ROUGE度量
rouge_metric load_metric(rouge)# 选择测试数据中的1000个样本并打乱
test_sampled dataset[test].shuffle(seed42).select(range(1000))# 评估摘要
score evaluate_summaries_baseline(test_sampled, rouge_metric)# 提取ROUGE指标名称
rouge_names score.keys()# 格式化得分为字典
rouge_dict {rn: score[rn][mid][fmeasure] for rn in rouge_names}# 转换为DataFrame并打印
df pd.DataFrame.from_dict(rouge_dict, orientindex, columns[baseline]).T
print(df)由于ROUGE和BLEU比人工评估的损失或准确率更好因此在构建文本生 成模型时应重点关注它们并仔细探索和选择解码策略。然而这些 指标远非完美因此应始终考虑人工评估。 现在我们已经有了评估函数可以训练我们自己的摘要模型了。
10.训练摘要模型
1我们将使用三星开发的SAMSum数据集 https://oreil.ly/n1ggq该数据集包含了一系列对话以及简短 的摘要。这些对话可以代表客户与客服中心之间的互动并以此生成 准确的摘要以帮助改善客户服务并检测客户请求中的常见模式。我 们先加载数据集并查看一个样本
dataset_samsum load_dataset(samsum, cache_dirNone,encodingutf-8) 加入后面的参数解决utf-8 codec cant decode byte 0x8b in position 1: invalid start byte的bug
不pip install py7zr才可以解决这个bug去掉上面的encoding参数
from datasets import load_dataset# 删除缓存
dataset_samsum load_dataset(samsum, cache_dirNone)split_lengths [len(dataset_samsum[split])for split in dataset_samsum]
print(fSplit lengths:{split_lengths})
print(fFeatures:{dataset_samsum[train].column_names})
print(\nDialogue:)
print(dataset_samsum[test][0][dialogue ])
print(\nSummary:)
print(dataset_samsum[test ][0][summary ])
2评估PEGASUS在SAMSum上的性能
from datasets import load_dataset, load_metric
from transformers import PegasusTokenizer, PegasusForConditionalGeneration, pipeline
import pandas as pd# 加载数据集
dataset_samsum load_dataset(samsum)# 加载模型和tokenizer
model_name google/pegasus-xsum
model PegasusForConditionalGeneration.from_pretrained(model_name)
tokenizer PegasusTokenizer.from_pretrained(model_name)# 初始化文本生成管道
pipe pipeline(text-generation, modelmodel_name, tokenizertokenizer)# 生成文本
pipe_out pipe(dataset_samsum[test][0][dialogue], max_length1024)# 加载ROUGE评估指标
rouge_metric load_metric(rouge)# 定义ROUGE评估函数
def evaluate_summaries_pegasus(dataset, metric, model, tokenizer, batch_size16, devicetorch.device(cuda if torch.cuda.is_available() else cpu), column_textarticle, column_summaryhighlights):def chunks(data, batch_size):for i in range(0, len(data), batch_size):yield data[i:i batch_size]article_batches list(chunks(dataset[column_text], batch_size))target_batches list(chunks(dataset[column_summary], batch_size))for article_batch, target_batch in tqdm(zip(article_batches, target_batches), totallen(article_batches)):inputs tokenizer(article_batch, max_length1024, truncationTrue, paddingmax_length, return_tensorspt)summaries model.generate(input_idsinputs[input_ids].to(device),attention_maskinputs[attention_mask].to(device),length_penalty0.8,num_beams8,max_length128)decoded_summaries [tokenizer.decode(s, skip_special_tokensTrue, clean_up_tokenization_spacesTrue) for s in summaries]decoded_summaries [d.replace(n, ) for d in decoded_summaries]metric.add_batch(predictionsdecoded_summaries, referencestarget_batch)score metric.compute()return score# 评估摘要
score evaluate_summaries_pegasus(dataset_samsum[test], rouge_metric, model, tokenizer, column_textdialogue, column_summarysummary, batch_size8)# 获取ROUGE分数
rouge_names [rouge1, rouge2, rougeL, rougeLsum]
rouge_dict {rn: score[rn].mid.fmeasure for rn in rouge_names}# 转换为DataFrame
df_rouge pd.DataFrame(rouge_dict, index[pegasus])
print(df_rouge)3微调PEGASUS
from transformers import TrainingArguments, Trainertraining_args TrainingArguments(output_dirpegasus-samsum,num_train_epochs1,warmup_steps500,per_device_train_batch_size1,per_device_eval_batch_size1,weight_decay0.01,logging_steps10,push_to_hubTrue,evaluation_strategysteps,eval_steps500,save_stepsint(1e6),gradient_accumulation_steps16
)trainer Trainer(modelmodel,argstraining_args,tokenizertokenizer,data_collatorseq2seq_data_collator,train_datasetdataset_samsum[train],eval_datasetdataset_samsum[validation]
)
# 开始训练
trainer.train()# 定义rouge_metric
from datasets import load_metric
rouge_metric load_metric(rouge)# 评估
score evaluate_summaries_pegasus(dataset_samsum[test],rouge_metric,trainer.model,tokenizer,batch_size2,column_textdialogue,column_summarysummary
)# 生成rouge_dict
rouge_dict {rn: score[rn].mid.fmeasure for rn in rouge_metric.compute().keys()}# 创建DataFrame
import pandas as pd
pd.DataFrame(rouge_dict, index[pegasus])第七章构建问答系统
1.构建基于评论的问答系统
1准备数据集
本书使用SubjQA数据集 来构建问答系统该数据集包含10 000多条 关于商品和服务的英文用户评论涉及六个领域旅行社、餐厅、电 影、书籍、电子产品和杂货店。如图7-2所示每条数据都包含一个问 题和一个评论评论里面会有一个或多个词条可以准确回答该问题
本次示例将专注为电子领域上面的electronics构建问答系统。在 确定了领域之后便要开始加载electronics数据子集我们将该值传 给load_dataset()函数的name参数即可 from datasets import load_dataset subjqaload_dataset(subjqa,nameelectronics) subjqa 对于文本分类等其他场景最常用的方法是将超出长度的文本直接截 断并丢弃因为即使缺失一部分文档也能得出正确的分类预测结 果。但在问答场景这种做法是有问题的因为问题的答案很可能位 于上下文的末尾附近。
处理这种问题的标准方法是在输 入上加一个滑动窗口将原始长文本处理成多个短文本其中每个窗 口都包含完全适配模型上下文的词元数量。
Hugging Face Transformers库提供了相关的API可以在词元分析 器中设置return_overflowing_tokensTrue来启用滑动窗口功能滑 动窗口的大小由max_seq_length参数控制步幅大小则由doc stride 参数控制。
2.用Haystack构建问答pipelin
现在的问答系统通常都基于检索器-阅读器 Retriever-Reader架构从名称就可以看出这种架构有两个主 要组件
检索器
如果向量的大部分元素为0则称该向量是稀疏向量。
检索器Retriever旨在为给定查询操作检索相关文档目前的检索 器通常分为三类稀疏检索器Sparse Retriever、密集检索器 Dense Retriever和迭代检索器Iterative Retriever。稀 疏检索器使用词频将每个文档和查询表示为一个稀疏向量 sparse vector 然后通过计算向量内积来确定查询和文档的 相关性但稀疏检索不能解决术语不匹配问题在问题与文档相似但 不存在重复术语的情况下稀疏检索效果较差。随着深度学习的逐渐 成熟发展出了密集检索器密集检索器使用类似Transformer的编码 器将查询和文档表示为上下文嵌入密集向量。这些嵌入将语义编 码通过理解查询内容来提高搜索准确率。迭代检索器则是通过多次 迭代从大集合从检索相关文档。
阅读器
阅读器Reader旨在从检索器输出的文档中提取答案。阅读器通常 是一个阅读理解模型在本章末尾我们将见到可以生成具有自由格式 答案的模型样例。
如图7-9所示还有其他组件能对检索器获取的文档或阅读器提取的答 案做后处理。例如检索出来的文档可能需要以重新排序的方式来 消除噪声或那些可能让使用者无法理解的文档。同样当正确答案来 自长文档不同段落时通常需要对答案进行后处理。 我们将使用Haystack库https://haystack.deepset.ai来构建问答 系统它是一家专注于NLP领域的德国公司deepset https://deepset.ai所开发的。Haystack基于检索器-阅读器架 构它将构建问答系统的大部分复杂性抽象出来并与 Hugging Face Transformers库紧密集成。
Document Store
文档存储器用于存储在检索时提供给检索器的文档和元数据。
Pipeline
流程控制器封装了问答系统所有流程组件用以编写自定义检索流 程或者合并多个检索器处理后的文档等。
在本章中会使用稀疏检索器和密集检索器因此这里选用 Elasticsearch作为文档存储器 初始化检索器
from haystack.document_store.elasticsearch import ElasticsearchDocumentStore
document_storeElasticsearchDocumentStore(return_embeddingTrue)
初始化阅读器
第八章 Transformer模型调优
在本章中我们将探讨四种互补技术可用于加速你的Transformer模 型的预测并减少内存使用知识蒸馏、量化、剪枝和使用 Open Neural Network ExchangeONNX格式和ONNX Runtime ORT进行图优化。
1.创建性能基准
2.第一个压缩技术知 识蒸馏。
知识蒸馏是一种通用的方法用于训练一个较小的学生模型来模仿一 个更慢但表现更好的较大的教师模型的行为。这种方法最初在2016年 集成模型的背景下引入 后来在一篇著名的2015年的论文中得到推 广。其被推广到深度神经网络并应用于图像分类和自动语音识别 。
微调知识蒸馏
为在许多情况下教师模型会给一个 类别分配高概率而其他类别的概率接近于零。当出现这种情况时 教师模型除了提供基准标注外并没有提供太多附加信息所以我们 在应用softmax前会使用一个超参数温度T对logit进行缩放以使概 率变得“更软”
基于知识蒸馏的预训练技术
知识蒸馏还可以在预训练期间使用我们可以创建一个通用的学生模 型随后可对下游任务进行微调。在这种情况下教师模型是一个预 训练语言模型例如BERT它将其关于掩码语言建模的知识迁移给学 生模型。例如在DistilBERT论文中 掩码语言建模损失L由知识蒸 馏术语和余弦嵌入损失L1-coshsht扩展以调整教师模型和学 生模型之间的隐藏状态向量方向
由于我们已经有了经过微调的BERT-base模型我们看看如何使用知识 蒸馏来微调更小、更快的模型。为了实现这一点我们需要一种将交 叉熵损失与LKD项相结合的方法。幸运的是我们可以通过创建自己的 训练器来做到这一点
创建知识蒸馏训练器
从文献上来看一个好的经验法则 是当教师模型和学生模型为同一模型类型时知识蒸馏效果最好 。 这种情况的一个可能原因是不同的模型类型比如BERT和RoBERTa可 以具有不同的输出嵌入空间这会阻碍学生模型模仿教师模型的能 力。在我们的案例研究中教师模型是BERT因此DistilBERT是一个 很自然的备选项因为它的参数数量少了40%并且已经被证明在下游 任务上取得了强大的结果。
3.利用量化技术使模型运算更快
4.使用权重剪枝使模型更稀疏
5.使用ONNX和ONNX Runtime进行推理优化
第九章.零样本学习和少样本学习
GPT-3模 型甚至可以仅用几十个样本就能处理各种不同的任务