语言模型中的子词分词算法BPE和worldPiece

本文深入探讨了Subword算法,包括BPE和WordPiece两种主要方法。BPE通过合并高频字节对来创建子词,而WordPiece则选择能最大化语言模型概率的子词进行合并。文章详细介绍了这两种算法的工作原理、编码和解码过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. subword与传统分隔tokenization技术相比

  • 传统词表示方法无法很好的处理未登录词汇
  • 传统分词方法不利于模型学习词缀之间的关系
  • Character embedding作为OOV的解决方法粒度太细,而且character embedding 粒度太细能很好的表示原本词的意思,如将now用3个character emebedding 来表示未必没有直接用一个helpl 词向量表示准确,毕竟无论用何重方式将character embedding拼成 now很有可能和单词own意思搞混
  • subword粒度在词与字符之间能够较好平衡OOV问题

2. BPE(Byte Pair Encoding)

transformer使用了BPE分词,BPE编码是一种简单的数据压缩形式,将最常见的一对连续字节数据被替换为该数据中不存在的字节。后期使用时需要一个替换表来重建原始数据
优点
可以有效的平衡词汇表大小和步数
缺点
基于贪婪和确定的符号替换,不能提供带概率的多个分片结果

2.1 算法流程

(1)准备足够大的训练语料
(2)确定期望的subword词表大小
(3)将单词拆分为字符序列并在末尾添加后缀“ </ w>”,统计单词频率。 本阶段的subword的粒度是字符。 例如,“ low”的频率为5,那么我们将其改写为“ l o w </ w>”:5
(4)统计每一个连续字节对的出现频率,选择最高频者合并成新的subword
(5)重复第4步直到达到第2步设定的subword词表大小或下一个最高频的字节对出现频率为1

停止符"“的意义在于表示subword是词后缀。举例来说:“st"字词不加”“可以出现在词首如"st ar”,加了”“表明改字词位于词尾,如"wide st”,二者意义截然不同。

每次合并后词表可能出现3种变化:

  • +1,表明加入合并后的新字词,同时原来的2个子词还保留(2个字词不是完全同时连续出现)
  • +0,表明加入合并后的新字词,同时原来的2个子词中一个保留,一个被消解(一个字词完全随着另一个字词的出现而紧跟着出现)
  • -1,表明加入合并后的新字词,同时原来的2个子词都被消解(2个字词同时连续出现)

实际上,随着合并的次数增加,词表大小通常先增加后减小

实例
输入:

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}

Iter 1, 最高频连续字节对"e"和"s"出现了6+3=9次,合并成"es"。输出:

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}

Iter 2, 最高频连续字节对"es"和"t"出现了6+3=9次, 合并成"est"。输出:

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}

Iter 3, 以此类推,最高频连续字节对为"est"和"" 输出:

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}

Iter n, 继续迭代直到达到预设的subword词表大小或下一个最高频的字节对出现频率为1

2.2 BPE实现

import re, collections

def get_stats(vocab):
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i],symbols[i+1]] += freq
    return pairs

def merge_vocab(pair, v_in):
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

vocab = {'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
num_merges = 1000
for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    print(best)

# print output
# ('e', 's')
# ('es', 't')
# ('est', '</w>')
# ('l', 'o')
# ('lo', 'w')
# ('n', 'e')
# ('ne', 'w')
# ('new', 'est</w>')
# ('low', '</w>')
# ('w', 'i')
# ('wi', 'd')
# ('wid', 'est</w>')
# ('low', 'e')
# ('lowe', 'r')
# ('lower', '</w>')

2.3 编码和解码

编码
在之前的算法中,我们已经得到了subword的词表,对该词表按照子词长度由大到小排序。编码时,对于每个单词,遍历排好序的子词词表寻找是否有token是当前单词的子字符串,如果有,则该token是表示单词的tokens之一。

我们从最长的token迭代到最短的token,尝试将每个单词中的子字符串替换为token。 最终,我们将迭代所有tokens,并将所有子字符串替换为tokens。 如果仍然有子字符串没被替换但所有token都已迭代完毕,则将剩余的子词替换为特殊token,如<unk>。

例子

# 给定单词序列
[“the</w>”, “highest</w>”, “mountain</w>”]

# 假设已有排好序的subword词表
[“errrr</w>”, “tain</w>”, “moun”, “est</w>”, “high”, “the</w>”, “a</w>”]

# 迭代结果
"the</w>" -> ["the</w>"]
"highest</w>" -> ["high", "est</w>"]
"mountain</w>" -> ["moun", "tain</w>"]

编码的计算量很大。 在实践中,我们可以pre-tokenize所有单词,并在词典中保存单词tokenize的结果。 如果我们看到字典中不存在的未知单词。 我们应用上述编码方法对单词进行tokenize,然后将新单词的tokenization添加到字典中备用。

解码
将所有的tokens拼在一起

# 编码序列
[“the</w>”, “high”, “est</w>”, “moun”, “tain</w>”]

# 解码序列
“the</w> highest</w> mountain</w>”

3. WordPiece

Bert模型在分词的时候使用的是WordPiece算法。与BPE算法类似,WordPiece算法也是每次从词表中选出两个子词合并成新的子词。与BPE的最大区别在于,如何选择两个子词进行合并:BPE选择频数最高的相邻子词合并,而WordPiece选择能够提升语言模型概率最大的相邻子词加入词表

假设句子 S = ( t 1 , t 2 , . . . , t n ) S=(t_1, t_2, ..., t_n) S=(t1,t2,...,tn) n n n 个子词组成, t i t_i ti 表示子词,且假设各个子词之间是独立存在的,则句子 S S S 的语言模型似然值等价于所有子词概率的乘积
l o g P ( S ) = ∑ i = 1 n l o g P ( t i ) logP(S)=\sum_{i=1}^nlogP(t_i) logP(S)=i=1nlogP(ti)

假设把相邻位置的 x x x y y y 两个子词进行合并,合并后产生的子词记为 z z z,此时句子 S S S 似然值的变化可表示为:
l o g P ( t z ) − ( l o g P ( t x ) + l o g P ( t y ) ) = l o g ( P ( t z ) P ( t x ) P ( t y ) ) logP(t_z)-(logP(t_x)+logP(t_y))=log(\frac{P(t_z)}{P(t_x)P(t_y)}) logP(tz)(logP(tx)+logP(ty))=log(P(tx)P(ty)P(tz))

所以,似然值的变化就是两个子词之间的互信息。简而言之,WordPiece每次选择合并的两个子词,他们具有最大的互信息值,也就是两子词在语言模型上具有较强的关联性,它们经常在语料中以相邻方式同时出现。

参考

深入理解NLP Subword算法:BPE、WordPiece、ULM

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值