HuggingfaceNLP笔记6.1Training a new tokenizer from an old one

如果你对感兴趣的语言没有预训练的语言模型,或者你的语料库与模型训练的语料库差异很大,你可能需要从头开始重新训练模型,并使用适应你的数据的分词器。这需要在你的数据集上训练一个新的分词器。但这具体意味着什么呢?在第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

# 这可能需要几分钟时间加载,所以你可以趁此时间喝杯咖啡或茶!
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不会一次性加载所有内容到内存,而是将数据集元素存储在磁盘上的特性。

执行以下操作会创建一个包含1000个文本的嵌套列表,但会将所有内容加载到内存中:

# 如果您的数据集较小,请勿取消注释以下行!
# 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)
)

这段代码不会加载数据集中的任何元素;它只是创建了一个可以在Python for循环中使用的对象。只有在需要它们的时候,文本才会被加载(即在for循环的相应步骤中),并且一次只加载1000个文本。这样,即使处理大型数据集,也不会耗尽所有内存。

生成器对象的问题是它只能使用一次。因此,以下代码不会两次输出前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 文本的语料库,它非常快(在带有 12 核 AMD Ryzen 9 3900X 处理器的计算机上,只需要 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', 'ĊĠĠĠĠ']

除了表示缩进的令牌外,我们还可以看到表示两个缩进的令牌:ĊĠĠĠĠĠĠĠ。特殊Python词汇,如classinitcallselfreturn,都被分词为一个令牌,我们可以看到,分词器不仅会在下划线和点号处分割,还能正确地处理驼峰式命名: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的新存储库,包含tokenizer文件。然后,你可以使用from_pretrained()方法从任何地方加载tokenizer:

# 将 "huggingface-course" 替换为你的实际命名空间,以使用你自己的tokenizer
tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")

现在,你已经准备好从头开始训练语言模型,并针对你的任务进行微调!我们将在第7章(https://huggingface.co/course/chapter7)中深入探讨这一点。但在本章的剩余部分,我们将更深入地了解快速tokenizer,并详细研究当我们调用train_new_from_iterator()方法时会发生什么。

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HITzwx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值