LLM 课程文档
从旧 tokenizer 训练新 tokenizer
并获取增强的文档体验
开始
从旧 tokenizer 训练新 tokenizer
如果您感兴趣的语言没有可用的语言模型,或者您的语料库与您的语言模型训练所用的语料库差异很大,您很可能需要从头开始重新训练模型,并使用适合您数据的 tokenizer。这将需要在您的数据集上训练新的 tokenizer。但这到底意味着什么呢?当我们在第 2 章中首次研究 tokenizer 时,我们看到大多数 Transformer 模型使用子词 tokenizer 算法。为了识别哪些子词是感兴趣的,并且在手头的语料库中出现频率最高,tokenizer 需要仔细查看语料库中的所有文本——我们将此过程称为训练。控制此训练的确切规则取决于所使用的 tokenizer 类型,我们将在本章后面介绍三种主要的算法。
⚠️ 训练 tokenizer 与训练模型不同!模型训练使用随机梯度下降,使每个批次的损失略微减小。它的本质是随机的(这意味着您必须设置一些种子,以便在两次执行相同的训练时获得相同的结果)。训练 tokenizer 是一个统计过程,旨在识别哪些子词最适合给定的语料库,而用于选择它们的精确规则取决于 tokenizer 算法。它是确定性的,这意味着当使用相同的算法在相同的语料库上进行训练时,您总是会得到相同的结果。
组装语料库
🤗 Transformers 中有一个非常简单的 API,您可以使用它来训练具有与现有 tokenizer 相同特征的新 tokenizer:AutoTokenizer.train_new_from_iterator()
。为了看到它的实际效果,假设我们想从头开始训练 GPT-2,但使用英语以外的语言。我们的首要任务将是以该语言收集大量数据,形成训练语料库。为了提供每个人都能理解的示例,我们不会在此处使用俄语或中文等语言,而是使用一种专门的英语:Python 代码。
🤗 Datasets 库可以帮助我们组装 Python 源代码的语料库。我们将使用常用的 load_dataset()
函数来下载和缓存 CodeSearchNet 数据集。此数据集是为 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")
我们可以查看训练拆分,以了解我们可以访问哪些列
raw_datasets["train"]
Dataset({
features: ['repository_name', 'func_path_in_repository', 'func_name', 'whole_func_string', 'language',
'func_code_string', 'func_code_tokens', 'func_documentation_string', 'func_documentation_tokens', 'split_name',
'func_code_url'
],
num_rows: 412178
})
我们可以看到数据集将文档字符串与代码分开,并建议对两者进行 tokenizer 化。在这里,我们将仅使用 whole_func_string
列来训练我们的 tokenizer。我们可以通过索引到 train
拆分中来查看其中一个函数的示例
print(raw_datasets["train"][123456]["whole_func_string"])
应该打印以下内容
def handle_simple_responses(
self, timeout_ms=None, info_cb=DEFAULT_MESSAGE_CALLBACK):
"""Accepts normal responses from the device.
Args:
timeout_ms: Timeout in milliseconds to wait for each response.
info_cb: Optional callback for text sent from the bootloader.
Returns:
OKAY packet's message.
"""
return self._accept_responses('OKAY', info_cb, timeout_ms=timeout_ms)
我们需要做的第一件事是将数据集转换为文本列表的迭代器——例如,文本列表的列表。使用文本列表将使我们的 tokenizer 能够更快地运行(在文本批次上进行训练,而不是逐个处理单个文本),并且如果我们想要避免一次将所有内容都加载到内存中,它应该是一个迭代器。如果您的语料库很大,您将需要利用 🤗 Datasets 不会将所有内容加载到 RAM 中,而是将数据集的元素存储在磁盘上的事实。
执行以下操作将创建一个包含 1,000 个文本列表的列表,但会将所有内容加载到内存中
# Don't uncomment the following line unless your dataset is small!
# training_corpus = [raw_datasets["train"][i: i + 1000]["whole_func_string"] for i in range(0, len(raw_datasets["train"]), 1000)]
使用 Python 生成器,我们可以避免 Python 在实际需要之前将任何内容加载到内存中。要创建这样的生成器,您只需将方括号替换为圆括号
training_corpus = (
raw_datasets["train"][i : i + 1000]["whole_func_string"]
for i in range(0, len(raw_datasets["train"]), 1000)
)
此行代码不会获取数据集的任何元素;它只是创建一个您可以在 Python for
循环中使用的对象。文本只会在您需要它们时(即,当您处于 for
循环的步骤,需要它们时)才会被加载,并且一次只会加载 1,000 个文本。这样,即使您正在处理庞大的数据集,也不会耗尽所有内存。
生成器对象的问题在于它只能使用一次。因此,与其给我们两次前 10 位数字的列表
gen = (i for i in range(10))
print(list(gen))
print(list(gen))
我们得到一次,然后得到一个空列表
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
这就是为什么我们定义一个返回生成器的函数的原因
def get_training_corpus():
return (
raw_datasets["train"][i : i + 1000]["whole_func_string"]
for i in range(0, len(raw_datasets["train"]), 1000)
)
training_corpus = get_training_corpus()
您还可以在 for
循环内使用 yield
语句定义生成器
def get_training_corpus():
dataset = raw_datasets["train"]
for start_idx in range(0, len(dataset), 1000):
samples = dataset[start_idx : start_idx + 1000]
yield samples["whole_func_string"]
这将生成与之前完全相同的生成器,但允许您使用比列表推导式中更复杂的逻辑。
训练新的 tokenizer
现在我们有了以文本批次的迭代器形式存在的语料库,我们准备训练新的 tokenizer 了。为此,我们首先需要加载我们想要与我们的模型(此处为 GPT-2)配对的 tokenizer
from transformers import AutoTokenizer
old_tokenizer = AutoTokenizer.from_pretrained("gpt2")
即使我们将要训练新的 tokenizer,这样做也是一个好主意,以避免完全从头开始。这样,我们就不必指定有关 tokenizer 算法或我们想要使用的特殊 token 的任何内容;我们的新 tokenizer 将与 GPT-2 完全相同,唯一会改变的是词汇表,这将由我们语料库上的训练来确定。
首先,让我们看一下此 tokenizer 将如何处理示例函数
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 为每个空格返回单独的 token,而它可以将缩进级别组合在一起(因为在代码中,具有四或八个空格的集合将非常常见)。它还以有点奇怪的方式拆分了函数名称,不习惯看到带有 _
字符的单词。
让我们训练一个新的 tokenizer,看看它是否解决了这些问题。为此,我们将使用方法 train_new_from_iterator()
tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 52000)
如果您的语料库非常大,则此命令可能需要一些时间,但对于这个 1.6 GB 的文本数据集来说,它的速度非常快(在具有 12 个内核的 AMD Ryzen 9 3900X CPU 上为 1 分 16 秒)。
请注意,AutoTokenizer.train_new_from_iterator()
仅在您使用的 tokenizer 是“快速” tokenizer 时才有效。正如您将在下一节中看到的那样,🤗 Transformers 库包含两种类型的 tokenizer:一些是用纯 Python 编写的,另一些(快速 tokenizer)由 🤗 Tokenizers 库支持,该库是用 Rust 编程语言编写的。Python 是最常用于数据科学和深度学习应用程序的语言,但是当任何东西需要并行化以提高速度时,都必须用另一种语言编写。例如,模型计算核心的矩阵乘法是用 CUDA 编写的,CUDA 是一个针对 GPU 优化的 C 库。
用纯 Python 训练一个全新的 tokenizer 将会非常缓慢,这就是我们开发 🤗 Tokenizers 库的原因。请注意,正如您不必学习 CUDA 语言就能在 GPU 上对一批输入执行模型一样,您也不需要学习 Rust 就能使用快速 tokenizer。🤗 Tokenizers 库为许多内部调用 Rust 中某些代码片段的方法提供了 Python 绑定;例如,并行化您的新 tokenizer 的训练,或者正如我们在第 3 章中看到的那样,对一批输入进行 tokenizer 化。
大多数 Transformer 模型都有可用的快速 tokenizer(您可以在此处查看一些例外情况),并且 AutoTokenizer
API 始终为您选择快速 tokenizer(如果可用)。在下一节中,我们将了解快速 tokenizer 的一些其他特殊功能,这些功能对于 token 分类和问答等任务非常有用。但是,在深入探讨之前,让我们在之前的示例中尝试一下我们全新的 tokenizer
tokens = tokenizer.tokenize(example) tokens
['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']
在这里,我们再次看到了表示空格和换行符的特殊符号 Ġ
和 Ċ
,但我们也可以看到我们的 tokenizer 学习了一些高度特定于 Python 函数语料库的 token:例如,有一个表示缩进的 ĊĠĠĠ
token,以及一个表示启动文档字符串的三个引号的 Ġ"""
token。Tokenizer 还正确地在 _
上拆分了函数名称。这是一个非常紧凑的表示形式;相比之下,在同一示例中使用普通英语 tokenizer 将为我们提供更长的句子
print(len(tokens))
print(len(old_tokenizer.tokenize(example)))
27
36
让我们看另一个例子
example = """class LinearLayer():
def __init__(self, input_size, output_size):
self.weight = torch.randn(input_size, output_size)
self.bias = torch.zeros(output_size)
def __call__(self, x):
return x @ self.weights + self.bias
"""
tokenizer.tokenize(example)
['class', 'ĠLinear', 'Layer', '():', 'ĊĠĠĠ', 'Ġdef', 'Ġ__', 'init', '__(', 'self', ',', 'Ġinput', '_', 'size', ',',
'Ġoutput', '_', 'size', '):', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'weight', 'Ġ=', 'Ġtorch', '.', 'randn', '(', 'input', '_',
'size', ',', 'Ġoutput', '_', 'size', ')', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'bias', 'Ġ=', 'Ġtorch', '.', 'zeros', '(',
'output', '_', 'size', ')', 'ĊĊĠĠĠ', 'Ġdef', 'Ġ__', 'call', '__(', 'self', ',', 'Ġx', '):', 'ĊĠĠĠĠĠĠĠ',
'Ġreturn', 'Ġx', 'Ġ@', 'Ġself', '.', 'weights', 'Ġ+', 'Ġself', '.', 'bias', 'ĊĠĠĠĠ']
除了与缩进对应的 token 之外,在这里我们还可以看到双重缩进的 token:ĊĠĠĠĠĠĠĠ
。像 class
、init
、call
、self
和 return
这样的特殊 Python 单词都被 tokenizer 化为一个 token,我们可以看到,除了在 _
和 .
上拆分之外,tokenizer 甚至正确地拆分了驼峰式命名:LinearLayer
被 tokenizer 化为 ["ĠLinear", "Layer"]
。
保存 tokenizer
为了确保我们以后可以使用它,我们需要保存我们的新 tokenizer。与模型一样,这是通过 save_pretrained()
方法完成的
tokenizer.save_pretrained("code-search-net-tokenizer")
这将创建一个名为 code-search-net-tokenizer 的新文件夹,其中将包含 tokenizer 重新加载所需的所有文件。如果您想与您的同事和朋友分享此 tokenizer,您可以通过登录您的帐户将其上传到 Hub。如果您在笔记本电脑上工作,有一个方便的功能可以帮助您完成此操作
from huggingface_hub import notebook_login
notebook_login()
这将显示一个 widget,您可以在其中输入您的 Hugging Face 登录凭据。如果您不在笔记本电脑上工作,只需在终端中键入以下行
huggingface-cli login
登录后,您可以通过执行以下命令推送您的 tokenizer
tokenizer.push_to_hub("code-search-net-tokenizer")
这将在您的命名空间中创建一个名为 code-search-net-tokenizer
的新存储库,其中包含 tokenizer 文件。然后,您可以从任何地方使用 from_pretrained()
方法加载 tokenizer
# Replace "huggingface-course" below with your actual namespace to use your own tokenizer
tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")
现在,您已准备好从头开始训练语言模型,并针对您手头的任务对其进行微调!我们将在第 7 章中介绍这一点,但首先,在本章的其余部分中,我们将仔细研究快速 tokenizer,并详细探讨当我们调用方法 train_new_from_iterator()
时实际发生了什么。