本文基本转载于
深入理解NLP Subword算法:BPE、WordPiece、ULM
对于一句话你需要:
- 将输入切分成小块
- 将输入表示成向量
对于第一点,对同一句话,可以有不同的切割方法。
1. 传统的空格分隔的tokenization技术对比
- 传统词表示方法无法很好的处理未知或罕见的词汇(OOV, out-of-vocabulary:不在词库)
- 传统的tokenization方法不利于模型学习词缀之间的关系:E.g. 模型学到的“old”, “older”, and “oldest”之间的关系无法泛化到“smart”, “smarter”, and “smartest”。
- Character embedding作为OOV的解决方法粒度太细(不是很懂)
- Subword粒度在词与字符之间,能够较好的平衡OOV问题(不是很懂)
2. Byte Pair Encoding(BPE)
BPE:字节对编码或二元编码。旨在解决OOV问题。在NLP领域最早用于机器翻译。
- 对于不在词库的词,需要去查稀有词汇词表,但是因为一个词可能对应于多个意思,因此在实际场景中,并不知道用哪个。
- 对于翻译来说,有些词即使没见过,也可以通过已知的子词翻译出来,BPE分词通过不断的合并出现最多的字符来捕获这种可识别的子词。
BPE代码及实现
import re
import sys
import 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_pattern = re.escape(" ".join(pair))
p = re.compile(r"(?<!\S)" + bigram_pattern + 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 = 15
for i in range(num_merges):
pairs = get_stats(vocab)
try:
#max() 函数就是作用在 values 上,也就是求出所有 values 的最大值,
#但是返回的仍是key,也就是返回 value 最大的 key。
best = max(pairs, key=pairs.get)
except ValueError:
break
if pairs[best] < 2:
sys.stderr.write("no pair has frequency > 1. stoppint\n")
break
vocab = merge_vocab(best, vocab)
print(best)
第一步:遍历语料库,分词,统计每个词出现的频率,在每个单词末尾添加后缀"</w>":
{'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}
第二部:将单词分割成字母,计算两个连续字符出现的次数:
{('l', 'o'): 7, ('o', 'w</w>'): 5, ('o', 'w'): 2, ('w', 'e'): 8, ('e', 'r</w>'): 2, ('n', 'e'): 6, ('e', 'w'): 6, ('e', 's'): 9, ('s', 't</w>'): 9, ('w', 'i'): 3, ('i', 'd'): 3, ('d', 'e'): 3}
第三步:合并pairs中词频最高的字符对('e', 's'): 9 。作为一个新的字符(es连在一起作为一个字符),并更新得到vocab:
{'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}
第四步:重复迭代,知道num_merges循环结束,或者统计最高词频低于阈值。
第五步:记录每次合并频次最高的字母的,存入词表。 (这里得到的词表可能不唯一,原因是因为选择最大的频次时,可能有多个相同频次的字母对)
('e', 's')
('es', 't</w>')
('l', 'o')
('n', 'e')
('ne', 'w')
('new', 'est</w>')
('lo', 'w</w>')
('w', 'i')
('wi', 'd')
('wid', 'est</w>')
('lo', 'w')
('low', 'e')
('lowe', 'r</w>')
对于OOV问题,即未在词表中的词lowest,作为例子,用以BPE进行分词。
添加结束符</w> : l o w e s t</w>
根据词表,首先将词频最高的字符组合('e', 's')合并得到:l o w es t</w>;
在合并('es', 't</w>')得到:l o w est</w>;
在合并('l', 'o')得到:lo w est</w>;
在合并('lo', 'w')得到:low est</w>;
最后得到:lowest</w>;
BPE编码与解码
编码
通过上面的算法,我们得到了subword词表,编码时,对每个单词寻找是否在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拼到一起,结束符为</w>。
# 编码序列 [“the</w>”, “high”, “est</w>”, “moun”, “tain</w>”] # 解码序列 “the</w> highest</w> mountain</w>”
3. Wordpiece
Bert模型在分词时使用的就是Wordpiece算法。与BPE算法类似,Wordpiece算法也是每次从词表中选出两个子词合并成新的子词。与BPE最大的区别在于,如何选择两个子词进行合并;BPE选择频数最高的相邻子词合并,而Wordpiece选择能够提升语言模型概率最大(似然)的相邻子词加入词表。
关于如何获得似然,先将整个语料按当前词表分解,接着在分解后的语料上训练语言模型,对整个语料获得一个似然值。之后在已有的词表上组合词对,获得新的词表,重新训练语言模型,对整个语料获得一个似然值。对比所有词对候选,挑选其中语言模型似然值提升最大的词对,将其正式加入词表。不断进行此操作,直到整个词表量达到设定值。
通过以下策略来降低计算量:
- 只测试语料中出现的词对;
- 只测试有很大可能(高优先)是最好词对的候选;
- 同时测试几个词对,只要它们互不影响;
- 重训语言模型(并不需要是神经网络型),只重新计算受影响的部分。
算法代码和详细分析bert源码学习(一)——tokenization.py&&WordPiece
主要流程:
- 将输入的token转化为字符列表,如"unaffable" -> ["u","n","a","f","f","a","b","l","e"],如果字符列表长度大于200,这直接输出<UNK>标识符。
- 对于长度小于200的字符列表
- 从star=0,len(chars)开始,在词汇表中查找 "".join(chars[start:end])得到的子词是否在词汇表中;
- 如果不在,则令end=end-1,重复以上步骤:
- 如果在,则令start=end,end=len(chars),即在原单词中将该字词移除,对剩下的部分继续查找,直到start=len(chars),即从头遍历结束;
- 此时如果依旧没有找到对应于词汇表中的任一字词,这对该token输出<UNK>标识符,如果又找到就输出这些找到的子词。
输入为“unaffable”,输出为[“un”, “##aff”, “##able”]
BPE和Wordpiece都是增量法,即先初始化一个词表,在建立一个评估标准,每次挑最好的词对加入词表,而Unigram Language Model则是减量法。
4. Unigram Language Model
- 首先它与Wordpiece都是用到语言模型来挑选子词,而不是像BPE统计频次。
- BPE和Wordpiece都是先初始化一个词表,在建立一个评估标准,每次挑最好的词对加入词表。增量法;而Unigram Language Model却时先初始化大词表,接着通过语言模型评估不断减少词表,直到达到限定的词汇量。
- 先建立一个足够大的总子词表,可以用所有字符的组合加上语料中常见的子字符串,也可以使用BPE进行生成;
- 固定词表,用EM算法(期望最大化算法)来优化当前词表在语料上的概率;
- 之后计算每个子词的loss,对应的loss相当于子词有多大可能使总loss降低;
- 接着按照每个子词loss的大小排序,保留最大一定比例(比如80%)的子词;
- 不断重复2-4,直到词表量减少到限定范围。
训练方法
子词正则(subword regularization)
相对于上述 BPE 这样拆分子词确定了的训练,子词正则在不同情况用不同拆分方法来拆分一个词。因此在训练中,作者们会用 Viterbi(维特比) 算法来对当前如何分词进行采样,之后获得分完的子词。
这个方法给训练的分词过程带来了随机性,结果表明相比起只用一种方案的确定性分词法,子词正则能够获得很大的提升。但同时也正如看到的,子词正则加上 Unigram Language Model 法过于复杂,所以应用难度也相应增大,不像 BPE 应用广泛。
BPE dropout
采用和子词正则相同的思路,对BPE算法训练做了些改进,加入了一定的随机性,具体在每次对训练数据进行分词时,设定一定概率(10%)让一些融合不通过,于是即使时相同词,每次用BPE dropout生成出来的子词也都不一样。
图中绿色是每次融合字符对成功的,而红色是dropout掉的。
通过该方法,可假定模型通过不同的分词方案,来获得对整词更好更全面的理解。巨大提升。