如何自定义 tokenizer

1,666 阅读9分钟

Hugging Face Tokenizer 库

tokenizers 库的各个组件的使用查看 huggingface 官方文档:The tokenization pipeline

在 tokenizers 库中,对文本进行编码一般包括以下四个步骤:

  1. normalization(规范化):规范化是指对文本进行标准化处理,以消除文本中的噪音、统一文本格式,确保不同形式的相同文本能够被正确处理。规范化可能包括转换为小写、移除特殊字符、处理缩写、消除重音符号等操作。
  2. pre-tokenization(预分词):预分词是指在将文本分割成 token 之前的预处理步骤。在这一阶段,文本可能会被分割成单词、字符或者子词。预分词的目的是为了使后续的模型能够更好地处理文本,例如在基于子词的模型中,预分词可能会使用 BPE 或者 SentencePiece 算法将文本分割成子词单元。
  3. model(模型):模型阶段是指使用特定的子词模型或者词汇表对文本进行编码。在这一阶段,文本会被映射到相应的子词或者词汇表中的 token,以便进一步的处理和分析。
  4. post-processing(后处理):后处理阶段是对编码后的文本进行一些额外的处理步骤,例如添加特殊标记、截断或者填充文本,以确保编码后的文本符合特定模型的输入要求。

对一个输入文本进行编码的流程如下:

这些步骤在对文本进行编码和处理时非常重要,能够确保文本数据被正确地转换成适合模型处理的格式,并确保模型的输入数据符合预期的格式和要求。

从头开始训练 WordPiece

如果你感兴趣的语言中没有语言模型,或者你的语料库与训练语言模型的语料库非常不同,你很可能希望使用适合你数据的tokenizer从头开始重新训练模型. 这将需要在你的数据集上训练一个新的tokenizer。

训练一个分词器并不同于训练一个模型!模型训练使用随机梯度下降来使损失在每个批次中变得更小一点。它的性质是随机的(这意味着在进行相同的训练两次时,你必须设置一些种子以获得相同的结果)。训练一个分词器是一个统计过程,试图确定对于给定语料库来说哪些子词是最好的选择,用于选择它们的确切规则取决于分词算法。它是确定性的,这意味着当在相同的语料库上使用相同的算法进行训练时,你总是会得到相同的结果。

从头开始训练tokenizer代码:

from tokenizers import decoders, models, normalizers, \
    pre_tokenizers, processors, trainers, Tokenizer

# 使用 WordPiece 模型
model = models.WordPiece(unk_token="[UNK]")  # 未设置 vocab, 因为词表需要从数据中训练
tokenizer = Tokenizer(model)

################# Step1: Normalization(规范化) ###################
tokenizer.normalizer = normalizers.Sequence(
    [normalizers.NFD(),
     # NFD Unicode normalizer, 否则 StripAccents normalizer 无法正确识别带重音的字符
     normalizers.Lowercase(),
     normalizers.StripAccents()]
)  # 这个整体等价于 normalizers.BertNormalizer(lowercase=True)

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

################# Step2: Pre-tokenization(预分词) ###################
tokenizer.pre_tokenizer = pre_tokenizers.Sequence(
    [pre_tokenizers.WhitespaceSplit(),
     pre_tokenizers.Punctuation()]
)  # 这个整体等价于 pre_tokenizers.BertPreTokenizer()

print(tokenizer.pre_tokenizer.pre_tokenize_str("This's me  ."))
# [('This', (0, 4)), ("'", (4, 5)), ('s', (5, 6)), ('me', (7, 9)), ('.', (11, 12))]

################# Step3: Trainer(模型训练器) ###################
special_tokens = ["[UNK]", "[PAD]", "[CLS]", "[SEP]", "[MASK]"]
trainer = trainers.WordPieceTrainer(vocab_size=25000, special_tokens=special_tokens)

################# Step4: dataset(数据集) ###################
from modelscope.msdatasets import MsDataset  # pip install modelscope

dataset = MsDataset.load('modelscope/wikitext', subset_name='wikitext-103-v1', split='train')


def get_training_corpus():
    for i in range(0, len(dataset), 1000):
        yield dataset[i: i + 1000]["text"]  # batch size = 1000


################# Step5: train(训练) ####################
tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer, length=len(dataset))
# tokenizer.train(["wikitext-2.txt"], trainer=trainer) # 也可以从文本文件来训练

## 测试训练好的 WordPiece
encoding = tokenizer.encode("This's me  .")
print(encoding)
# Encoding(num_tokens=5, attributes=[ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing])
print(encoding.ids)
# [1511, 11, 61, 1607, 18]
print(encoding.type_ids)
# [0, 0, 0, 0, 0]
print(encoding.tokens)
# ['this', "'", 's', 'me', '.']
print(encoding.offsets)
# [(0, 4), (4, 5), (5, 6), (7, 9), (11, 12)]
print(encoding.attention_mask)
# [1, 1, 1, 1, 1]
print(encoding.special_tokens_mask)
# [0, 0, 0, 0, 0]
print(encoding.overflowing)
# []

