自然语言基础: 文本标记算法 (Tokenization Algorithm) : Byte-Pair Encoding (BPE) 和 WordPiece

自然语言基础: 文本标记算法 (Tokenization Algorithm) : Byte-Pair Encoding (BPE) 和 WordPiece

BPE最初是用于文本压缩的算法,当前是最常见tokenizer的编码方法,用于 GPT (OpenAI) 和 Bert (Google) 的 Pre-training Model。

1. 算法

a. Corups

Corpus(语料库)是指收集和组织的一系列文本的集合。它可以是不同类型的文本,如书籍、新闻文章、网页内容、社交媒体帖子等等。语料库是自然语言处理(NLP)和文本挖掘中的重要资源,用于训练和评估模型,研究语言规律和分析文本的特征。

语料库的实际情况可能是由很多句子,或者段落,甚至文章组成,就是训练NLP模型的数据集。 为了简化BPE, 这里假设语料库就下述5个单词:

Corpus = “hug”, “pug”, “pun”, “bun”, “hugs”

b. Characters

Characters(字符)是组成语言的最小单位,它可以是字母、数字、标点符号、空格或其他特殊字符。在自然语言处理(NLP)中,字符是对文本进行处理和分析的基本单元。

每个字符都有一个唯一的Unicode编码,可以使用这些编码来表示和处理字符。不同的语言和字符集可以使用不同的字符集编码,如UTF-8、ASCII、GBK等。

在文本处理中,可以按字符级别进行分析,如文本分类、语言识别、命名实体识别等。字符级别的处理可以捕捉到更细粒度的语义和特征。 例如,在某个字符级别的文本分类任务中,可以将文本分割为单个字符,然后将这些字符提供给模型进行分类。这可以用于识别笔迹、辨识字符或者进行文本生成等任务。

c. Vocabulary

Vocabulary(词汇表)指的是在 语料库 中 所有 不同单词或符 的 集合。它是自然语言处理(NLP)中常用的概念,用于表示某个特定领域或语言的词汇范围。

如果被编码的字符或单词不在语料库中,一般为通过 未知标记符([UNK]) 进行编码。

字节编码每个字符用一个字节表示,即2的8次方,256位。

回到刚才那个例子, Corups语料库中出现的字符组成最初的词汇表 (base vocabulary):

Vocabulary: [“b”, “g”, “h”, “n”, “p”, “s”, “u”]

d. Merges

即,词汇表压缩算法

首先 统计 字符 在 词汇表 中出现的频率, 按频率高低进行统计,我们假设5个单词出现的频率如下:

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

之后我们按字符对上述 统计的频率词汇表,进行词汇分割 (splitting each word into characters from initial vocabulary as a list of tokens):

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

3.2 我们按频率进行首次字符合并:

比如(“h”,“u”)在语料库中总共出现了15次,出现最多的是(“u”、“g”),在词汇表中总共出现了20次。

因此,BPE学习的第一个合并规则是(“u”,“g”)->“ug”,这意味着“ug”将被添加到新的词汇表中,合并的 字符组合 将应用到 语料库 中。在这个阶段结束时,词汇和语料库看起来是这样的:

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)

重复上述步骤,,当前最频繁的 对(pair) 是(“u”,“n”),在语料库中出现了16次,因此学习的第二个合并规则是(“u”,“n”)->“un”。将其添加到词汇表中,合并后:

Vocabulary: [“b”, “g”, “h”, “n”, “p”, “s”, “u”, “ug”, “un”]

Crpus: (“h” “ug”, 10), (“p” “ug”, 5), (“p” “un”, 12), (“b” “un”, 4), (“h” “ug” “s”, 5)

这时最频繁的配对是(“h”,“ug”),所以我们学习合并规则(“h,”ug“)-> (“hug”),这给了我们第一个三个字母的令牌。合并后,语料库如下所示:

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)

重复上述步骤,直到 Vocabulary size 压缩到预先设定的值 (GPT-3 是 50257)

2.代码复现

设置一个句子组成的Corups:


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.",
]

我们需要将该语料库预先标记为单词, 这里使用gpt2令牌化器进行预令牌化 (pre-tokenize),

之后在进行预标记化时计算语料库中每个单词的频率, 用word_freqs保存:

from transformers import AutoTokenizer
from collections import defaultdict

tokenizer = AutoTokenizer.from_pretrained("gpt2")

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保存:

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

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

并写一个函数来计算每个字符对的频率。我们需要在培训的每一步都使用此功能:

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

设置一个压缩词汇表的size,并依次压缩(合并)频率最高的 byte-pair:

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

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) # 词汇表最终由: 特殊标记、初始字母表 和 合并的pairs 组成:

# ['<|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']

现在写一个函数,对一个新文本进行标记(tokenize),我们先对其进行标记,然后对其进行拆分,然后应用学到的所有合并规则:

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', '.']

tips:

如果存在未知字符,我们的实现将抛出错误,因为我们没有采取任何措施来处理它们。
GPT-2实际上没有未知字符,
实际上,使用字节级BPE时不可能获得未知字符。
但这可能发生在这里,因为我们没有在初始词汇表中包括所有可能的字符(字节型) 。
BPE的这一方面超出了本节的范围,因此我们省略了细节。

3. WordPiece

与BPE类似,WordPiece从 特殊标记 和 初始字母表 开始建立 词汇表,主要用于Bert。

WordPiece 添加 prefix前缀(例如BERT中的##)来标识一个词里的非首位字符。

单词最初被分割,将该前缀添加到单词内的所有字符。例如,"word"被分割如下:

“word” = w ##o ##r ##d

WordPiece学习合并规则, 不会选择频率最高的pair对,而是使用以下公式为每个词对计算一个得分:

score=(freq_of_pair)/(freq_of_first_element×freq_of_second_element)

由于分母是 一个pair中 左右元素的乘积,因此其中任意元素较高的频率将不会优先何并(Merge)

这可以看作是BPE的优化,即较高频率子元素将单独作为一个token标记

  • 特殊标记

在bert词汇表中有如下特殊标记:

BERT_list = [“[PAD]”, “[UNK]”, “[CLS]”, “[SEP]”, “[MASK]”]

写个例子:

tokenize("This is the Hugging Face course!")

#['Th', '##i', '##s', 'is', 'th', '##e', 'Hugg', '##i', '##n', '##g', 'Fac', '##e', 'c', '##o', '##u', '##r', '##s',
 # '##e', '[UNK]']

4.Reference

  • https://huggingface.co/learn/nlp-course/chapter6/5?fw=pt

  • https://huggingface.co/learn/nlp-course/chapter6/6?fw=pt

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值