分词器
分词器是 NLP 管道中的核心组件之一。它们只有一个目的:将文本转换为模型可以处理的数据。模型只能处理数字,因此分词器需要将我们的文本输入转换为数值数据。在本节中,我们将探讨分词管道中究竟发生了什么。
在 NLP 任务中,通常处理的数据是原始文本。以下是一个这样的文本示例
Jim Henson was a puppeteer
但是,模型只能处理数字,因此我们需要找到一种方法将原始文本转换为数字。这就是分词器的作用,并且有很多方法可以做到这一点。目标是找到最有意义的表示——也就是说,对模型最有意义的表示——并且,如果可能,是最小的表示。
让我们看一些分词算法的示例,并尝试回答您可能对分词的一些疑问。
基于词语的
首先想到的分词器类型是基于词语的。它通常很容易设置和使用,只需几个规则,并且通常会产生不错的结果。例如,在下图中,目标是将原始文本分成单词,并为每个单词找到数值表示
有很多方法可以分割文本。例如,我们可以使用空格将文本分词为单词,方法是使用 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>”。如果您发现分词器产生了大量此类词元,这通常是一个不好的信号,因为它无法检索词语的合理表示,您会在此过程中丢失信息。构建词汇表的目标是以这样一种方式进行构建,即分词器尽可能少地将词语分词为未知词元。
减少未知词元数量的一种方法是更深入一层,使用基于字符的分词器。
基于字符的
基于字符的分词器将文本分成字符,而不是单词。这有两个主要好处
- 词汇表要小得多。
- 由于每个单词都可以由字符构成,因此词库外的(未知)词元要少得多。
但这里也出现了一些关于空格和标点符号的问题
这种方法也不是完美的。由于表示现在基于字符而不是单词,有人可能会认为,直观地说,它意义更少:每个字符本身并不意味着很多,而单词则不然。但是,这又取决于语言;例如,在中文中,每个字符比拉丁语中的字符承载更多信息。
另一个需要考虑的事情是我们最终将获得大量词元供模型处理:虽然一个单词在基于词语的分词器中只会有一个词元,但当它被转换为字符时,它很容易变成 10 个或更多词元。
为了兼顾两者的优点,我们可以使用第三种技术,它结合了这两种方法:子词分词。
子词分词
子词分词算法依赖于以下原则:频繁使用的词语不应拆分为更小的子词,但罕见的词语应分解为有意义的子词。
例如,“annoyingly”可能被认为是一个罕见的词语,可以分解为“annoying”和“ly”。它们都可能更频繁地作为独立的子词出现,同时“annoyingly”的含义通过“annoying”和“ly”的组合含义得以保留。
以下是一个示例,展示子词分词算法如何对序列“Let’s do tokenization!”进行分词
这些子词最终提供了大量的语义含义:例如,在上面的例子中,“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")
类似于 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 节中使用的输入句子(“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 转换回字符串。然而,我们只是触及了皮毛。在下一节中,我们将把我们的方法推向极限,看看如何克服它们。