00251 NLP Course - Training a new tokenizer from an old one


前言

如果你感兴趣的语言中没有语言模型,或者你的语料库与你的语言模型训练的语料库非常不同,你很可能会想使用适合你数据的标记器从头开始重新训练模型。这将需要在你的数据集上训练一个新的标记器。但这究竟意味着什么呢?当我们在第二章中第一次查看标记器时,我们看到了大多数Transformer模型使用子词标记算法。为了确定哪些子词是有趣的,并且在手头的语料库中频繁出现,标记器需要仔细查看语料库中的所有文本——我们称之为训练的过程。管理此训练的确切规则取决于使用的标记器类型,我们将在本章后面介绍三种主要算法。

⚠️ 训练标记器与训练模型不同!模型训练使用随机梯度下降,使每个批次的损失稍微减小。它本质上是随机的(这意味着在进行两次相同的训练时,你需要设置一些种子来获得相同的结果)。训练标记器是一个统计过程,试图确定对于给定的语料库,哪些子词是最佳选择,用于选择它们的精确规则取决于标记算法。它是确定性的,意味着当你使用相同的算法在相同的语料库上进行训练时,你总是得到相同的结果。

src link: https://huggingface.co/learn/nlp-course/chapter6/2

Operating System: Ubuntu 22.04.4 LTS

参考文档

  1. NLP Course - Training a new tokenizer from an old one

组装语料库

在🤗 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
})

我们可以看到数据集将文档字符串与代码分开,并建议对两者进行标记化。在这里,我们将只使用whole_func_string列来训练我们的标记器。我们可以通过索引到训练分割中来查看这些函数的一个例子。

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()

您还可以通过使用yield语句在for循环中定义您的生成器:

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的文本数据集来说,它非常快(在12核的AMD Ryzen 9 3900X CPU上只需1分钟16秒)。

请注意,AutoTokenizer.train_new_from_iterator() 只有在您使用的标记器是“快速”标记器时才有效。正如您在下一节中将会看到的那样,🤗 Transformers库包含两种类型的标记器:一些是纯Python编写的,而另一些(快速标记器)则由🤗 Tokenizers库支持,该库是用Rust编程语言编写的。Python是数据科学和深度学习应用中最常用的语言,但是当任何需要并行化以提高速度时,它必须用另一种语言编写。例如,模型计算核心的矩阵乘法是用CUDA编写的,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()方法时实际发生了什么。

结语

第二百五十一篇博文写完,开心!!!!

今天,也是充满希望的一天。


文章作者: LuYF-Lemon-love
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LuYF-Lemon-love !
  目录