【自己创建分词器】WordPiece tokenizer
【自己创建分词器】BPE tokenizer
【自己创建分词器】Unigram tokenizer
1 整体步骤
分词包括以下几个步骤:
- 标准化(Normalization,对文本进行必要的清理,例如去除空格、重音、Unicode标准化等)。
- 预分词(Pre-tokenization,将输入拆分为单词)。
- 将输入传递给模型(Model,使用预分词的单词生成令牌序列)。
- 后处理(Post-processing,添加分词器的特殊令牌,生成注意力掩码和令牌类型ID)。
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个文本,后面将使用这些文本来训练分词器。
分词器还可以直接在文本文件上进行训练。以下是如何生成一个包含所有来自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")
3 全过程
3.1 import
要使用Tokenizers库构建一个分词器,首先我们实例化一个Tokenizer对象,然后将其normalizer、pre_tokenizer、post_processor和decoder属性设置为所需的值。
在这个示例中,我们将创建一个带有WordPiece模型的分词器:
from tokenizers import (
decoders,
models,
normalizers,
pre_tokenizers,
processors,
trainers,
Tokenizer,
)
tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))
我们需要指定unk_token,这样模型就知道在遇到它以前没有见过的字符时该返回什么。我们还可以在这里设置其他参数,包括我们模型的词汇表(我们将训练模型,所以不需要设置这个参数),以及max_input_chars_per_word,它指定每个单词的最大长度(超过这个长度的单词将被拆分)。
3.2 normalization
分词的第一步是规范化,让我们从这一步开始。由于BERT被广泛使用,所以有一个BertNormalizer,其中包含我们可以为BERT设置的经典选项:lowercase和strip_accents,分别用于转成小写和去除口音;clean_text用于删除所有控制字符,并将重复的空格替换为一个;handle_chinese_chars用于在中文字符周围放置空格。要复制bert-base-uncased分词器,我们只需设置这个规范化器:
"""
第一步:normalization
对于Bert的是BertNormalizer
操作有:
lowercase:转成小写
strip_accents:去除个体性(口音)
clean_text:删除所有控制字符并将重复空格替换为单个控制字符
handle_chinese_chars:在汉字周围放置空格
"""
# 使用现成的BertNormalizer
tokenizer.normalizer = normalizers.BertNormalizer(lowercase=True)
然而,一般来说,在构建一个新的分词器时,您可能无法像Tokenizers库中已实现的那样轻松地获取这种便捷的规范化器。因此,让我们看看如何手动创建BERT规范化器。库中提供了一个Lowercase规范化器和一个StripAccents规范化器,您可以使用Sequence组合多个规范化器:
# 没有现成的
# 手工创建 BERT 标准化器:
tokenizer.normalizer = normalizers.Sequence(
[
# NFD Unicode 规范化程序
normalizers.NFD(),
# 转小写
normalizers.Lowercase(),
# 去口音
normalizers.StripAccents()
]
)
这里还使用了NFD Unicode规范化器,否则StripAccents规范化器将无法正确识别带重音的字符,从而无法将它们去除。
正如之前看到的,我们可以使用规范化器的normalize_str()方法来查看它对给定文本的影响:
"""
使用normalize_str()方法验证normalizer是否产生预期效果
"""
print(tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))
3.3 pre-tokenization
同样,这里也有一个我们可以使用的预先创建好的BertPreTokenizer:
# 依赖库中现成的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步骤一样,你也可以使用一个序列来组合多个不同的pre-tokenizers:
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))]
3.4 Model
在标记化流程中的下一步是将输入通过模型。我们已经在初始化中指定了我们的模型,但我们仍然需要对其进行训练,这将需要一个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,其中包含了分词器各种必要的输出属性:ids、type_ids、tokens、offsets、attention_mask、special_tokens_mask 以及 overflowing。
3.5 post-processing
分词流程的最后一步是post-processing。我们需要在开头添加[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)
out:2 3
编写TemplateProcessor的模板,我们需要指定如何处理单个句子和一对句子。对于这两种情况,我们编写我们想要使用的特殊标记;第一个(或单一)句子由
A
表示,而第二个句子(如果编码为一对)由
A表示,而第二个句子(如果编码为一对)由
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)
out:"let's test this tokenizer... on a pair of sentences."
3.6 save and load
save
我们可以将标记器保存在一个 JSON 文件中,如下所示:
tokenizer.save("tokenizer.json")
load
然后,我们可以使用 from _ file ()方法在 Tokenizer 对象中重新加载该文件:
new_tokenizer = Tokenizer.from_file("tokenizer.json")
4 最后
要在Transformers中使用这个分词器,我们需要将它包装在一个PreTrainedTokenizerFast中。我们可以使用通用的类,或者如果我们的分词器对应于现有的模型,则可以使用相应的类(例如,BertTokenizerFast)。如果您构建一个全新的分词器,您将需要使用第一个选项。
要将分词器包装在PreTrainedTokenizerFast中,我们可以传递我们构建的分词器作为tokenizer_object,也可以传递我们保存的分词器文件作为tokenizer_file。需要记住的关键是,我们必须手动设置所有特殊标记,因为该类无法从分词器对象中推断出哪个标记是掩码标记,[CLS]标记等等:
from transformers import PreTrainedTokenizerFast
wrapped_tokenizer = PreTrainedTokenizerFast(
tokenizer_object=tokenizer,
# tokenizer_file="tokenizer.json", # 或者,从文件中加载
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()方法保存它。
参考:
https://huggingface.co/learn/nlp-course/chapter6/8?fw=pt#building-a-wordpiece-tokenizer-from-scratch