LLM 课程文档

分词器

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始入门

分词器

Ask a Question Open In Colab Open In Studio Lab

分词器是 NLP pipeline 的核心组件之一。它们服务于一个目的:将文本翻译成模型可以处理的数据。模型只能处理数字,因此分词器需要将我们的文本输入转换为数值数据。在本节中,我们将准确地探讨分词 pipeline 中发生的事情。

在 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 相同 checkpoint 训练的 BERT 分词器,其操作方式与加载模型相同,只是我们使用 BertTokenizer

from transformers import BertTokenizer

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

AutoModel 类似,AutoTokenizer 类将根据 checkpoint 名称在库中抓取适当的分词器类,并且可以直接与任何 checkpoint 一起使用

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() 方法实例化它时下载的部分。同样,我们需要使用与模型预训练时使用的词汇表相同的词汇表。

为了更好地理解这两个步骤,我们将分别探讨它们。请注意,我们将使用一些单独执行分词 pipeline 部分的方法来向您展示这些步骤的中间结果,但在实践中,您应该直接在您的输入上调用分词器(如第 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 转换回字符串。但是,我们只是冰山一角。在下一节中,我们将把我们的方法发挥到极限,并看看如何克服它们。

< > GitHub 上更新