LLM 课程文档

分词器

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

分词器

Ask a Question Open In Colab Open In Studio Lab

分词器是自然语言处理(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来识别每个单词。

如果我们想用基于词的分词器完全覆盖一种语言,我们需要为该语言中的每个词提供一个标识符,这将生成大量的词元。例如,英语中有超过 50 万个词,所以要建立一个从每个词到输入 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")

我们将在第三章中详细讨论`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 节中使用的输入句子(“我一生都在等待一门 HuggingFace 课程。”和“我太讨厌这个了!”)重复最后两个步骤(分词和转换为输入 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 上更新