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”,这两个词具有语义含义,同时空间效率高(只需要两个词来表示一个长词)。这使我们能够用较小的词汇量获得相对较好的覆盖率,并且几乎没有未知词。

这种方法在土耳其语等粘着语中特别有用,在这些语言中,可以通过将子词串联在一起形成(几乎)任意长的复杂词。

还有更多!

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

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

你现在应该对标记器的工作原理有足够的了解,可以开始使用 API。

加载和保存

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

加载与 BERT 相同检查点训练的 BERT 标记器与加载模型的方式相同,不同之处在于我们使用 BertTokenizer

from transformers import BertTokenizer

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

AutoModel 类似,AutoTokenizer 类将根据检查点名称在库中获取正确的标记器类,并且可以直接与任何检查点一起使用

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")

我们将在第 3 章中详细介绍 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 转换回字符串。但是,我们只是触及了皮毛。在下一节中,我们将把我们的方法发挥到极致,看看如何克服它们。