为什么要训练分词器
如果我感兴趣的语言没有现成的语言模型,或者我的语料库与我语言模型训练所用的语料库差异很大,我很可能需要使用适合我数据的 tokenizer从头开始重新训练模型。这将需要在我自己的数据集上训练一个新的 tokenizer。分词器需要分析我的语料库中的所有文本,识别哪些子词在当前语料库中具有重要性且出现频率最高,这就是分词器的训练。
训练分词器与训练模型并不相同。模型训练使用随机梯度下降,目标是使每个批次的损失稍微减小。从而得到训练后的权值。这个过程有随机性。而分词器训练是一个统计过程,试图识别给定语料库中哪些子词是最佳选择,而选择它们的规则取决于分词算法。这个过程是确定的。
Transformers 中有一个非常简单的 API,你可以用它来训练一个与现有分词器具有相同特性的新分词器: AutoTokenizer.train_new_from_iterator()
假设我们想从头开始训练 GPT-2,但使用的语言不是英语,而是 Python 语言。第一个任务是收集大量该语言的数据,形成一个训练语料库。
CodeSearchNet 数据集 中获取目标数据集
含了 GitHub 上多个编程语言的开源库中的数百万个函数。在这里,我们将加载这个数据集中 Python 部分的内容。
from datasets import load_dataset
# This can take a few minutes to load, so grab a coffee or tea while you wait!
raw_datasets = load_dataset("code_search_net", "python")
事实是,数据集将文档字符串和代码分开,并建议对两者进行分词。在这里,我们将仅使用 whole_func_string 列来训练我们的分词器。
注意避免将所有预料一股脑儿加载到内存,像这样:
training_corpus = [
    raw_datasets["train"][i: i + 1000]["whole_func_string"]
    for i in range(0, len(raw_datasets["train"]), 1000)
]
相反地,我们应该使用 使用 Python 生成器,它在真正需要这个数据时才将数据加载到内存中:
training_corpus = (
    raw_datasets["train"][i : i + 1000]["whole_func_string"]
    for i in range(0, len(raw_datasets["train"]), 1000)
)
注意生成器对象只能被使用一次,所以将其包装在一个函数中,以便多次使用。
训练一个新的分词器
有了语料,现在训练新的分词器。我们首先需要加载我们想要与模型配对的分词器(这里,GPT-2):
from transformers import AutoTokenizer
old_tokenizer = AutoTokenizer.from_pretrained("gpt2")
上面的表达表示了 每个模型有其特定的分词器。尽管我要训练一个新的分词器,但先这样做可以避免完全从零开始。这样,我就不必指定分词算法或想要使用的特殊符号;我的新分词器将与 GPT-2 完全相同,唯一变化的是词汇表(vocab.txt),它将由我们在语料库上的训练决定。
如果使用 GPT2 的分词器处理一个 python 函数,结果会是怎样的?
example = '''def add_numbers(a, b):
    """Add the two numbers `a` and `b`."""
    return a + b'''
tokens = old_tokenizer.tokenize(example)
tokens
返回
['def', 'Ġadd', '_', 'n', 'umbers', '(', 'a', ',', 'Ġb', '):', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo',
 'Ġnumbers', 'Ġ`', 'a', '`', 'Ġand', 'Ġ`', 'b', '`', '."', '""', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']
其中有特殊符号,比如 Ġ 和 Ċ ,这并不太高效,
所以需要训练一个新的分词器,看看它是否能解决这些问题。tokenizer 的训练:
tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 52000)
上述在我使用分词器是“快速”分词器时才有效。
Transformers 库包含两种类型的分词器:
- 一些是纯 Python 编写的(用纯 Python 训练一个全新的 tokenizer 会极其缓慢)。
 - 另一类是快速分词器。快速分词器是由 Tokenizers 库支持,前文提到该库是用 Rust 编程语言编写的。性能是很好的。(Python 是数据科学和深度学习应用中最常用的语言,但当任何需要并行化以实现快速执行时,就必须用其他语言编写。比如这里的Rust, 比如模型计算过程中的各种算子用CUDA)
 
看看快速分词器的效果:
tokens = tokenizer.tokenize(example)
tokens
['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
 'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']
分词器学习了一些针对 Python 函数语料库特定的标记:例如,有一个 ĊĠĠĠ 标记表示缩进,有一个 Ġ""" 标记表示开始文档字符串的三引号。分词器还正确地在 _ 处分割了函数名。并且与为训练的能力相比,其长度是更短的,即一个更紧凑的表示。
KAQ:训练了Tokenizer之后,还要更新Transformer的embedding层?
通常需要同步训练或调整 Transformer 的 embedding 层,因为词汇表大小(vocab_size)必须与 embedding 矩阵的行数([vocab_size, hidden_size])保持一致。冻结 Transformer 其他层,只训练新 Embedding 层。
保存训练好的分词器
保存到本地并 upload 到 HF。
Offset mapping
Offset mapping 是分词器(tokenizer)在处理文本时生成的一种映射关系,记录每个 token 在原始输入文本中的字符位置范围(起始和结束偏移量)。即,它将分词后的 token 映射回原始文本的字符索引,表示每个 token 对应原始文本的哪个部分。
他的格式是,对于每个 token,offset mapping 提供一个元组 [start, end),表示该 token 在原始文本中的字符起始位置和结束位置(不包含 end)。