购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 将文本转换成词元

像DistilBERT这样的Transformer模型不能接收原始字符串作为输入。它假定文本已经被词元化并编码为数字向量。词元化(tokenization)是指将字符串分解为给模型使用的原子单元的步骤。词元化的策略有好几种,具体哪种最佳通常需要从语料库中学习。在讨论DistilBERT使用的Tokenizer之前,我们先讨论两种极端情况:字符词元化和单词词元化。

2.2.1 字符词元化

最简单的词元化方案是按每个字符单独馈送到模型中。在Python中,str对象实际上是一组数据,这使我们可以用一行代码快速实现字符词元化:

这是一个很好的开始,但我们还没有完成任务。我们的模型希望把每个字符转换为一个整数,有时这个过程被称为数值化(numericalization)。一个简单的方法是用一个唯一的整数来编码每个唯一的词元(在这里为字符):

我们可以看到,我们得到了一个包括了每个字符到一个唯一性整数的映射,即词元分析器的词表。我们现在可以使用token2idx将词元化的文本转换成一个整数列表:

现在,每个词元都已映射到唯一的数字标识符(因此称为input_ids)。最后一步是将input_ids转换为独热向量(one-hot vector)的二维张量。在机器学习中,独热向量常常用于编码分类数据(包括有序和无序数据)。例如,假设我们想对《变形金刚》中的角色名称进行编码。一种方法是将每个Name映射到唯一的ID,如下所示:

这种方法的问题在于它会给Name这一列的值引入虚假的顺序关系,这个缺陷在某些情况下可能会导致神经网络学习到错误的模式和关系,从而降低模型的性能。因此,我们可以为每个类别创建一个新列,在该类别为true时分配1,否则分配0。在Pandas中,可以使用get_dummies()函数实现这点:

以上DataFrame中的每行是一个独热向量,一整行只有一个1,其他都是0。现在,看看我们的input_ids,我们有类似的问题:这些元素的取值之间引入了虚假的顺序关系。因为这个关系是虚假的,所以对两个ID进行加减是一个没意义的操作。

如果我们将input_ids改成独热编码,结果就很容易解释:“热”的两个条目表明是相同的两个词元。在PyTorch中,我们可以使用one_hot()函数对input_ids进行独热编码:

本例共有38个输入词元,我们得到了一个20维的独热向量,因为我们的词表包含了20个唯一字符。

在one_hot()函数中,始终要设置num_classes参数,否则独热向量可能会比词表长度短(需要手动用零来填充)。在TensorFlow中,对应的函数是tf.one_hot(),对应num_classes的参数是depth。

通过输出input_ids[0]的如下信息来检查第一个向量,我们可以看到有个位置出现了1:

从我们这个简单的示例可以看出,字符级别的词元化忽略了文本中的任何结构,将整个字符串视为一串字符流。虽然这有助于处理拼写错误和生僻词,但主要缺点是语言结构(如单词)需要从数据中学习。这需要大量的计算、内存和数据。因此,字符词元化在实践中很少使用。如果想保留文本的某些结构,那么单词词元化是实现这一目标的一种简单直接的方法,让我们来看看它是如何工作的。

2.2.2 单词词元化

与字符词元化相比,单词词元化将文本细分为单词,并将每个单词映射到一个整数。单词词元化使模型跳过从字符学习单词的步骤,从而降低训练过程的复杂性。

有一种简单的单词词元化方法是使用空格来分割文本。我们可以直接在原始文本上应用Python的split()函数(就像我们统计推文字数那样)来实现这一点:

这里我们可以采取相同的步骤,将每个字符映射到ID。但是,我们已经可以看到这种词元化方案中的一个潜在问题:未考虑标点,因此将NLP.视为一个单独词元。单词词元化有一个缺点:鉴于单词可以包括变形、派生或拼写错误,词表的大小很容易增长到数百万!

有些单词词元分析器支持标点以及词干提取或词形还原,因此它们可以将单词规范化成其词干(例如将great、greater和greatest都变成great),但这样会丢失文本中一些信息。

词表太大是一个问题,因为它导致了神经网络需要大量的参数。举例来说,假设词表中有100万个唯一词项,并按照大多数NLP架构中的标准步骤将100万维输入向量压缩到第一层神经网络中的1000维向量。这样就导致了第一层的权重矩阵将包含100万×1千=10亿个权重。这已经可以与最大的GPT-2模型 [4] 看齐了,该模型总计拥有大约15亿个参数!

自然,我们希望避免在模型参数上浪费过多,因为模型训练成本高昂,而且规模较大的模型更难维护。一种常见的方法是只取语料库中最常见的100 000个单词并丢弃罕见的单词来避免词表过大。词表之外的单词统一归类为“unknown”(未知,UNK),映射到一个共用的UNK词元。这意味着我们在单词词元化过程中丢失了一些可能很重要的信息,因为模型无法获得与UNK关联的单词的信息。

