笔者看书时,看到书中提到的这个 WordPiece,只是短短的一句带过。
于是查了些资料,做了深入了解。
BERT 使用的 WordPiece 词元化方法是一种基于子词分割的技术,其目的是有效地解决自然语言处理中的词汇表问题,同时提高模型的泛化能力。本文将详细阐述 WordPiece 的工作原理,并通过实例与代码进行深度解析。
WordPiece 的背景与目标
在自然语言处理中,词汇表的大小直接影响模型的性能与效率。传统的词级别词元化方法存在两个主要问题:
- 词汇表过大:直接以单词为单位构建词汇表可能导致存储和计算成本显著增加。
- 词汇覆盖率不足:由于语言的多样性和词形变化,训练过程中难以覆盖所有可能的单词,导致模型遇到未登录词时表现不佳。
WordPiece 通过将单词进一步分解为子词单元,从而在保持词汇表规模适中的同时,显著提高了词汇覆盖率。
WordPiece 的核心思想
WordPiece 基于子词单元构建词汇表。其基本假设是:频繁共现的字符序列更有可能构成有意义的单元。通过频率统计和合并规则,WordPiece 逐步生成最优的子词分割方案。
WordPiece 的分割过程可以总结为以下几个步骤:
- 初始化词汇表:将所有字符(Unicode)作为初始词汇表的基本单元。
- 统计字符对:对训练语料中的每个单词,统计相邻字符对的出现频率。
- 合并最频繁字符对:将最频繁的字符对作为一个新单元加入词汇表,并更新训练语料中的分割方式。
- 重复上述过程:直到词汇表达到预设的大小或频率不再增加显著。
这种方法确保了高频单词能够保留整体单元,而低频单词则被分解为子词甚至单字符,从而提高了模型对未登录词的处理能力。
具体案例解析
为说明 WordPiece 的工作机制,以下是一个实际示例,展示如何从零构建一个简单的词汇表。
假设的语料
假设训练语料包含以下单词:
["low", "lower", "lowest", "new", "newer", "newest"]
目标是利用 WordPiece 构建一个子词词汇表。
初始化词汇表
初始词汇表包含所有可能的字符:
["l", "o", "w", "e", "r", "n", "s", "t"]
第一步:统计字符对
在语料中统计每个单词的字符对(包括空格作为分隔符):
- "low": l-o, o-w
- "lower": l-o, o-w, w-e, e-r
- "lowest": l-o, o-w, w-e, e-s, s-t
- "new": n-e, e-w
- "newer": n-e, e-w, w-e, e-r
- "newest": n-e, e-w, w-e, e-s, s-t
字符对的频率统计如下:
字符对 | 频率 |
---|---|
l-o | 3 |
o-w | 3 |
w-e | 4 |
e-r | 2 |
e-s | 2 |
s-t | 2 |
n-e | 3 |
e-w | 3 |
第二步:合并最频繁字符对
找到频率最高的字符对 w-e
,将其作为新单元 we
加入词汇表,同时更新语料:
更新后的单词:
["low", "lower", "lowest", "new", "newer", "newest"]
分割形式变为:
- "low": l-o, o-w
- "lower": l-o, o-w, we-r
- "lowest": l-o, o-w, we-s, s-t
- "new": n-e, e-w
- "newer": n-e, e-w, we-r
- "newest": n-e, e-w, we-s, s-t
更新后的字符对频率:
字符对 | 频率 |
---|---|
l-o | 3 |
o-w | 3 |
we-r | 2 |
e-r | 0 |
we-s | 2 |
s-t | 2 |
n-e | 3 |
e-w | 3 |
重复上述过程,逐步生成最终的词汇表。
Python 实现代码
以下代码展示了 WordPiece 的简单实现:
from collections import Counter, defaultdict
# 初始化语料
corpus = ["low", "lower", "lowest", "new", "newer", "newest"]
# 将语料分割为字符
def split_to_chars(word):
return list(word) + ["#"] # 添加特殊字符标记子词结尾
# 构建初始语料
split_corpus = [split_to_chars(word) for word in corpus]
# 统计字符对频率
def get_pair_statistics(corpus):
pairs = Counter()
for word in corpus:
for i in range(len(word) - 1):
pairs[(word[i], word[i + 1])] += 1
return pairs
# 合并字符对
def merge_pair(pair, corpus):
new_corpus = []
for word in corpus:
new_word = []
i = 0
while i < len(word):
if i < len(word) - 1 and (word[i], word[i + 1]) == pair:
new_word.append(word[i] + word[i + 1])
i += 2
else:
new_word.append(word[i])
i += 1
new_corpus.append(new_word)
return new_corpus
# 构建词汇表
def build_vocab(corpus, vocab_size):
vocab = set()
for word in corpus:
vocab.update(word)
while len(vocab) < vocab_size:
pairs = get_pair_statistics(corpus)
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
corpus = merge_pair(best_pair, corpus)
vocab.update(["".join(best_pair)])
return vocab
# 构建目标词汇表
vocab = build_vocab(split_corpus, vocab_size=20)
print("Generated Vocabulary:", vocab)
运行上述代码可以观察到 WordPiece 词汇表的逐步生成过程。
真实世界的应用
在 BERT 模型中,WordPiece 不仅优化了词汇表,还极大提高了模型对多语言任务的适应能力。例如,在处理英文和中文混合的句子时,WordPiece 能够自动将未登录词分解为熟悉的子词,从而降低 OOV(Out-of-Vocabulary)问题的影响。
示例分析
输入句子:
"I love natural language processing."
WordPiece 分割结果:
["I", "love", "natural", "lan", "##guage", "pro", "##cessing"]
这种分割方式保留了高频词 "I" 和 "love" 的整体性,同时将低频词 "language" 和 "processing" 分解为子词单元,确保词汇表大小适中。
总结
WordPiece 是 BERT 词元化中的关键技术,其通过高效的子词分割方法,在保证词汇表规模适中的前提下,显著提升了模型的泛化能力。通过分析其分割过程与实现细节,我们可以更深入地理解其工作原理和应用场景。这种方法不仅适用于 BERT,还在其他 NLP 模型中得到了广泛应用。