原文:
zh.annas-archive.org/md5/a6f6476d556185fc0ddb2dae043823f3
译者:飞龙
序言
在过去的 20 年间,我们在自然语言处理(NLP)领域已经见证了巨大的变化。在此期间,我们经历了不同的范式,最终进入了由神奇的Transformers架构主宰的新时代。这种深度学习架构是通过继承多种方法而形成的。诸如上下文词嵌入、多头自注意力、位置编码、可并行化的架构、模型压缩、迁移学习和跨语言模型等方法都在其中。从各种基于神经网络的自然语言处理方法开始,Transformers架构逐渐演变成为一个基于注意力的编码器-解码器架构,并持续至今。现在,我们在文献中看到了这种架构的新成功变体。有些出色的模型只使用了其编码器部分,比如 BERT,或者只使用了其解码器部分,比如 GPT。
在整本书中,我们将涉及这些自然语言处理方法,并且能够轻松使用来自 Hugging Face 社区的 Transformers 库与Transformers模型进行交互。我们将逐步提供各种自然语言处理问题的解决方案,涵盖从摘要到问答等广泛的话题。我们将看到,借助Transformers的帮助,我们可以取得最先进的结果。
本书的受众
本书适合深度学习研究人员、实践型自然语言处理从业者,以及希望以Transformers架构开始自己学习之旅的机器学习/自然语言处理教育者和学生。初级机器学习知识和良好的 Python 掌握能力将帮助您更好地理解本书的内容。
本书涵盖的内容
第一章,从词袋模型到Transformers,简要介绍了自然语言处理的历史,对比了传统方法、深度学习模型(如 CNN、RNN 和 LSTM)与Transformers模型。
第二章,主题的实践导论,深入探讨了如何使用Transformers模型。我们将通过实例描述分词器和 BERT 等模型。
第三章,自编码语言模型,将让您了解如何从头开始在任何给定语言上训练自编码语言模型。此训练将包括模型的预训练和特定任务的训练。
第四章,自回归和其他语言模型,探讨了自回归语言模型的理论细节,并教会您如何在其特定语料库上进行预训练。您将学会如何在自己的文本上预训练 GPT-2 等任何语言模型,并将其用于诸如语言生成等各种任务。
第五章,微调语言模型进行文本分类,是您将学习如何配置预训练模型进行文本分类以及如何为任何文本分类下游任务,例如情感分析或多类分类进行微调的地方。
第六章,微调语言模型进行标记分类,教您如何微调语言模型以用于诸如 NER、POS 标注和问答等标记分类任务。
第七章,文本表示,是您将学习有关文本表示技术以及如何有效利用 Transformer 架构,特别是对于无监督任务,例如聚类、语义搜索和主题建模的地方。
第八章,高效 Transformer 的工作,向您展示了如何通过蒸馏、修剪和量化将经过训练的模型制作成高效模型。然后,您将了解到关于高效稀疏 Transformer 的知识,例如 Linformer 和 BigBird,以及如何与它们一起工作。
第九章,跨语言和多语言语言建模,是您将学习有关多语言和跨语言语言模型预训练以及单语和多语预训练之间的区别的地方。该章节还涵盖了因果语言建模和翻译语言建模等其他主题。
第十章,服务 Transformer 模型,将详细介绍如何在具有 CPU/GPU 的环境中提供基于 Transformer 的 NLP 解决方案。还将在此处描述使用 TensorFlow Extended(TFX)进行机器学习部署。
第十一章,注意力可视化和实验跟踪,将涵盖两个不同的技术概念:注意力可视化和实验跟踪。我们将使用诸如 exBERT 和 BertViz 等复杂工具来进行实践。
要充分利用本书
要遵循本书,您需要具备 Python 编程语言的基本知识。您还需要了解自然语言处理、深度学习以及深度神经网络的基础知识。
重要提示
本书中的所有代码都是在 Python 3.6 版本中执行的,因为 Python 3.9 版本中的一些库仍处于开发阶段。
如果您使用的是本书的数字版,我们建议您自己输入代码或从本书的 GitHub 代码库中获取代码(链接在下一节中提供)。这样做可以帮助您避免与复制粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 上下载本书的示例代码文件,网址为github.com/PacktPublishing/Mastering-Transformers
。若代码有更新,将在 GitHub 仓库中更新。
我们还有其他代码捆绑包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/
找到。欢迎查阅!
代码实践
本书的代码实践视频可在bit.ly/3i4vFzJ
观看。
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的截图和图表的彩色图像。您可以在此处下载:static.packt-cdn.com/downloads/9781801077651_ColorImages.pdf
。
使用的惯例
本书中使用了许多文本惯例。
文本中的代码
:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、废弃的 URL、用户输入和 Twitter 句柄。举个例子:“短于max_sen_len
(最大句子长度)的序列将使用PAD
值填充,直到其长度达到max_sen_len
。”
代码块显示如下:
max_sen_len=max([len(s.split()) for s in sentences])
words = ["PAD"]+ list(set([w for s in sentences for w in s.split()]))
word2idx= {w:i for i,w in enumerate(words)}
max_words=max(word2idx.values())+1
idx2word= {i:w for i,w in enumerate(words)}
train=[list(map(lambda x:word2idx[x], s.split())) for s in sentences]
当我们希望引起您对代码块的特别关注时,相关行或条目将以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出均以以下形式呈现:
$ conda activate transformers
$ conda install -c conda-forge tensorflow
粗体:表示新术语、重要词汇或屏幕上看到的词语。例如,菜单或对话框中的词以粗体显示。举个例子:“现在我们必须关注特定模型在给定环境下的计算成本(随机存取存储器(RAM),CPU 和 GPU),包括内存使用和速度。”
提示或重要笔记
如此呈现。
第一部分:介绍 - 领域内的最新发展,安装和 Hello World 应用程序
在本节中,您将以初级水平了解 Transformer 的各个方面。您将通过加载社区提供的预训练语言模型编写您的第一个hello-world
程序,并在有或没有 GPU 的情况下运行相关代码。本节还将详细说明安装和使用tensorflow
,pytorch
,conda
,transformers
和sentenceTransformers
库。
本节包括以下章节:
-
第一章, 从词袋模型到 Transformer
-
第二章, 主题的实践介绍
第一章:从词袋模型到 Transformer
在本章中,我们将讨论在过去的 20 年中自然语言处理(NLP)发生了什么变化。我们经历了不同的范式,最终进入了 Transformer 架构的时代。所有这些范式都帮助我们更好地表示单词和文档以解决问题。分布语义描述了单词或文档的意义,具有矢量表示,观察在文集中的分布证据。矢量用于在受控和非受控流程中解决许多问题。对于语言生成问题,n-gram 语言模型长期以来一直被用作传统方法。然而,这些传统方法存在许多缺点,在整整一章中我们将进行讨论。
我们将进一步讨论经典的深度学习(DL)架构,如循环神经网络(RNNs),前馈神经网络(FFNNs)和卷积神经网络(CNNs)。这些架构已经改善了该领域问题的性能,并克服了传统方法的局限。然而,这些模型也存在各自的问题。最近,由于 Transformer 模型在从文本分类到文本生成的所有 NLP 任务中的有效性,它们引起了巨大的兴趣。然而,主要的成功在于 Transformer 有效地提高了多语言和多任务 NLP 问题的性能,以及单语言和单任务。这些贡献使得迁移学习(TL)在 NLP 中更为可能,其目标是使模型可在不同任务或不同语言中重复使用。
从注意机制开始,我们将简要讨论 Transformer 架构和之前 NLP 模型的区别。与理论讨论并行,我们将展示流行的 NLP 框架的实际示例。为简单起见,我们将选择尽可能简短的入门代码示例。
在本章中,我们将涵盖以下主题:
-
NLP 向 Transformer 的演变
-
理解分布语义
-
利用深度学习
-
Transformer 架构概述
-
使用 Transformer 进行迁移学习
技术要求
我们将使用 Jupyter Notebook 来运行需要安装python >=3.6.0
及以下包的编码练习:
-
sklearn
-
nltk==3.5.0
-
gensim==3.8.3
-
fasttext
-
keras>=2.3.0
-
Transformers >=4.00
所有带有编码练习的笔记本都可在以下 GitHub 链接处找到:github.com/PacktPublishing/Advanced-Natural-Language-Processing-with-Transformers/tree/main/CH01
。
查看以下链接以查看视频代码示例:bit.ly/2UFPuVd
NLP 向 Transformer 的发展
过去 20 年来,我们已经看到了自然语言处理(NLP)领域的深刻变化。在这段时间里,我们经历了不同的范式,最终进入了一个主要由神奇的Transformer架构主导的新时代。这种架构并非从天而降。从各种基于神经网络的 NLP 方法开始,它逐渐演变成了基于注意力的编码-解码类型的架构,并且仍在不断发展。过去十年中,由于以下发展,该架构及其各种变体取得了成功:
-
上下文词嵌入
-
更好的子词标记算法,用于处理未知单词或稀有单词
-
将额外的记忆标记注入到句子中,比如
Doc2vec
中的段落 ID
或来自 Transformer 的双向编码器表示(BERT)中的分类(CLS)标记 -
注意力机制,克服将输入句子强制编码到一个上下文向量中的问题
-
多头自注意力
-
用于处理单词顺序的位置编码
-
可并行化的架构,使训练和微调更快
-
模型压缩(蒸馏、量化等)
-
TL(跨语言、多任务学习)
多年来,我们使用了传统的 NLP 方法,例如n-gram 语言模型、基于 TF-IDF 的信息检索模型和one-hot 编码的文档-术语矩阵。所有这些方法都为解决许多 NLP 问题(如序列分类、语言生成、语言理解等)做出了很大贡献。另一方面,这些传统的 NLP 方法也有其自身的弱点—例如,在解决稀疏性、未知单词表示、跟踪长期依赖关系等问题上存在不足。为了应对这些弱点,我们开发了基于深度学习的方法,如以下所示:
-
RNNs
-
CNNs
-
FFNNs
-
几种 RNNs、CNNs 和 FFNNs 的变种
2013 年,作为两层 FFNN 单词编码器模型,Word2vec
通过产生短而稠密的单词表示(称为词嵌入)解决了维度问题。这个早期模型成功地产生了快速而有效的静态词嵌入。它通过预测上下文中的目标单词或基于滑动窗口预测相邻单词,将无监督的文本数据转换为受监督的数据(自监督学习)。GloVe,另一个被广泛使用和普遍流行的模型,认为基于计数的模型可能比神经模型更好。它利用语料库的全局和局部统计数据来学习基于单词共现统计的嵌入。它在一些句法和语义任务上表现良好,如下面的截图所示。截图告诉我们,术语之间的嵌入偏移有助于应用矢量导向推理。我们可以学习性别关系的泛化,这是从男人和女人之间的偏移关系推导出来的语义关系(男人-> 女人)。然后,我们可以通过将男演员的矢量和之前计算出的偏移矢量相加来算出女演员的矢量。同样,我们可以学习词的复数形式。例如,如果给出Actor,Actors和Actress的矢量,我们可以估算Actresses的矢量:
图 1.1 - 用于关系提取的单词嵌入偏移
递归和卷积架构,比如 RNN、长短期记忆(LSTM)和 CNN,开始在序列到序列(seq2seq)问题中被用作编码器和解码器。这些早期模型的主要挑战是多义词。由于给每个单词分配了单一的固定表示,因此忽略了单词的含义,这对多义词和句子语义尤其是一个严重的问题。
进一步的先驱神经网络模型,比如通用语言模型微调(ULMFit)和语言模型嵌入(ELMo),成功地对句子级信息进行编码,并最终缓解了一词多义的问题,与静态词嵌入不同。这两种重要方法基于 LSTM 网络。它们还引入了预训练和微调的概念。它们帮助我们应用迁移学习,利用在大量文本数据集上进行常规任务训练的预训练模型。然后,我们可以很容易地通过在目标任务上继续对预训练网络进行训练,进行微调。这些表示与传统的词嵌入不同,每个单词表示是整个输入句子的函数。现代 Transformer 架构充分利用了这个想法。
与此同时,注意力机制的想法在自然语言处理领域引起了强烈的印象,并取得了显著的成功,特别是在 seq2seq 问题上。早期的方法会传递最后一个状态(称为输入句子中的加拿大政府
,用于英语到土耳其语的翻译任务。在输出句子中,Kanada Hükümeti
标记与输入短语建立了强连接,并与输入中的其他单词建立了较弱的连接,如下方截图所示:
图 1.2 – 注意力机制的草图可视化
因此,这种机制使得模型在翻译、问答和文本摘要等 seq2seq 问题中更加成功。
2017 年,基于 Transformer 的编码器-解码器模型被提出并被发现成功。该设计基于 FFNN,丢弃了 RNN 的递归,并仅使用注意力机制(Vaswani et al., All you need is attention, 2017)。到目前为止,基于 Transformer 的模型已经克服了其他方法所面临的许多困难,并成为了一个新的范式。在本书中,你将探索并理解 Transformer 模型的工作原理。
理解分布语义
分布语义描述了单词的含义,并通过矢量表示,最好是查看其分布证据,而不是查看其预定义的词典定义。该理论表明,在相似环境中共同出现的单词倾向于共享相似的含义。这最早由学者哈里斯提出(Distributional Structure Word, 1954)。例如,诸如狗和猫这样的相似单词大多在相同的上下文中共同出现。分布式方法的一个优点是帮助研究人员理解和监测单词随时间和领域的语义演变,也被称为词汇语义变化问题。
多年来,传统方法一直应用词袋模型(BoW)和 n-gram 语言模型来构建单词和句子的表示。在词袋模型中,单词和文档以一种稀疏的方式表示为 one-hot 编码,也被称为向量空间模型(VSM)。
多年来,这些 one-hot 编码技术已解决了文本分类、单词相似度、语义关系提取、单词意义消歧等许多自然语言处理问题。另一方面,n-gram 语言模型为单词序列分配概率,以便我们可以计算一个序列属于语料库的概率,或者基于给定语料库生成一个随机序列。
BoW 实现
一种词袋(BoW)是一种文档的表征技术,通过计算其中的单词来实现。该技术的主要数据结构是文档词项矩阵。让我们用 Python 看一个 BoW 的简单实现。以下代码片段说明了如何使用 Python 的 sklearn
库为一个三句话的玩具语料库构建文档词项矩阵:
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
import pandas as pd
toy_corpus= ["the fat cat sat on the mat",
"the big cat slept",
"the dog chased a cat"]
vectorizer=TfidfVectorizer()
corpus_tfidf=vectorizer.fit_transform(toy_corpus)
print(f"The vocabulary size is \
{len(vectorizer.vocabulary_.keys())} ")
print(f"The document-term matrix shape is\
{corpus_tfidf.shape}")
df=pd.DataFrame(np.round(corpus_tfidf.toarray(),2))
df.columns=vectorizer.get_feature_names()
代码的输出是一个文档词项矩阵,如下图所示。其大小为 (3 x 10),但在现实场景中,矩阵的大小可以增长到更大的数字,例如 10K x 10M:
图 1.3 – 文档词项矩阵
表格表示的是一种基于计数的数学矩阵,在其中单元格的值按照词频-逆文档频率(TF-IDF)加权模式进行转换。此方法不关心单词的位置。由于单词顺序强烈决定了含义,忽略它会导致意义丧失。这是 BoW 方法中的常见问题,最终通过 RNN 中的递归机制和Transformers中的位置编码得到解决。
矩阵中的每一列代表词汇表中一个词的向量,每一行代表一个文档的向量。可以应用语义相似性指标来计算单词和文档的相似性或非相似性。大多数情况下,我们使用二元组,例如 cat_sat
和 the_street
来丰富文档的表示。例如,当参数 ngram_range=(1,2)
传递给 TfidfVectorizer
时,它构建一个包含 unigrams (big, cat, dog
) 和 bigrams (big_cat
, big_dog
) 的向量空间。因此,这样的模型也被称为词袋式 n-grams,它是BoW的自然扩展。
如果一个词在每篇文章中都经常出现,那么它可以被视为高频词,例如 and 和 the。相反,一些词在文章中很少出现,称为低频(或稀有)词。由于高频和低频词可能会妨碍模型的正常工作,因此在这里使用了 TF-IDF 作为解决方案,这是最重要和著名的加权机制之一。
the
没有区分力,而 chased
可能具有高信息量,可以提供关于文本主题的线索。这是因为高频词(停用词,功能词)在理解文档时具有很少的区分能力。
词的区分度也取决于领域,例如,DL 文章列表中几乎每篇文章都可能有单词 network
。IDF 可以通过使用单词的文档频率(DF)来缩小所有词的权值,其中单词的 DF 通过单词出现在的文档数计算得出。词频(TF)是文档中词(术语)的原始计数。
一种基于 TF-IDF 的 BoW 模型的一些优缺点列举如下:
表 1 - TF-IDF BoW 模型的优缺点
克服维度问题
为了克服 BoW 模型的维度问题,潜在语义分析(LSA)被广泛用于在低维空间中捕捉语义。它是一种线性方法,捕捉术语之间的成对相关性。基于 LSA 的概率方法仍然可以被看作是一个隐藏主题变量的单层。然而,当前的 DL 模型包括多个隐藏层,参数量达到数十亿。除此之外,基于 Transformer 的模型表明,它们可以比传统模型更好地发现潜在表示。
对于自然语言理解(NLU)任务,传统的流水线从一些准备步骤开始,如 tokenization、stemming、noun phrase detection、chunking、stop-word elimination 等等。之后,使用任何加权模式构建文档-术语矩阵,其中 TF-IDF 是最流行的。最后,该矩阵作为 机器学习(ML)流水线、情感分析、文档相似性、文档聚类或测量查询与文档之间关联分数的表格化输入。同样,术语被表示为一个表格矩阵,并且可以作为一个 token 分类问题的输入,其中我们可以应用命名实体识别、语义关系提取等。
分类阶段包括对监督式机器学习算法的直接实现,如支持向量机(SVM)、随机森林、逻辑回归、朴素贝叶斯和多个学习器(Boosting 或 Bagging)。实际上,这样一个流水线的实现可以简单地编码如下:
from sklearn.pipeline import make_pipeline
from sklearn.svm import SVC
labels= [0,1,0]
clf = SVC()
clf.fit(df.to_numpy(), labels)
如前面的代码所示,我们可以轻松地应用 sklearn
应用程序编程接口(API)来进行适配操作。为了将学习到的模型应用于训练数据,执行以下代码:
clf.predict(df.to_numpy())
Output: array([0, 1, 0])
让我们继续下一节吧!
语言建模与生成
对于语言生成问题,传统方法是基于利用 n-gram 语言模型。这也被称为马尔可夫过程,它是一种随机模型,其中每个词(事件)都依赖于前面的一部分词—unigram、bigram 或 n-gram,如下所述:
-
一元组(所有词都是独立的,没有链):这估计了词汇表中每个词的概率,简单地通过它在总词数中的频率计算得到。
-
二元组(一阶马尔可夫过程):这估计了 P(wordi | wordi-1),即 wordi 取决于 wordi-1 的概率,简单地通过 P(wordi,wordi-1) 与 P(wordi-1) 的比率计算得到。
-
N 元组(N 阶马尔可夫过程):这估计了 P(wordi | word0, …, wordi-1)。
让我们用自然语言工具包(NLTK)库进行一个简单的语言模型实现。在以下实现中,我们使用最大似然估计器(MLE)训练了一个n=2的模型。我们可以选择任何 n-gram 顺序,比如n=1代表 unigrams,n=2代表 bigrams,n=3代表 trigrams 等等:
import nltk
from nltk.corpus import gutenberg
from nltk.lm import MLE
from nltk.lm.preprocessing import padded_everygram_pipeline
nltk.download('gutenberg')
nltk.download('punkt')
macbeth = gutenberg.sents('shakespeare-macbeth.txt')
model, vocab = padded_everygram_pipeline(2, macbeth)
lm=MLE(2)
lm.fit(model,vocab)
print(list(lm.vocab)[:10])
print(f"The number of words is {len(lm.vocab)}")
nltk
包首先下载古腾堡
语料库,其中包括来自古腾堡项目电子文本存档的一些文本,托管在www.gutenberg.org
。它还下载用于标点处理的punkt
分词器工具。该分词器使用无监督算法将原始文本划分为句子列表。nltk
包已经包含了一个预先训练的英文punkt
分词器模型,用于缩写词和搭配词。在使用之前,可以对任何语言的一系列文本进行训练。在后续章节中,我们将讨论如何为 Transformer 模型训练不同和更高效的分词器。以下代码展示了语言模型目前学到的内容:
print(f"The frequency of the term 'Macbeth' is {lm.counts['Macbeth']}")
print(f"The language model probability score of 'Macbeth' is {lm.score('Macbeth')}")
print(f"The number of times 'Macbeth' follows 'Enter' is {lm.counts[['Enter']]['Macbeth']} ")
print(f"P(Macbeth | Enter) is {lm.score('Macbeth', ['Enter'])}")
print(f"P(shaking | for) is {lm.score('shaking', ['for'])}")
这是输出:
The frequency of the term 'Macbeth' is 61
The language model probability score of 'Macbeth' is 0.00226
The number of times 'Macbeth' follows 'Enter' is 15
P(Macbeth | Enter) is 0.1875
P(shaking | for) is 0.0121
n-gram 语言模型保留n-gram计数,并计算生成句子的条件概率。lm=MLE(2)
代表最大似然估计,在每个令牌的概率中得出最可能的句子。以下代码使用<s>
开头条件生成一个包含 10 个单词的随机句子:
lm.generate(10, text_seed=['<s>'], random_seed=42)
输出如下所示:
['My', 'Bosome', 'franchis', "'", 's', 'of', 'time', ',', 'We', 'are']
我们可以通过text_seed
参数提供特定的起始条件,使得生成受到前文的影响。在我们先前的例子中,前文是<s>
,这是一个特殊的令牌,表示句子的开头。
到目前为止,我们已经讨论了传统 NLP 模型的基础范式,并用流行的框架提供了非常简单的实现。现在我们将转向 DL 部分,讨论神经语言模型如何塑造 NLP 领域,以及神经模型如何克服传统模型的局限性。
利用 DL
NLP 是 DL 架构广泛且成功应用的领域之一。几十年来,在词和句子表示中特别出现了成功的架构。在本节中,我们将分享这些不同方法的故事,并介绍常用的框架。
学习词嵌入
自从能够在更大的数据集上训练更复杂的神经架构以构建短而密集的表示以来,基于神经网络的语言模型有效地解决了特征表示和语言建模问题。2013 年,Word2vec 模型,这是一种流行的词嵌入技术,使用了简单而有效的架构来学习高质量的连续词表示。它在各种句法和语义语言任务上的表现优于其他模型,如情感分析、释义检测、关系提取等等。该模型的另一个关键因素是其更低的计算复杂性。它最大化了给定任何周围上下文词的当前词的概率,或者反之亦然。
以下代码段说明了如何为剧作 麦克白 的句子训练词向量:
from gensim.models import Word2vec
model = Word2vec(sentences=macbeth, size=100, window= 4, min_count=10, workers=4, iter=10)
该代码通过一个长度为 5 的滑动窗口训练具有 100 个向量大小的词嵌入。为了可视化词嵌入,我们需要通过应用主成分分析(PCA)将维度降低到 3,如下面的代码片段所示:
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
import random
np.random.seed(42)
words=list([e for e in model.wv.vocab if len(e)>4])
random.shuffle(words)
words3d = PCA(n_components=3,random_state=42).fit_transform(model.wv[words[:100]])
def plotWords3D(vecs, words, title):
...
plotWords3D(words3d, words, "Visualizing Word2vec Word Embeddings using PCA")
这是输出:
图 1.4 – 用 PCA 可视化词嵌入
如图所示,莎士比亚剧作的主要角色——麦克白、马尔科姆、班克、麦克达夫等——被映射到彼此附近。同样,辅助动词shall、should 和 would 出现在图 1.4的左下方,彼此靠近。我们还可以通过使用嵌入偏移来捕获类似 man-woman= uncle-aunt 的类比。有关此主题的更多有趣视觉示例,请查看以下项目:projector.tensorflow.org/
。
类似 Word2vec 的模型通过采用基于预测的神经架构来学习词嵌入。它们对一些目标函数和附近词预测进行梯度下降。虽然传统方法采用基于计数的方法,但神经模型设计了一个基于预测的架构用于分布语义。基于计数的方法还是基于预测的方法对于分布式词表示更好? GloVe 方法解决了这个问题,并认为这两种方法并没有明显的区别。Jeffrey Penington 等人甚至支持基于计数的方法可能更成功,因为它们捕捉了全局统计信息。他们指出 GloVe 在词类比、词相似性和命名实体识别(NER)任务上胜过其他神经网络语言模型。
然而,这两种范式对于未知单词和单词语义问题并没有提供有效的解决方案。它们不利用子词信息,因此无法学习稀有和未知单词的嵌入。
FastText,另一个广泛使用的模型,提出了一种使用子词信息的新的丰富方法,其中每个单词被表示为一组字符 n-gram。该模型为每个字符 n-gram 设置一个常量向量,并将单词表示为其子向量的和,这是 Hinrich Schütze 首次引入的一种想法(Word Space, 1993)。模型可以计算即使对于未见过的单词也可以学习单词的内部结构,例如后缀/词缀,这在形态丰富的语言(如芬兰语、匈牙利语、土耳其语、蒙古语、韩语、日语、印尼语等)中尤为重要。当前,现代 Transformer 架构使用各种子词标记化方法,例如WordPiece,SentencePiece或字节对编码(BPE)。
RNN 的简要概述
RNN 模型可以通过在较早的时间步中滚动其他标记的信息来学习每个标记表示,并在最后一个时间步学习句子表示。这种机制在许多方面都被发现有益,概述如下:
-
首先,RNN 可以在语言生成或音乐生成的一对多模型中进行重新设计。
-
其次,多对一模型可用于文本分类或情感分析。
-
最后,多对多模型用于 NER 问题。多对多模型的第二个用途是解决编码器-解码器问题,例如机器翻译,问答和文本摘要。
与其他神经网络模型一样,RNN 模型接受通过令牌化算法生成的标记,该算法将整个原始文本分解为原子单位,也称为标记。此外,它将标记单元与数字向量(标记嵌入)相关联,这些向量在训练期间学习。作为替代方案,我们可以事先将嵌入式学习任务分配给著名的单词嵌入算法,例如 Word2vec 或 FastText。
以下是句子The cat is sad.
的 RNN 架构的简单示例,其中 x0 是the
的向量嵌入,x1 是cat
的向量嵌入,依此类推。图 1.5说明了一个被展开成完整深度神经网络(DNN)的 RNN。
The cat is sad.
序列,我们关心一个包含五个词的序列。每一层的隐藏状态充当网络的记忆。它编码了所有先前时间步和当前时间步发生了什么的信息。这在下图中表示:
图 1.5 - 一个 RNN 架构
以下是 RNN 架构的一些优势:
-
可变长度输入:具有处理可变长度输入的能力,无论输入的句子大小如何。我们可以将 3 个或 300 个词的句子喂给网络而不更改参数。
-
关注单词顺序:它按顺序逐个处理序列中的单词,关心单词的位置。
-
适用于各种模式(多对多,一对多):我们可以使用相同的循环范式训练机器翻译模型或情感分析。这两种架构都将基于 RNN。
RNN 架构的缺点如下:
-
长期依赖问题:当我们处理一个非常长的文档并尝试链接相距很远的术语时,我们需要关心并编码这些术语之间的所有不相关的其他术语。
-
容易出现梯度爆炸或消失问题:当处理长文档时,更新最初几个单词的权重是一个大问题,这会导致模型由于梯度消失问题而无法训练。
-
难以应用并行训练:并行化将主问题分解为更小的问题,并同时执行解决方案,但是 RNN 遵循经典的顺序方法。每一层都强烈依赖于前面的层,这使得并行化变得不可能。
-
计算速度随序列长度增加而变慢:对于短文本问题,RNN 可能非常高效。但它在处理长文档时速度非常慢,而且存在长期依赖问题。
尽管理论上 RNN 可以在许多时间步骤上处理信息,但在现实世界中,长文档和长期依赖等问题是不可能发现的。长序列在许多深层中被表示。许多研究已经解决了这些问题,其中一些概述如下:
-
Hochreiter 和 Schmidhuber。长短期记忆。1997 年。
-
Bengio 等人。用梯度下降学习长期依赖性是困难的。1993 年。
-
K. Cho 等人。使用 RNN 编码器-解码器学习短语表示的统计机器翻译。2014 年。
LSTM 和门控循环单元
LSTM(Schmidhuber,1997)和门控循环单元(GRUs)(Cho,2014)是 RNN 的新变种,已经解决了长期依赖问题,并引起了极大关注。LSTM 特别是针对长期依赖问题进行了开发。LSTM 模型的优势在于它使用了额外的细胞状态,这是 LSTM 单元顶部的水平序列线。该细胞状态由专用门控制,用于忘记、插入或更新操作。LSTM 架构的复杂单元如下图所示:
图 1.6 – 一个 LSTM 单元
它能够决定以下内容:
-
我们将在细胞状态中存储什么样的信息
-
哪些信息将被遗忘或删除
在原始 RNN 中,为了学习 I 令牌的状态,它会在 timestep0 和 timestepi-1 之间递归处理先前令牌的整个状态。携带来自较早时间步的完整信息会导致梯度消失问题,使得模型无法训练。LSTM 中的门机制允许架构在某个时间步跳过一些不相关的令牌或记住长期状态以便学习当前令牌状态。
GRU 在很多方面与 LSTM 相似,主要区别在于 GRU 不使用细胞状态。相反,该架构简化了,将细胞状态的功能转移到隐藏状态,并且只包括两个门:一个 更新门 和一个 重置门。更新门确定了来自先前和当前时间步的信息将被推送多远。这个特性帮助模型保留了过去的相关信息,从而最小化了梯度消失问题的风险。重置门检测到不相关的数据并使模型忘记它们。
一个使用 Keras 的 LSTM 的温和实现
我们需要从 通用语言理解评估(GLUE)基准中下载 斯坦福情感树库(SST-2)情感数据集。我们可以通过运行以下代码来实现这一点:
$ wget https://dl.fbaipublicfiles.com/glue/data/SST-2.zip
$ unzip SST-2.zip
重要提示
SST-2:这是一个完全标记的解析树,允许在英语中进行完整的情感分析。该语料库最初包括约 12K 个从电影评论中提取的单个句子。它使用斯坦福解析器进行解析,并包含由三名人类评审员注释的 200K 多个唯一短语。有关更多信息,请参见 Socher 等人,用组合矢量语法解析,EMNLP。2013 (nlp.stanford.edu/sentiment
)。
下载数据后,让我们将其作为 pandas 对象读取,如下所示:
import tensorflow as tf
import pandas as pd
df=pd.read_csv('SST-2/train.tsv',sep="\t")
sentences=df.sentence
labels=df.label
我们需要设置最大句子长度,构建词汇表和字典(word2idx
,idx2words
),最后将每个句子表示为索引列表而不是字符串。我们可以通过运行以下代码来实现这一点:
max_sen_len=max([len(s.split()) for s in sentences])
words = ["PAD"]+\
list(set([w for s in sentences for w in s.split()]))
word2idx= {w:i for i,w in enumerate(words)}
max_words=max(word2idx.values())+1
idx2word= {i:w for i,w in enumerate(words)}
train=[list(map(lambda x:word2idx[x], s.split()))\
for s in sentences]
比 max_sen_len
(最大句子长度)短的序列将使用 PAD
值填充,直到它们的长度达到 max_sen_len
。另一方面,更长的序列将被截断,以使其适合 max_sen_len
。以下是实现:
from keras import preprocessing
train_pad = preprocessing.sequence.pad_sequences(train,
maxlen=max_sen_len)
print('Train shape:', train_pad.shape)
Output: Train shape: (67349, 52)
我们准备设计和训练一个 LSTM 模型,如下所示:
from keras.layers import LSTM, Embedding, Dense
from keras.models import Sequential
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop',loss='binary_crossentropy', metrics=['acc'])
history = model.fit(train_pad,labels, epochs=30, batch_size=32, validation_split=0.2)
模型将训练 30 个 epochs。为了绘制 LSTM 模型到目前为止学到了什么,我们可以执行以下代码:
import matplotlib.pyplot as plt
def plot_graphs(history, string):
...
plot_graphs(history, 'acc')
plot_graphs(history, 'loss')
该代码生成以下图表,显示了基于 LSTM 的文本分类的训练和验证性能:
图 1.7 – LSTM 网络的分类性能
正如我们之前提到的,基于 RNN 的编码器-解码器模型的主要问题是它为一个序列生成一个固定的表示。 但是,注意机制使得 RNN 能够专注于输入标记的某些部分,并将它们映射到输出标记的某些部分。 发现这种注意机制非常有用,并已成为 Transformer 架构的基本思想之一。 我们将在接下来的部分以及整本书的讨论中讨论 Transformer 架构如何利用注意力。
CNN 的简要概述
CNN 在计算机视觉方面取得成功后,也被用于 NLP,用于建模句子或语义文本分类等任务。 在许多实践中,CNN 由卷积层组成,然后是一个密集的神经网络。 卷积层对数据进行处理以提取有用的特征。 与任何 DL 模型一样,卷积层扮演自动化特征提取的角色。 在 NLP 的情况下,这个特征层是由一个嵌入层提供输入,该层将句子以 one-hot 矢量化格式作为输入。 这些 one-hot 矢量是通过为组成句子的每个单词生成一个token-id
来生成的。 以下截图的左侧显示了句子的 one-hot 表示:
图 1.8 - One-hot 矢量
每个由 one-hot 矢量表示的标记都被送入嵌入层。 嵌入层可以通过随机值或使用预训练的单词向量(如 GloVe、Word2vec 或 FastText)进行初始化。 然后,该句子将转换为一个 NxE 形状的密集矩阵(其中N是句子中标记的数量,E是嵌入的大小)。 以下截图说明了 1D CNN 如何处理该密集矩阵:
图 1.9 - 五个标记的句子的 1D CNN 网络
卷积将在不同层和核之上进行。 卷积层的超参数是核大小和核的数量。 值得注意的是,这里应用的是 1D 卷积,原因是标记嵌入不能被视为局部的,我们希望应用能够依次看到多个标记的核。 您可以将其视为具有指定窗口的 n-gram。 使用浅层 TL 结合 CNN 模型也是这些模型的另一个良好的能力。 正如以下截图所示,我们还可以用许多标记的表示的组合来传播网络,正如 2014 年由 Yoon Kim 提出的研究中所建议的那样,《句子分类的卷积神经网络》:
图 1.10 - CNN 中许多表示的组合
例如,我们可以使用三个嵌入层而不是一个,并为每个令牌连接它们。在这种设置下,如果所有三个不同的嵌入的大小为 128,则诸如fell的令牌将具有大小为 3x128 的向量。这些嵌入可以使用来自 Word2vec、GloVe 和 FastText 的预训练向量进行初始化。每一步的卷积运算将使用其各自的三个向量来查看 N 个单词(N 是卷积滤波器的大小)。这里使用的卷积类型是 1D 卷积。这里的维度表示进行操作时可能的移动。例如,2D 卷积将沿两个轴移动,而 1D 卷积只沿一个轴移动。下面的屏幕截图显示了它们之间的区别:
图 1.11 – 卷积方向
以下代码片段是一个处理与 LSTM 管道中使用的相同数据的 1D CNN 实现。它包括Conv1D
和MaxPooling
层的组合,以及GlobalMaxPooling
层。我们可以通过调整参数并添加更多层来扩展该管道以优化模型:
from keras import layers
model = Sequential()
model.add(layers.Embedding(max_words, 32, input_length=max_sen_len))
model.add(layers.Conv1D(32, 8, activation='relu'))
model.add(layers.MaxPooling1D(4))
model.add(layers.Conv1D(32, 3, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1, activation= 'sigmoid')
model.compile(loss='binary_crossentropy', metrics=['acc'])
history = model.fit(train_pad,labels, epochs=15, batch_size=32, validation_split=0.2)
结果表明,CNN 模型与其 LSTM 对应物表现出可比较的性能。虽然 CNN 在图像处理中已成为标准,但我们已经看到了许多成功应用 CNN 在 NLP 中。而 LSTM 模型被训练用于识别跨越时间的模式,CNN 模型则识别跨越空间的模式。
Transformer 架构概述
Transformer模型因其在从文本分类到文本生成等巨大范围的 NLP 问题中的有效性而受到极大关注。注意力机制是这些模型的重要部分,并发挥着非常关键的作用。在 Transformer 模型出现之前,注意力机制被提出作为帮助改进传统 DL 模型(如 RNNs)的工具。为了更好地理解 Transformer 及其对 NLP 的影响,我们将首先研究注意力机制。
注意力机制
*Bahdanau 等人(2015 年)*提出了注意力机制的第一个变体之一。该机制基于这样一个事实,即基于 RNN 的模型(如 GRUs 或 LSTMs)在诸如token-id
之类的任务上存在信息瓶颈,并以递归方式处理它(编码器)。然后,处理过的中间表示被馈送到另一个递归单元(解码器)以提取结果。这种雪崩般的信息就像一个滚动的球,消耗了所有的信息,而将其滚动出来对于解码器部分来说是困难的,因为解码器部分并没有看到所有的依赖关系,只获得中间表示(上下文向量)作为输入。
为了调整这个机制,Bahdanau 提出了一种注意机制,在中间隐藏值上使用权重。这些权重可以调整模型在每个解码步骤中对输入的关注程度。这种出色的指导帮助模型在特定任务中,比如 NMT(许多对许多任务),做出了很大的帮助。这里提供了一个典型注意机制的示意图:
图 1.12 – 注意机制
不同的注意机制提出了不同的改进。 加性,乘性,一般 和 点积 注意机制出现在这些机制家族中。后者,这是一个带有缩放参数的修改版本,被称为缩放点积注意力。这种特定的注意类型是 Transformer 模型的基础,被称为多头注意力机制。加性注意力也是之前在 NMT 任务中引入的一项显着变化。你可以在这里看到不同类型的注意机制的概述:
表 2 – 注意机制的类型(图片灵感来自 https://lilianweng.github.io/lil-log/2018/06/24/attention-attention.html)
由于注意机制不仅限于 NLP,它们也被用于各种领域的不同用例,从计算机视觉到语音识别。以下截图展示了一个用于神经图像字幕训练的多模态方法的可视化(K Xu 等,Show, attend and tell: Neural image caption generation with visual attention, 2015):
图 1.13 – 计算机视觉中的注意机制
下图所示的多头注意机制是 Transformer 架构中的一个重要部分:
图 1.14 – 多头注意机制
接下来,让我们了解多头注意机制。
多头注意机制
在深入研究缩放点积注意力机制之前,最好先对自注意力有一个良好的理解。如图 1.15所示,自注意力是一种缩放自注意力机制的基本形式。该机制使用一个显示为X的输入矩阵,并在X中的各个项目之间产生注意力分数。我们将X视为一个 3x4 矩阵,其中 3 代表令牌的数量,4 代表嵌入大小。图 1.15中的Q也称为查询,K称为键,V称为值。在产生Q、K和V之前,三种类型的矩阵被称为theta、phi和g,它们与X相乘。查询(Q)和键(K)之间的乘积结果产生一个注意力分数矩阵。这也可以看作是一个数据库,我们使用查询和键来了解各种项目在数值评估方面的关联程度。注意力分数和V矩阵的乘积产生这种类型的注意力机制的最终结果。其被称为自注意力的主要原因是因为它的统一输入X; Q、K和V是从X计算出来的。你可以在下图中看到所有这些的描述:
图 1.15 – 注意力机制的数学表示(图片来源于 https://blogs.oracle.com/datascience/multi-head-self-attention-in-nlp)
一个缩放点积注意力机制与自注意力(点积)机制非常相似,只是它使用了一个缩放因子。另一方面,多头部分确保模型能够在各个层面上查看输入的各个方面。Transformer 模型关注编码器注释和来自过去层的隐藏值。Transformer 模型的架构没有逐步流程; 相反,它使用位置编码来获取有关输入序列中每个令牌位置的信息。嵌入值(随机初始化)和位置编码的固定值的串联值是输入传递到第一个编码器部分中的层,并通过体系结构传播,如下图所示:
图 1.16 – 一个 Transformer
通过评估不同频率的正弦和余弦波来获取位置信息。位置编码的一个示例在以下截图中可视化:
图 1.17 – 位置编码(图片来源于 http://jalammar.github.io/illustrated-Transformer/)
在下面的流行截图中给出了 Transformer 架构和缩放点积注意力机制的性能的一个很好的例子:
图 1.18 – Transformer 的注意力映射(图片灵感来自 https://ai.googleblog.com/2017/08/Transformer-novel-neural-network.html)
它 这个词在不同的语境中指代不同的实体,正如前面的截图所示。使用 Transformer 架构的另一个改进在于并行性。传统的顺序循环模型如 LSTM 和 GRU 并没有这样的能力,因为它们逐个处理输入标记。另一方面,前馈层的速度会更快一些,因为单个矩阵乘法比循环单元要快得多。多头注意力层的堆叠可以更好地理解复杂的句子。一个很好的多头注意力机制的可视化示例如下截图所示:
图 1.19 – 多头注意力机制(图片灵感来自 https://imgur.com/gallery/FBQqrxw)
在注意力机制的解码器侧,采用了与编码器非常相似的方法,但有一些小的修改。多头注意力机制是相同的,但也使用了编码器堆栈的输出。这个编码被提供给第二个多头注意力层中的每个解码器堆栈。这个小修改在解码时引入了编码器堆栈的输出。这个修改让模型在解码时意识到编码器输出,并同时在训练期间帮助它在各个层之间有更好的梯度流动。解码器层末端的最终 softmax 层用于为各种用例提供输出,例如原始 Transformer 架构引入的 NMT。
这个架构有两个输入,分别标注为输入和输出(向右移动)。一个始终存在(输入),无论是训练还是推断,而另一个只存在于训练和推断中,它由模型产生。我们之所以不在推断中使用模型预测,是为了防止模型自行出错。但这是什么意思呢?想象一个神经机器翻译模型试图将一句英文翻译成法文——在每个步骤中,它都会对一个词进行预测,并使用该预测的词来预测下一个词。但如果在某个步骤出错了,那么后面的所有预测都会错误。为了防止模型像这样出错,我们提供正确的词作为右移版本。
下图显示了一个 Transformer 模型的可视化示例。它展示了一个具有两个编码器和两个解码器层的 Transformer 模型。这张图中的 加法 & 标准化 层从这张图中的 前馈 层接收输入后添加和标准化它:
图 1.20 – Transformer 模型(图片灵感来自 http://jalammar.github.io/illustrated-Transformer/)
另一个由基于 Transformer 架构使用的主要改进是基于一种简单的通用文本压缩方案,以防止输入端出现未见标记。这种方法通过使用不同的方法(如字节对编码和句子片段编码)进行,提高了 Transformer 在处理未见标记时的性能。它还在模型遇到形态接近的标记时指导模型。这样的标记在过去是不可见的,并且在训练中很少使用,然而,在推理中可能会看到。在某些情况下,训练中会看到其部分内容;在形态丰富的语言(如土耳其语、德语、捷克语和拉脱维亚语)的情况下会发生后者。例如,模型可能看到单词 training,但没有看到 trainings。在这种情况下,它可以将 trainings 标记为 training+s。当我们将它们视为两个部分时,这两者是常见的。
基于 Transformer 的模型具有相当普遍的特征——例如,它们都是基于这种原始架构的,不同之处在于它们使用和不使用的步骤。在某些情况下,会做出较小的改进——例如,改进多头注意力机制。
使用 Transformer 进行 TL
TL 是人工智能(AI)和机器学习(ML)的一个领域,旨在使模型可在不同任务中重用,例如,在给定任务(如 A)上训练的模型可在不同任务(如 B)上重用(微调)。在 NLP 领域,通过使用可以捕获语言理解的 Transformer-like 架构来实现这一目标。这种模型称为语言模型——它们为其训练的语言提供了一个模型。TL 不是一种新技术,它已经被用于各种领域,如计算机视觉。ResNet、Inception、VGG 和 EfficientNet 是可以用作预训练模型的示例,可在不同的计算机视觉任务上进行微调。
浅层 TL 使用诸如 Word2vec、GloVe 和 Doc2vec 这样的模型在 NLP 中也是可能的。它被称为 浅层,因为这种 TL 背后没有模型,而是利用了预训练的单词/标记向量。你可以使用这些标记或文档嵌入模型,接着使用分类器,或者将它们与其他模型(如 RNNs)结合使用,而不是使用随机嵌入。
在 NLP 中使用 Transformer 模型进行 TL 也是可能的,因为这些模型可以在没有任何标注数据的情况下学习一门语言本身。语言建模是一种用于为各种问题训练可转移权重的任务。掩码语言建模是用于学习一门语言本身的方法之一。与 Word2vec 的基于窗口的模型预测中心词元相似,掩码语言建模采用类似的方法,但有关键差异。给定一个概率,每个词都被掩码并替换为特殊标记,如 [MASK]。语言模型(在我们的情况下是基于 Transformer 的模型)必须预测被掩码的词。与 Word2vec 不同,不是使用一个窗口,而是给出整个句子,模型的输出必须是相同的带有掩码词的句子。
使用 Transformer 架构进行语言建模的第一个模型之一是BERT,它基于 Transformer 架构的编码器部分。通过在训练语言模型之前和之后使用相同的方法,BERT 完成了掩码语言建模。BERT 是一个可转移的语言模型,适用于不同的 NLP 任务,如标记分类、序列分类,甚至问答任务。
每一个任务都是对 BERT 进行微调的任务,一旦一个语言模型被训练完成。BERT 最为人所知的是其在基础 Transformer 编码器模型上的关键特性,通过改变这些特性,提出了不同版本的它——小型、微型、基础、大型和超大型。上下文嵌入使得模型能够根据所处的上下文正确理解每个单词的含义——例如,单词 冷 在两个不同的句子中可能有不同的含义:冷酷无情的杀手 和 寒冷的天气。编码器部分的层数、输入维度、输出嵌入维度和多头注意机制的数量是这些关键特性,如下面的截图所示:
图 1.21 – BERT 的预训练和微调流程(图片灵感来自 J. Devlin 等人,《Bert: Pre-training of deep bidirectional Transformers for language understanding》,2018 年)
如你所见,在图 1.21中,预训练阶段还包括另一个称为下一句预测的目标。我们知道,每个文档由相互跟随的句子组成,而模型理解语言的另一个重要部分是理解句子之间的关系,换句话说,它们是否相关。为了完成这些任务,BERT 引入了特殊的标记,如*[CLS]和[SEP]。[CLS]标记是一个最初没有意义的标记,用作所有任务的起始标记,并包含关于句子的所有信息。在诸如 NSP 之类的序列分类任务中,会在此标记的输出(0位置的输出)之上使用分类器。它还有助于评估句子的意义或捕获其语义,例如,当使用孪生 BERT 模型时,通过诸如余弦相似度之类的度量来比较不同句子的这两个[CLS]标记非常有帮助。另一方面,[SEP]用于区分两个句子,它仅用于分隔两个句子。在预训练之后,如果有人打算在情感分析等序列分类任务上对 BERT 进行微调,那么他们将在[CLS]*的输出嵌入之上使用一个分类器。值得注意的是,在微调期间,所有 TL 模型都可以被冻结或释放;冻结意味着将模型内的所有权重和偏差视为常量,并停止对它们进行训练。在情感分析的示例中,如果模型被冻结,只有分类器会被训练,而不是模型。
摘要
通过这一章,我们现在来到了结束。你现在应该对 NLP 方法和方法的演变有所了解,从 BoW 到 Transformers。我们看了如何实现基于 BoW、RNN 和 CNN 的方法,并了解了 Word2vec 是什么,以及它如何通过浅层 TL 改进传统的 DL 方法。我们还深入了解了 Transformer 架构的基础,以 BERT 为例。到本章结束时,我们已经了解了 TL 以及 BERT 如何利用它。
到目前为止,我们已经学习了继续阅读下一章所必需的基本信息。我们了解了基于 Transformer 的架构的主要思想以及如何使用此架构应用 TL。
在下一节中,我们将看到如何从头开始运行一个简单的 Transformer 示例。将提供有关安装步骤的相关信息,并且还将详细调查与数据集和基准的工作。
参考文献
-
Mikolov, T., Chen, K., Corrado, G. & Dean, J. (2013). Efficient estimation of word representations in vector space. arXiv preprint arXiv:1301.3781.
-
Bahdanau, D., Cho, K. & Bengio, Y. (2014). Neural machine translation by jointly learning to align and translate. arXiv preprint arXiv:1409.0473.
-
Pennington, J., Socher, R. & Manning, C. D. (2014, 十月). GloVe: 用于词表示的全局向量. 在 2014 年自然语言处理会议(EMNLP)论文集中的论文 (pp. 1532-1543).
-
Hochreiter, S. & Schmidhuber, J. (1997). 长短期记忆网络. 神经计算, 9(8), 1735-1780.
-
Bengio, Y., Simard, P, & Frasconi, P. (1994). 使用梯度下降学习长期依赖关系困难. IEEE 神经网络交易, 5(2), 157-166.
-
Cho, K., Van Merriënboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H. & Bengio, Y. (2014). 使用 RNN 编码器-解码器学习短语表示进行统计机器翻译. arXiv 预印本 arXiv:1406.1078.
-
Kim, Y. (2014). 句子分类的卷积神经网络. CoRR abs/1408.5882 (2014). arXiv 预印本 arXiv:1408.5882.
-
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N. & Polosukhin, I. (2017). 注意力就是一切. arXiv 预印本 arXiv:1706.03762.
-
Devlin, J., Chang, M. W., Lee, K. & Toutanova, K. (2018). Bert: 深度双向 Transformer 的预训练用于语言理解. arXiv 预印本 arXiv:1810.04805.
第二章:主题的实践介绍
到目前为止,我们已经总体了解了基于深度学习(DL)方法的自然语言处理(NLP)的演变。我们也学习了一些有关 Transformer 及其各自架构的基本信息。在本章中,我们将深入研究如何使用 transformer 模型。在本章中,像双向编码器表示的 Transformer(BERT)这样的分词器和模型将以更加技术性的细节进行描述,并提供实际示例,包括如何加载分词器/模型和使用社区提供的预训练模型。但在使用任何特定模型之前,我们需要了解使用安装 Anaconda 所需的安装步骤以提供必要的环境。在安装步骤中,将涵盖在各种操作系统如 Linux、Windows 和 macOS 上安装库和程序的内容。还会展示PyTorch和TensorFlow的安装,在CPU和GPU的两个版本上。还提供了一个Google Colaboratory(Google Colab)安装 Transformer 库的快速指南。还有一个专门讨论在 PyTorch 和 TensorFlow 框架中使用模型的部分。
HuggingFace 模型存储库也是本章的另一个重要部分,讨论了查找不同模型和使用各种管道的步骤,例如,详细介绍了像双向自回归 Transformer(BART)、BERT 和表格解析(TAPAS)这样的模型,以及一瞥生成式预训练 Transformer 2(GPT-2)文本生成。然而,这只是一个概述,在本章中涉及到的部分是准备环境和使用预训练模型,这里不讨论模型训练,因为这在接下来的章节中被赋予了更重要的意义。
一切就绪,我们已经了解了如何通过社区提供的模型使用Transformer
库进行推理,接下来介绍datasets
库。在这里,我们将介绍加载各种数据集、基准测试和使用指标的方法。加载特定数据集并从中获取数据是我们在这里主要关注的领域之一。这里还会考虑跨语言数据集以及如何使用datasets
库中的本地文件。datasets
库中的map
和filter
函数是模型训练的重要函数,并且在本章中也会被研究。
本章是书中的重要部分,因为这里更详细地介绍了datasets
库。了解如何使用社区提供的模型并准备好系统以供接下来的内容是非常重要的。
总而言之,本章中我们将涵盖以下主题:
-
使用 Anaconda 安装 Transformer
-
使用语言模型和分词器进行工作
-
使用社区提供的模型进行工作
-
处理基准测试和数据集
-
速度和内存的基准测试
技术要求
您需要安装接下来列出的库和软件。虽然拥有最新版本是一个优点,但是强制安装与彼此兼容版本是必须的。有关 HuggingFace Transformer 的最新版本安装的更多信息,请查看它们在huggingface.co/transformers/installation.html
的官方网页:
-
Anaconda
-
Transformer 4.0.0
-
PyTorch 1.1.0
-
TensorFlow 2.4.0
-
数据集 1.4.1
最后,本章中显示的所有代码都可以在本书的 GitHub 存储库 https://github.com/PacktPublishing/Mastering-Transformer/tree/main/CH02 中找到。
点击以下链接查看动态代码演示视频:bit.ly/372ek48
使用 Anaconda 安装 Transformer
Transformer
库。但是,也可以在没有 Anaconda 的帮助下安装此库。使用 Anaconda 的主要动机是更容易解释过程并调节使用的软件包。
要开始安装相关库,安装 Anaconda 是必须的一步。Anaconda 文档提供了官方指南,简单介绍了为常见操作系统(macOS,Windows 和 Linux)安装 Anaconda 的步骤。
Linux 安装
用户可以享受许多 Linux 发行版,但其中Ubuntu是偏爱的之一。在本节中,将介绍安装 Anaconda 的步骤。请按以下步骤进行:
-
从 https://www.anaconda.com/products/individual#Downloads 下载 Linux 版本的 Anaconda 安装程序并转到 Linux 部分,如下截图所示:
图 2.1 - Linux 的 Anaconda 下载链接
-
运行
bash
命令来安装它并完成以下步骤: -
打开终端并运行以下命令:
bash Terminal./FilePath/For/Anaconda.sh
-
按下Enter键查看许可协议,如果你不想全部阅读,按Q键,然后执行以下操作:
-
点击是同意。
-
点击
conda
根环境。 -
在终端中运行
python
命令后,您应该看到 Python 版本信息后出现 Anaconda 提示符。 -
您可以通过从终端运行
anaconda-navigator
命令来访问 Anaconda Navigator。结果,您将看到 Anaconda 图形用户界面(GUI)开始加载相关模块,如下面的截图所示:
图 2.2 - Anaconda Navigator
让我们继续下一节!
Windows 安装
下面的步骤描述了如何在 Windows 操作系统上安装 Anaconda:
-
从 https://www.anaconda.com/products/individual#Downloads 下载安装程序并转到 Windows 部分,如下截图所示:
图 2.3 – Windows 上的 Anaconda 下载链接
-
打开安装程序并通过点击我同意按钮按照指南进行操作。
-
选择安装位置,如下截图所示:
图 2.4 – Windows 上的 Anaconda 安装程序
-
不要忘记从 Windows shell 或 Windows 命令行检查
python
命令:图 2.5 – Windows 上的 Anaconda 安装程序高级选项
-
按照其余的安装说明进行操作并完成安装。
现在您应该能够从开始菜单启动 Anaconda Navigator。
macOS 上的安装
安装 Anaconda 在 macOS 上必须遵循以下步骤:
-
从 https://www.anaconda.com/products/individual#Downloads 下载安装程序并转到 macOS 部分,如下截图所示:
图 2.6 – macOS 上的 Anaconda 下载链接
-
打开安装程序。
-
按照说明并点击安装按钮在预定义位置安装 macOS,如下截图所示。你可以更改默认目录,但不建议这样做:
图 2.7 – macOS 上的 Anaconda 安装程序
完成安装后,您应该能够访问 Anaconda Navigator。
安装 TensorFlow、PyTorch 和 Transformer
安装 TensorFlow 和 PyTorch 作为用于 DL 的两个主要库可以通过pip
或conda
本身进行。conda
提供了一个用于更轻松安装这些库的命令行界面(CLI)。
为了进行干净的安装并避免中断其他环境,最好为huggingface
库创建一个conda
环境。您可以通过运行以下代码来实现:
conda create -n Transformer
此命令将创建一个空的环境以安装其他库。一旦创建,我们需要激活它,如下:
conda activate Transformer
安装Transformer
库非常简单,只需运行以下命令:
conda install -c conda-forge tensorflow
conda install -c conda-forge pytorch
conda install -c conda-forge Transformer
conda install
命令中的-c
参数让 Anaconda 使用额外的渠道来搜索库。
请注意,必须安装 TensorFlow 和 PyTorch,因为Transformer
库同时使用这两个库。另一个注意事项是,通过 Conda 轻松处理 TensorFlow 的 CPU 和 GPU 版本。如果在tensorflow
之后简单地放置–gpu
,它将自动安装 GPU 版本。通过cuda
库(GPU 版本)安装 PyTorch 时,需要相关库,如cuda
,但conda
会自动处理这个,不需要进一步的手动设置或安装。以下屏幕截图显示了conda
如何自动处理安装 PyTorch GPU 版本,安装相关的cudatoolkit
和cudnn
库:
图 2.8 – 使用 Conda 安装 PyTorch 和相关 cuda 库
请注意,所有这些安装也可以在没有conda
的情况下进行,但使用 Anaconda 的原因是它的易用性。在使用环境或安装 TensorFlow 或 PyTorch 的 GPU 版本方面,Anaconda 像魔术一样工作,是一个很好的时间节省工具。
使用 Google Colab 进行安装
即使使用 Anaconda 节省时间且有用,但在大多数情况下,不是每个人都有这样好的和合理的计算资源可用。在这种情况下,Google Colab 是一个很好的替代方案。在 Colab 中安装Transformer
库的命令如下进行:
!pip install Transformer
在语句前加上感叹号会使代码在 Colab shell 中运行,这相当于在终端中而不是使用 Python 解释器运行代码。这将自动安装Transformer
库。
使用语言模型和分词器
在本节中,我们将介绍如何使用Transformer
库与语言模型及其相关的分词器。为了使用任何指定的语言模型,我们首先需要导入它。我们将从谷歌提供的 BERT 模型开始,并使用其预训练版本,如下所示:
>>> from Transformer import BERTTokenizer
>>> tokenizer = \
BERTTokenizer.from_pretrained('BERT-base-uncased')
前面代码片段的第一行导入了 BERT 分词器,第二行下载了 BERT 基础版本的预训练分词器。请注意,无大小写版本是使用无大小写字母训练的,因此字母出现在大写或小写中都没有关系。要测试并查看输出,必须运行以下代码行:
>>> text = "Using Transformer is easy!"
>>> tokenizer(text)
这将是输出:
{'input_ids': [101, 2478, 19081, 2003, 3733, 999, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1]}
input_ids
显示每个标记的标记 ID,token_type_ids
显示每个标记的类型,它们分隔了第一个和第二个序列,如下面的屏幕截图所示:
图 2.9 – BERT 的序列分隔
attention_mask
是用于显示Transformers模型序列的起始和结束的 0 和 1 的掩码,以防止不必要的计算。每个分词器都有将特殊标记添加到原始序列的自己方式。对于 BERT 分词器,它在序列的开头添加了一个 [CLS]
标记,在序列的结尾添加了一个 [SEP]
标记,可以通过 101 和 102 观察到。这些数字来自预训练分词器的标记 ID。
分词器可用于基于 PyTorch 和 TensorFlow 的 Transformer
模型。为了每个模型都有输出,必须在 return_tensors
中使用 pt
和 tf
关键字。例如,您可以通过简单运行以下命令来使用分词器:
>>> encoded_input = tokenizer(text, return_tensors="pt")
encoded_input
具有将被 PyTorch 模型使用的标记化文本。为了运行模型,例如 BERT 基础模型,可以使用以下代码从 huggingface
模型存储库下载模型:
>>> from Transformer import BERTModel
>>> model = BERTModel.from_pretrained("BERT-base-uncased")
分词器的输出可以通过以下代码行传递给已下载的模型:
>>> output = model(**encoded_input)
这将以嵌入和交叉注意输出的形式给您模型的输出。
在加载和导入模型时,您可以指定要使用的模型的版本。如果您只需在模型名称前加上 TF
,Transformer
库将加载其 TensorFlow 版本。以下代码显示了如何加载和使用 TensorFlow 版本的 BERT base:
from Transformer import BERTTokenizer, TFBERTModel
tokenizer = \
BERTTokenizer.from_pretrained('BERT-base-uncased')
model = TFBERTModel.from_pretrained("BERT-base-uncased")
text = " Using Transformer is easy!"
encoded_input = tokenizer(text, return_tensors='tf')
output = model(**encoded_input)
对于特定任务,如使用语言模型填充掩码,huggingface
设计了准备就绪的管道。例如,可以在以下代码片段中看到填充掩码的任务:
>>> from Transformer import pipeline
>>> unmasker = \
pipeline('fill-mask', model='BERT-base-uncased')
>>> unmasker("The man worked as a [MASK].")
此代码将生成以下输出,显示分数和可能放置在 [MASK]
标记中的标记:
[{'score': 0.09747539460659027, 'sequence': 'the man worked as a carpenter.', 'token': 10533, 'token_str': 'carpenter'}, {'score': 0.052383217960596085, 'sequence': 'the man worked as a waiter.', 'token': 15610, 'token_str': 'waiter'}, {'score': 0.049627091735601425, 'sequence': 'the man worked as a barber.', 'token': 13362, 'token_str': 'barber'}, {'score': 0.03788605332374573, 'sequence': 'the man worked as a mechanic.', 'token': 15893, 'token_str': 'mechanic'}, {'score': 0.03768084570765495, 'sequence': 'the man worked as a salesman.', 'token': 18968, 'token_str': 'salesman'}]
要使用 pandas 获取清晰的视图,请运行以下代码:
>>> pd.DataFrame(unmasker("The man worked as a [MASK]."))
结果可以在以下截图中看到:
图 2.10 – BERT 填充掩码的输出
到目前为止,我们已经学习了如何加载和使用预训练的 BERT 模型,并了解了分词器的基础知识,以及模型的 PyTorch 和 TensorFlow 版本之间的区别。在下一节中,我们将学习如何使用社区提供的模型,通过加载不同的模型,阅读模型作者提供的相关信息,并使用不同的管道,如文本生成或问答(QA)管道。
使用社区提供的模型
Hugging Face 拥有大量由来自大型人工智能(AI)和信息技术(IT)公司(如谷歌和 Facebook)的合作者提供的社区模型。还有许多个人和大学提供的有趣模型。访问和使用它们也非常容易。要开始,请访问他们网站上提供的 Transformer 模型目录(https://huggingface.co/models),如下截图所示:
图 2.11 – Hugging Face 模型库
除了这些模型,还有很多好用的数据集可供 NLP 任务使用。要开始使用其中一些模型,你可以通过关键字搜索或只需指定你的主要 NLP 任务和流水线来探索它们。
例如,我们正在寻找一个表格 QA 模型。在找到我们感兴趣的模型之后,从 Hugging Face 网站(huggingface.co/google/tapas-base-finetuned-wtq
)会提供类似下面这个页面:
图 2.12 – TAPAS 模型页面
右侧有一个面板,你可以在这里测试这个模型。请注意,这是一个可以回答关于提供给模型的表格的问题的表格 QA 模型。如果你问一个问题,它会用高亮显示的方式来回复你。以下截图展示了它如何获取输入并为特定表格提供答案:
图 2.13 – 使用 TAPAS 进行表格 QA
每个模型都有一个由模型作者提供的页面,也被称为huggingface
库页面,并查看作者提供的示例(huggingface.co/gpt2
),如下截图所示:
图 2.14 – 来自 Hugging Face GPT-2 页面的文本生成代码示例
使用流水线是推荐的,因为所有繁琐的工作都由Transformer
库处理好了。举个例子,假设你需要一个开箱即用的零-shot 分类器。下面的代码片段展示了实现和使用这样一个预训练模型是多么容易:
>>> from Transformer import pipeline
>>> classifier = pipeline("zero-shot-classification", model="facebook/bart-large-mnli")
>>> sequence_to_classify = "I am going to france."
>>> candidate_labels = ['travel', 'cooking', 'dancing']
>>> classifier(sequence_to_classify, candidate_labels)
上述代码将提供以下结果:
{'labels': ['travel', 'dancing', 'cooking'], 'scores': [0.9866883158683777, 0.007197578903287649, 0.006114077754318714], 'sequence': 'I am going to france.'}
我们已经完成了安装和hello-world
应用程序部分。到目前为止,我们介绍了安装过程,完成了环境设置,并体验了第一个 transformer 流水线。接下来,我们将介绍datasets
库,这将是我们接下来的实验章节中的必备工具。
与基准和数据集一起工作
在介绍datasets
库之前,我们最好先谈谈重要的基准,比如Transformer
库,我们可以将从一个特定任务学到的内容转移到一个相关任务中,这被称为迁移学习(TL)。通过在相关问题之间迁移表示,我们能够训练出共享通用语言知识的通用模型,也就是多任务学习(MTL)。TL 的另一个方面是跨语言传递知识(多语言模型)。
重要的基准指标
在这一部分中,我们将介绍被基于 Transformer 的架构广泛使用的重要基准。这些基准专门对 MTL 和多语言以及零-shot 学习做出了很大贡献,包括许多具有挑战性的任务。我们将看看以下基准:
-
GLUE
-
SuperGLUE
-
XTREME
-
XGLUE
-
SQuAD
为了使用更少的页面,我们只详细介绍 GLUE 基准的任务,所以我们先看看这个基准。
GLUE 基准
最近的研究指出,多任务训练方法可以比单任务学习获得更好的结果,作为一个特定任务的特定模型。在这个方向上,为了 MTL 引入了 GLUE 基准,这是一个用于评估 MTL 模型在一系列任务上的性能的工具和数据集集合。它提供了一个公共排行榜,用于监控基准测试提交的性能,以及一个总结 11 个任务的单一数字度量标准。该基准包括许多基于现有任务的理解句子任务,这些任务涵盖了各种数据集,包括不同大小、文本类型和难度级别的文本。这些任务按照以下三种类型分类:
-
单句子任务
-
CoLA:Corpus of Linguistic Acceptability 数据集。这个任务由来自语言学理论文章的英语可接受性判断组成。
-
pos
/neg
标签。 -
相似度和释义任务
-
MRPC:Microsoft Research Paraphrase Corpus 数据集。这个任务是看一对句子是否语义上等价。
-
QQP:Quora Question Pairs 数据集。这个任务决定一对问题是否语义上等价。
-
STS-B:Semantic Textual Similarity Benchmark 数据集。这个任务是从新闻标题中抽取的句子对集合,其相似性得分在 1 到 5 之间。
-
推理任务
-
MNLI:Multi-Genre Natural Language Inference 语料库。这是一组带有文本蕴涵的句子对。任务是预测文本是否蕴涵假设(蕴涵)、否定假设(否定)或者既不是(中性)。
-
QNLI:Question Natural Language Inference 数据集。这是 SQuAD 的转换版本。任务是检查一个句子是否包含问题的答案。
-
RTE:Recognizing Textual Entailment 数据集。这是一个文本蕴涵挑战任务,将来自各种来源的数据结合起来。这个数据集类似于前面的 QNLI 数据集,其中任务是检查第一个文本是否蕴涵第二个文本。
-
WNLI:Winograd Natural Language Inference 架构挑战。这原本是一个代词解析任务,将一个代词和一个句子中的短语联系起来。GLUE 将问题转换为句子对分类,如下面详细说明。
SuperGLUE 基准
与 Glue 类似,SuperGLUE是一个新的基准测试,具有一组更难的语言理解任务,并提供大约八项语言任务的公共排行榜,借鉴了现有数据,与 GLUE 的类似,都使用单一数字性能指标。其背后的动机是,截至撰写本书,在写作时间,当前的 GLUE 得分(90.8)超过人类表现(87.1)。因此,SuperGLUE 提供了一个更具挑战性和多样化的任务,以实现通用目的的语言理解技术。
你可以在gluebenchmark.com上访问 GLUE 和 SuperGLUE 基准测试。
XTREME 基准测试
近年来,自然语言处理研究人员越来越注重学习通用表示而不是单一任务,可以应用于许多相关任务。构建通用语言模型的另一种方法是使用多语言任务。已经观察到最近的多语言模型,如多语言 BERT(mBERT)和 XLM-R,预训练了大量的多语言语料库,当将它们转移到其他语言时表现更好。因此,这里的主要优势是跨语言泛化使我们能够通过零样本跨语言传输在资源匮乏的语言中构建成功的自然语言处理应用程序。
在这个方向上,XTREME基准测试已经设计好。它目前包括约 40 种属于 12 个语言家族的不同语言,并包括需要在各种句法或语义水平上进行推理的 9 个不同任务。然而,将模型扩展到覆盖超过 7000 种世界语言仍然具有挑战性,并存在语言覆盖和模型能力之间的权衡。有关此事的更多详细信息,请查看以下链接:sites.research.google/xtreme
。
XGLUE 基准测试
XGLUE是另一个跨语言基准测试,用于评估和改进自然语言理解(NLU)和自然语言生成(NLG)的跨语言预训练模型的性能。它最初由 19 种语言的 11 项任务组成。与 XTREME 的主要区别在于,每项任务的训练数据仅在英语中可用。这迫使语言模型仅从英语文本数据中学习,并将这些知识传递给其他语言,这被称为零样本跨语言传输能力。第二个区别是它同时具有 NLU 和 NLG 任务。有关此事的更多详细信息,请查看以下链接:microsoft.github.io/XGLUE/
。
SQuAD 基准测试
SQuAD是 NLP 领域中广泛使用的 QA 数据集。它提供了一组 QA 对,用于评估 NLP 模型的阅读理解能力。它包含一系列问题、一个阅读段落和由众包工作者在一系列维基百科文章上注释的答案。问题的答案是阅读段落中的一段文本。最初的版本,SQuAD1.1,在数据集收集时没有一个无法回答的选项,因此每个问题都有一个在阅读段落中某处可以找到的答案。即使这看起来不可能,NLP 模型也被强制回答问题。SQuAD2.0 是一个改进版本,其中 NLP 模型不仅在可能时必须回答问题,而且在不可能回答时也必须弃权。SQuAD2.0 包含由众包工作者以与可回答问题相似的方式编写的 50,000 个无法回答的问题。此外,它还有来自 SQuAD1.1 的 100,000 个问题。
使用应用程序编程接口访问数据集
datasets
库通过 Hugging Face hub 为加载、处理和共享数据集提供了非常高效的实用工具。与 TensorFlow 数据集一样,它使直接从原始数据集主机下载、缓存和动态加载集合变得更加容易。该库还提供了与数据一起的评估指标。事实上,hub 不持有或分发数据集。相反,它保留了关于数据集的所有信息,包括所有者、预处理脚本、描述和下载链接。我们需要检查我们是否有权限使用相应许可证下的数据集。要查看其他功能,请查看相应数据集在 GitHub 存储库下的dataset_infos.json
和DataSet-Name.py
文件,网址为github.com/huggingface/datasets/tree/master/datasets
。
让我们从安装dataset
库开始,如下所示:
pip install datasets
以下代码自动使用 Hugging Face hub 加载cola
数据集。datasets.load_dataset()
函数如果数据尚未缓存,则从实际路径下载加载脚本:
from datasets import load_dataset
cola = load_dataset('glue', 'cola')
cola['train'][25:28]
重要提示
数据集的可重用性:当您多次运行代码时,datasets
库会开始缓存您的加载和操作请求。它首先存储数据集,并开始缓存您在数据集上的操作,例如拆分、选择和排序。您会看到一条警告消息,例如reusing dataset xtreme (/home/savas/.cache/huggingface/dataset…)或loading cached sorted…。
在前面的示例中,我们从 GLUE 基准测试中下载了cola
数据集,并从其中的train
拆分中选择了一些示例。
目前,有 661 个 NLP 数据集和 21 个度量标准用于各种任务,如以下代码片段所示:
from pprint import pprint
from datasets import list_datasets, list_metrics
all_d = list_datasets()
metrics = list_metrics()
print(f"{len(all_d)} datasets and {len(metrics)} metrics exist in the hub\n")
pprint(all_d[:20], compact=True)
pprint(metrics, compact=True)
这是输出结果:
661 datasets and 21 metrics exist in the hub.
['acronym_identification', 'ade_corpus_v2', 'adversarial_qa', 'aeslc', 'afrikaans_ner_corpus', 'ag_news', 'ai2_arc', 'air_dialogue', 'ajgt_twitter_ar', 'allegro_reviews', 'allocine', 'alt', 'amazon_polarity', 'amazon_reviews_multi', 'amazon_us_reviews', 'ambig_qa', 'amttl', 'anli', 'app_reviews', 'aqua_rat']
['accuracy', 'BERTscore', 'bleu', 'bleurt', 'comet', 'coval', 'f1', 'gleu', 'glue', 'indic_glue', 'meteor', 'precision', 'recall', 'rouge', 'sacrebleu', 'sari', 'seqeval', 'squad', 'squad_v2', 'wer', 'xnli']
数据集可能具有多个配置。例如,作为一个聚合基准的 GLUE 有许多子集,如前面提到的 CoLA、SST-2 和 MRPC。要访问每个 GLUE 基准数据集,我们传递两个参数,第一个是glue
,第二个是其示例数据集(cola
或sst2
)中的特定数据集。同样,维基百科数据集提供了几种语言的几种配置。
数据集带有DatasetDict
对象,包括多个Dataset
实例。当使用拆分选择(split='...')
时,我们会得到Dataset
实例。例如,CoLA
数据集带有DatasetDict
,其中包含三个拆分:train、validation和test。虽然训练和验证数据集包括两个标签(1
表示可接受,0
表示不可接受),但测试拆分的标签值为-1
,表示无标签。
让我们看一下CoLA
数据集对象的结构,如下所示:
>>> cola = load_dataset('glue', 'cola')
>>> cola
DatasetDict({
train: Dataset({
features: ['sentence', 'label', 'idx'],
num_rows: 8551 })
validation: Dataset({
features: ['sentence', 'label', 'idx'],
num_rows: 1043 })
test: Dataset({
features: ['sentence', 'label', 'idx'],
num_rows: 1063 })
})
cola['train'][12]
{'idx': 12, 'label':1,'sentence':'Bill rolled out of the room.'}
>>> cola['validation'][68]
{'idx': 68, 'label': 0, 'sentence': 'Which report that John was incompetent did he submit?'}
>>> cola['test'][20]
{'idx': 20, 'label': -1, 'sentence': 'Has John seen Mary?'}
数据集对象具有一些额外的元数据信息,这可能对我们有所帮助:split
、description
、citation
、homepage
、license
和info
。让我们运行以下代码:
>>> print("1#",cola["train"].description)
>>> print("2#",cola["train"].citation)
>>> print("3#",cola["train"].homepage)
1# GLUE, the General Language Understanding Evaluation benchmark(https://gluebenchmark.com/) is a collection of resources for training,evaluating, and analyzing natural language understanding systems.2# @article{warstadt2018neural, title={Neural Network Acceptability Judgments}, author={Warstadt, Alex and Singh, Amanpreet and Bowman, Samuel R}, journal={arXiv preprint arXiv:1805.12471}, year={2018}}@inproceedings{wang2019glue, title={{GLUE}: A Multi-Task Benchmark and Analysis Platform for Natural Language Understanding}, author={Wang, Alex and Singh, Amanpreet and Michael, Julian and Hill, Felix and Levy, Omer and Bowman, Samuel R.}, note={In the Proceedings of ICLR.}, year={2019}}3# https://nyu-mll.github.io/CoLA/
GLUE 基准提供了许多数据集,如前所述。让我们下载 MRPC 数据集,如下所示:
>>> mrpc = load_dataset('glue', 'mrpc')
类似地,要访问其他 GLUE 任务,我们将更改第二个参数,如下所示:
>>> load_dataset('glue', 'XYZ')
为了对数据可用性进行合理性检查,运行以下代码片段:
>>> glue=['cola', 'sst2', 'mrpc', 'qqp', 'stsb', 'mnli',
'mnli_mismatched', 'mnli_matched', 'qnli', 'rte',
'wnli', 'ax']
>>> for g in glue:
_=load_dataset('glue', g)
XTREME(使用跨语言数据集)是另一个我们已经讨论过的流行跨语言数据集。让我们从 XTREME 集中选择MLQA
示例。MLQA 是 XTREME 基准的子集,专为评估跨语言问答模型的性能而设计。它包括约 5,000 个基于 SQuAD 格式的抽取式问答实例,涵盖七种语言,即英语、德语、阿拉伯语、印地语、越南语、西班牙语和简体中文。
例如,MLQA.en.de
是一个英德问答示例数据集,可以按如下方式加载:
>>> en_de = load_dataset('xtreme', 'MLQA.en.de')
>>> en_de \
DatasetDict({
test: Dataset({features: ['id', 'title', 'context', 'question', 'answers'], num_rows: 4517
}) validation: Dataset({ features: ['id', 'title', 'context', 'question', 'answers'], num_rows: 512})})
将其视为 pandas DataFrame 可能更方便,如下所示:
>>> import pandas as pd
>>> pd.DataFrame(en_de['test'][0:4])
以下是前述代码的输出:
图 2.15 – 英德跨语言问答数据集
使用 datasets 库进行数据操作
数据集带有许多子集的字典,其中split
参数用于决定要加载哪些子集或子集的哪一部分。如果默认情况下为none
,它将返回所有子集(train
、test
、validation
或任何其他组合)的数据集字典。如果指定了split
参数,它将返回单个数据集而不是字典。对于以下示例,我们只检索cola
数据集的train
子集:
>>> cola_train = load_dataset('glue', 'cola', split ='train')
我们可以得到一个train
和validation
子集的混合,如下所示:
>>> cola_sel = load_dataset('glue', 'cola', split = 'train[:300]+validation[-30:]')
split
表达式意味着获取train
的前 300 个示例和validation
的最后 30 个示例,结果为cola_sel
。
我们可以应用不同的组合,如下所示的拆分示例:
-
如下所示是来自
train
和validation
的前 100 个示例:split='train[:100]+validation[:100]'
-
train
的 50%和validation
的最后 30%,如下所示:split='train[:50%]+validation[-30%:]'
-
train
的前 20%和从validation
的切片 [30:50] 中的示例,如下所示:split='train[:20%]+validation[30:50]'
排序、索引和洗牌
以下执行调用 cola_sel
对象的 sort()
函数。我们看到前 15 个和最后 15 个标签:
>>> cola_sel.sort('label')['label'][:15]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> cola_sel.sort('label')['label'][-15:]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
我们已经熟悉 Python 的切片表示法。同样,我们也可以使用类似的切片表示法或索引列表来访问多行,如下所示:
>>> cola_sel[6,19,44]
{'idx': [6, 19, 44],
'label': [1, 1, 1],
'sentence':['Fred watered the plants flat.',
'The professor talked us into a stupor.',
'The trolley rumbled through the tunnel.']}
我们按以下方式洗牌数据集:
>>> cola_sel.shuffle(seed=42)[2:5]
{'idx': [159, 1022, 46],
'label': [1, 0, 1],
'sentence': ['Mary gets depressed if she listens to the Grateful Dead.',
'It was believed to be illegal by them to do that.',
'The bullets whistled past the house.']}
重要提示
种子值:在洗牌时,我们需要传递一个种子值来控制随机性,实现作者和读者之间的一致输出。
缓存和可重用性
使用缓存文件可以通过内存映射(如果数据集适合驱动器)使用快速后端加载大型数据集。这种智能缓存有助于保存和重用在驱动器上执行的操作结果。要查看关于数据集的缓存日志,运行以下代码:
>>> cola_sel.cache_files
[{'filename': '/home/savas/.cache/huggingface...,'skip': 0, 'take': 300}, {'filename': '/home/savas/.cache/huggingface...','skip': 1013, 'take': 30}]
数据集过滤和映射函数
我们可能想要处理数据集的特定选择。例如,我们可以仅检索具有 cola
数据集中包含术语 kick
的句子,如下面的执行所示。datasets.Dataset.filter()
函数返回包含 kick
的句子,其中应用了匿名函数和 lambda
关键字:
>>> cola_sel = load_dataset('glue', 'cola', split='train[:100%]+validation[-30%:]')
>>> cola_sel.filter(lambda s: "kick" in s['sentence'])["sentence"][:3]
['Jill kicked the ball from home plate to third base.', 'Fred kicked the ball under the porch.', 'Fred kicked the ball behind the tree.']
以下过滤用于从集中获取正面(可接受的)示例:
>>> cola_sel.filter(lambda s: s['label']== 1 )["sentence"][:3]
["Our friends won't buy this analysis, let alone the next one we propose.",
"One more pseudo generalization and I'm giving up.",
"One more pseudo generalization or I'm giving up."]
在某些情况下,我们可能不知道类标签的整数代码。假设我们有许多类,而 culture
类的代码难以记住在 10 个类中。我们可以在我们之前的示例中传递一个 acceptable
标签给 str2int()
函数,代替在我们之前的示例中给出整数代码 1
,即 acceptable
的代码,如下所示:
>>> cola_sel.filter(lambda s: s['label']== cola_sel.features['label'].str2int('acceptable'))["sentence"][:3]
这产生与之前执行相同的输出。
用映射函数处理数据
datasets.Dataset.map()
函数在数据集上迭代,对集合中的每个示例应用处理函数,并修改示例的内容。以下执行显示添加一个新的 'len'
特征,表示句子的长度:
>>> cola_new=cola_sel.map(lambda e:{'len': len(e['sentence'])})
>>> pd.DataFrame(cola_new[0:3])
这是前面代码片段的输出:
图 2.16 – 带有附加列的 Cola 数据集
作为另一个示例,以下代码片段在 20 个字符后剪切句子。我们不创建新特征,而是更新句子特性的内容,如下所示:
>>> cola_cut=cola_new.map(lambda e: {'sentence': e['sentence'][:20]+ '_'})
输出如下所示:
图 2.17 – 带有更新的 Cola 数据集
使用本地文件工作
要从本地文件加载数据集(在csv
、text
或json
中),以及加载脚本load_dataset()
到通用加载脚本。如下代码片段所示,在../data/
文件夹中,有三个 CSV 文件(a.csv
、b.csv
和c.csv
),这些文件是从 SST-2 数据集中随机选择的玩具示例。我们可以加载单个文件,如data1
对象所示,合并多个文件,如data2
对象所示,或进行数据集分割,如data3
所示:
from datasets import load_dataset
data1 = load_dataset('csv', data_files='../data/a.csv', delimiter="\t")
data2 = load_dataset('csv', data_files=['../data/a.csv','../data/b.csv', '../data/c.csv'], delimiter="\t")
data3 = load_dataset('csv', data_files={'train':['../data/a.csv','../data/b.csv'], 'test':['../data/c.csv']}, delimiter="\t")
为了以其他格式获取文件,我们传递json
或text
而不是csv
,如下所示:
>>> data_json = load_dataset('json', data_files='a.json')
>>> data_text = load_dataset('text', data_files='a.txt')
到目前为止,我们已经讨论了如何加载、处理和操作数据集,这些数据集要么已经托管在 Hub 上,要么在我们的本地驱动器上。现在,我们将研究如何为 Transformer 模型训练准备数据集。
准备数据集以进行模型训练
让我们从标记化过程开始吧。每个模型都有自己的标记化模型,在实际的语言模型之前进行了训练。我们将在下一章节详细讨论这个问题。为了使用标记器,我们应该已经安装了Transformer
库。下面的示例从预训练的distilBERT-base-uncased
模型加载了标记器模型。我们使用map
和带有lambda
的匿名函数将标记器应用于data3
中的每个拆分。如果在map
函数中选择了batched
为True
,它会将一批例子传递给tokenizer
函数。batch_size
值默认为1000
,这是传递给函数的每批例子的数量。如果没有选择,则整个数据集作为单个批次传递。代码可以在这里看到:
from Transformer import DistilBERTTokenizer
tokenizer = \ DistilBERTTokenizer.from_pretrained('distilBERT-base-uncased')
encoded_data3 = data3.map(lambda e: tokenizer( e['sentence'], padding=True, truncation=True, max_length=12), batched=True, batch_size=1000)
如下输出所示,我们看到了data3
和encoded_data3
之间的区别,这里添加了两个额外特征——attention_mask
和input_ids
——并相应地添加到了数据集中。我们已经在本章的前面部分介绍了这两个特征。简而言之,input_ids
是与句子中每个标记对应的索引。这些特征是 Transformer 的Trainer
类需要的特征,我们将在接下来的微调章节中讨论。
我们通常一次传递多个句子(称为max_length
参数,在这个玩具示例中为12
)。我们还截断较长的句子以符合最大长度。代码可以在下面的代码片段中看到:
>>> data3
DatasetDict({
train: Dataset({
features: ['sentence','label'], num_rows: 199 })
test: Dataset({
features: ['sentence','label'], num_rows: 100 })})
>>> encoded_data3
DatasetDict({
train: Dataset({
features: ['attention_mask', 'input_ids', 'label', 'sentence'],
num_rows: 199 })
test: Dataset({
features: ['attention_mask', 'input_ids', 'label', 'sentence'],
num_rows: 100 })})
>>> pprint(encoded_data3['test'][12])
{'attention_mask': [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], 'input_ids': [101, 2019, 5186, 16010, 2143, 1012, 102, 0, 0, 0, 0, 0], 'label': 0, 'sentence': 'an extremely unpleasant film . '}
我们已经完成了datasets
库的使用。到目前为止,我们已经评估了数据集的所有方面。我们涵盖了类似于 GLUE 的基准测试,在这里,我们考虑了分类指标。在接下来的部分中,我们将专注于如何对速度和内存的计算性能进行基准测试,而不是分类。
速度和内存基准测试
仅仅比较大型模型在特定任务或基准上的分类性能已经不再足够。现在,我们必须关注特定环境对于特定模型的计算成本(Transformer
库,PyTorchBenchmark
和TensorFlowBenchmark
使得可以为 TensorFlow 和 PyTorch 的模型进行基准测试。
在我们开始实验之前,我们需要检查我们的 GPU 能力,采用以下执行:
>>> import torch
>>> print(f"The GPU total memory is {torch.cuda.get_device_properties(0).total_memory /(1024**3)} GB")
The GPU total memory is 2.94921875 GB
输出是从 NVIDIA GeForce GTX 1050 (3 Transformer
库当前仅支持单设备基准测试。当我们在 GPU 上进行基准测试时,我们需要指示 Python 代码将在哪个 GPU 设备上运行,这是通过设置CUDA_VISIBLE_DEVICES
环境变量来完成的。例如,export CUDA_VISIBLE_DEVICES=0
。 0
表示将使用第一个cuda
设备。
在接下来的代码示例中,我们探索了两个网格。我们比较了四个随机选择的预训练 BERT 模型,如models
数组中所列。要观察的第二个参数是sequence_lengths
。我们将批量大小保持为4
。如果您有更好的 GPU 容量,可以将批量值扩展到 4-64 的范围以及其他参数的搜索空间:
from Transformer import PyTorchBenchmark, PyTorchBenchmarkArguments
models= ["BERT-base-uncased","distilBERT-base-uncased","distilroBERTa-base", "distilBERT-base-german-cased"]
batch_sizes=[4]
sequence_lengths=[32,64, 128, 256,512]
args = PyTorchBenchmarkArguments(models=models, batch_sizes=batch_sizes, sequence_lengths=sequence_lengths, multi_process=False)
benchmark = PyTorchBenchmark(args)
重要注意事项
TensorFlow 的基准测试:本部分的代码示例是用于 PyTorch 基准测试的。对于 TensorFlow 的基准测试,我们简单地使用TensorFlowBenchmarkArguments
和TensorFlowBenchmark
相应的类。
我们已准备通过运行以下代码进行基准测试实验:
>>> results = benchmark.run()
这可能会花费一些时间,这取决于您的 CPU/GPU 容量和参数选择。如果遇到内存不足的问题,您应该采取以下措施来解决问题:
-
重新启动您的内核或操作系统。
-
在开始之前删除内存中所有不必要的对象。
-
设置较低的批处理大小,例如 2,甚至 1。
以下输出显示了推理速度性能。由于我们的搜索空间有四种不同的模型和五种不同的序列长度,我们在结果中看到了 20 行:
图 2.18 – 推理速度性能
同样,我们将看到 20 种不同情景的推理内存使用情况,如下所示:
图 2.19 – 推理内存使用情况
为了观察跨参数的内存使用情况,我们将使用存储统计数据的results
对象进行绘制。以下执行将绘制模型和序列长度的推理时间性能:
import matplotlib.pyplot as plt
plt.figure(figsize=(8,8))
t=sequence_lengths
models_perf=[list(results.time_inference_result[m]['result'][batch_sizes[0]].values()) for m in models]
plt.xlabel('Seq Length')
plt.ylabel('Time in Second')
plt.title('Inference Speed Result')
plt.plot(t, models_perf[0], 'rs--', t, models_perf[1], 'g--.', t, models_perf[2], 'b--^', t, models_perf[3], 'c--o')
plt.legend(models)
plt.show()
如下截图所示,两个 DistillBERT 模型表现出近似的结果,并且比其他两个模型表现更好。BERT-based-uncased
模型在特别是序列长度增加时表现不佳:
图 2.20 – 推理速度结果
要绘制内存性能,请使用results
对象的memory_inference_result
结果,而不是前述代码中显示的time_inference_result
。
欲了解更多有趣的基准测试示例,请查看以下链接:
现在我们完成了本节,成功地完成了本章。恭喜您完成安装,运行第一个hello-world
变换器程序,使用datasets
库并进行基准测试!
总结
在本章中,我们涵盖了各种入门主题,并且也亲手操作了hello-world
变换器应用程序。另一方面,这一章在将迄今所学应用于即将到来的章节方面起着关键作用。那么,到目前为止学到了什么呢?我们迈出了第一小步,设置了环境和系统安装。在这个背景下,anaconda
软件包管理器帮助我们安装了主要操作系统所需的模块。我们还介绍了语言模型、社区提供的模型和分词过程。此外,我们介绍了多任务(GLUE)和跨语言基准测试(XTREME),这使得这些语言模型变得更强大和更准确。我们介绍了datasets
库,该库为社区提供的 NLP 数据集提供了高效的访问方式。最后,我们学会了如何评估特定模型在内存使用和速度方面的计算成本。变换器框架使得可以为 TensorFlow 和 PyTorch 的模型进行基准测试。
本节中使用的模型已由社区预先训练并与我们共享。现在,轮到我们训练语言模型并将其传播给社区了。
在下一章中,我们将学习如何训练 BERT 语言模型以及分词器,并了解如何与社区共享它们。
第二部分:变换模型-从自编码到自回归模型
在本节中,您将了解自编码模型(如 BERT)和自回归模型(如 GPT)的架构。您将学习如何为各种自然语言理解和生成问题训练、测试和微调模型。您还将学习如何与社区共享模型,以及如何微调社区共享的其他预训练语言模型。
本节包括以下章节:
-
第三章, 自编码语言模型
-
第四章, 自回归和其他语言模型
-
第五章, 文本分类的语言模型微调
-
第六章, 标记分类的语言模型微调
-
第七章, 文本表示
第三章:自动编码语言模型
在上一章中,我们查看并研究了如何使用 HuggingFace 的 Transformers 的典型 Transformer 模型。到目前为止,所有主题都包括如何使用预定义或预构建模型,而对于特定模型及其训练的信息较少。
在本章中,我们将了解如何从头开始在任何给定语言上训练自动编码语言模型。这种训练将包括模型的预训练和任务特定训练。首先,我们将从 BERT 模型的基本知识和其工作原理开始。然后,我们将使用一个简单且小型的语料库来训练语言模型。之后,我们将看看如何将该模型用于任何 Keras 模型内。
为了了解本章将学到的内容,我们将讨论以下主题:
-
BERT——其中之一自动编码语言模型
-
任何语言的自动编码语言模型训练
-
与社区共享模型
-
了解其他自动编码模型
-
使用标记化算法工作
技术要求
本章的技术要求如下:
-
Anaconda
-
Transformers >= 4.0.0
-
PyTorch >= 1.0.2
-
TensorFlow >= 2.4.0
-
数据集 >= 1.4.1
-
标记器
请还要检查第三章
对应的 GitHub 代码:
github.com/PacktPublishing/Advanced-Natural-Language-Processing-with-Transformers/tree/main/CH03
.
查看以下链接以查看代码实战视频:bit.ly/3i1ycdY
BERT——其中之一自动编码语言模型
来自变换器的双向编码器表示,也被称为BERT,是最早使用编码器 Transformer 堆栈的自动编码语言模型之一,稍作修改用于语言建模。
BERT 架构是基于 Transformer 原始实现的多层 Transformer 编码器。Transformer 模型本身最初用于机器翻译任务,但 BERT 所做的主要改进是利用该体系结构的这一部分来提供更好的语言建模。这种语言模型在预训练之后,能够提供对其训练语言的全局理解。
BERT 语言模型预训练任务
要清楚了解 BERT 所使用的遮罩语言建模,让我们更详细地定义它。遮罩语言建模是训练模型的任务,输入是一句话,其中有一些遮罩标记,输出是填满遮罩标记的完整句子。但是这样做为什么能帮助模型在分类等下游任务中获得更好的结果呢?答案很简单:如果模型能够完成完形填空测试(一种通过填写空白来评估语言理解能力的语言测试),那么它就对语言本身有了一般的理解。对于其他任务,它已经进行了预训练(通过语言建模),并且将表现更好。
这是一道完形填空的例子:
乔治·华盛顿是 ___ 州的第一任总统。
预期 United 应该填入空白处。对于遮罩语言模型,应用了同样的任务,需要填补遮罩标记。不过,遮罩标记是从一句话中随机选择的。
BERT 受训的另一个任务是下一句预测(NSP)。这个预训练任务确保 BERT 不仅学习了预测遮罩标记中所有令牌之间的关系,还帮助其理解两个句子之间的关系。会选择一对句子,并在它们之间放上一个*[SEP]* 分隔符令牌。数据集中还知道第二个句子是在第一个句子之后还是之前。
以下是 NSP 的示例:
读者需要填写空白。比特币价格相比其他替代币高得太多了。
在这个例子中,模型需要预测为否定(这两个句子之间没有关联)。
这两种预训练任务使 BERT 能够对语言本身有所了解。BERT 令牌嵌入为每个令牌提供上下文嵌入。上下文嵌入意味着每个令牌的嵌入与周围令牌完全相关。与 Word2Vec 和其他模型不同,BERT 为每个令牌嵌入提供更好的信息。另一方面,NSP 任务使 BERT 能够为*[CLS]* 令牌提供更好的嵌入。正如在第一章中讨论的那样,此令牌提供关于整个输入的信息。[CLS] 用于分类任务,并且在预训练部分学习整个输入的总体嵌入。下图显示了 BERT 模型的整体外观。图 3.1 显示了 BERT 模型的相应输入和输出:
图 3.1 – BERT 模型
让我们继续下一部分!
深入了解 BERT 语言模型
标记器是许多 NLP 应用程序中各自流水线中最重要的部分之一。 对于 BERT,使用的是 WordPiece 标记。 通常,WordPiece,SentencePiece和BytePairEncoding(BPE)是最广为人知的三种标记器,由不同的基于 Transformer 的架构使用,也将在接下来的部分中介绍。 BERT 或任何其他基于 Transformer 的架构使用子词标记化的主要原因是这些标记器处理未知标记的能力。
BERT 还使用位置编码来确保将标记的位置提供给模型。如果您还记得章节 1,从词袋模型到 Transformer,BERT 和类似的模型使用非顺序操作,如密集神经层。 传统模型,如基于 LSTM 和 RNN 的模型,通过序列中标记的顺序获得位置。 为了为 BERT 提供这些额外信息,位置编码非常有用。
BERT 的预训练(如自动编码模型)为模型提供了语言信息,但在实践中,当处理不同的问题,如序列分类,标记分类或问题回答时,会使用模型输出的不同部分。
例如,在序列分类任务(如情感分析或句子分类)的情况下,原始 BERT 文章提出了必须使用最后一层的*[CLS]嵌入。然而,还有其他研究使用 BERT 进行分类,使用不同的技术(使用所有标记的平均标记嵌入,在最后一层部署 LSTM,甚至在最后一层之上使用 CNN)。 序列分类的最后一个[CLS]*嵌入可以被任何分类器使用,但提出的,也是最常见的方法是使用具有输入大小等于最终标记嵌入大小和输出大小等于类数量的 softmax 激活函数的密集层。 当输出可能是多标签并且问题本身是多标签分类问题时,使用 sigmoid 也是另一种选择。
为了给您更详细的关于 BERT 如何实际工作的信息,以下说明显示了一个 NSP 任务的示例。请注意,这里对标记化进行了简化,以便更好地理解:
图 3.2 - 用于 NSP 任务的 BERT 示例
BERT 模型有不同的变体,具有不同的设置。例如,输入大小是可变的。在前面的示例中,它被设置为512,而模型可以接受的最大序列大小是512。但是,这个大小包括特殊标记*[CLS]和[SEP],因此它会被缩减为510*。另一方面,使用 WordPiece 作为标记器会产生子词标记,作为序列输入之前可以有较少的词,标记化之后,大小会增加,因为标记器会将词分解为子词,如果在预训练语料库中没有看到它们常见。
以下图显示了 BERT 用于不同任务的示例。对于 NER 任务,使用每个令牌的输出,而不是*[CLS]。在问答情景中,使用[SEP]分隔符令牌将问题和答案连接起来,然后使用最后一层的Start/End和Span输出标记答案。在这种情况下,Paragraph是Question所询问的Context*:
图 3.3 – 用于各种 NLP 任务的 BERT 模型
不管这些任务如何,BERT 最重要的能力是对文本的上下文表示。它成功的原因在于 Transformer 编码器架构,它以密集向量的形式表示输入。这些向量可以通过非常简单的分类器轻松转换为输出。
到目前为止,您已经了解了 BERT 以及它的工作原理。您已经详细了解了 BERT 可以用于的各种任务的重要信息以及这种架构的重要要点。
在下一节中,您将学习如何预先训练 BERT,并在训练后使用它。
任何语言的自编码语言模型训练
我们已经讨论了 BERT 的工作原理以及可以使用 HuggingFace 库提供的预训练版本。在本节中,您将学习如何使用 HuggingFace 库来训练您自己的 BERT。
在开始之前,有一个很重要的问题,那就是需要有良好的训练数据,这将用于语言建模。这些数据称为语料库,通常是一大堆数据(有时经过预处理和清理)。这些无标签的语料库必须适合您希望训练语言模型的用例;例如,如果您尝试为英语单独创建一个特殊的 BERT。尽管有成千上万的巨大优秀数据集,比如 Common Crawl(commoncrawl.org/
),我们更倾向于一个小一点的数据集,以便更快地训练。
50K 电影评论的 IMDB 数据集(可在www.kaggle.com/lakshmi25npathi/imdb-dataset-of-50k-movie-reviews
找到)是一个用于情感分析的大型数据集,但如果您将其用作语料库来训练语言模型,则算是小型的:
-
你可以使用以下代码轻松下载并保存为
.txt
格式,用于语言模型和分词器训练:import pandas as pd imdb_df = pd.read_csv("IMDB Dataset.csv") reviews = imdb_df.review.to_string(index=None) with open("corpus.txt", "w") as f: f.writelines(reviews)
-
在准备语料库之后,必须训练分词器。
tokenizers
库提供了快速简单的 WordPiece 分词器训练。为了在你的语料库上训练它,需要运行以下代码:>>> from tokenizers import BertWordPieceTokenizer >>> bert_wordpiece_tokenizer =BertWordPieceTokenizer() >>> bert_wordpiece_tokenizer.train("corpus.txt")
-
这将训练分词器。你可以通过使用训练好的
tokenizer
对象的get_vocab()
函数来访问训练好的词汇表。你可以通过以下代码获取词汇表:>>> bert_wordpiece_tokenizer.get_vocab()
以下是输出:
{'almod': 9111, 'events': 3710, 'bogart': 7647, 'slapstick': 9541, 'terrorist': 16811, 'patter': 9269, '183': 16482, '##cul': 14292, 'sophie': 13109, 'thinki': 10265, 'tarnish': 16310, '##outh': 14729, 'peckinpah': 17156, 'gw': 6157, '##cat': 14290, '##eing': 14256, 'successfully': 12747, 'roomm': 7363, 'stalwart': 13347,...}
-
保存分词器以供以后使用是必不可少的。使用对象的
save_model()
函数并提供目录将保存分词器词汇表供以后使用:>>> bert_wordpiece_tokenizer.save_model("tokenizer")
-
你可以使用
from_file()
函数重新加载它:>>> tokenizer = \ BertWordPieceTokenizer.from_file("tokenizer/vocab.txt")
-
你可以按照以下示例使用分词器:
>>> tokenized_sentence = \ tokenizer.encode("Oh it works just fine") >>> tokenized_sentence.tokens ['[CLS]', 'oh', 'it', 'works', 'just', 'fine','[SEP]']
特殊的标记
[CLS]
和[SEP]
将自动添加到标记列表中,因为 BERT 需要它们来处理输入。 -
让我们尝试使用我们的分词器来另一个句子:
>>> tokenized_sentence = \ tokenizer.encode("ohoh i thougt it might be workingg well") ['[CLS]', 'oh', '##o', '##h', 'i', 'thoug', '##t', 'it', 'might', 'be', 'working', '##g', 'well', '[SEP]']
-
对于嘈杂和拼写错误的文本,似乎是一个很好的分词器。现在你已经准备好并保存了你的分词器,你可以训练你自己的 BERT。第一步是使用
Transformers
库中的BertTokenizerFast
。你需要使用以下命令加载上一步训练好的分词器:>>> from Transformers import BertTokenizerFast >>> tokenizer = \ BertTokenizerFast.from_pretrained("tokenizer")
我们使用了
BertTokenizerFast
,因为它是由 HuggingFace 文档建议使用的。还有BertTokenizer
,根据库文档中的定义,它没有实现快速版本那么快。在大多数预训练模型的文档和卡片中,强烈建议使用BertTokenizerFast
版本。 -
下一步是通过以下命令准备语料库以加快训练速度:
>>> from Transformers import LineByLineTextDataset >>> dataset = \ LineByLineTextDataset(tokenizer=tokenizer, file_path="corpus.txt", block_size=128)
-
并且需要为掩码语言建模提供数据收集器:
>>> from Transformers import DataCollatorForLanguageModeling >>> data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15)
数据收集器获取数据并为训练准备好。例如,上面的数据收集器获取数据并准备好使用概率为
0.15
的掩码语言建模。使用这种机制的目的是在运行时进行预处理,这样可以使用更少的资源。另一方面,它会减慢训练过程,因为每个样本都必须在训练时动态进行预处理。 -
训练参数还为训练器在训练阶段提供信息,可以使用以下命令设置:
>>> from Transformers import TrainingArguments >>> training_args = TrainingArguments( output_dir="BERT", overwrite_output_dir=True, num_train_epochs=1, per_device_train_batch_size=128)
-
现在我们将创建 BERT 模型本身,我们将使用默认配置(注意力头数、Transformer 编码器层数等):
>>> from Transformers import BertConfig, BertForMaskedLM >>> bert = BertForMaskedLM(BertConfig())
-
最后一步是创建一个训练器对象:
>>> from Transformers import Trainer >>> trainer = Trainer(model=bert, args=training_args, data_collator=data_collator, train_dataset=dataset)
-
最后,你可以使用以下命令训练你的语言模型:
>>> trainer.train()
它会显示一个进度条,指示训练的进度:
图 3.4 - BERT 模型训练进度
在模型训练过程中,将使用名为
runs
的日志目录存储步骤检查点:图 3.5 – BERT 模型检查点
-
训练结束后,您可以使用以下命令轻松保存模型:
>>> trainer.save_model("MyBERT")
直到目前为止,您已经学会了如何训练您希望的任何特定语言的 BERT。您已经学会了如何训练标记器和 BERT 模型,使用您准备的语料库。
-
您提供的 BERT 默认配置是此训练过程中最关键的部分,它定义了 BERT 的架构和超参数。您可以使用以下代码查看这些参数:
>>> from Transformers import BertConfig >>> BertConfig()
输出如下:
图 3.6 – BERT 模型配置
如果您希望复制
max_position_embedding
、num_attention_heads
、num_hidden_layers
、intermediate_size
和hidden_size
,直接影响训练时间。将它们增加会显著增加大型语料库的训练时间。 -
例如,您可以使用以下代码轻松为小型 BERT 制作新配置以加快训练速度:
>>> tiny_bert_config = \ BertConfig(max_position_embeddings=512, hidden_size=128, num_attention_heads=2, num_hidden_layers=2, intermediate_size=512) >>> tiny_bert_config
以下是代码的结果:
图 3.8 – 小型 BERT 模型配置
-
使用相同的方法,我们可以使用这个配置制作一个微小的 BERT 模型:
>>> tiny_bert = BertForMaskedLM(tiny_bert_config)
-
并且使用相同的参数进行训练,您可以训练这个微小的新 BERT:
>>> trainer = Trainer(model=tiny_bert, args=training_args, data_collator=data_collator, train_dataset=dataset) >>> trainer.train()
输出如下:
图 3.9 – 小型 BERT 模型配置
显然,训练时间显著减少,但您应该意识到这是一个具有更少层和参数的微小版 BERT,不如 BERT Base 好。
到目前为止,您已经学会了如何从头开始训练自己的模型,但需要注意的是在处理用于训练语言模型的数据集或利用它执行特定任务的数据集时,使用
datasets
库是更好的选择。 -
BERT 语言模型也可以作为嵌入层与任何深度学习模型结合使用。例如,您可以加载任何预训练的 BERT 模型或您在上一步中训练过的自己的版本。以下代码显示了如何加载它以在 Keras 模型中使用:
>>> from Transformers import\ TFBertModel, BertTokenizerFast >>> bert = TFBertModel.from_pretrained( "bert-base-uncased") >>> tokenizer = BertTokenizerFast.from_pretrained( "bert-base-uncased")
-
但您不需要整个模型;相反,您可以使用以下代码访问层:
>>> bert.layers [<Transformers.models.bert.modeling_tf_bert.TFBertMainLayer at 0x7f72459b1110>]
-
如您所见,只有一个来自
TFBertMainLayer
的单层,您可以在 Keras 模型中访问它。但在使用之前,最好先测试它,看看它提供了什么样的输出:>>> tokenized_text = tokenizer.batch_encode_plus( ["hello how is it going with you", "lets test it"], return_tensors="tf", max_length=256, truncation=True, pad_to_max_length=True) >>> bert(tokenized_text)
输出如下:
图 3.10 – BERT 模型输出
如结果所示,有两个输出:一个是最后的隐藏状态,一个是 pooler 输出。最后的隐藏状态提供了来自 BERT 的所有标记嵌入,同时在开头和结尾分别加上了*[CLS]和[SEP]*标记。
-
现在你已经了解了 TensorFlow 版本的 BERT 更多信息,你可以使用这个新的嵌入创建一个 keras 模型:
from tensorflow import keras import tensorflow as tf max_length = 256 tokens = keras.layers.Input(shape=(max_length,), dtype=tf.dtypes.int32) masks = keras.layers.Input(shape=(max_length,), dtype=tf.dtypes.int32) embedding_layer = bert.layers0[0][:,0,:] dense = tf.keras.layers.Dense(units=2, activation="softmax")(embedding_layer) model = keras.Model([tokens,masks],dense)
-
模型对象,即一个 Keras 模型,有两个输入:一个用于标记,一个用于掩码。标记具有来自分词器输出的
token_ids
,掩码将具有attention_mask
。让我们试一试,看会发生什么:>>> tokenized = tokenizer.batch_encode_plus( ["hello how is it going with you", "hello how is it going with you"], return_tensors="tf", max_length= max_length, truncation=True, pad_to_max_length=True)
-
使用
tokenizer
时重要的是使用max_length
、truncation
和pad_to_max_length
这些参数。这些参数确保你通过将其填充到之前定义的 256 的最大长度来获得可用形状的输出。现在你可以使用这个样本运行模型了:>>>model([tokenized["input_ids"],tokenized["attention_mask"]])
以下是输出结果:
图 3.11 – BERT 模型分类输出
-
当训练模型时,你需要使用
compile
函数进行编译:>>> model.compile(optimizer="Adam", loss="categorical_crossentropy", metrics=["accuracy"]) >>> model.summary()
输出如下:
图 3.12 – BERT 模型摘要
-
从模型摘要中,你可以看到模型有 109,483,778 个可训练参数,包括 BERT。但如果你有预训练好的 BERT 模型,并且想要在特定任务的训练中冻结它,你可以使用以下命令:
>>> model.layers[2].trainable = False
据我们所知,嵌入层的层索引为 2,因此我们可以简单地冻结它。如果你重新运行 summary 函数,你会看到可训练参数减少到了 1,538,这是最后一层的参数个数:
图 3.13 – BERT 模型摘要,包括较少的可训练参数
-
如你所记得的,我们使用了 IMDB 情感分析数据集来训练语言模型。现在你可以用它来训练基于 Keras 的情感分析模型。但首先,你需要准备输入和输出:
import pandas as pd imdb_df = pd.read_csv("IMDB Dataset.csv") reviews = list(imdb_df.review) tokenized_reviews = \ tokenizer.batch_encode_plus(reviews, return_tensors="tf", max_length=max_length, truncation=True, pad_to_max_length=True) import numpy as np train_split = int(0.8 * \ len(tokenized_reviews["attention_mask"])) train_tokens = tokenized_reviews["input_ids"]\ [:train_split] test_tokens = tokenized_reviews["input_ids"][train_split:] train_masks = tokenized_reviews["attention_mask"]\ [:train_split] test_masks = tokenized_reviews["attention_mask"]\ [train_split:] sentiments = list(imdb_df.sentiment) labels = np.array([[0,1] if sentiment == "positive" else\ [1,0] for sentiment in sentiments]) train_labels = labels[:train_split] test_labels = labels[train_split:]
-
最后,你的数据准备好了,你可以拟合你的模型:
>>> model.fit([train_tokens,train_masks],train_labels, epochs=5)
在拟合模型之后,你的模型就准备好使用了。到目前为止,你已经学会了如何对分类任务进行模型训练。你已经学会了如何保存它,在下一节中,你将学会如何与社区分享训练好的模型。
与社区共享模型
HuggingFace 提供了一个非常方便的模型共享机制:
-
你可以简单地使用以下
cli
工具进行登录:Transformers-cli login
-
使用你自己的凭据登录后,你可以创建一个仓库:
Transformers-cli repo create a-fancy-model-name
-
你可以为
a-fancy-model-name
参数随意取任何模型名称,然后确保你安装了 git-lfs 是非常重要的:git lfs install
Git LFS 是一个用于处理大文件的 Git 扩展。HuggingFace 预训练模型通常是大文件,需要额外的库(如 LFS)来处理 Git。
-
然后,你可以克隆你刚刚创建的仓库:
git clone https://huggingface.co/username/a-fancy-model-name
-
随后,您可以随意向仓库中添加和删除,然后,就像使用 Git 一样,您必须运行以下命令:
git add . && git commit -m "Update from $USER" git push
自编码模型依赖于原始 Transformer 左编码器一侧,非常有效地解决分类问题。尽管 BERT 是自编码模型的典型示例,但文献中讨论了许多替代方案。让我们看一下这些重要的替代方案。
理解其他自编码模型
在这部分中,我们将回顾略微修改原始 BERT 的自编码模型替代方案。这些替代方案的重新实现通过利用许多来源(优化预训练过程和层或头的数量、改进数据质量、设计更好的目标函数等)导致了更好的下游任务。改进的来源大致分为两部分:更好的架构设计选择 和 预训练控制。
最近共享了许多有效的替代方案,因此不可能在这里理解和解释它们全部。我们可以看一些文献中引用最多的模型和 NLP 基准测试中使用最多的模型。让我们从 Albert 开始,作为对架构设计选择特别关注的 BERT 的重新实现。
引入 ALBERT
语言模型的性能被认为随着其规模的增大而提高。然而,由于内存限制和较长的训练时间,训练这些模型变得更加具有挑战性。为了解决这些问题,Google 团队提出了 Albert 模型(A Lite BERT 用于语言表示的自监督学习),这实际上是通过利用几种新技术对 BERT 架构进行重新实现,从而减少了内存消耗并增加了训练速度。新设计导致语言模型比原始 BERT 更好地扩展。与原始 BERT-large 模型相比,Albert 参数减少了 18 倍,训练速度提高了 1.7 倍。
Albert 模型主要由对原始 BERT 的三种修改组成:
-
因子化嵌入参数化
-
跨层参数共享
-
句间连贯性损失
前两种修改是与原始 BERT 中模型大小和内存消耗问题相关的参数减少方法。第三种对应于一个新的目标函数:句子顺序预测(SOP),取代了原始 BERT 的 下一句预测(NSP)任务,从而导致了一个更薄的模型和更好的性能。
使用分解嵌入参数化将大词汇嵌入矩阵分解为两个小矩阵,这两个矩阵将隐藏层的大小与词汇表的大小分开。这种分解将嵌入参数从 O(V × H) 减少到 O(V × E + E × H),其中 V 是 词汇表,H 是 隐藏层大小,E 是 嵌入,如果满足 H >> E,则可以更有效地利用总模型参数。
跨层参数共享可以防止网络加深时总参数数量的增加。这一技术被认为是提高参数效率的另一种方式,因为我们可以通过共享或复制来保持参数大小较小。在原始论文中,他们尝试了许多共享参数的方法,例如跨层仅共享 FF 参数、仅共享注意力参数或整个参数。
Albert 的另一个修改是跨句连贯性损失。正如我们已经讨论过的,BERT 架构利用了两种损失计算,即 Masked Language Modeling (MLM) 损失和 NSP。NSP 使用二进制交叉熵损失来预测原始文本中是否连续出现两个段落。负例通过从不同文档中选择两个段落获得。然而,Albert 团队批评 NSP 是一个主题检测问题,被认为是一个相对容易的问题。因此,团队提出了一个基于连贯性而不是主题预测的损失。他们利用了 SOP 损失,它主要关注建模句子间的连贯性而不是主题预测。SOP 损失使用与 BERT 相同的正例技术(即来自同一文档的两个连续段落),并将相同的两个连续段落作为负例,但交换它们的顺序。然后,模型被迫学习更细粒度的话语层面的连贯性特性之间的区别。
-
让我们使用
Transformers
库来比较原始的 BERT 和 Albert 配置。以下代码片段展示了如何配置一个 BERT-Base 初始模型。如您在输出中所见,参数数量约为 110 M:#BERT-BASE (L=12, H=768, A=12, Total Parameters=110M) >> from Transformers import BertConfig, BertModel >> bert_base= BertConfig() >> model = BertModel(bert_base) >> print(f"{model.num_parameters() /(10**6)}\ million parameters") 109.48224 million parameters
-
以下代码片段显示了如何使用
Transformers
库定义具有两个类AlbertConfig
和AlbertModel
的 Albert 模型:# Albert-base Configuration >>> from Transformers import AlbertConfig, AlbertModel >>> albert_base = AlbertConfig(hidden_size=768, num_attention_heads=12, intermediate_size=3072,) >>> model = AlbertModel(albert_base) >>> print(f"{model.num_parameters() /(10**6)}\ million parameters") 11.683584 million parameters
由此,默认 Albert 配置指向 Albert-xxlarge。我们需要设置隐藏大小、注意力头数和中间大小以适应 Albert-base。代码显示 Albert-base 模式为 11M,比 BERT-base 模型小 10 倍。ALBERT 的原始论文报告了以下表格中的基准性能:
图 3.14 – Albert 模型基准测试
-
从这一点开始,为了从头开始训练一个 Albert 语言模型,我们需要通过使用统一的 Transformers API 在前面的章节中已经说明过的类似阶段。在这里没有必要解释相同的步骤!相反,让我们加载一个已经训练好的 Albert 语言模型,如下所示:
from Transformers import AlbertTokenizer, AlbertModel tokenizer = \ AlbertTokenizer.from_pretrained("albert-base-v2") model = AlbertModel.from_pretrained("albert-base-v2") text = "The cat is so sad ." encoded_input = tokenizer(text, return_tensors='pt') output = model(**encoded_input)
-
上述代码片段从 HuggingFace hub 或我们的本地缓存目录下载 Albert 模型权重及其配置,如果已经缓存,则表示您已经调用了
AlbertTokenizer.from_pretrained()
函数。由于该模型对象是一个预训练语言模型,目前我们可以对该模型做的事情是有限的。我们需要在下游任务上对其进行训练,以便将其用于推理,这将是后续章节的主要主题。相反,我们可以利用其掩码语言模型目标,如下所示:from Transformers import pipeline fillmask= pipeline('fill-mask', model='albert-base-v2') pd.DataFrame(fillmask("The cat is so [MASK] ."))
以下是输出:
图 3.15 – albert-base-v2 的填充遮罩输出结果
fill-mask
管道使用 SoftMax()
函数计算每个词汇标记的分数,并对概率最高的标记进行排序,其中 cute
是获胜者,概率得分为 0.281。您可能注意到 token_str 列中的条目以 _
字符开头,这是由于 Albert 的分词器的 metaspace 组件造成的。
让我们来看看下一个备选项,RoBERTa,它主要侧重于预训练阶段。
RoBERTa
鲁棒优化的 BERT 预训练方法(RoBERTa)是另一种流行的 BERT 重新实现。它在训练策略上提供了更多的改进,而不是架构设计。它在 GLUE 上的几乎所有单个任务中都优于 BERT。动态遮罩是其原始设计选择之一。虽然对于某些任务来说,静态遮罩更好,但 RoBERTa 团队表明,动态遮罩可以在整体性能方面表现良好。让我们比较与 BERT 的变化并总结所有功能如下:
架构的变化如下:
-
移除下一句预测训练目标
-
动态地改变遮罩模式,而不是静态的遮罩,这是通过在将序列馈送到模型时生成遮罩模式来完成的
-
BPE 子词分词器
训练的变化如下:
-
控制训练数据:使用更多的数据,如 160 GB,而不是最初在 BERT 中使用的 16 GB。在研究中考虑到的不仅是数据的大小,还有质量和多样性。
-
较长的迭代次数,最多达到 500K 的预训练步骤。
-
更长的批量大小。
-
更长的序列,这导致更少的填充。
-
一个大的 50K BPE 词汇表,而不是 30K BPE 词汇表。
多亏了Transformers统一的 API,就像上面的 Albert 模型管道一样,我们将 RoBERTa 模型初始化如下:
>>> from Transformers import RobertaConfig, RobertaModel
>>> conf= RobertaConfig()
>>> model = RobertaModel(conf)
>>> print(f"{model.num_parameters() /(10**6)}\
million parameters")
109.48224 million parameters
为了加载预训练模型,我们执行以下代码片段:
from Transformers import RobertaTokenizer, RobertaModel
tokenizer = \
RobertaTokenizer.from_pretrained('roberta-base')
model = RobertaModel.from_pretrained('roberta-base')
text = "The cat is so sad ."
encoded_input = tokenizer(text, return_tensors='pt')
output = model(**encoded_input)
这些行说明了模型如何处理给定的文本。目前,最后一层的输出表示并不有用。正如我们已经多次提到的那样,我们需要对主要语言模型进行微调。以下执行使用 roberta-base
模型应用 fill-mask
函数:
>>> from Transformers import pipeline
>>> fillmask= pipeline("fill-mask ",model="roberta-base",
tokenizer=tokenizer)
>>> pd.DataFrame(fillmask("The cat is so <mask> ."))
以下是输出:
图 3.16 – roberta-base 的填充掩码任务结果
类似于之前的 ALBERT fill-mask
模型,这个流水线对适当的候选词进行排名。请忽略令牌中的前缀 Ġ
– 这是由字节级 BPE 分词器生成的编码空格字符,我们稍后会讨论。您应该已经注意到我们在 ALBERT 和 RoBERTa 流水线中使用了 [MASK]
和 <mask>
令牌,以便为掩码令牌留出位置。这是由于 tokenizer
的配置。要了解将使用哪个令牌表达式,您可以检查 tokenizer.mask_token
。请参阅以下执行:
>>> tokenizer = \
AlbertTokenizer.from_pretrained('albert-base-v2')
>>> print(tokenizer.mask_token)
[MASK]
>>> tokenizer = \
RobertaTokenizer.from_pretrained('roberta-base')
>>> print(tokenizer.mask_token)
<mask>
为了确保正确使用掩码令牌,我们可以将 fillmask.tokenizer.mask_token
表达式添加到管道中,如下所示:
fillmask(f"The cat is very\
{fillmask.tokenizer.mask_token}.")
ELECTRA
ELECTRA 模型(由 Kevin Clark 等人于 2020 年提出)专注于利用被替换的令牌检测训练目标的新掩码语言模型。在预训练期间,模型被迫学习区分真实输入令牌和合成生成的替换令牌,其中合成的负例是从可信令牌而不是随机抽样的令牌中抽样的。Albert 模型批评了 BERT 的 NSP 目标,认为它是一个主题检测问题,并且使用了低质量的负例。ELECTRA 训练两个神经网络,一个生成器和一个鉴别器,前者产生高质量的负例,而后者区分原始令牌和替换令牌。我们从计算机视觉领域知道了 GAN 网络,在这个网络中,生成器G产生假图像,并试图欺骗鉴别器D,而鉴别器网络试图避免被欺骗。ELECTRA 模型几乎应用了相同的生成器-鉴别器方法来用高质量的合成生成的可信替换替换原始令牌。
为了不重复与其他示例相同的代码,我们只提供了一个简单的 fill-mask
示例作为 Electra 生成器,如下所示:
fillmask = \
pipeline("fill-mask", model="google/electra-small-generator")
fillmask(f"The cat is very \{fillmask.tokenizer.mask_token} .")
您可以在以下链接中看到完整的模型列表:huggingface.co/Transformers/model_summary.html
。
模型检查点可以在 huggingface.co/models
找到。
干得好!我们终于完成了自动编码模型部分。现在我们将转向标记化算法,这对于 Transformers 的成功具有重要影响。
使用标记化算法工作
在本章的开头部分,我们使用了特定的分词器,即BertWordPieceTokenizer
来训练 BERT 模型。现在值得在这里详细讨论标记化过程。标记化是将文本输入分割成标记并在将其馈送到神经网络架构之前为每个标记分配一个标识符的一种方式。最直观的方法是根据空格将序列分割成较小的块。然而,这种方法不符合一些语言的要求,例如日语,并且也可能导致巨大的词汇问题。几乎所有的 Transformer 模型都利用子词标记化来降低维度,不仅编码训练中未见过的罕见(或未知)单词,而且还为每个单词分配一个唯一的标识符。标记化依赖于这样一个思想,即包括罕见单词或未知单词在内的每个单词都可以分解为在训练语料库中广泛出现的有意义的较小块。
一些传统的标记器是在 Moses 和nltk
库中开发的,应用了先进的基于规则的技术。但是与 Transformers 一起使用的标记化算法基于自监督学习,并从语料库中提取规则。基于规则的标记化的简单直观解决方案是使用字符、标点符号或空格。基于字符的标记化会导致语言模型丢失输入的含义。尽管它可以减少词汇量,这是好的,但它使模型难以通过字符c
、a
和t
的编码来捕获cat
的含义。此外,输入序列的维度变得非常大。同样,基于标点符号的模型不能正确处理一些表达,例如haven’t或ain’t。
最近,一些先进的子词标记化算法,如 BPE,已成为 Transformer 架构的一个组成部分。这些现代标记化过程由两个阶段组成:预标记化阶段只是使用空格或语言相关的规则将输入分割为标记。其次,标记化训练阶段是为了训练分词器并基于标记构建一个合理大小的基本词汇表。在训练我们自己的分词器之前,让我们加载一个预训练的分词器。以下代码加载了一个土耳其分词器,类型为BertTokenizerFast
,词汇量为 32K:
>>> from Transformers import AutoModel, AutoTokenizer
>>> tokenizerTUR = AutoTokenizer.from_pretrained(
"dbmdz/bert-base-turkish-uncased")
>>> print(f"VOC size is: {tokenizerTUR.vocab_size}")
>>> print(f"The model is: {type(tokenizerTUR)}")
VOC size is: 32000
The model is: Transformers.models.bert.tokenization_bert_fast.BertTokenizerFast
以下代码加载了一个英语 BERT 分词器,用于bert-base-uncased
模型:
>>> from Transformers import AutoModel, AutoTokenizer
>>> tokenizerEN = \
AutoTokenizer.from_pretrained("bert-base-uncased")
>>> print(f"VOC size is: {tokenizerEN.vocab_size}")
>>> print(f"The model is {type(tokenizerEN)}")
VOC size is: 30522
The model is ... BertTokenizerFast
让我们看看它们是如何工作的!我们使用这两个标记器对单词telecommunication
进行标记化:
>>> word_en="telecommunication"
>>> print(f"is in Turkish Model ? \
{word_en in tokenizerTUR.vocab}")
>>> print(f"is in English Model ? \
{word_en in tokenizerEN.vocab}")
is in Turkish Model ? False
is in English Model ? True
word_en
标记已经在英语分词器的词汇表中,但不在土耳其分词器的词汇表中。所以让我们看看土耳其分词器会发生什么:
>>> tokens=tokenizerTUR.tokenize(word_en)
>>> tokens
['tel', '##eco', '##mm', '##un', '##ica', '##tion']
由于土耳其分词器模型的词汇表中没有这样一个词,它需要将单词分解成对它有意义的部分。所有这些分割的标记已经存储在模型词汇表中。请注意以下执行的输出:
>>> [t in tokenizerTUR.vocab for t in tokens]
[True, True, True, True, True, True]
让我们使用我们已经加载的英语分词器对相同的单词进行分词:
>>> tokenizerEN.tokenize(word_en)
['telecommunication']
由于英语模型在基础词汇表中有单词telecommunication
,它不需要将其分割成部分,而是将其作为一个整体。通过从语料库中学习,分词器能够将一个单词转换为大部分语法逻辑的子部分。让我们以土耳其语的一个难例为例。作为一种聚合语言,土耳其语允许我们在一个词干上加入许多后缀,构成非常长的单词。以下是土耳其语中使用的最长单词之一(来源于en.wikipedia.org/wiki/Longest_word_in_Turkish
):
Muvaffakiyetsizleştiricileştiriveremeyebileceklerimizdenmişsinizcesine
它的意思是仿佛你是我们中那些我们不能轻易快速地使成为失败者的人之一。土耳其 BERT 分词器可能在训练中没有见过这个单词,但它已经看到了它的部分;muvaffak(成功)作为词干,##iyet(成功性),##siz(不成功),##leş(变得不成功),等等。当将结果与维基百科文章进行比较时,土耳其分词器提取出了在土耳其语中看起来是语法合乎逻辑的组件。
>>> print(tokenizerTUR.tokenize(long_word_tur))
['muvaffak', '##iyet', '##siz', '##les', '##tir', '##ici', '##les', '##tir', '##iver', '##emeye', '##bilecekleri', '##mi', '##z', '##den', '##mis', '##siniz', '##cesine']
土耳其分词器是 WordPiece 算法的一个例子,因为它与 BERT 模型协同工作。几乎所有的语言模型,包括 BERT、DistilBERT 和 ELECTRA,都需要一个 WordPiece 分词器。
现在,我们准备研究用于 Transformers 的分词方法。首先,我们将简要讨论一下 BPE、WordPiece 和 SentencePiece 的广泛使用的分词,然后用 HuggingFace 的快速分词器
库进行训练。
字节对编码
BPE 是一种数据压缩技术。它会扫描数据序列,并迭代地用一个单一的符号替换最常见的字节对。它最初是在Neural Machine Translation of Rare Words with Subword Units, Sennrich et al. 2015中提出来解决机器翻译中未知单词和稀有单词的问题。目前,它成功应用于 GPT-2 和许多其他领先的模型中。许多现代分词算法都是基于这样的压缩技术。
它将文本表示为字符 n-gram 的序列,也称为字符级子词。训练首先从语料库中看到的所有 Unicode 字符(或符号)的词汇表开始。对于英语来说,这可能很小,但对于日语等字符丰富的语言来说可能很大。然后,它迭代计算字符二元组,并将最常见的字符替换为特殊的新符号。例如,t 和 h 是频繁出现的符号。我们用 th 符号替换连续的符号。该过程一直迭代运行,直到词汇表达到所需的词汇量为止。最常见的词汇量约为 30K。
BPE 在表示未知词时特别有效。然而,它可能无法保证处理包含罕见子词或罕见词的情况。在这种情况下,它将罕见字符与特殊符号 关联起来,这可能会导致词义稍微丢失。作为潜在解决方案,Byte-Level BPE (BBPE) 已被提出,它使用 256 字节的词汇表,而不是 Unicode 字符,以确保每个基本字符都包含在词汇表中。
WordPiece 分词
WordPiece 是另一种广泛与 BERT、DistilBERT 和 Electra 配合使用的流行分词算法。它由 Schuster 和 Nakajima 在 2012 年提出,旨在解决日语和韩语语音问题。该工作的动机是,虽然对于英语来说不是一个大问题,但对于许多亚洲语言来说,分词是重要的预处理,因为在这些语言中,空格很少使用。因此,在亚洲语言的自然语言处理研究中,我们经常会遇到分词方法。与 BPE 类似,WordPiece 使用大型语料库来学习词汇和合并规则。虽然 BPE 和 BBPE 基于共现统计学习合并规则,但 WordPiece 算法使用最大似然估计从语料库中提取合并规则。它首先用 Unicode 字符(也称为词汇符号)初始化词汇表。它将训练语料库中的每个词视为符号列表(最初是 Unicode 字符),然后根据最大似然估计从所有可能的候选符号对中选择两个符号进行合并,而不是根据频率。该生产管道持续进行,直到达到所需的词汇量为止。
Sentence piece 分词
以前的标记化算法将文本视为以空格分隔的单词列表。这种基于空格的分割在一些语言中不起作用。在德语中,复合名词是没有空格的,例如 menschenrechte(人权)。解决方案是使用特定于语言的预标记器。在德语中,NLP 流水线利用复合分割模块来检查一个词是否可以分解为较小的词。然而,东亚语言(例如中文、日文、韩文和泰文)之间不使用空格。下划线_
字符,这也是我们之前在 Albert 模型示例的输出中看到_
的原因。其他使用 SentencePiece 的流行语言模型有 XLNet、Marian 和 T5。
到目前为止,我们已经讨论了子词标记化方法。现在是时候开始使用tokenizers
库进行训练实验了。
标记化器库
您可能已经注意到,以前的代码示例中土耳其语和英语的已经训练好的标记化器是Transformers
库的一部分。另一方面,HuggingFace 团队独立于Transformers
库提供了tokenizers
库,以便更快地给我们更多的自由。该库最初是用 Rust 编写的,这使得多核并行计算成为可能,并且用 Python 进行了包装(github.com/huggingface/tokenizers
)。
要安装tokenizers
库,我们使用这个:
$ pip install tokenizers
tokenizers
库提供了几个组件,以便我们从预处理原始文本到解码标记化单元 ID 构建端到端的分词器:
Normalizer→ PreTokenizer → 建模 → 后处理 → 解码
下图描述了标记化流水线:
图 3.17 – 标记化流水线
-
规范化器允许我们应用原始文本处理,例如小写处理、剥离、Unicode 规范化和去除重音。
-
预标记器为下一个训练阶段准备语料库。它根据规则(例如空格)将输入拆分为标记。
-
模型训练是一个子词标记化算法,例如BPE、BBPE和WordPiece,我们已经讨论过了。它发现子词/词汇并学习生成规则。
-
后处理提供了与 Transformer 模型(如 BertProcessors)兼容的高级类构造。我们主要是在馈送给架构之前,向标记化的输入中添加特殊标记,例如*[CLS]和[SEP]*。
-
解码器负责将标记 ID 转换回原始字符串。它只是用来检查发生了什么。
训练 BPE
让我们使用莎士比亚的戏剧来训练一个 BPE 分词器:
-
加载如下:
import nltk from nltk.corpus import gutenberg nltk.download('gutenberg') nltk.download('punkt') plays=['shakespeare-macbeth.txt','shakespeare-hamlet.txt', 'shakespeare-caesar.txt'] shakespeare=[" ".join(s) for ply in plays \ for s in gutenberg.sents(ply)]
对于所有的分词算法,我们需要一个后处理器(
TemplateProcessing
)。我们需要自定义后处理器以便为特定的语言模型提供方便的输入。例如,以下模板适用于 BERT 模型,因为它需要在输入开头有*[CLS]标记,在末尾和中间都有[SEP]*标记。 -
我们如下定义模板:
from tokenizers.processors import TemplateProcessing special_tokens=["[UNK]","[CLS]","[SEP]","[PAD]","[MASK]"] temp_proc= TemplateProcessing( single="[CLS] $A [SEP]", pair="[CLS] $A [SEP] $B:1 [SEP]:1", special_tokens=[ ("[CLS]", special_tokens.index("[CLS]")), ("[SEP]", special_tokens.index("[SEP]")), ], )
-
我们导入必要的组件来构建一个端到端的分词流程:
from tokenizers import Tokenizer from tokenizers.normalizers import \ (Sequence,Lowercase, NFD, StripAccents) from tokenizers.pre_tokenizers import Whitespace from tokenizers.models import BPE from tokenizers.decoders import BPEDecoder
-
我们首先实例化BPE如下:
tokenizer = Tokenizer(BPE())
-
预处理部分有两个组件:normalizer和pre-tokenizer。我们可能有多个 normalizer。因此,我们组成一个包含多个 normalizer 的
Sequence
,其中NFD()
是一个 Unicode 正规化组件,而StripAccents()
会移除重音符号。对于 pre-tokenization,Whitespace()
会根据空格来分隔文本。由于解码器组件必须与模型兼容,因此选择了BPEDecoder
作为BPE
模型的解码器:tokenizer.normalizer = Sequence( [NFD(),Lowercase(),StripAccents()]) tokenizer.pre_tokenizer = Whitespace() tokenizer.decoder = BPEDecoder() tokenizer.post_processor=temp_proc
-
好了!我们已经准备好对数据进行分词器训练。以下代码实例化
BpeTrainer()
,它帮助我们通过设置超参数来组织整个训练过程。由于我们的莎士比亚语料库相对较小,我们将词汇表大小参数设置为 5K。对于大规模项目,我们使用更大的语料库,通常将词汇表大小设置在 30K 左右:>>> from tokenizers.trainers import BpeTrainer >>> trainer = BpeTrainer(vocab_size=5000, special_tokens= special_tokens) >>> tokenizer.train_from_iterator(shakespeare, trainer=trainer) >>> print(f"Trained vocab size:\ {tokenizer.get_vocab_size()}" ) Trained vocab size: 5000
我们已经完成了训练!
重要提示
从文件系统训练:为了开始训练过程,我们将一个内存中的莎士比亚对象作为字符串列表传递给
tokenizer.train_from_iterator()
。对于一个具有大语料库的大规模项目,我们需要设计一个 Python 生成器,主要通过消耗文件系统的文件而不是内存存储来产生字符串行。您还应该在上面的 BERT 训练部分检查tokenizer.train()
,以便从文件系统存储中进行训练。 -
让我们随机选取一句来自 Macbeth 剧本的句子,将其命名为
sen
,并使用我们的新分词器对其进行分词:>>> sen= "Is this a dagger which I see before me,\ the handle toward my hand?" >>> sen_enc=tokenizer.encode(sen) >>> print(f"Output: {format(sen_enc.tokens)}") Output: ['[CLS]', 'is', 'this', 'a', 'dagger', 'which', 'i', 'see', 'before', 'me', ',', 'the', 'hand', 'le', 'toward', 'my', 'hand', '?', '[SEP]']
-
多亏了上面的后处理器函数,我们在正确的位置看到了额外的*[CLS]和[SEP]标记。由于我们传递给模型的是 Macbeth 剧本中的一句话,模型已经知道这个句子,所以只有一个分割单词handle*(hand,le)。此外,我们使用了一个小语料库,分词器没有强制使用压缩。让我们传递一个有挑战性的短语
Hugging Face
,分词器可能不认识:>>> sen_enc2=tokenizer.encode("Macbeth and Hugging Face") >>> print(f"Output: {format(sen_enc2.tokens)}") Output: ['[CLS]', 'macbeth', 'and', 'hu', 'gg', 'ing', 'face', '[SEP]']
-
术语
Hugging
被转换为小写,并拆分为三段hu
,gg
,ing
,因为模型的词汇表包含了除Hugging
之外的所有其他标记。现在让我们传递两个句子:>>> two_enc=tokenizer.encode("I like Hugging Face!", "He likes Macbeth!") >>> print(f"Output: {format(two_enc.tokens)}") Output: ['[CLS]', 'i', 'like', 'hu', 'gg', 'ing', 'face', '!', '[SEP]', 'he', 'li', 'kes', 'macbeth', '!', '[SEP]']
请注意,后处理器将
[SEP]
标记作为指示器插入。 -
是时候保存模型了。我们可以保存子词分词模型,也可以保存整个分词流程。首先,让我们仅保存 BPE 模型:
>>> tokenizer.model.save('.') ['./vocab.json', './merges.txt']
-
有关词汇和合并规则,模型保存了两个文件。
merge.txt
文件由 4948 个合并规则组成:$ wc -l ./merges.txt 4948 ./merges.txt
-
前五条规则按如下所示排名,我们会发现 (
t
,h
) 是由于成为最常见的一对而排名第一。对于测试,模型会扫描文本输入,如果适用,会首先尝试合并这两个符号:$ head -3 ./merges.txt t h o u a n th e r e
BPE 算法根据频率对规则进行排序。当您在莎士比亚语料库中手动计算字符二元组时,您会发现 (
t
,h
) 是最常见的一对。 -
现在让我们保存和加载整个分词流程:
>>> tokenizer.save("MyBPETokenizer.json") >>> tokenizerFromFile = \ Tokenizer.from_file("MyBPETokenizer.json") >>> sen_enc3 = \ tokenizerFromFile.encode("I like Hugging Face and Macbeth") >>> print(f"Output: {format(sen_enc3.tokens)}") Output: ['[CLS]', 'i', 'like', 'hu', 'gg', 'ing', 'face', 'and', 'macbeth', '[SEP]']
我们成功重新加载了分词器!
训练 WordPiece 模型
在本节中,我们将训练 WordPiece 模型:
-
我们首先导入必要的模块:
from tokenizers.models import WordPiece from tokenizers.decoders import WordPiece \ as WordPieceDecoder from tokenizers.normalizers import BertNormalizer
-
以下代码示例创建一个空的 WordPiece 分词器,并准备好进行训练。
BertNormalizer
是一个预定义的规范化序列,包括文本清理、变换重音、处理中文字符和小写化的过程:tokenizer = Tokenizer(WordPiece()) tokenizer.normalizer=BertNormalizer() tokenizer.pre_tokenizer = Whitespace() tokenizer.decoder= WordPieceDecoder()
-
现在,我们创建一个适当的训练器,
WordPieceTrainer()
用于WordPiece()
,以组织训练过程:>>> from tokenizers.trainers import WordPieceTrainer >>> trainer = WordPieceTrainer(vocab_size=5000,\ special_tokens=["[UNK]", "[CLS]", "[SEP]",\ "[PAD]", "[MASK]"]) >>> tokenizer.train_from_iterator(shakespeare, trainer=trainer) >>> output = tokenizer.encode(sen) >>> print(output.tokens) ['is', 'this', 'a', 'dagger', 'which', 'i', 'see', 'before', 'me', ',', 'the', 'hand', '##le', 'toward', 'my', 'hand', '?']
-
让我们使用
WordPieceDecoder()
正确处理句子:>>> tokenizer.decode(output.ids) 'is this a dagger which i see before me, the handle toward my hand?'
-
我们在输出中没有遇到任何
[UNK]
标记,因为分词器以某种方式知道或分割输入进行编码。让我们强制模型生成[UNK]
标记,就像下面的代码中所示。让我们向分词器传递一句土耳其语的句子:>>> tokenizer.encode("Kralsın aslansın Macbeth!").tokens '[UNK]', '[UNK]', 'macbeth', '!']
做得好!我们有一些未知标记,因为分词器无法从合并规则和基础词汇中找到给定单词的拆分方式。
到目前为止,我们已经设计好了从规范化组件到解码器组件的分词流程。另一方面,tokenizers
库为我们提供了一个已经准备好的(未经训练的)空的分词流程,其中包括适当的组件,用于快速创建生产原型。以下是一些预先制作的分词器:
-
CharBPETokenizer
:原始的 BPE -
ByteLevelBPETokenizer
:BPE 的字节级版本 -
SentencePieceBPETokenizer
:与SentencePiece使用的 BPE 兼容的 BPE 实现 -
BertWordPieceTokenizer
:著名的 BERT 分词器,使用 WordPiece
以下代码导入了这些分词流程:
>>> from tokenizers import (ByteLevelBPETokenizer,
CharBPETokenizer,
SentencePieceBPETokenizer,
BertWordPieceTokenizer)
所有这些分词流程都已经为我们设计好。其余的流程(如训练、保存模型和使用分词器)与之前的 BPE 和 WordPiece 训练过程相同。
做得好!我们取得了巨大的进展,并且已经训练了我们的第一个 Transformer 模型以及其分词器。
摘要
在本章中,我们从理论和实践两方面体验了自动编码模型。从对 BERT 的基本知识开始,我们从零开始训练了它以及相应的分词器。我们还讨论了如何在其他框架内工作,例如 Keras。除了 BERT,我们还回顾了其他自动编码模型。为了避免过多的代码重复,我们没有提供训练其他模型的完整实现。在 BERT 训练期间,我们训练了 WordPiece 分词算法。在最后一部分,我们检查了其他分词算法,因为讨论和理解它们都是值得的。
自动编码模型使用原始 Transformer 的左解码器侧,主要用于分类问题的微调。在下一章中,我们将讨论并学习 Transformer 的右解码器部分,以实现语言生成模型。