Pytorch
TensorFlow

#
标记器（Tokenizer）

标记器(Tokenizer)是 NLP 管道的核心组件之一。它们有一个目的：将文本转换为模型可以处理的数据。模型只能处理数字，因此标记器(Tokenizer)需要将我们的文本输入转换为数字数据。在本节中，我们将确切地探讨标记化管道中发生的事情。

在 NLP 任务中，通常处理的数据是原始文本。这是此类文本的示例

    Jim Henson was a puppeteer

但是，模型只能处理数字，因此我们需要找到一种将原始文本转换为数字的方法。这就是标记器（tokenizer）所做的，并且有很多方法可以解决这个问题。目标是找到最有意义的表示——即对模型最有意义的表示——并且如果可能的话，找到最小的表示。

让我们看一下标记化算法的一些示例，并尝试回答您可能对标记化提出的一些问题。

##
基于词的(Word-based)

想到的第一种标记器是基于词的(word-based).它通常很容易设置和使用，只需几条规则，并且通常会产生不错的结果。例如，在下图中，目标是将原始文本拆分为单词并为每个单词找到一个数字表示：

有多种方法可以拆分文本。例如，我们可以通过应用Python的`split()`函数，使用空格将文本标记为单词：

    tokenized_text = "Jim Henson was a puppeteer".split()
    print(tokenized_text)

    ['Jim', 'Henson', 'was', 'a', 'puppeteer']

还有一些单词标记器的变体，它们具有额外的标点符号规则。使用这种标记器，我们最终可以得到一些非常大的“词汇表”，其中词汇表由我们在语料库中拥有的独立标记的总数定义。

每个单词都分配了一个 ID，从 0 开始一直到词汇表的大小。该模型使用这些 ID 来识别每个单词。

如果我们想用基于单词的标记器(tokenizer)完全覆盖一种语言，我们需要为语言中的每个单词都有一个标识符，这将生成大量的标记。例如，英语中有超过 500,000 个单词，因此要构建从每个单词到输入 ID 的映射，我们需要跟踪这么多 ID。此外，像“dog”这样的词与“dogs”这样的词的表示方式不同，模型最初无法知道“dog”和“dogs”是相似的：它会将这两个词识别为不相关。这同样适用于其他相似的词，例如“run”和“running”，模型最初不会认为它们是相似的。

最后，我们需要一个自定义标记(token)来表示不在我们词汇表中的单词。这被称为“未知”标记(token)，通常表示为“[UNK]”或”<unk>“。如果你看到标记器产生了很多这样的标记，这通常是一个不好的迹象，因为它无法检索到一个词的合理表示，并且你会在这个过程中丢失信息。制作词汇表时的目标是以这样一种方式进行，即标记器将尽可能少的单词标记为未知标记。

减少未知标记数量的一种方法是使用更深一层的标记器(tokenizer)，即基于字符的(character-based)标记器(tokenizer)。

##
基于字符(Character-based)

基于字符的标记器(tokenizer)将文本拆分为字符，而不是单词。这有两个主要好处：

- 词汇量要小得多。
- 词汇外（未知）标记(token)要少得多，因为每个单词都可以从字符构建。

但是这里也出现了一些关于空格和标点符号的问题：

这种方法也不是完美的。由于现在表示是基于字符而不是单词，因此人们可能会争辩说，从直觉上讲，它的意义不大：每个字符本身并没有多大意义，而单词就是这种情况。然而，这又因语言而异；例如，在中文中，每个字符比拉丁语言中的字符包含更多的信息。

另一件要考虑的事情是，我们的模型最终会处理大量的词符(token)：虽然使用基于单词的标记器(tokenizer)，单词只会是单个标记，但当转换为字符时，它很容易变成 10 个或更多的词符(token)。

为了两全其美，我们可以使用结合这两种方法的第三种技术：子词标记化(subword tokenization)。

##
子词标记化

子词分词算法依赖于这样一个原则，即不应将常用词拆分为更小的子词，而应将稀有词分解为有意义的子词。

例如，“annoyingly”可能被认为是一个罕见的词，可以分解为“annoying”和“ly”。这两者都可能作为独立的子词出现得更频繁，同时“annoyingly”的含义由“annoying”和“ly”的复合含义保持。

