LLM 课程文档
WordPiece 词元化
并获得增强的文档体验
开始使用
WordPiece 词元化
WordPiece 是 Google 开发的用于预训练 BERT 的词元化算法。此后,它在许多基于 BERT 的 Transformer 模型中被重复使用,例如 DistilBERT、MobileBERT、Funnel Transformers 和 MPNET。它在训练方面与 BPE 非常相似,但实际的词元化方式有所不同。
💡 本节深入探讨 WordPiece,甚至展示了完整的实现。如果您只想大致了解词元化算法,可以跳到结尾。
训练算法
⚠️ Google 从未开源其 WordPiece 训练算法的实现,因此以下内容是我们根据已发表的文献做出的最佳猜测。它可能不是 100% 准确。
与 BPE 类似,WordPiece 从一个小词汇表开始,其中包括模型使用的特殊标记和初始字母表。由于它通过添加前缀(例如 BERT 的 ##
)来识别子词,因此每个单词最初都会通过将该前缀添加到单词内部的所有字符来拆分。因此,例如,"word"
会像这样拆分
w ##o ##r ##d
因此,初始字母表包含出现在单词开头的全部字符,以及出现在单词内部并以 WordPiece 前缀开头的字符。
然后,再次像 BPE 一样,WordPiece 学习合并规则。主要区别在于选择要合并的对的方式。WordPiece 不是选择最频繁的对,而是使用以下公式计算每对的分数
通过将词对的频率除以其每个部分的频率的乘积,该算法优先合并词汇表中各个部分不太频繁的词对。例如,即使 ("un", "##able")
词对在词汇表中非常频繁地出现,它也不一定会合并,因为 "un"
和 "##able"
这两个词对可能各自出现在许多其他单词中并具有很高的频率。相比之下,像 ("hu", "##gging")
这样的词对可能会更快地合并(假设单词“hugging”经常出现在词汇表中),因为 "hu"
和 "##gging"
单独出现的频率可能较低。
让我们看一下我们在 BPE 训练示例中使用的相同词汇表
("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)
因此,初始词汇表将是 ["b", "h", "p", "##g", "##n", "##s", "##u"]
(如果我们暂时忘记特殊标记)。最频繁的词对是 ("##u", "##g")
(出现 20 次),但 "##u"
的单独频率非常高,因此其得分不是最高的(为 1/36)。所有带有 "##u"
的词对实际上都具有相同的分数 (1/36),因此最佳分数给了词对 ("##g", "##s")
— 唯一一个没有 "##u"
的词对 — 得分为 1/20,并且学到的第一个合并是 ("##g", "##s") -> ("##gs")
。
请注意,当我们合并时,我们会删除两个标记之间的 ##
,因此我们将 "##gs"
添加到词汇表并在语料库的单词中应用合并
Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs"]
Corpus: ("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("h" "##u" "##gs", 5)
此时,"##u"
存在于所有可能的词对中,因此它们都以相同的分数结束。假设在这种情况下,第一个词对被合并,因此 ("h", "##u") -> "hu"
。这将我们带到
Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs", "hu"]
Corpus: ("hu" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("hu" "##gs", 5)
然后,下一个最佳分数由 ("hu", "##g")
和 ("hu", "##gs")
共享(为 1/15,而所有其他词对为 1/21),因此合并了得分最高的第一个词对
Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs", "hu", "hug"]
Corpus: ("hug", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("hu" "##gs", 5)
我们像这样继续,直到达到所需的词汇量大小。
✏️ 现在轮到你了! 下一个合并规则是什么?
词元化算法
WordPiece 和 BPE 的词元化方式不同之处在于,WordPiece 仅保存最终词汇表,而不保存学习到的合并规则。从要词元化的单词开始,WordPiece 找到词汇表中最长的子词,然后在该处拆分。例如,如果我们使用上面示例中学习到的词汇表,对于单词 "hugs"
,从开头开始的最长子词是 "hug"
,它在词汇表中,因此我们在那里拆分并得到 ["hug", "##s"]
。然后我们继续处理 "##s"
,它在词汇表中,因此 "hugs"
的词元化结果是 ["hug", "##s"]
。
使用 BPE,我们将按顺序应用学习到的合并并将其词元化为 ["hu", "##gs"]
,因此编码不同。
作为另一个示例,让我们看看单词 "bugs"
将如何词元化。"b"
是从单词开头开始的最长子词,它在词汇表中,因此我们在那里拆分并得到 ["b", "##ugs"]
。然后 "##u"
是从 "##ugs"
开头开始的最长子词,它在词汇表中,因此我们在那里拆分并得到 ["b", "##u, "##gs"]
。最后,"##gs"
在词汇表中,因此最后一个列表是 "bugs"
的词元化结果。
当词元化达到无法在词汇表中找到子词的阶段时,整个单词将被词元化为未知 — 因此,例如,"mug"
将被词元化为 ["[UNK]"]
,"bum"
也是如此(即使我们可以从 "b"
和 "##u"
开始,"##m"
不在词汇表中,并且最终的词元化结果将只是 ["[UNK]"]
,而不是 ["b", "##u", "[UNK]"]
)。这与 BPE 的另一个不同之处,BPE 只会将词汇表中不存在的单个字符分类为未知。
✏️ 现在轮到你了! 单词 "pugs"
将如何词元化?
实现 WordPiece
现在让我们看一下 WordPiece 算法的实现。与 BPE 一样,这只是教学性的,您无法在大型语料库上使用它。
我们将使用与 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.",
]
首先,我们需要将语料库预分词化为单词。由于我们正在复制 WordPiece 分词器(如 BERT),我们将使用 bert-base-cased
分词器进行预分词化
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
然后,我们在执行预分词化时计算语料库中每个单词的频率
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
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():
if word[0] not in alphabet:
alphabet.append(word[0])
for letter in word[1:]:
if f"##{letter}" not in alphabet:
alphabet.append(f"##{letter}")
alphabet.sort()
alphabet
print(alphabet)
['##a', '##b', '##c', '##d', '##e', '##f', '##g', '##h', '##i', '##k', '##l', '##m', '##n', '##o', '##p', '##r', '##s',
'##t', '##u', '##v', '##w', '##y', '##z', ',', '.', 'C', 'F', 'H', 'T', 'a', 'b', 'c', 'g', 'h', 'i', 's', 't', 'u',
'w', 'y']
我们还将模型使用的特殊标记添加到该词汇表的开头。在 BERT 的情况下,它是列表 ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]
vocab = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"] + alphabet.copy()
接下来,我们需要拆分每个单词,所有非首字母的字母都以 ##
为前缀
splits = {
word: [c if i == 0 else f"##{c}" for i, c in enumerate(word)]
for word in word_freqs.keys()
}
现在我们准备好进行训练了,让我们编写一个函数来计算每对的分数。在训练的每个步骤中,我们都需要使用它
def compute_pair_scores(splits):
letter_freqs = defaultdict(int)
pair_freqs = defaultdict(int)
for word, freq in word_freqs.items():
split = splits[word]
if len(split) == 1:
letter_freqs[split[0]] += freq
continue
for i in range(len(split) - 1):
pair = (split[i], split[i + 1])
letter_freqs[split[i]] += freq
pair_freqs[pair] += freq
letter_freqs[split[-1]] += freq
scores = {
pair: freq / (letter_freqs[pair[0]] * letter_freqs[pair[1]])
for pair, freq in pair_freqs.items()
}
return scores
让我们看一下初始拆分后此字典的一部分
pair_scores = compute_pair_scores(splits)
for i, key in enumerate(pair_scores.keys()):
print(f"{key}: {pair_scores[key]}")
if i >= 5:
break
('T', '##h'): 0.125
('##h', '##i'): 0.03409090909090909
('##i', '##s'): 0.02727272727272727
('i', '##s'): 0.1
('t', '##h'): 0.03571428571428571
('##h', '##e'): 0.011904761904761904
现在,找到得分最高的词对只需要一个快速循环
best_pair = ""
max_score = None
for pair, score in pair_scores.items():
if max_score is None or max_score < score:
best_pair = pair
max_score = score
print(best_pair, max_score)
('a', '##b') 0.2
因此,要学习的第一个合并是 ('a', '##b') -> 'ab'
,我们将 'ab'
添加到词汇表中
vocab.append("ab")
要继续,我们需要在我们的 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:
merge = a + b[2:] if b.startswith("##") else a + b
split = split[:i] + [merge] + split[i + 2 :]
else:
i += 1
splits[word] = split
return splits
我们可以看一下第一次合并的结果
splits = merge_pair("a", "##b", splits)
splits["about"]
['ab', '##o', '##u', '##t']
现在我们拥有了循环所需的一切,直到我们学习了所有想要的合并。让我们以 70 的词汇量大小为目标
vocab_size = 70
while len(vocab) < vocab_size:
scores = compute_pair_scores(splits)
best_pair, max_score = "", None
for pair, score in scores.items():
if max_score is None or max_score < score:
best_pair = pair
max_score = score
splits = merge_pair(*best_pair, splits)
new_token = (
best_pair[0] + best_pair[1][2:]
if best_pair[1].startswith("##")
else best_pair[0] + best_pair[1]
)
vocab.append(new_token)
然后我们可以查看生成的词汇表
print(vocab)
['[PAD]', '[UNK]', '[CLS]', '[SEP]', '[MASK]', '##a', '##b', '##c', '##d', '##e', '##f', '##g', '##h', '##i', '##k',
'##l', '##m', '##n', '##o', '##p', '##r', '##s', '##t', '##u', '##v', '##w', '##y', '##z', ',', '.', 'C', 'F', 'H',
'T', 'a', 'b', 'c', 'g', 'h', 'i', 's', 't', 'u', 'w', 'y', 'ab', '##fu', 'Fa', 'Fac', '##ct', '##ful', '##full', '##fully',
'Th', 'ch', '##hm', 'cha', 'chap', 'chapt', '##thm', 'Hu', 'Hug', 'Hugg', 'sh', 'th', 'is', '##thms', '##za', '##zat',
'##ut']
正如我们所见,与 BPE 相比,此分词器学习单词部分作为标记的速度稍快一些。
💡 在同一语料库上使用 train_new_from_iterator()
不会产生完全相同的词汇表。这是因为 🤗 Tokenizers 库没有实现 WordPiece 用于训练(因为我们不完全确定其内部结构),而是使用了 BPE。
要对新文本进行分词,我们对其进行预分词化、拆分,然后在每个单词上应用分词算法。也就是说,我们查找从第一个单词开头开始的最大子词并拆分它,然后在第二部分重复该过程,依此类推,处理该单词的其余部分以及文本中的后续单词
def encode_word(word):
tokens = []
while len(word) > 0:
i = len(word)
while i > 0 and word[:i] not in vocab:
i -= 1
if i == 0:
return ["[UNK]"]
tokens.append(word[:i])
word = word[i:]
if len(word) > 0:
word = f"##{word}"
return tokens
让我们在一个词汇表中的单词和一个不在词汇表中的单词上对其进行测试
print(encode_word("Hugging"))
print(encode_word("HOgging"))
['Hugg', '##i', '##n', '##g']
['[UNK]']
现在,让我们编写一个对文本进行分词的函数
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]
encoded_words = [encode_word(word) for word in pre_tokenized_text]
return sum(encoded_words, [])
我们可以在任何文本上尝试它
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]']
这就是 WordPiece 算法的全部内容!现在让我们看一下 Unigram。
< > 在 GitHub 上更新