那么有没有这么一种方法:介于字符词元化和单词词元化之间,既可以保留输入信息又能保留文本结构?确实有,这种方法叫子词词元化。

2.2.3 子词词元化

子词词元化背后的基本思想是将字符和单词词元化的优点结合起来。一方面,我们希望将生僻单词拆分成更小的单元,以使模型能够处理复杂单词和拼写错误。另一方面,我们希望将常见单词作为唯一实体保留下来,以便我们将输入长度保持在可管理的范围内。子词词元化(以及单词词元化)是使用统计规则和算法从预训练语料库中学习的。

在NLP中常用的子词词元化算法有几种,我们先从WordPiece [5] 算法开始,这是BERT和DistilBERT词元分析器使用的算法。了解WordPiece如何工作最简单的方法是看它的运行过程。Hugging Face Transformers库提供了一个很方便的AutoTokenizer类,它能令你快速加载与预训练模型相关联的词元分析器——只需要提供模型在Hub上的ID或本地文件路径,然后调用它的from_pretrained()方法即可。我们先加载DistilBERT的词元分析器:

AutoTokenizer类是“auto”类的一种( https://oreil.ly/h4YPz ),其任务是根据checkpoint的名称自动检索模型的配置、预训练权重或词表。使用以上代码的优点是可以快速切换模型,但是你也可以手动加载特定类。例如,我们可以按照下面的方式加载DistilBERTTokenizer:

当你第一次运行AutoTokenizer.from_pretrained()方法时,你将看到一个进度条,显示从Hugging Face Hub加载的预训练词元分析器的参数。当你第二次运行代码时,它会从缓存(通常是 ~/.cache/huggingface )中加载词元分析器。

我们输入“Tokenizing text is a core task of NLP.”这一简单样本来检验这个词元分析器是如何工作的:

和字符词元化一样,我们可以看到,单词映射成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()方法可以做到这一点,所以让我们将它应用到我们的词元:

AutoTokenizer类还有几个属性可以提供该词元分析器的其他信息。例如,我们可以检查词表的大小:

还有模型的最大上下文大小:

还有另一个有趣的属性,模型在前向传递中期望的字段名称:

现在我们了解了处理单个字符串的词元化过程,接下来我们看看如何对整个数据集进行词元化!

在使用预训练模型时,确保使用与模型训练时相同的词元分析器非常重要。从模型的角度来看,更换词元分析器就像打乱词表一样。如果身边的每个人都开始随机使用单词,比如将“house”换成“cat”,那么你会很难理解发生了什么!

2.2.4 对整个数据集进行词元化

我们将使用DatasetDict对象的map()方法来对整个语料库进行词元化。在本书中,我们将多次遇到这种方法,因为它提供了一种方便的方法,可以将处理函数应用于数据集中的每个元素。我们很快就会看到,map()方法还可以用于创建新的行和列。

我们要做的第一件事就是编写一个将我们的样本进行词元化的处理函数:

这个函数将词元分析器应用于一个批量样本。padding=True表示以零填充样本,以达到批量中最长样本的长度,truncation=True表示将样本截断为模型的最大上下文大小。为了观察tokenize()具体做了什么,现在我们从训练集中取两个批量样本传给tokenizer()函数:

这里我们可以看到填充的结果:input_ids的第一个元素比第二个短,因此向该元素填充零以使两个元素具有相同的长度。这些零在词表中具有对应的[PAD]词元,而特殊词元集还包括我们之前遇到的[CLS]和[SEP]词元:

此外,除了将编码后的推文返回为input_ids外,词元分析器还返回一系列attention_mask数组。这是因为我们不希望模型被额外的填充词元所困惑:注意力掩码(attention mask)允许模型忽略输入的填充部分。图2-3提供了输入ID和注意力掩码如何填充的视觉解释。

图2-3:在每个批量中,输入序列将填充到批量中最大序列的长度。模型使用注意力掩码来忽略输入张量中的填充区域

在定义完处理函数之后,我们可以通过一行代码将该函数应用到语料库整个数据集:

map()方法默认按单个样本操作,因此将batched设置为True以按批量对推文进行编码。由于我们设置了batch_size=None,所以将把整个数据集作为一个批量应用tokenize()函数。这可以确保全局输入张量和注意力掩码具有相同的形状,我们可以看到此操作已将新的input ids和attention mask列添加到数据集中:

在后面章节中,我们将看到如何使用数据整理器来动态地填充每个批量中的张量。在下一节中,我们从整个语料库中提取特征矩阵时,全局填充将派上用场。 2KR3j5UtCO96l4LgnunQpZpyFecDxT5wUAyGCu3dH8/+sd7t6NFDMCFpXCMPjoXq

点击中间区域
呼出菜单
上一章
目录
下一章
×