【自己创建分词器tokenizer】(1)——WordPiece tokenizer


【自己创建分词器】WordPiece tokenizer
【自己创建分词器】BPE tokenizer
【自己创建分词器】Unigram tokenizer

1 整体步骤

分词包括以下几个步骤:

  1. 标准化(Normalization,对文本进行必要的清理,例如去除空格、重音、Unicode标准化等)。
  2. 预分词(Pre-tokenization,将输入拆分为单词)。
  3. 将输入传递给模型(Model,使用预分词的单词生成令牌序列)。
  4. 后处理(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

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值