这是一个示例，展示了子词标记化算法如何标记序列“Let’s do tokenization!”：

这些子词最终提供了很多语义含义：例如，在上面的示例中，“tokenization”被拆分为“token”和“ization”，这两个具有语义意义同时节省空间的词符(token)（只需要两个标记(token)代表一个长词）。这使我们能够对较小的词汇表进行相对较好的覆盖，并且几乎没有未知的标记

这种方法在土耳其语等粘着型语言(agglutinative languages)中特别有用，您可以通过将子词串在一起来形成（几乎）任意长的复杂词。

###
还有更多！

不出所料，还有更多的技术。仅举几例：

- Byte-level BPE, 用于 GPT-2
- WordPiece, 用于 BERT
- SentencePiece or Unigram, 用于多个多语言模型

您现在应该对标记器(tokenizers)的工作原理有足够的了解，以便开始使用 API。

##
加载和保存

加载和保存标记器(tokenizer)就像使用模型一样简单。实际上，它基于相同的两种方法： `from_pretrained()` 和 `save_pretrained()` 。这些方法将加载或保存标记器(tokenizer)使用的算法（有点像建筑学(architecture)的模型）以及它的词汇（有点像权重(weights)模型）。

加载使用与 BERT 相同的检查点训练的 BERT 标记器(tokenizer)与加载模型的方式相同，除了我们使用 `BertTokenizer` 类：

    from transformers import BertTokenizer

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

如同 `AutoModel`，`AutoTokenizer` 类将根据检查点名称在库中获取正确的标记器(tokenizer)类，并且可以直接与任何检查点一起使用：

    from transformers import AutoTokenizer

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

我们现在可以使用标记器(tokenizer)，如上一节所示：

    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)与保存模型相同:

    tokenizer.save_pretrained("directory_on_my_computer")

我们在Chapter 3中将更多地谈论`token_type_ids`，稍后我们将解释 `attention_mask` 键。首先，让我们看看 `input_ids` 如何生成。为此，我们需要查看标记器(tokenizer)的中间方法。

##
编码

将文本翻译成数字被称为编码(encoding).编码分两步完成：标记化，然后转换为输入 ID。

正如我们所见，第一步是将文本拆分为单词（或单词的一部分、标点符号等），通常称为标记(token)。有多个规则可以管理该过程，这就是为什么我们需要使用模型名称来实例化标记器(tokenizer)，以确保我们使用模型预训练时使用的相同规则。

第二步是将这些标记转换为数字，这样我们就可以用它们构建一个张量并将它们提供给模型。为此，标记器(tokenizer)有一个词汇(vocabulary)，这是我们在实例化它时下载的部分 `from_pretrained()` 方法。同样，我们需要使用模型预训练时使用的相同词汇。

为了更好地理解这两个步骤，我们将分别探讨它们。请注意，我们将使用一些单独执行部分标记化管道的方法来向您展示这些步骤的中间结果，但实际上，您应该直接在您的输入上调用标记器(tokenizer)（如第 2 部分所示）。

###
标记化

标记化过程由标记器(tokenizer)的`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)

此方法的输出是一个字符串列表或标记(token)：

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

这个标记器(tokenizer)是一个子词标记器(tokenizer)：它对词进行拆分，直到获得可以用其词汇表表示的标记(token)。`transformer` 就是这种情况，它分为两个标记：`transform` 和 `##er`。

###
从词符(token)到输入 ID

输入 ID 的转换由标记器(tokenizer)的`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 是否与我们之前获得的相同！

##
解码

解码(Decoding) 正好相反：从词汇索引中，我们想要得到一个字符串。这可以通过 `decode()` 方法实现，如下：

    decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
    print(decoded_string)

    'Using a Transformer network is simple'

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

到现在为止，您应该了解标记器(tokenizer)可以处理的原子操作：标记化、转换为 ID 以及将 ID 转换回字符串。然而，我们只是刮到了冰山一角。在下一节中，我们将采用我们的方法来克服它的限制，并看看如何克服它们。

< > Update on GitHub