前言

分词器是 NLP 管道中的核心组件之一。它们有一个目的:将文本转换成模型可以处理的数据。模型只能处理数字,所以分词器需要将我们的文本输入转换为数值数据。在本节中,我们将探索分词管道中确切发生的事情。

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

1
Jim Henson was a puppeteer

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

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

src link: https://huggingface.co/learn/nlp-course/chapter2/4

Operating System: Ubuntu 22.04.4 LTS

参考文档

  1. NLP Course - Tokenizers

基于单词的分词

首先想到的分词器类型是基于单词的。它通常非常容易设置和使用,只需遵循少数规则,而且往往能产生不错的结果。例如,在下面的图片中,目标是将原始文本分割成单词,并为每个单词找到一个数字表示:

分割文本有不同的方法。例如,我们可以使用空白字符来通过应用Python的split()函数将文本分词为单词:

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

也存在一些单词标记器的变体,它们对标点符号有额外的规则。使用这种标记器,我们可能会得到一些相当大的“词汇表”,其中词汇表是由我们在语料库中拥有的独立标记的总数来定义的。

每个单词都会被分配一个从0开始到词汇表大小结束的ID。模型使用这些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类:

1
2
3
from transformers import BertTokenizer

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

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

1
2
3
from transformers import AutoTokenizer

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

我们现在可以使用分词器,方式如前一节所示:

1
tokenizer("Using a Transformer network is simple")
1
2
3
{'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]}

保存分词器与保存模型的方式相同:

1
tokenizer.save_pretrained("directory_on_my_computer")

我们将在第3章中更多地讨论token_type_ids,稍后我们将解释attention_mask键。首先,让我们看看input_ids是如何生成的。为此,我们需要查看分词器的方法。

编码

将文本翻译成数字的过程称为编码。编码分为两个步骤:首先是分词,然后是将分词转换为输入ID。

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

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

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

分词

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

1
2
3
4
5
6
7
8
from transformers import AutoTokenizer

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

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

print(tokens)

这个方法的输出是一个字符串列表,或者说是tokens:

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

这个分词器是一个子词分词器:它将单词分割,直到得到可以被其词汇表表示的tokens。这里的transformer就是这样,被分割成两个tokens:transform和##er。

从tokens到输入ID

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

1
2
3
ids = tokenizer.convert_tokens_to_ids(tokens)

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

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

解码

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

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

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

到现在为止,您应该已经理解了分词器可以处理的原子操作:分词、转换为ID,以及将ID转换回字符串。然而,我们只是触及了冰山一角。在接下来的部分,我们将把我们的方法发挥到极致,并探讨如何克服它们。

结语

第二百一十一篇博文写完,开心!!!!

今天,也是充满希望的一天。