################# Step6: Post-Processing ####################
cls_token_id = tokenizer.token_to_id("[CLS]")
sep_token_id = tokenizer.token_to_id("[SEP]")
print(cls_token_id)
# 2
print(sep_token_id)
# 3

tokenizer.post_processor = processors.TemplateProcessing(
    single="[CLS]:0 $A:0 [SEP]:0",
    pair="[CLS]:0 $A:0 [SEP]:0 $B:1 [SEP]:1",
    special_tokens=[("[CLS]", cls_token_id), ("[SEP]", sep_token_id)],
)

## 测试训练好的 WordPiece(单个句子)
encoding = tokenizer.encode("This's me  .")
print(encoding)
# Encoding(num_tokens=7, attributes=[ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing])
print(encoding.ids)
# [2, 1511, 11, 61, 1607, 18, 3]
print(encoding.type_ids)
# [0, 0, 0, 0, 0, 0, 0]
print(encoding.tokens)
# ['[CLS]', 'this', "'", 's', 'me', '.', '[SEP]']
print(encoding.offsets)
# [(0, 0), (0, 4), (4, 5), (5, 6), (7, 9), (11, 12), (0, 0)]
print(encoding.attention_mask)
# [1, 1, 1, 1, 1, 1, 1]
print(encoding.special_tokens_mask)
# [1, 0, 0, 0, 0, 0, 1]
print(encoding.overflowing)
# []

## 测试训练好的 WordPiece(多个句子)
encoding = tokenizer.encode("This's me  .", "That's is fine-tuning.")
print(encoding)
# Encoding(num_tokens=17, attributes=[ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing])
print(encoding.ids)
# [2, 1511, 11, 61, 1607, 18, 3, 1389, 11, 61, 1390, 6774, 17, 4992, 1343, 18, 3]
print(encoding.type_ids)
# [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
print(encoding.tokens)
# ['[CLS]', 'this', "'", 's', 'me', '.', '[SEP]', 'that', "'", 's', 'is', 'fine', '-', 'tun', '##ing', '.', '[SEP]']
print(encoding.offsets)
# [(0, 0), (0, 4), (4, 5), (5, 6), (7, 9), (11, 12), (0, 0), (0, 4), (4, 5), (5, 6), (7, 9), (10, 14), (14, 15), (15, 18), (18, 21), (21, 22), (0, 0)]
print(encoding.attention_mask)
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
print(encoding.special_tokens_mask)
# [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
print(encoding.overflowing)
# []

################# Step7: Decode ####################
tokenizer.decoder = decoders.WordPiece(prefix="##")
tokenizer.decode(encoding.ids)  # 注意:空格没有被还原
# "this's me. that's is fine - tuning."

################# Step8: Save ####################
tokenizer.save("tokenizer.json")
new_tokenizer = Tokenizer.from_file("tokenizer.json")
print(new_tokenizer.decode(encoding.ids))
# this's me. that's is fine - tuning.


################# Step9: Save with Transformers ####################

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]",
)

wrapped_tokenizer.save_pretrained("./mytokenizer")
# 会保存三个文件:tokenizer_config.json、special_tokens_map.json、tokenizer.json
print('保存自定义的tokenizer成功')

################# Step10: Use with Transformers 使用自己训练好的tokenizer进行编码和解码 ####################
from transformers import AutoTokenizer

mytokenizer = AutoTokenizer.from_pretrained("./mytokenizer")  
# 这里可以直接使用 AutoTokenizer 加载,因为在 Step9 中已经保存了 tokenizer.json 文件中设置 "tokenizer_class": "PreTrainedTokenizerFast"
inputs = mytokenizer("Hello, World!")

print(inputs)
# {'input_ids': [2, 21394, 16, 1824, 5, 3], 'token_type_ids': [0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1]}
prompt = mytokenizer.decode(inputs['input_ids'], skip_special_tokens=True)

print(prompt)  # 输出发现将大写转换为了小写,因为我们在tokenier的 normalizer 中设置了 normalizers.Lowercase()
# hello, world!

从旧的 tokenizer 训练新的 tokenizer

通常情况下不需要从头训练一个新的 tokenizer,我们可以从旧的 tokenizer 训练新的 tokenizer,这样就不必指定任何有关tokenizer算法或我们要使用的特殊token的信息,因为新的 tokenizer 会使用相同的组件配置。

使用 PreTrainedTokenizerFast类的train_new_from_iterator()方法可以实现,该方法的作用和参数如下:

作用:

使用当前tokenizer相同的组件配置(normalization、pre-tokenization、model、post-processor),对tokenizer进行训练生成一个新的 tokenizer,以生成适合特定任务的词汇表和特殊token。

