NLP 课程文档

分词器

Hugging Face's logo
加入 Hugging Face 社区

并获得增强型文档体验

开始使用

分词器

Ask a Question Open In Colab Open In Studio Lab

分词器是 NLP 管道中的核心组件之一。它们只有一个目的:将文本转换为模型可以处理的数据。模型只能处理数字,因此分词器需要将我们的文本输入转换为数值数据。在本节中,我们将探讨分词管道中究竟发生了什么。

在 NLP 任务中,通常处理的数据是原始文本。以下是一个这样的文本示例

Jim Henson was a puppeteer

但是,模型只能处理数字,因此我们需要找到一种方法将原始文本转换为数字。这就是分词器的作用,并且有很多方法可以做到这一点。目标是找到最有意义的表示——也就是说,对模型最有意义的表示——并且,如果可能,是最小的表示。

让我们看一些分词算法的示例,并尝试回答您可能对分词的一些疑问。

基于词语的

首先想到的分词器类型是基于词语的。它通常很容易设置和使用,只需几个规则,并且通常会产生不错的结果。例如,在下图中,目标是将原始文本分成单词,并为每个单词找到数值表示

An example of word-based tokenization.

有很多方法可以分割文本。例如,我们可以使用空格将文本分词为单词,方法是使用 Python 的 split() 函数

tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']

还有一些基于词语的分词器的变体,它们对标点符号有额外的规则。使用这种分词器,我们最终可能会得到一些非常大的“词汇表”,其中词汇表由语料库中所有独立词元的总数定义。

每个单词都会被分配一个 ID,从 0 开始,一直到词汇表的大小。模型使用这些 ID 来识别每个单词。

如果我们想要使用基于词语的分词器完全覆盖一种语言,我们需要为该语言中的每个单词提供一个标识符,这将生成大量词元。例如,英语中有超过 500,000 个单词,因此要构建一个从每个单词到输入 ID 的映射,我们需要跟踪这么多 ID。此外,像“dog”这样的词语与像“dogs”这样的词语的表示方式不同,模型最初无法知道“dog”和“dogs”是相似的:它会将这两个词语识别为不相关的。同样适用于其他类似的词语,例如“run”和“running”,模型最初不会将它们视为相似词语。

最后,我们需要一个自定义词元来表示不在我们词汇表中的词语。这被称为“未知”词元,通常表示为“ [UNK]”或“<unk>”。如果您发现分词器产生了大量此类词元,这通常是一个不好的信号,因为它无法检索词语的合理表示,您会在此过程中丢失信息。构建词汇表的目标是以这样一种方式进行构建,即分词器尽可能少地将词语分词为未知词元。

减少未知词元数量的一种方法是更深入一层,使用基于字符的分词器。

基于字符的

基于字符的分词器将文本分成字符,而不是单词。这有两个主要好处

  • 词汇表要小得多。
  • 由于每个单词都可以由字符构成,因此词库外的(未知)词元要少得多。

但这里也出现了一些关于空格和标点符号的问题

An example of character-based tokenization.

这种方法也不是完美的。由于表示现在基于字符而不是单词,有人可能会认为,直观地说,它意义更少:每个字符本身并不意味着很多,而单词则不然。但是,这又取决于语言;例如,在中文中,每个字符比拉丁语中的字符承载更多信息。

另一个需要考虑的事情是我们最终将获得大量词元供模型处理:虽然一个单词在基于词语的分词器中只会有一个词元,但当它被转换为字符时,它很容易变成 10 个或更多词元。

为了兼顾两者的优点,我们可以使用第三种技术,它结合了这两种方法:子词分词

子词分词

子词分词算法依赖于以下原则:频繁使用的词语不应拆分为更小的子词,但罕见的词语应分解为有意义的子词。

例如,“annoyingly”可能被认为是一个罕见的词语,可以分解为“annoying”和“ly”。它们都可能更频繁地作为独立的子词出现,同时“annoyingly”的含义通过“annoying”和“ly”的组合含义得以保留。

以下是一个示例,展示子词分词算法如何对序列“Let’s do tokenization!”进行分词

A subword tokenization algorithm.

