本文从PaddleNLP源码入手,分析WordPiece是如何基于词表对输入的文本进行子词切分的。
为了更好地阅读本文,你需要知道子词切分与tokenize相关的知识,可以参考我之前的文章:
WordPiece采用了一种贪心的最长匹配搜索算法来将原始文本切分成子词。
为简单起见,假设词表中只有三个子词:['un', 'aff', 'able']
,我们要切分的单词是“unaffable”。具体做法是,初始化两个位置变量(start
和end
,分别表示最左侧字符的位置和最右侧字符的位置),然后将end
逐个减1,每次移动后(包括初始时)都将从start到end
的字符拼接起来,并查看它们是否在词表中。
另外,如果start
不为0(即对应的字符不是开头的字符),那么需要在子词前面加上##
。
本部分代码如下:
output_tokens = []
for token in whitespace_tokenize(text):
# whitespace_tokenize是先将text按照空格切分,这对于输入一个句子的情况下有用
# 接下来,把token想象成单词“unaffable”
chars = list(token)
if len(chars) > self.max_input_chars_per_word:
# 这里做了一个限制:如果一个单词的长度超过了设定值(默认是100),那么便被标记为预先定义的字符,一般是`UNK`
output_tokens.append(self.unk_token)
continue
is_bad = False
start = 0
sub_tokens = []
# 初始化了start
while start < len(chars):
end = len(chars)
cur_substr = None
while start < end:
# 从最后一个字符逐个向左遍历,保证匹配到的子词是最长的
substr = "".join(chars[start:end])
if start > 0: # 添加特殊的连接符
substr = "##" + substr
if substr in self.vocab: # 姑且把vocab理解为一个列表或键为词表中单词的字典
cur_substr = substr
break
end -= 1
if cur_substr is None:
# 这里是一个否决条件,如果end走了一遍仍没有找到合适的子词,那么说明当前从start到end组成的子词不在词表中
is_bad = True
break
sub_tokens.append(cur_substr)
start = end
if is_bad:
# 只有有任意一部分不在词表中,那么当前token就被标记为`UNK`
output_tokens.append(self.unk_token)
else:
output_tokens.extend(sub_tokens)