参数解释如下:

  1. text_iterator (generator of List[str]):
    • 这个参数是训练语料的迭代器,应该是一个生成器,用于提供文本的批次。例如,如果你已经将所有文本加载到内存中,那么它可以是一个文本列表的列表。
  1. vocab_size (int):
    • 这个参数表示你想要为你的tokenizer设置的词汇表大小。
  1. length (int, optional):
    • 这个参数表示迭代器中的总序列数。这个参数用于提供有意义的训练进度跟踪信息。这是一个可选参数。
  1. new_special_tokens (list of str or AddedToken, optional):
    • 这个参数表示要添加到正在训练的tokenizer中的新特殊token的列表。这是一个可选参数。
  1. special_tokens_map (Dict[str, str], optional):
    • 如果你想要重命名tokenizer使用的一些特殊token,你可以传递一个旧特殊token名称到新特殊token名称的映射字典到这个参数中。这也是一个可选参数。
  1. kwargs (Dict[str, Any], optional):
    • 这个参数是传递给训练器的其他关键字参数,来自于🤗 Tokenizers库。这是一个可选的参数,用于传递一些其他可能需要的参数。

使用gpt2的tokenizer进行训练代码示例:

from transformers import AutoTokenizer
from modelscope.msdatasets import MsDataset  # pip install modelscope

dataset = MsDataset.load('modelscope/wikitext', subset_name='wikitext-103-v1', split='train')

def get_training_corpus():
    for i in range(0, len(dataset), 1000):
        yield dataset[i: i + 1000]["text"]  # batch size = 1000


old_tokenizer = AutoTokenizer.from_pretrained("gpt2")

# 使用训练数据开始训练新的 tokenizer
new_tokenizer = old_tokenizer.train_new_from_iterator(get_training_corpus, 52000)

# 保存新的 tokenizer
tokenizer.save_pretrained("./mytokenizer")

Transformers tokenizer 有一个属性叫做 backend_tokenizer 它提供了对 Tokenizers 库中底层tokenizer的访问。backend_tokenizer 的 normalizer 属性可以获取执行标准化的 normalizer ,backend_tokenizer 的 pre_tokenizer 属性可以获取执行 pre-tokenization 的 pre_tokenizer

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
print(type(tokenizer.backend_tokenizer))
# <class 'tokenizers.Tokenizer'>

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

pre_tokenizer = tokenizer.backend_tokenizer.pre_tokenizer
print(pre_tokenizer.pre_tokenize_str("hello how are  u?")) # are 和 u 之间是双空格
# [('hello', (0, 5)), ('how', (6, 9)), ('are', (10, 13)), ('u', (15, 16)), ('?', (16, 17))]

AutoTokenizer.from_pretrained("gpt2").backend_tokenizer.pre_tokenizer.pre_tokenize_str("hello how are u?")  # are 和 u 之间是双空格
# [('hello', (0, 5)),
#  ('Ġhow', (5, 9)),
#  ('Ġare', (9, 13)),
#  ('Ġ', (13, 14)),
#  ('Ġu', (14, 16)),
#  ('?', (16, 17))]
AutoTokenizer.from_pretrained("t5-small").backend_tokenizer.pre_tokenizer.pre_tokenize_str("hello how are u?")  # are 和 u 之间是双空格
# [('▁hello', (0, 5)), ('▁how', (6, 9)), ('▁are', (10, 13)), ('▁u?', (15, 17))]

集成 tokenizer 到 transformers

  1. 要在 Transformers 中使用这个 tokenizer,我们可以将它封装在一个 PreTrainedTokenizerFast 类中。
  • 如果是Transformers 已有的模型,如 BERT,那么就可以用对应的 PreTrainedTokenizerFast 子类,如 BertTokenizerFast
from transformers import BertTokenizerFast


wrapped_tokenizer = BertTokenizerFast(tokenizer_object=tokenizer)
# wrapped_tokenizer = BertTokenizerFast(tokenizer_file="tokenizer.json")
  • 或者也可以直接使用 PreTrainedTokenizerFast,方法为:
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]",
)

注意:我们必须手动设置所有 special token ,因为 PreTrainedTokenizerFast 无法从 tokenizer 对象推断出这些 special token 。

虽然 tokenizer 有 special token 属性,但是这个属性是所有 special token 的集合,无法区分哪个是 CLS、哪个是 SEP 。

  1. 最后,这些 wrapped_tokenizer 可以使用 save_pretrained() 方法或 push_to_hub()方法来保存到 Hugging Face Hub ,其中 save_pretrained() 方法会保存三个文件:tokenizer_config.jsonspecial_tokens_map.jsontokenizer.json
wrapped_tokenizer.save_pretrained("./mytokenizer")
  1. 使用 transformers 加载前面训练的 tokenizer :
from transformers import AutoTokenizer

mytokenizer = AutoTokenizer.from_pretrained("./mytokenizer")  
# 这里可以直接使用 AutoTokenizer 加载,因为在 Step9 保存 tokenizer_config.json 文件时,transfromers 帮我们设置了 "tokenizer_class": "PreTrainedTokenizerFast"
inputs = mytokenizer("Hello, World!")

print(inputs)
# {'input_ids': [2, 21394, 16, 1824, 5, 3], 'token_type_ids': [0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1]}
prompt = mytokenizer.decode(inputs['input_ids'], skip_special_tokens=True)

print(prompt)  # 输出发现将大写转换为了小写,因为我们在tokenier的 normalizer 中设置了 normalizers.Lowercase()
# hello, world!

参考文章

深入理解 tokenizer

Tokenizers

github.com/huggingface…