Hugging Face Course-Diving in 抱抱脸 Tokenizers library (Introduction & BPE)

在这里插入图片描述

Introduction

在前几章中,使用了tokenizer库中别人在特定数据集上已经训练好的tokenizer,但是我在尝试用bert-base的checkpoint加载tokenizer到自己的数据集上,发现并不适用,所以就有了这一个章节,我们使用与模型预训练相同的tokenizer——但是当我们想从头开始训练模型时,我们该怎么做??在这些情况下,使用在来自另一个领域或语言的语料库上预训练的标记器通常是次优的。例如,在英语语料库上训练的分词器在日语文本语料库上表现不佳,因为两种语言中空格和标点符号的使用非常不同。

  • 如何在新的文本语料库上训练与给定checkpoint使用的tokenizer相似的新tokenizer
  • fast tokenizer的特点
  • 当今 NLP 中使用的三种主要子词标记化算法之间的差异
  • 如何使用 🤗 Tokenizers 库从头开始构建一个分词器并在一些数据上训练它

Training a new tokenizer from an old one 从旧的分词器训练新的分词器

如果你感兴趣的语言中没有语言模型,或者你的语料库与训练语言模型的语料库非常不同,你很可能希望使用适合你数据的tokenizer从头开始重新训练模型. 这将需要在你的数据集上训练一个新的分词器

Assembling a corpus

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 这一行来训练我们的tokenizer

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

Training a new tokenizer

现在我们的语料库是文本批量迭代器的形式,我们准备训练一个新的分词器。为此,我们首先需要加载要与模型配对的标记器(此处为 GPT-2):

from transformers import AutoTokenizer

old_tokenizer = AutoTokenizer.from_pretrained("gpt2")

尽管我们要训练一个新的tokenizer,但最好这样做以避免完全从头开始。这样,我们就不必指定任何有关标记化算法或我们要使用的特殊标记的信息;我们的新分词器将与 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)
tokens = tokenizer.tokenize(example)
tokens

输出:

['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
 'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']

