# 逐塊地構建標記器

正如我們在前幾節中看到的，標記化包括幾個步驟：

- 規範化（任何認為必要的文本清理，例如刪除空格或重音符號、Unicode 規範化等）
- 預標記化（將輸入拆分為單詞）
- 通過模型處理輸入（使用預先拆分的詞來生成一系列標記）
- 後處理（添加標記器的特殊標記，生成注意力掩碼和標記類型 ID）

提醒一下，這裡再看一下整個過程

🤗 Tokenizers 庫旨在為每個步驟提供多個選項，您可以將它們混合和匹配在一起。在本節中，我們將看到如何從頭開始構建標記器，而不是像我們第二節 2那樣從舊的標記器訓練新的標記器.然後，您將能夠構建您能想到的任何類型的標記器！

更準確地說，該庫是圍繞一個中央「Tokenizer」類構建的，構建這個類的每一部分可以在子模塊的列表中重新組合：

- `normalizers` 包含你可以使用的所有可能的Normalizer類型（完整列表在這裡）。
- `pre_tokenizesr` 包含您可以使用的所有可能的PreTokenizer類型（完整列表在這裡）。
- `models` 包含您可以使用的各種類型的Model，如BPE、WordPiece和Unigram（完整列表在這裡）。
- `trainers` 包含所有不同類型的 trainer，你可以使用一個語料庫訓練你的模型（每種模型一個；完整列表在這裡）。
- `post_processors` 包含你可以使用的各種類型的PostProcessor（完整列表在這裡）。
- `decoders` 包含各種類型的Decoder，可以用來解碼標記化的輸出（完整列表在這裡）。

您可以在這裡找到完整的模塊列表。

## 獲取語​​料庫

為了訓練我們的新標記器，我們將使用一個小的文本語料庫（因此示例運行得很快）。獲取語​​料庫的步驟與我們在[在這章的開始]((/course/chapter6/2)那一小節，但這次我們將使用WikiText-2數據集：

    from datasets import load_dataset

    dataset = load_dataset("wikitext", name="wikitext-2-raw-v1", split="train")

    def get_training_corpus():
        for i in range(0, len(dataset), 1000):
            yield dataset[i : i + 1000]["text"]

get_training_corpus() 函數是一個生成器，每次調用的時候將產生 1,000 個文本，我們將用它來訓練標記器。

🤗 Tokenizers 也可以直接在文本文件上進行訓練。以下是我們如何生成一個文本文件，其中包含我們可以在本地使用的來自 WikiText-2 的所有文本/輸入：

    with open("wikitext-2.txt", "w", encoding="utf-8") as f:
        for i in range(len(dataset)):
            f.write(dataset[i]["text"] + "\n")

接下來，我們將向您展示如何逐塊構建您自己的 BERT、GPT-2 和 XLNet 標記器。這將為我們提供三個主要標記化算法的示例：WordPiece、BPE 和 Unigram。讓我們從 BERT 開始吧！

##
從頭開始構建 WordPiece 標記器

要使用 🤗 Tokenizers 庫構建標記器，我們首先使用 model 實例化一個 Tokenizer 對象，然後將 normalizer , pre_tokenizer , post_processor ， 和 decoder 屬性設置成我們想要的值。

對於這個例子，我們將創建一個 Tokenizer 使用 WordPiece 模型：

    from tokenizers import (
        decoders,
        models,
        normalizers,
        pre_tokenizers,
        processors,
        trainers,
        Tokenizer,
    )

    tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))

我們必須指定 unk_token 這樣模型才知道當它遇到以前沒有見過的字符時要返回什麼。我們可以在此處設置的其他參數包括我們模型的vocab（字典）（我們將訓練模型，所以我們不需要設置它）和 max_input_chars_per_word 即每個單詞的最大長度（比傳遞的值長的單詞將被拆分）

標記化的第一步是規範化，所以讓我們從它開始。 由於 BERT 被廣泛使用，所以有一個可以使用的 `BertNormalizer`，我們可以為 BERT 設置經典的選項：`lowercase（小寫）` 和 `strip_accents（去除音調）`，不言自明； `clean_text` 刪除所有控制字符並將重複的空格替換為一個； 和 `handle_chinese_chars`，在漢字周圍放置空格。 要實現 `bert-base-uncased` ，我們可以這樣設置這個規範器：

    tokenizer.normalizer = normalizers.BertNormalizer(lowercase=True)

