LLM 課程文件
逐塊構建分詞器
並獲得增強的文件體驗
開始使用
逐塊構建分詞器
正如我們在前幾節中看到的,分詞包括幾個步驟:
- 規範化(對文字進行任何必要的清理,例如刪除空格或重音符號,Unicode 規範化等)
- 預分詞(將輸入文字分割成單詞)
- 透過模型執行輸入(使用預分詞的單詞生成一系列詞元)
- 後處理(新增分詞器的特殊詞元,生成注意力掩碼和詞元型別 ID)
提醒一下,這是整體過程的另一個檢視:
🤗 Tokenizers 庫旨在為這些步驟中的每個步驟提供多種選項,您可以將它們混合搭配使用。在本節中,我們將看到如何從頭開始構建分詞器,而不是像第 2 節中所做的那樣從舊分詞器訓練新分詞器。然後,您將能夠構建您能想到的任何型別的分詞器!
更準確地說,該庫圍繞一個核心 Tokenizer
類構建,其構建塊在子模組中進行分組:
normalizers
包含所有可能型別的Normalizer
(完整列表在此)。pre_tokenizers
包含所有可能型別的PreTokenizer
(完整列表在此)。models
包含您可以使用的各種型別的Model
,例如BPE
、WordPiece
和Unigram
(完整列表在此)。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 庫構建分詞器,我們首先例項化一個帶有 model
的 Tokenizer
物件,然後將其 normalizer
、pre_tokenizer
、post_processor
和 decoder
屬性設定為我們想要的值。
在此示例中,我們將使用 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 設定的經典選項:lowercase
和 strip_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
標準化器將無法正確識別帶重音的字元,從而無法去除它們。
正如我們之前所見,我們可以使用 normalizer
的 normalize_str()
方法來檢視它對給定文字的影響:
print(tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))
hello how are u?
更進一步 如果您在包含 unicode 字元 u"\u0085"
的字串上測試之前兩個規範化器的版本,您肯定會注意到這兩個規範化器並不完全等效。為了不過度複雜化 normalizers.Sequence
版本,我們沒有包含 BertNormalizer
在 clean_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_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()
方法將其上傳到 Hub。
現在我們已經瞭解瞭如何構建 WordPiece 分詞器,接下來我們將為 BPE 分詞器做同樣的事情。我們將稍微快一點,因為您知道所有步驟,並且只突出顯示差異。
從頭開始構建 BPE 分詞器
現在讓我們構建一個 GPT-2 分詞器。與 BERT 分詞器一樣,我們首先使用 BPE 模型初始化一個 Tokenizer
:
tokenizer = Tokenizer(models.BPE())
同樣,如果有一個詞彙表,我們可以用它來初始化這個模型(在這種情況下,我們需要傳遞 vocab
和 merges
),但由於我們將從頭開始訓練,所以我們不需要這樣做。我們也不需要指定 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_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."
太棒了!現在我們完成了,我們可以像以前一樣儲存分詞器,如果想在 🤗 Transformers 中使用它,可以將其包裝在 PreTrainedTokenizerFast
或 GPT2TokenizerFast
中:
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 中使用它,可以將其包裝在 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)
現在您已經瞭解瞭如何使用各種構建塊來構建現有分詞器,您應該能夠使用 🤗 Tokenizers 庫編寫任何您想要的分詞器,並能夠在 🤗 Transformers 中使用它。
< > 在 GitHub 上更新