HuggingfaceNLP笔记6.8Building a tokenizer, block by block

在前面的章节中,我们已经了解到分词包括以下几个步骤:

  • 规范化(对文本进行必要的清理,如移除空格或重音,进行 Unicode 规范化等)
  • 预分词(将输入分割成单词)
  • 使用模型处理输入(使用预分词后的单词生成一序列令牌)
  • 后处理(添加分词器的特殊令牌,生成注意力掩码和类型 ID)

为了回顾,这是整个过程的概述:

在这里插入图片描述

🤗 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() 函数是一个生成器,它将按批次(每次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 库构建分词器,我们首先创建一个 Tokenizer 对象,指定一个 model,然后设置其 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库中预先实现的这种方便的正常izer。让我们看看如何手动创建BERT正常器。库提供了LowercaseStripAccents正常器,你可以使用Sequence来组合多个正常器:

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

我们还使用了NFDUnicode正常化器,否则StripAccents正常器可能无法正确识别带音符的字符,从而无法移除它们。

如前所见,我们可以使用normalizernormalize_str()方法来检查它对给定文本的影响:

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

深入学习 如果你在一个包含Unicode字符u"\u0085"的字符串上测试这两个版本的正常器,你肯定会注意到这两个正常器并不完全等价。为了不使使用normalizers.Sequence的版本过于复杂,我们没有包含clean_text设置为True(默认行为)时BertNormalizer所需的正则替换。但别担心,完全可以通过在正常器序列中添加两个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对象中推断出哪个令牌是掩码令牌、[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()方法保存它,或者使用push_to_hub()方法将其上传到Hub。

现在我们已经了解了如何构建WordPiece分词器,接下来让我们做同样的事情,构建一个BPE分词器。由于你已经知道了所有步骤,我们会快速进行,并突出显示差异。

从头开始构建BPE分词器

现在,让我们构建一个GPT-2分词器。与BERT分词器类似,我们首先初始化一个Tokenizer,使用BPE模型:

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=[""])
tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

WordPieceTrainer一样,我们也可以指定min_frequency,或者如果我们有词尾后缀(如</w>),我们可以使用end_of_word_suffix设置它。

这个分词器也可以在文本文件上进行训练:

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

让我们看看一个示例文本的分词:

这是Markdown文件的中文翻译,保持Markdown语法不变:

encoding = tokenizer.encode("让我们测试这个分词器。")
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="",
    eos_token="",
)

或:

from transformers import GPT2TokenizerFast

wrapped_tokenizer = GPT2TokenizerFast(tokenizer_object=tokenizer)

最后一个示例,我们将展示如何从头开始构建一个Unigram分词器。

从头开始构建Unigram分词器

现在,让我们构建一个XLNet分词器。像之前的分词器一样,我们首先初始化一个Tokenizer,使用Unigram模型:

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("让我们测试这个分词器。")
print(encoding.tokens)
['Let', "'", 's', 'test', 'this', 'to', 'ken', 'izer', '.']

XLNet的一个特性是它将<cls> token放在句子的末尾,类型ID为2(以区分其他token)。结果是左边填充。我们可以像处理BERT一样处理所有特殊token和token类型ID,但首先我们需要获取<cls><sep> token的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

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中使用它。在使用PreTrainedTokenizerFast时,请注意,除了特殊token,还需要告诉🤗 Transformers库在左边进行填充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NJU_AI_NB

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值