Subword算法:BPE,WordPiece,ULM

本文基本转载于

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

bpe分词

子词技巧:The Tricks of Subword

对于一句话你需要:

  • 将输入切分成小块
  • 将输入表示成向量

对于第一点,对同一句话,可以有不同的切割方法。

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却时先初始化大词表,接着通过语言模型评估不断减少词表,直到达到限定的词汇量。
  1. 先建立一个足够大的总子词表,可以用所有字符的组合加上语料中常见的子字符串,也可以使用BPE进行生成;
  2. 固定词表,用EM算法(期望最大化算法)来优化当前词表在语料上的概率;
  3. 之后计算每个子词的loss,对应的loss相当于子词有多大可能使总loss降低;
  4. 接着按照每个子词loss的大小排序,保留最大一定比例(比如80%)的子词;
  5. 不断重复2-4,直到词表量减少到限定范围。

训练方法

子词正则(subword regularization)

相对于上述 BPE 这样拆分子词确定了的训练,子词正则在不同情况用不同拆分方法来拆分一个词。因此在训练中,作者们会用 Viterbi(维特比) 算法来对当前如何分词进行采样,之后获得分完的子词。

这个方法给训练的分词过程带来了随机性,结果表明相比起只用一种方案的确定性分词法,子词正则能够获得很大的提升。但同时也正如看到的,子词正则加上 Unigram Language Model 法过于复杂,所以应用难度也相应增大,不像 BPE 应用广泛。

BPE dropout

采用和子词正则相同的思路,对BPE算法训练做了些改进,加入了一定的随机性,具体在每次对训练数据进行分词时,设定一定概率(10%)让一些融合不通过,于是即使时相同词,每次用BPE dropout生成出来的子词也都不一样。

图中绿色是每次融合字符对成功的,而红色是dropout掉的。

通过该方法,可假定模型通过不同的分词方案,来获得对整词更好更全面的理解。巨大提升。

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值