Hugging Face Tokenizer 库
tokenizers 库的各个组件的使用查看 huggingface 官方文档:The tokenization pipeline
在 tokenizers 库中,对文本进行编码一般包括以下四个步骤:
- normalization(规范化):规范化是指对文本进行标准化处理,以消除文本中的噪音、统一文本格式,确保不同形式的相同文本能够被正确处理。规范化可能包括转换为小写、移除特殊字符、处理缩写、消除重音符号等操作。
- pre-tokenization(预分词):预分词是指在将文本分割成 token 之前的预处理步骤。在这一阶段,文本可能会被分割成单词、字符或者子词。预分词的目的是为了使后续的模型能够更好地处理文本,例如在基于子词的模型中,预分词可能会使用 BPE 或者 SentencePiece 算法将文本分割成子词单元。
- model(模型):模型阶段是指使用特定的子词模型或者词汇表对文本进行编码。在这一阶段,文本会被映射到相应的子词或者词汇表中的 token,以便进一步的处理和分析。
- 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。
参数解释如下:
- text_iterator (generator of
List[str]):
-
- 这个参数是训练语料的迭代器,应该是一个生成器,用于提供文本的批次。例如,如果你已经将所有文本加载到内存中,那么它可以是一个文本列表的列表。
- vocab_size (
int):
-
- 这个参数表示你想要为你的tokenizer设置的词汇表大小。
- length (
int, optional):
-
- 这个参数表示迭代器中的总序列数。这个参数用于提供有意义的训练进度跟踪信息。这是一个可选参数。
- new_special_tokens (list of
strorAddedToken, optional):
-
- 这个参数表示要添加到正在训练的tokenizer中的新特殊token的列表。这是一个可选参数。
- special_tokens_map (
Dict[str, str], optional):
-
- 如果你想要重命名tokenizer使用的一些特殊token,你可以传递一个旧特殊token名称到新特殊token名称的映射字典到这个参数中。这也是一个可选参数。
- 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
- 要在
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 。
- 最后,这些
wrapped_tokenizer可以使用save_pretrained()方法或push_to_hub()方法来保存到Hugging Face Hub,其中save_pretrained()方法会保存三个文件:tokenizer_config.json、special_tokens_map.json、tokenizer.json。
wrapped_tokenizer.save_pretrained("./mytokenizer")
- 使用 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!
参考文章