这些子词最终提供了大量的语义含义:例如,在上面的例子中,“tokenization”被拆分为“token”和“ization”,这两个词语都具有语义含义,同时节省了空间(只需要两个词语来表示一个长词)。这使得我们能够用较小的词汇量获得相对较好的覆盖率,并且几乎没有未知词语。

这种方法在诸如土耳其语之类的粘着语中特别有用,因为你可以通过将子词串联起来来形成(几乎)任意长的复杂词语。

还有更多!

不出所料,还有许多其他技术。举几个例子

  • 字节级 BPE,如 GPT-2 中使用的那样
  • WordPiece,如 BERT 中使用的那样
  • SentencePiece 或 Unigram,如几个多语言模型中使用的那样

现在你应该已经了解分词器的工作原理,可以开始使用 API 了。

加载和保存

加载和保存分词器与模型一样简单。实际上,它是基于相同的两种方法:from_pretrained()save_pretrained()。这些方法将加载或保存分词器使用的算法(类似于模型的架构)以及它的词汇表(类似于模型的权重)。

加载与 BERT 相同的检查点训练的 BERT 分词器与加载模型的方式相同,只是我们使用的是 BertTokenizer

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-cased")

类似于 AutoModelAutoTokenizer 类将根据检查点名称在库中获取适当的分词器类,并且可以直接与任何检查点一起使用

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

现在我们可以像上一节中显示的那样使用分词器

tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}

保存分词器与保存模型相同

tokenizer.save_pretrained("directory_on_my_computer")

我们将在 第三章 中详细介绍 token_type_ids,稍后我们会解释 attention_mask 键。首先,让我们看看 input_ids 是如何生成的。为此,我们需要查看分词器的中间方法。

编码

将文本转换为数字被称为编码。编码通过两步完成:分词,然后转换为输入 ID。

正如我们所见,第一步是将文本拆分为词语(或词语的一部分、标点符号等),通常称为词语。有多个规则可以控制这个过程,这就是为什么我们需要使用模型的名称实例化分词器,以确保我们使用与模型预训练时相同的规则。

第二步是将这些词语转换为数字,这样我们就可以用它们构建一个张量,并将它们输入模型。为此,分词器有一个词汇表,这是我们在使用 from_pretrained() 方法实例化分词器时下载的部分。同样,我们需要使用与模型预训练时相同的词汇表。

为了更好地理解这两个步骤,我们将分别探讨它们。请注意,我们将使用一些方法来分别执行分词流程的某些部分,以向您展示这些步骤的中间结果,但在实践中,您应该直接在输入上调用分词器(如第 2 节所示)。

分词

分词过程由分词器的 tokenize() 方法完成

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)

print(tokens)

此方法的输出是一个字符串列表,或词语

['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']

此分词器是一个子词分词器:它会拆分单词,直到获得可以通过它的词汇表表示的词语。这里 transformer 的情况就是这样,它被拆分为两个词语:transform##er

从词语到输入 ID

转换为输入 ID 由 convert_tokens_to_ids() 分词器方法处理

ids = tokenizer.convert_tokens_to_ids(tokens)

print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]

这些输出,一旦转换为适当的框架张量,就可以用作本章前面所述模型的输入。

✏️ 试试看! 对第 2 节中使用的输入句子(“I’ve been waiting for a HuggingFace course my whole life.” 和 “I hate this so much!”)重复最后两个步骤(分词和转换为输入 ID)。检查你是否得到了与之前相同的输入 ID!

解码

解码是反过来:从词汇表索引中,我们想要得到一个字符串。这可以使用 decode() 方法完成,如下所示

decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'

请注意,decode 方法不仅将索引转换回词语,而且还会将属于同一词语的词语组合在一起,以生成可读的句子。当我们使用预测新文本的模型(无论是从提示生成的文本,还是用于序列到序列问题的文本,例如翻译或摘要)时,这种行为将非常有用。

到目前为止,你应该已经了解分词器可以处理的基本操作:分词、转换为 ID 以及将 ID 转换回字符串。然而,我们只是触及了皮毛。在下一节中,我们将把我们的方法推向极限,看看如何克服它们。