然而，一般來說，在構建新的標記器時，您可以使用已經在 🤗 Tokenizers庫中實現的非常方便的normalizer——所以讓我們看看如何手動創建 BERT normalizer。 該庫提供了一個“Lowercase（小寫）”的normalizer和一個“StripAccents”的normalizer，您可以使用“序列”組合多個normalizer：

    tokenizer.normalizer = normalizers.Sequence(
        [normalizers.NFD(), normalizers.Lowercase(), normalizers.StripAccents()]
    )

我們也在使用 NFD Unicode normalizer，否則 StripAccents normalizer 無法正確識別帶重音的字符，因此沒辦法刪除它們。

正如我們之前看到的，我們可以使用 normalize 的 normalize_str() 方法查看它對給定文本的影響：

    print(tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))

    hello how are u?

更進一步如果您在包含 unicode 字符的字符串上測試先前normalizers的兩個版本，您肯定會注意到這兩個normalizers並不完全等效。 為了不過度使用 `normalizers.Sequence` 使版本過於複雜，我們沒有包含當 `clean_text` 參數設置為 `True` 時 `BertNormalizer` 需要的正則表達式替換 - 這是默認行為。 但不要擔心：通過在normalizer序列中添加兩個 `normalizers.Replace` 可以在不使用方便的 `BertNormalizer` 的情況下獲得完全相同的規範化。

接下來是預標記步驟。 同樣，我們可以使用一個預構建的“BertPreTokenizer”：

    tokenizer.pre_tokenizer = pre_tokenizers.BertPreTokenizer()

或者我們可以從頭開始構建它：

    tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()

