tokenize的目标是把输入的文本流,切分成一个个子串,每个子串相对有完整的语义,便于学习embedding表达和后续模型的使用。
tokenize有三种粒度:word/subword/char
优点:能够保存较为完整的语义信息
缺点:
将word-level的分词方法改成 char-level的分词方法,对于英文来说,就是字母界别的,比如 "China"拆分为"C","h","i","n","a",对于中文来说,"中国"拆分为"中","国",
优点:
缺点:
为了两全其美,transformer使用了混合了char-level和word-level的分词方式,称之为subword-level的分词方式。subword-level的分词方式遵循的原则是:尽量不分解常用词,而是将不常用词分解为常用的子词,
例如,"annoyingly"可能被认为是一个罕见的单词,并且可以分为"annoying"和"ly"。"annoying"并"ly"作为独立的子词会更频繁地出现,同时,"annoyingly"是由"annoying"和"ly"这两个子词的复合含义构成的复杂含义,这在诸如土耳其语之类的凝集性语言中特别有用,在该语言中,可以通过将子词串在一起来形成(几乎)任意长的复杂词。
subword-level的分词方式使模型相对合理的词汇量(不会太多也不会太少),同时能够学习有意义的与上下文无关的表示形式(另外,subword-level的分词方式通过将模型分解成已知的子词,使模型能够处理以前从未见过的词(oov问题得到了很大程度上的缓解)。
subword-level又分为不同的切法,这里就到huggingface的tokenizers的实现部分了,常规的char-level或者word-level的分词用spacy,nltk之类的工具就可以胜任了。
subword的分词往往包含了两个阶段,一个是encode阶段,形成subword的vocabulary dict,一个是decode阶段,将原始的文本通过subword的vocabulary dict 转化为 token的index然后进入embedding层.
分词之后,统计每个词出现的频次供后续计算使用。例如,我们统计到了5个词的词频
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
建立基础词汇表base vocabulary,包括所有的字符,即:
["b", "g", "h", "n", "p", "s", "u"]
1 2 3 4 5 6 7 8 9 10 11 |
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5) # count pair h + u = 10 + 5 = 15 u + g = 10 + 5 + = 20 ... # merge top k set k = 1 ug -> vocabulary base vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug"] loop until vocabulary match vocab_size |
最终词汇表的大小 = 基础字符词汇表大小 + 合并串的数量,比如像GPT,它的词汇表大小 40478 = 478(基础字符) + 40000(merges)。添加完后,我们词汇表变成:
["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
持续迭代直到达到人工预设的subword词表大小或下一个最高频的字节对出现频率为1
实际使用中,如果遇到未知字符用
也有可能的问题:基于贪婪和确定的符号替换,不能提供带概率的多个分片结果(相对于unigram来说),最终会导致decode的时候面临含糊不清的问题.
BPE以词频top-k数量建立的词典;但是针对字符相对杂乱的日文和字符较丰富的中文,往往他们的罕见词难以表示,就中文来说,字符级别就是到单个中文字。
BPE的一个问题是,如果遇到了unicode,基本字符集可能会很大。一种处理方法是我们以一个字节为一种“字符”,不管实际字符集用了几个字节来表示一个字符。这样的话,基础字符集的大小就锁定在了256。通常词表大小包括256 个基本bytes +
例如,像GPT-2的词汇表大小为50257 = 256 +
BBPE整体和BPE的逻辑类似,不同的是,粒度更细致,BPE最多做到字符级别,但是BBPE是做到byte级别
BPE中,统计每一个连续字节对的出现频率,选择最高频者合并成新的subword,而wordpiece则使用了概率相除的方法
wordpiece则是从整个句子的层面出发去确认subword的合并结果,假设有个句子是:
"see you next week"初始拆分为字符之后是
"s","e","e"....... "e","k"
则语言模型概率为:
n表示这个句子拆分成字符之后的长度(继续迭代的话就是拆分成subword的长度了),P(ti)表示"ti"这个字符或者subword在词表中占比的概率值,不过我们只需要计算下面的式子就可以:
可以看到,这里和决策树的分裂过层非常类似,两个两个相邻字符或subword之间进行分裂判断分裂增益是否增大,增大则合并。子词结合的互信息的计算过程和决策树是相似但相反的,决策树是越切分越细,而子词的结合则是越结合越粗
从上面的公式,很容易发现,似然值的变化就是两个子词之间的互信息。简而言之,WordPiece每次选择合并的两个子词,他们具有最大的互信息值,也就是两子词在语言模型上具有较强的关联性,它们经常在语料中以相邻方式同时出现。(最大化训练集数据似然的merge)
以es为例子,用es出现的概率,分别除以 e和s的概率,如果这个计算的结果是所有其它的 token pairs中计算结果最大的,则es合并为新token,其它和bpe没什么区别。因为除法可以通过对数运算转化为加减法,所以有上面的这个公式
Unigram 不再是通过合并base vocabulary 中的subword 来新增,他选择在初始化时初始化一个非常大的subword set(可以用所有字符的组合加上语料中常见的子字符串或BPE生成),通过计算是否需要将一个subword 切分为多个base subword (remove 这个subword)来减小vocabulary size 直到达到vocab size。
这里有一个假设:句子之间是独立的,subword 与 subword 之间是独立的。对应的句子的语言模型似然值就是其subword 的概率的乘积。目标是保存vocab size 的同时语言模型似然值最大。
整个求解过程是一个简单的EM 或者说一个迭代过程:
维特比算法(Viterbi Algorithm):
一种动态规划算法,可以HMM三大问题中的解码问题(给定模型和观测序列,如何找到与此观测序列最匹配的状态序列的问题)进行求解。
该算法包括计算网格图上在时刻t到达各个状态的路径和接收序列之间的相似度,或者说距离。维特比算法考虑的是,去除不可能成为最大似然选择对象的网格图上的路径,即如果有两条路径到达同一个状态,则具有最佳量度的路径被选中,称为幸存路径。
语言模型概率
假设句子S=(t1,t2,...,tn)由n个子词组成,t_i 表示子词,且假设各个子词之间是独立存在的,则句子 S 的语言模型似然值(语言模型概率)等价于所有子词概率的乘积:
假设训练文档中的所有词分别为 x1;x2...xN ,而每个词tokenize的方法是一个集合 S(xi) 。当一个词汇表确定时,每个词tokenize的方法集合 S(xi) 就是确定的,而每种方法对应着一个概率p(x)。如果从词汇表中删除部分词,则某些词的tokenize的种类集合就会变少,log(*)中的求和项就会减少,从而增加整体loss。
Unigram算法每次会从词汇表中挑出使得loss增长最小的10%~20%的词汇来删除。
一般Unigram算法会与SentencePiece算法连用。
SentencePiece 其实并不是一个新的tokenizer 方法,他其实是一个实现了BPE/Unigram tokenizer 的一个集合,不过他有一些创新的地方。
上述方法中有一些问题:
SentencePiece 的做法:
NLP BERT GPT等模型中 tokenizer 类别说明详解-腾讯云开发者社区-腾讯云
tokenizers小结 - 知乎
tokenizers 总结 | 小蛋子