LLM 課程文件

逐塊構建分詞器

Hugging Face's logo
加入 Hugging Face 社群

並獲得增強的文件體驗

開始使用

逐塊構建分詞器

Ask a Question Open In Colab Open In Studio Lab

正如我們在前幾節中看到的,分詞包括幾個步驟:

  • 規範化(對文字進行任何必要的清理,例如刪除空格或重音符號,Unicode 規範化等)
  • 預分詞(將輸入文字分割成單詞)
  • 透過模型執行輸入(使用預分詞的單詞生成一系列詞元)
  • 後處理(新增分詞器的特殊詞元,生成注意力掩碼和詞元型別 ID)

提醒一下,這是整體過程的另一個檢視:

The tokenization pipeline.

🤗 Tokenizers 庫旨在為這些步驟中的每個步驟提供多種選項,您可以將它們混合搭配使用。在本節中,我們將看到如何從頭開始構建分詞器,而不是像第 2 節中所做的那樣從舊分詞器訓練新分詞器。然後,您將能夠構建您能想到的任何型別的分詞器!

更準確地說,該庫圍繞一個核心 Tokenizer 類構建,其構建塊在子模組中進行分組:

  • normalizers 包含所有可能型別的 Normalizer(完整列表在此)。
  • pre_tokenizers 包含所有可能型別的 PreTokenizer(完整列表在此)。
  • models 包含您可以使用的各種型別的 Model,例如 BPEWordPieceUnigram(完整列表在此)。
  • trainers 包含您可以用來在語料庫上訓練模型的各種型別的 Trainer(每種模型型別一個;完整列表在此)。
  • post_processors 包含您可以使用的各種型別的 PostProcessor(完整列表在此)。
  • decoders 包含您可以用來解碼分詞輸出的各種型別的 Decoder(完整列表在此)。

您可以在此處找到所有構建塊的完整列表。

獲取語料庫

為了訓練我們的新分詞器,我們將使用一小段文字語料庫(這樣示例執行速度快)。獲取語料庫的步驟與我們在本章開頭採取的步驟類似,但這次我們將使用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 庫構建分詞器,我們首先例項化一個帶有 modelTokenizer 物件,然後將其 normalizerpre_tokenizerpost_processordecoder 屬性設定為我們想要的值。

在此示例中,我們將使用 WordPiece 模型建立一個 Tokenizer

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 設定的經典選項:lowercasestrip_accents,這些是自解釋的;clean_text 用於刪除所有控制字元並將重複的空格替換為單個空格;以及 handle_chinese_chars,它在中文字元周圍放置空格。要複製 bert-base-uncased 分詞器,我們只需設定此標準化器:

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

然而,一般來說,在構建新的分詞器時,您將無法訪問 🤗 Tokenizers 庫中已經實現的這種方便的標準化器——所以讓我們看看如何手動建立 BERT 標準化器。該庫提供了一個 Lowercase 標準化器和一個 StripAccents 標準化器,您可以使用 Sequence 組合多個標準化器:

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

我們還使用了 NFD Unicode 標準化器,否則 StripAccents 標準化器將無法正確識別帶重音的字元,從而無法去除它們。

正如我們之前所見,我們可以使用 normalizernormalize_str() 方法來檢視它對給定文字的影響:

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

更進一步 如果您在包含 unicode 字元 u"\u0085" 的字串上測試之前兩個規範化器的版本,您肯定會注意到這兩個規範化器並不完全等效。為了不過度複雜化 normalizers.Sequence 版本,我們沒有包含 BertNormalizerclean_text 引數設定為 True(這是預設行為)時所需的正則表示式替換。但別擔心:透過在規範化器序列中新增兩個 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))]

與標準化器一樣,您可以使用 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_sizespecial_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,它包含了分詞器所有必要的輸出在其各種屬性中:idstype_idstokensoffsetsattention_maskspecial_tokens_maskoverflowing

分詞管道的最後一步是後處理。我們需要在開頭新增 [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() 方法將其上傳到 Hub。

現在我們已經瞭解瞭如何構建 WordPiece 分詞器,接下來我們將為 BPE 分詞器做同樣的事情。我們將稍微快一點,因為您知道所有步驟,並且只突出顯示差異。

從頭開始構建 BPE 分詞器

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

tokenizer = Tokenizer(models.BPE())

同樣,如果有一個詞彙表,我們可以用它來初始化這個模型(在這種情況下,我們需要傳遞 vocabmerges),但由於我們將從頭開始訓練,所以我們不需要這樣做。我們也不需要指定 unk_token,因為 GPT-2 使用位元組級 BPE,它不需要它。

GPT-2 不使用規範化器,因此我們跳過該步驟,直接進入預分詞:

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

我們在此處新增到 ByteLevel 的選項是不要在句子開頭新增空格(否則這是預設值)。我們可以像以前一樣檢視示例文字的預分詞:

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_sizespecial_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."

太棒了!現在我們完成了,我們可以像以前一樣儲存分詞器,如果想在 🤗 Transformers 中使用它,可以將其包裝在 PreTrainedTokenizerFastGPT2TokenizerFast 中:

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', '.']

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 中使用它,可以將其包裝在 PreTrainedTokenizerFastXLNetTokenizerFast 中。在使用 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)

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

< > 在 GitHub 上更新

© . This site is unofficial and not affiliated with Hugging Face, Inc.