LLM 课程文档
从旧分词器训练新分词器
并获得增强的文档体验
开始使用
从旧分词器训练新分词器
如果某种语言的模型不可用,或者您的语料库与训练语言模型的语料库非常不同,您很可能需要使用适应您数据的新分词器从头开始重新训练模型。这将需要在您的数据集上训练一个新的分词器。但这究竟意味着什么呢?当我们第一次在第2章中查看分词器时,我们发现大多数Transformer模型都使用子词分词算法。为了识别哪些子词是重要的并在当前语料库中出现最频繁,分词器需要仔细查看语料库中的所有文本——我们称之为训练的过程。管理这种训练的具体规则取决于所使用的分词器类型,我们将在本章后面介绍三种主要算法。
⚠️ 训练分词器与训练模型不同!模型训练使用随机梯度下降法使每个批次的损失略微减小。它本质上是随机的(这意味着您必须设置一些种子才能在两次相同的训练中获得相同的结果)。训练分词器是一个统计过程,旨在识别最适合给定语料库的子词,并且用于选择它们的精确规则取决于分词算法。它是确定性的,这意味着使用相同的算法在相同的语料库上进行训练时,您总是会得到相同的结果。
组装语料库
🤗 Transformers 中有一个非常简单的 API,您可以用来训练具有与现有分词器相同特性的新分词器:`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
})
我们可以看到数据集将文档字符串与代码分开,并建议对两者进行分词。在这里,我们将只使用 `whole_func_string` 列来训练我们的分词器。我们可以通过索引到 `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)
我们需要做的第一件事是将数据集转换为一个文本列表的*迭代器*——例如,一个文本列表的列表。使用文本列表将使我们的分词器运行更快(批量训练文本而不是逐个处理单个文本),如果我们要避免将所有内容一次性加载到内存中,它应该是一个迭代器。如果您的语料库非常庞大,您会希望利用 🤗 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"]
这将生成与之前完全相同的生成器,但允许您使用比列表推导式更复杂的逻辑。
训练新分词器
现在我们的语料库已经以文本批次的迭代器形式存在,我们就可以训练一个新的分词器了。为此,我们首先需要加载我们想要与模型配对的分词器(这里是 GPT-2)。
from transformers import AutoTokenizer
old_tokenizer = AutoTokenizer.from_pretrained("gpt2")
尽管我们将训练一个新的分词器,但这样做是个好主意,可以避免从头开始。这样,我们就不必指定任何关于分词算法或我们想要使用的特殊标记;我们的新分词器将与 GPT-2 完全相同,唯一会改变的是词汇表,它将由我们语料库的训练决定。
首先我们来看看这个分词器如何处理一个示例函数
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']
这个分词器有一些特殊符号,例如 `Ġ` 和 `Ċ`,分别表示空格和换行符。正如我们所看到的,这并不是很有效率:分词器为每个空格返回单独的标记,而它可以将缩进级别(因为四或八个空格的集合在代码中非常常见)组合在一起。它还以有点奇怪的方式拆分了函数名,不习惯看到带有 `_` 字符的单词。
让我们训练一个新分词器,看看它是否能解决这些问题。为此,我们将使用 `train_new_from_iterator()` 方法
tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 52000)
如果您的语料库非常大,此命令可能需要一些时间,但对于这个 1.6 GB 的文本数据集来说,它的速度非常快(在 AMD Ryzen 9 3900X CPU 上,12 核,仅需 1 分 16 秒)。
请注意,`AutoTokenizer.train_new_from_iterator()` 仅在您使用的分词器是“快速”分词器时才有效。正如您将在下一节中看到的,🤗 Transformers 库包含两种类型的分词器:一些完全用 Python 编写,另一些(快速的)则由用 Rust 编程语言编写的 🤗 Tokenizers 库支持。Python 是数据科学和深度学习应用程序中最常用的语言,但是当任何东西需要并行化才能快速时,它就必须用另一种语言编写。例如,模型计算核心的矩阵乘法是用 CUDA 编写的,这是一个针对 GPU 优化的 C 库。
纯 Python 训练一个全新的分词器会极其缓慢,这就是我们开发 🤗 Tokenizers 库的原因。请注意,就像您不需要学习 CUDA 语言就能在 GPU 上对一批输入执行模型一样,您也不需要学习 Rust 就能使用快速分词器。🤗 Tokenizers 库为许多方法提供了 Python 绑定,这些方法在内部调用 Rust 中的一段代码;例如,用于并行化新分词器的训练,或者正如我们在第 3 章中看到的,对一批输入进行分词。
大多数 Transformer 模型都提供了快速分词器(您可以在此处查看一些例外),并且 `AutoTokenizer` API 始终在可用时为您选择快速分词器。在下一节中,我们将介绍快速分词器的一些其他特殊功能,这些功能对于标记分类和问答等任务非常有用。但是,在深入探讨之前,让我们先用我们全新的分词器尝试之前的例子。
tokens = tokenizer.tokenize(example) tokens
['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']
这里我们再次看到表示空格和换行符的特殊符号`Ġ`和`Ċ`,但我们也可以看到我们的分词器学习了一些高度特定于Python函数语料库的标记:例如,有一个`ĊĠĠĠ`标记表示缩进,以及一个`Ġ"""`标记表示开始文档字符串的三个引号。分词器还正确地将函数名在`_`处进行了分割。这是一种相当紧凑的表示;相比之下,在相同的例子中使用纯英语分词器会给我们一个更长的句子。
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', 'ĊĠĠĠĠ']
除了表示缩进的标记外,这里我们还可以看到表示双重缩进的标记:`ĊĠĠĠĠĠĠĠ`。`class`、`init`、`call`、`self`和`return`等特殊 Python 单词都被标记为一个词元,我们可以看到,除了在`_`和`.`处分割外,分词器甚至正确地分割了驼峰命名法:`LinearLayer`被标记为`["ĠLinear", "Layer"]`。
保存分词器
为了确保我们以后可以使用它,我们需要保存我们新的分词器。与模型一样,这通过 `save_pretrained()` 方法完成
tokenizer.save_pretrained("code-search-net-tokenizer")
这将创建一个名为 *code-search-net-tokenizer* 的新文件夹,其中将包含分词器重新加载所需的所有文件。如果您想与同事和朋友分享此分词器,可以通过登录您的帐户将其上传到 Hub。如果您在笔记本中工作,有一个便捷函数可以帮助您完成此操作
from huggingface_hub import notebook_login
notebook_login()
这将显示一个您可以在其中输入 Hugging Face 登录凭据的小部件。如果您不在笔记本中工作,只需在终端中键入以下行:
huggingface-cli login
登录后,您可以执行以下命令推送您的分词器
tokenizer.push_to_hub("code-search-net-tokenizer")
这将在您的命名空间中创建一个名为 `code-search-net-tokenizer` 的新仓库,其中包含分词器文件。然后您可以使用 `from_pretrained()` 方法从任何地方加载分词器
# Replace "huggingface-course" below with your actual namespace to use your own tokenizer
tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")
现在您已准备好从头开始训练语言模型并在您的任务上对其进行微调!我们将在第 7 章中介绍这一点,但首先,在本章的其余部分,我们将仔细研究快速分词器,并详细探讨当我们调用 `train_new_from_iterator()` 方法时实际发生了什么。
< > 在 GitHub 上更新