請注意，`Whitespace` 預標記器會在空格和所有非字母、數字或下劃線字符的字符上進行拆分，因此在本次的例子中上會根據空格和標點符號進行拆分：

    tokenizer.pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")

    [('Let', (0, 3)), ("'", (3, 4)), ('s', (4, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre', (14, 17)),
     ('-', (17, 18)), ('tokenizer', (18, 27)), ('.', (27, 28))]

如果您只想在空白處進行拆分，則應使用 WhitespaceSplit 代替預標記器：

    pre_tokenizer = pre_tokenizers.WhitespaceSplit()
    pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")

    [("Let's", (0, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre-tokenizer.', (14, 28))]

像normalizers一樣，您可以使用 Sequence 組成幾個預標記器：

    pre_tokenizer = pre_tokenizers.Sequence(
        [pre_tokenizers.WhitespaceSplit(), pre_tokenizers.Punctuation()]
    )
    pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")

    [('Let', (0, 3)), ("'", (3, 4)), ('s', (4, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre', (14, 17)),
     ('-', (17, 18)), ('tokenizer', (18, 27)), ('.', (27, 28))]

標記化管道的下一步是輸入給模型。我們已經在初始化中指定了我們的模型，但我們仍然需要訓練它，這將需要一個 WordPieceTrainer .在 🤗 Tokenizers 中實例化訓練器時要記住的主要事情是，您需要將您打算使用的所有特殊標記傳遞給它 - 否則它不會將它們添加到詞彙表中，因為它們不在訓練語料庫中：

    special_tokens = ["[UNK]", "[PAD]", "[CLS]", "[SEP]", "[MASK]"]
    trainer = trainers.WordPieceTrainer(vocab_size=25000, special_tokens=special_tokens)

以及指定 vocab_size（詞典大小） 和 special_tokens（特殊的標記） ，我們可以設置 min_frequency （記號必須出現在詞彙表中的次數）或更改 continuing_subword_prefix （如果我們想使用與 ##指代存在與字詞相同的前綴 ）。

要使用我們之前定義的迭代器訓練我們的模型，我們只需要執行以下命令：

    tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

我們還可以使用文本文件來訓練我們的標記器，它看起來像這樣（我們需要先初始化一個空的 WordPiece ）：

    tokenizer.model = models.WordPiece(unk_token="[UNK]")
    tokenizer.train(["wikitext-2.txt"], trainer=trainer)

在這兩種情況下，我們都可以通過調用文本來測試標記器 encode() 方法：

    encoding = tokenizer.encode("Let's test this tokenizer.")
    print(encoding.tokens)

    ['let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '.']

這個 encoding 獲得的是一個 Encoding對象 ，它的屬性中包含標記器的所有必要輸出： ids , type_ids , tokens , offsets , attention_mask , special_tokens_mask ， 和 overflowing .

標記化管道的最後一步是後處理。我們需要添加 [CLS] 開頭的標記和 [SEP] 標記在末尾（或在每個句子之後，如果我們有一對句子）。我們將使用一個 TemplateProcessor 為此，但首先我們需要知道 [CLS] 和 [SEP] 在詞彙表中的ID：

    cls_token_id = tokenizer.token_to_id("[CLS]")
    sep_token_id = tokenizer.token_to_id("[SEP]")
    print(cls_token_id, sep_token_id)

    (2, 3)

為了給 TemplateProcessor 編寫模板，我們必須指定如何處理單個句子和一對句子。對於兩者，我們都編寫了我們想要使用的特殊標記；第一個（或單個）句子表示為 $A ，而第二個句子（如果對一對進行編碼）表示為 $B .對於這些特殊標記和句子，我們還需要使用在冒號後指定相應的標記類型 ID。

因此經典的 BERT 模板定義如下：

    tokenizer.post_processor = processors.TemplateProcessing(
        single=f"[CLS]:0 $A:0 [SEP]:0",
        pair=f"[CLS]:0 $A:0 [SEP]:0 $B:1 [SEP]:1",
        special_tokens=[("[CLS]", cls_token_id), ("[SEP]", sep_token_id)],
    )

請注意，我們需要傳遞特殊標記的 ID，以便標記器可以正確地將特殊標記轉換為它們的 ID。

添加後，我們之前的示例將輸出出：

    encoding = tokenizer.encode("Let's test this tokenizer.")
    print(encoding.tokens)

    ['[CLS]', 'let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '.', '[SEP]']

在一對句子中，我們得到了正確的結果：

    encoding = tokenizer.encode("Let's test this tokenizer...", "on a pair of sentences.")
    print(encoding.tokens)
    print(encoding.type_ids)

    ['[CLS]', 'let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '...', '[SEP]', 'on', 'a', 'pair', 'of', 'sentences', '.', '[SEP]']
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]

我們幾乎從頭開始構建了這個標記器——但是還有最後一步是指定一個解碼器：

    tokenizer.decoder = decoders.WordPiece(prefix="##")

讓我們測試一下我們之前的 encoding ：

    tokenizer.decode(encoding.ids)

    "let's test this tokenizer... on a pair of sentences."

很好！我們可以將標記器保存在一個 JSON 文件中，如下所示：

    tokenizer.save("tokenizer.json")

然後我們可以使用from_file() 方法從該文件裡重新加載 Tokenizer 對象：

    new_tokenizer = Tokenizer.from_file("tokenizer.json")

要在 🤗 Transformers 中使用這個標記器，我們必須將它包裹在一個 PreTrainedTokenizerFast 類中。我們可以使用泛型類，或者，如果我們的標記器對應於現有模型，則使用該類（例如這裡的 BertTokenizerFast ）。如果您應用本課來構建全新的標記器，則必須使用第一個選項。

要將標記器包裝在 `PreTrainedTokenizerFast` 類中，我們可以將我們構建的標記器作為`tokenizer_object` 傳遞，或者將我們保存為`tokenizer_file` 的標記器文件傳遞。 要記住的關鍵是我們必須手動設置所有特殊標記，因為該類無法從 `tokenizer` 對象推斷出哪個標記是掩碼標記、`[CLS]` 標記等：

    from transformers import PreTrainedTokenizerFast

    wrapped_tokenizer = PreTrainedTokenizerFast(
        tokenizer_object=tokenizer,
        # tokenizer_file="tokenizer.json", # You can load from the tokenizer file, alternatively
        unk_token="[UNK]",
        pad_token="[PAD]",
        cls_token="[CLS]",
        sep_token="[SEP]",
        mask_token="[MASK]",
    )

如果您使用特定的標記器類（例如 BertTokenizerFast )，您只需要指定與默認標記不同的特殊標記（此處沒有）：

    from transformers import BertTokenizerFast

    wrapped_tokenizer = BertTokenizerFast(tokenizer_object=tokenizer)

然後，您可以像使用任何其他 🤗 Transformers 標記器一樣使用此標記器。你可以用 save_pretrained() 方法，或使用 push_to_hub() 方法。

現在我們已經瞭解瞭如何構建 WordPiece 標記器，讓我們對 BPE 標記器進行同樣的操作。因為您已經知道了所有步驟，所以我們會進行地更快一點，並且只突出展示兩者不一樣的地方。

##
從頭開始構建 BPE 標記器

現在讓我們構建一個 GPT-2 標記器。與 BERT 標記器一樣，我們首先使用 Tokenizer 初始化一個BPE 模型：

    tokenizer = Tokenizer(models.BPE())

和 BERT 一樣，如果我們有一個詞彙表，我們可以用一個詞彙表來初始化這個模型（在這種情況下，我們需要傳遞 `vocab` 和 `merges`），但是由於我們將從頭開始訓練，所以我們不需要這樣去做。 我們也不需要指定“unk_token”，因為 GPT-2 使用的字節級 BPE，不需要“unk_token”。

GPT-2 不使用歸一化器，因此我們跳過該步驟並直接進入預標記化：

    tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)

我們在此處添加到 `ByteLevel` 的選項是不在句子開頭添加空格（默認為ture）。 我們可以看一下使用這個標記器對之前示例文本的預標記：

    tokenizer.pre_tokenizer.pre_tokenize_str("Let's test pre-tokenization!")

    [('Let', (0, 3)), ("'s", (3, 5)), ('Ġtest', (5, 10)), ('Ġpre', (10, 14)), ('-', (14, 15)),
     ('tokenization', (15, 27)), ('!', (27, 28))]

接下來是需要訓練的模型。對於 GPT-2，唯一的特殊標記是文本結束標記：

    trainer = trainers.BpeTrainer(vocab_size=25000, special_tokens=["<|endoftext|>"])
    tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

與 `WordPieceTrainer` 以及 `vocab_size` 和 `special_tokens` 一樣，我們可以指定 `min_frequency` 如果我們願意，或者如果我們有一個詞尾後綴（如 `</w>` )，我們可以使用 `end_of_word_suffix` 設置它。

這個標記器也可以在文本文件上訓練：

    tokenizer.model = models.BPE()
    tokenizer.train(["wikitext-2.txt"], trainer=trainer)

讓我們看一下示例文本的標記化後的結果：

    encoding = tokenizer.encode("Let's test this tokenizer.")
    print(encoding.tokens)

    ['L', 'et', "'", 's', 'Ġtest', 'Ġthis', 'Ġto', 'ken', 'izer', '.']

我們對 GPT-2 標記器添加字節級後處理，如下所示：

    tokenizer.post_processor = processors.ByteLevel(trim_offsets=False)

`trim_offsets = False` 選項指示我們應該保留以 ‘Ġ’ 開頭的標記的偏移量：這樣偏移量的開頭將指向單詞之前的空格，而不是第一個單詞的字符（因為空格在技術上是標記的一部分）。 讓我們看看我們剛剛編碼的文本的結果，其中 `'Ġtest'` 是索引第 4 處的標記：

    sentence = "Let's test this tokenizer."
    encoding = tokenizer.encode(sentence)
    start, end = encoding.offsets[4]
    sentence[start:end]

    ' test'

最後，我們添加一個字節級解碼器：

    tokenizer.decoder = decoders.ByteLevel()

我們可以仔細檢查它是否正常工作：

    tokenizer.decode(encoding.ids)

    "Let's test this tokenizer."

很好！現在我們完成了，我們可以像以前一樣保存標記器，並將它包裝在一個 PreTrainedTokenizerFast 或者 GPT2TokenizerFast 如果我們想在 🤗 Transformers中使用它：

    from transformers import PreTrainedTokenizerFast

    wrapped_tokenizer = PreTrainedTokenizerFast(
        tokenizer_object=tokenizer,
        bos_token="<|endoftext|>",
        eos_token="<|endoftext|>",
    )

或者：

    from transformers import GPT2TokenizerFast

    wrapped_tokenizer = GPT2TokenizerFast(tokenizer_object=tokenizer)

作為最後一個示例，我們將向您展示如何從頭開始構建 Unigram 標記器。

##
從頭開始構建 Unigram 標記器

現在讓我們構建一個 XLNet 標記器。與之前的標記器一樣，我們首先使用 Unigram 模型初始化一個 Tokenizer ：

    tokenizer = Tokenizer(models.Unigram())

同樣，如果我們有詞彙表，我們可以用詞彙表初始化這個模型。

對於標準化，XLNet 使用了一些替換的方法（來自 SentencePiece）：

    from tokenizers import Regex

    tokenizer.normalizer = normalizers.Sequence(
        [
            normalizers.Replace("``", '"'),
            normalizers.Replace("''", '"'),
            normalizers.NFKD(),
            normalizers.StripAccents(),
            normalizers.Replace(Regex(" {2,}"), " "),
        ]
    )

這會取代 “ 和 ” 和 ” 以及任何兩個或多個空格與單個空格的序列，以及刪除文本中的重音以進行標記。

用於任何 SentencePiece 標記器的預標記器是 `Metaspace`：

    tokenizer.pre_tokenizer = pre_tokenizers.Metaspace()

我們可以像以前一樣查看示例文本的預標記化：

    tokenizer.pre_tokenizer.pre_tokenize_str("Let's test the pre-tokenizer!")

    [("▁Let's", (0, 5)), ('▁test', (5, 10)), ('▁the', (10, 14)), ('▁pre-tokenizer!', (14, 29))]

接下來是需要訓練的模型。 XLNet 有不少特殊的標記：

    special_tokens = ["<cls>", "<sep>", "<unk>", "<pad>", "<mask>", "<s>", "</s>"]
    trainer = trainers.UnigramTrainer(
        vocab_size=25000, special_tokens=special_tokens, unk_token="<unk>"
    )
    tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

不要忘記`UnigramTrainer` 的一個非常重要的參數是`unk_token`。 我們還可以傳遞特定於 Unigram 算法的其他參數，例如刪除標記的每個步驟的“shrinking_factor（收縮因子）”（默認為 0.75）或指定給定標記的最大長度的“max_piece_length”（默認為 16） .

這個標記器也可以在文本文件上訓練：

    tokenizer.model = models.Unigram()
    tokenizer.train(["wikitext-2.txt"], trainer=trainer)

讓我們看一下示例文本的標記化後的結果：

    encoding = tokenizer.encode("Let's test this tokenizer.")
    print(encoding.tokens)

    ['▁Let', "'", 's', '▁test', '▁this', '▁to', 'ken', 'izer', '.']

A peculiarity of XLNet is that it puts the `<cls>` token at the end of the sentence, with a type ID of 2 (to distinguish it from the other tokens). It’s padding on the left, as a result. We can deal with all the special tokens and token type IDs with a template, like for BERT, but first we have to get the IDs of the `<cls>` and `<sep>` tokens: XLNet 的一個特點是它將`<cls>` 標記放在句子的末尾，類型ID 為2（以將其與其他標記區分開來）。它會將結果填充在左側。 我們可以使用模板處理所有特殊標記和標記類型 ID，例如 BERT，但首先我們必須獲取 `<cls>` 和 `<sep>` 標記的 ID：

    cls_token_id = tokenizer.token_to_id("<cls>")
    sep_token_id = tokenizer.token_to_id("<sep>")
    print(cls_token_id, sep_token_id)

    0 1

模板如下所示：

    tokenizer.post_processor = processors.TemplateProcessing(
        single="$A:0 <sep>:0 <cls>:2",
        pair="$A:0 <sep>:0 $B:1 <sep>:1 <cls>:2",
        special_tokens=[("<sep>", sep_token_id), ("<cls>", cls_token_id)],
    )

我們可以通過編碼一對句子來測試它的工作原理：

    encoding = tokenizer.encode("Let's test this tokenizer...", "on a pair of sentences!")
    print(encoding.tokens)
    print(encoding.type_ids)

    ['▁Let', "'", 's', '▁test', '▁this', '▁to', 'ken', 'izer', '.', '.', '.', '<sep>', '▁', 'on', '▁', 'a', '▁pair',
      '▁of', '▁sentence', 's', '!', '<sep>', '<cls>']
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]

最後，我們添加一個 Metaspace 解碼器：

    tokenizer.decoder = decoders.Metaspace()

我們完成了這個標記器！ 我們可以像以前一樣保存標記器，如果我們想在 🤗 Transformers 中使用它，可以將它包裝在 `PreTrainedTokenizerFast` 或 `XLNetTokenizerFast` 中。 使用 `PreTrainedTokenizerFast` 時要注意的一件事是，我們需要告訴🤗 Transformers 庫應該在左側填充特殊標記：

    from transformers import PreTrainedTokenizerFast

    wrapped_tokenizer = PreTrainedTokenizerFast(
        tokenizer_object=tokenizer,
        bos_token="<s>",
        eos_token="</s>",
        unk_token="<unk>",
        pad_token="<pad>",
        cls_token="<cls>",
        sep_token="<sep>",
        mask_token="<mask>",
        padding_side="left",
    )

或者：

    from transformers import XLNetTokenizerFast

    wrapped_tokenizer = XLNetTokenizerFast(tokenizer_object=tokenizer)

現在您已經瞭解瞭如何使用各種構建塊來構建現有的標記器，您應該能夠使用 🤗 tokenizer庫編寫您想要的任何標記器，並能夠在 🤗 Transformers中使用它。