在这里,我们再次看到特殊符号Ċ和Ġ那表示空格和换行,但是我们也可以看到,我们的标记者了解到一些token是非常具体的,以Python函数语料库:例如,有一个ĊĠĠĠ token表示,凹口和Ġ"""表示开始文档字符串的三个引号的标记。分词器还正确地拆分了 上的函数名称_。这是一个非常紧凑的表示;相比之下,在同一个例子中使用简单的英语分词器会给我们一个更长的句子:

>>>print(len(tokens))
>>>print(len(old_tokenizer.tokenize(example)))
27
36

保存分词器

tokenizer.save_pretrained("code-search-net-tokenizer")

这将创建一个名为code-search-net-tokenizer的新文件夹,其中将包含标记生成器需要重新加载的所有文件。

Fast tokenizers’ special powers

先鸽着

Fast tokenizers in the QA pipeline

先鸽着

Normalization and pre-tokenization 标准化和预词元化

在我们更深入地研究与 Transformer 模型(字节对编码 [BPE]、WordPiece 和 Unigram)一起使用的三种最常见的子词标记化算法之前,我们将首先看一下每个tokenizer应用于文本的预处理。以下是 tokenization pipeline中步骤的高度概述:

在这里插入图片描述
在将文本拆分为subtokens之前(根据其模型),分词器执行两个步骤:normalization(标准化) and pre-tokenization.(预词元化)

Normalization

规范化步骤涉及一些常规清理,例如删除不必要的空格、小写和/或删除重音。

这个tokenizer对象的normalizer属性有一个normalize_str()方法,我们可以使用它来查看规范化是如何执行的:

>>>print(tokenizer.backend_tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))
'hello how are u?'

在这个例子中我们用的是bert-base-uncasedcheckpoint,这个标准化将大小转化为小写并移除了重音

Pre-tokenization

正如我们将在下一节中看到的,分词器不能单独在原始文本上进行训练。相反,我们首先需要将文本拆分为小实体,例如单词。
这就是预标记化步骤的用武之地。

要查看快速标记器如何执行预标记化,我们可以使用对象pre_tokenize_str()的pre_tokenizer属性方法tokenizer:

>>>tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str("Hello, how are  you?")
[('Hello', (0, 5)), (',', (5, 6)), ('how', (7, 10)), ('are', (11, 14)), ('you', (16, 19)), ('?', (19, 20))]

SentencePiece

SentencePiece是一种用于文本预处理的标记化算法,您可以将其与我们将在接下来的三个部分中看到的任何模型一起使用。它将文本视为 Unicode 字符序列,并用特殊字符▁. 与 Unigram 算法结合使用(参见第 7 节),它甚至不需要预标记化步骤,这对于不使用空格字符的语言(如中文或日语)非常有用。

SentencePiece 的另一个主要特点是可逆标记化:由于没有对空格进行特殊处理,解码标记只需通过将它们连接起来并用_空格替换s 来完成——这会产生规范化的文本。正如我们之前看到的,BERT 分词器删除了重复的空格,因此它的分词是不可逆的。

Algorithm overview

在这里插入图片描述

Byte-Pair Encoding tokenization (BPE 字节对编码)

字节对编码 (BPE) 最初被开发为一种压缩文本的算法,然后在预训练 GPT 模型时被 OpenAI 用于标记化。许多 Transformer 模型都使用它,包括 GPT、GPT-2、RoBERTa、BART 和 DeBERTa。

Training algorithm 训练算法

BPE 训练首先计算语料库中使用的唯一单词集(在完成标准化和预标记化步骤之后),然后通过获取用于编写这些单词的所有符号来构建词汇表。作为一个非常简单的例子,假设我们的语料库使用了这五个词:

"hug", "pug", "pun", "bun", "hugs"

基本词汇将是[“b”, “g”, “h”, “n”, “p”, “s”, “u”],,基本词汇表将包含所有 ASCII 字符,至少,可能还包含一些 Unicode 字符。如果您正在标记的示例使用不在训练语料库中的字符,则该字符将转换为未知token。例如,这就是为什么许多 NLP 模型在分析带有表情符号的内容方面非常糟糕的原因之一。

GPT-2 和 RoBERTa 分词器(非常相似)有一个聪明的方法来处理这个问题:他们不把单词看成是用 Unicode 字符写的,而是用字节写的。这样,基本词汇表的大小很小 (256),但您能想到的每个字符仍将被包含在内,而不会最终转换为未知标记。这个技巧被称为字节级 BPE。

获得这个基本词汇后,我们添加新的token,直到通过学习合并达到所需的词汇量,这是将现有词汇的两个元素合并成一个新元素的规则。因此,在开始时,这些合并将创建具有两个字符的标记,然后随着训练的进行,会创建更长的子词。(一对一对的,意味着两个连续的tokens在一个单词里面).最常见的一对是将会被合并,we rinse and repeat for the next step.

回到我们之前的例子,让我们假设单词具有以下频率:

("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

把每个单词分成单个字母:

("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)

然后按照出现最多次数的 token对进行合并,repeat:

Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug"]
Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un"]
Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("h" "ug" "s", 5)
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
Corpus: ("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)

And we continue like this until we reach the desired vocabulary size.

Tokenization algorithm

词元化紧跟训练过程,从某种意义上说,通过应用以下步骤对新输入进行词元化:

  1. 标准化
  2. 预词元化
  3. 将单词拆分为单个字符
  4. 将学习到的合并规则按顺序应用于这些拆分
("u", "g") -> "ug"
("u", "n") -> "un"
("h", "ug") -> "hug"

Implementing BPE

首先我们需要一个语料库,所以让我们用几句话创建一个简单的语料库:

corpus = [
    "This is the Hugging Face course.",
    "This chapter is about tokenization.",
    "This section shows several tokenizer algorithms.",
    "Hopefully, you will be able to understand how they are trained and generate tokens.",
]

接下来,我们需要将该语料库预先词元化为单词。由于我们正在复制 BPE tokenizer(如 GPT-2),我们将使用gpt2 tokenizer进行预分词:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("gpt2")

然后我们在进行预词元化时计算语料库中每个单词的频率:

from collections import defaultdict

word_freqs = defaultdict(int)

for text in corpus:
    words_with_offsets = tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str(text)
    new_words = [word for word, offset in words_with_offsets]
    for word in new_words:
        word_freqs[word] += 1

print(word_freqs)

输出:

defaultdict(int, {'This': 3, 'Ġis': 2, 'Ġthe': 1, 'ĠHugging': 1, 'ĠFace': 1, 'ĠCourse': 1, '.': 4, 'Ġchapter': 1,
    'Ġabout': 1, 'Ġtokenization': 1, 'Ġsection': 1, 'Ġshows': 1, 'Ġseveral': 1, 'Ġtokenizer': 1, 'Ġalgorithms': 1,
    'Hopefully': 1, ',': 1, 'Ġyou': 1, 'Ġwill': 1, 'Ġbe': 1, 'Ġable': 1, 'Ġto': 1, 'Ġunderstand': 1, 'Ġhow': 1,
    'Ġthey': 1, 'Ġare': 1, 'Ġtrained': 1, 'Ġand': 1, 'Ġgenerate': 1, 'Ġtokens': 1})

下一步是计算基本词汇,由语料库中使用的所有字符组成:

alphabet = []

for word in word_freqs.keys():
    for letter in word:
        if letter not in alphabet:
            alphabet.append(letter)
alphabet.sort()

print(alphabet)

输出:

[ ',', '.', 'C', 'F', 'H', 'T', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's',
  't', 'u', 'v', 'w', 'y', 'z', 'Ġ']

我们还在该词汇表的开头添加了模型使用的特殊标记。在 GPT-2 的例子中,唯一的特殊标记是"<|endoftext|>":

vocab = ["<|endoftext|>"] + alphabet.copy()

我们现在需要将每个单词拆分为单独的字符,以便能够开始训练:

splits = {word: [c for c in word] for word in word_freqs.keys()}

现在我们已准备好进行训练,让我们编写一个函数来计算每对的频率。我们需要在训练的每个步骤中使用它:

def compute_pair_freqs(splits):
    pair_freqs = defaultdict(int)
    for word, freq in word_freqs.items():
        split = splits[word]
        if len(split) == 1:
            continue
        for i in range(len(split) - 1):
            pair = (split[i], split[i + 1])
            pair_freqs[pair] += freq
    return pair_freqs

让我们来看看这本字典在初始拆分后的一部分:

pair_freqs = compute_pair_freqs(splits)

for i, key in enumerate(pair_freqs.keys()):
    print(f"{key}: {pair_freqs[key]}")
    if i >= 5:
        break

输出:

('T', 'h'): 3
('h', 'i'): 3
('i', 's'): 5
('Ġ', 'i'): 2
('Ġ', 't'): 7
('t', 'h'): 3

现在,找到最频繁的对只需要一个快速的循环:

best_pair = ""
max_freq = None

for pair, freq in pair_freqs.items():
    if max_freq is None or max_freq < freq:
        best_pair = pair
        max_freq = freq

print(best_pair, max_freq)

输出:

('Ġ', 't') 7

So the first merge to learn is (‘Ġ’, ‘t’) -> ‘Ġt’, and we add ‘Ġt’ to the vocabulary:

merges = {("Ġ", "t"): "Ġt"}
vocab.append("Ġt")

重复下去,我们需要在我们的splits字典中应用该合并。让我们为此编写另一个函数:

def merge_pair(a, b, splits):
    for word in word_freqs:
        split = splits[word]
        if len(split) == 1:
            continue

        i = 0
        while i < len(split) - 1:
            if split[i] == a and split[i + 1] == b:
                split = split[:i] + [a + b] + split[i + 2 :]
            else:
                i += 1
        splits[word] = split
    return splits

我们可以看看第一次合并的结果:

splits = merge_pair("Ġ", "t", splits)
print(splits["Ġtrained"])

输出:

['Ġt', 'r', 'a', 'i', 'n', 'e', 'd']

现在我们有了循环所需的一切,直到我们学会了我们想要的所有合并。让我们的目标是 50 的词汇量:

vocab_size = 50

while len(vocab) < vocab_size:
    pair_freqs = compute_pair_freqs(splits)
    best_pair = ""
    max_freq = None
    for pair, freq in pair_freqs.items():
        if max_freq is None or max_freq < freq:
            best_pair = pair
            max_freq = freq
    splits = merge_pair(*best_pair, splits)
    merges[best_pair] = best_pair[0] + best_pair[1]
    vocab.append(best_pair[0] + best_pair[1])

结果,我们学习了 19 条合并规则(初始词汇表的大小为 31 ,30 个字母表字符,加上特殊标记):

>>>print(merges)
{('Ġ', 't'): 'Ġt', ('i', 's'): 'is', ('e', 'r'): 'er', ('Ġ', 'a'): 'Ġa', ('Ġt', 'o'): 'Ġto', ('e', 'n'): 'en',
 ('T', 'h'): 'Th', ('Th', 'is'): 'This', ('o', 'u'): 'ou', ('s', 'e'): 'se', ('Ġto', 'k'): 'Ġtok',
 ('Ġtok', 'en'): 'Ġtoken', ('n', 'd'): 'nd', ('Ġ', 'is'): 'Ġis', ('Ġt', 'h'): 'Ġth', ('Ġth', 'e'): 'Ġthe',
 ('i', 'n'): 'in', ('Ġa', 'b'): 'Ġab', ('Ġtoken', 'i'): 'Ġtokeni'}

词汇表由特殊标记、初始字母和所有合并结果组成:


>>>print(vocab)
['<|endoftext|>', ',', '.', 'C', 'F', 'H', 'T', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o',
 'p', 'r', 's', 't', 'u', 'v', 'w', 'y', 'z', 'Ġ', 'Ġt', 'is', 'er', 'Ġa', 'Ġto', 'en', 'Th', 'This', 'ou', 'se',
 'Ġtok', 'Ġtoken', 'nd', 'Ġis', 'Ġth', 'Ġthe', 'in', 'Ġab', 'Ġtokeni']

train_new_from_iterator()在同一个语料库上使用不会产生完全相同的词汇。这是因为当有最频繁对的选择时,我们选择遇到的第一个,而 🤗 Tokenizers 库根据其内部 ID 选择第一个。

为了对新文本进行分词,我们对其进行预分词、拆分,然后应用学到的所有合并规则:

def tokenize(text):
    pre_tokenize_result = tokenizer._tokenizer.pre_tokenizer.pre_tokenize_str(text)
    pre_tokenized_text = [word for word, offset in pre_tokenize_result]
    splits = [[l for l in word] for word in pre_tokenized_text]
    for pair, merge in merges.items():
        for idx, split in enumerate(splits):
            i = 0
            while i < len(split) - 1:
                if split[i] == pair[0] and split[i + 1] == pair[1]:
                    split = split[:i] + [merge] + split[i + 2 :]
                else:
                    i += 1
            splits[idx] = split

    return sum(splits, [])

我们可以在任何由字母表中的字符组成的文本上尝试这个:


>>>tokenize("This is not a token.")
['This', 'Ġis', 'Ġ', 'n', 'o', 't', 'Ġa', 'Ġtoken', '.']

⚠️ 如果有未知字符,我们的实现会抛出错误,因为我们没有做任何处理它们。GPT-2 实际上没有未知标记(使用字节级 BPE 时不可能得到未知字符),但这可能发生在这里,因为我们没有在初始词汇表中包含所有可能的字节。BPE 的这方面超出了本节的范围,因此我们忽略了细节。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值