分块构建分词器
正如我们在前几节中看到的,分词包含多个步骤
- 规范化(对文本进行任何必要清理,例如删除空格或重音符号、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()
函数是一个生成器,它将生成 1000 个文本的批次,我们将使用这些批次来训练分词器。
🤗 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())
与 BERT 一样,如果我们有一个词汇表,我们可以用它来初始化此模型(在这种情况下,我们需要传递 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 中使用它。