几种 tokenize 策略#
所谓的 tokenize 策略就是包括:"获取词表"和"使用词表对输入文本做编码"这么两步。不过有的方法像 WordLevel、CharacterLevel 等方法有了词表之后,对输入文本做编码的过程非常的容易,所以就忽略没有写如何使用该词表进行编码。
1、WordLevel#
对于英文来说,其文本中天然的有空格这种分隔符,WordLevel 方法就是直接使用空格进行分割。这种方法的优点是它足够简单,缺点则是由于英语单词非常的多,很容易出现 OOV(Out Of Vocab)问题。即便不考虑算力的问题,真的搞一个巨大的词表,能够把绝大部分英文单词都包含进去,那就会导致有相当一部分词是低频次,得不到充分的训练。
2、CharacterLevel#
该方法是直接使用单个英文字母进行 tokenize,这种方法确实解决了 OOV 问题,但是这种分割方式粒度太细了,把文本中的词汇信息、语法信息等等都丢失了,最终取得的效果比较差。
3、Subword#
其中 BPE 和 WordPiece 是非常相似的,都是先从单个 character 开始,然后依照某种策略进行合并,最终当整个词表的数量超过阈值或者满足其他的终止条件时就结束。它们两个方法之间主要的不同就是采用的合并策略不同,BPE 采用词频进行合并,WordPiece 采用互信息进行合并。所以下面在描述 BPE 时会描述整个算法的完整流程,但是在描述 WordPiece 时则只描述其与 BPE 不同的部分。
Subword 的方法相比于 WordLevel 和 CharacterLevel 来说有以下的优势:
- WordLevel 无法比较好的处理 OOV 问题;
- 无论是 WordLevel 还是 Character 都无法学习到词缀之间的信息;
- CharacterLevel 倒是能够解决 OOV 问题,但是其粒度太细,导致很多信息都丢失了;
- SubWord 是一种介于 CharacterLevel 和 WordLevel 之间的方法,能够较好的平衡 OOV 问题;
3.1 BPE (Byte Pair Encoding)#
3.1.1 获取词表#
BPE算法获取词表的步骤如下:
-
准备一份足够大的语料;
-
确定期望的词表的大小;
-
将单词拆分为最小单元,比如英语的话就拆分为26个英文字母,并且每个单词的结尾处添加 "</w>" 标识这是一个单词的结尾;
-
统计出每个单词的频率;【到这初始化就完成了,此时的词表中全部都是单个字母】
-
统计所有单词中连续出现的两个单元之间的频率,选取频率最高的单元对合并为一个新的单元,将这个新合并出来的单元添加到词表中;
-
检测被合并的那两个单元,如果某个单元在整个语料中都没有再出现过,那么就从词表中移除它;
-
不断重复上述第5步和第6步,直到词表的数量达到预设定的大小;
接下来举一个例子进行说明。假设原始语料里面各个单词的词频如下,接下来对这个语料走一遍 BPE 算法:
{'low':5,'lower':2,'newest':6,'widest':3}
-
完成上述 BPE 算法的步骤1、2、3、4,也就是完成初始化部分,可得:
语料词频 词表 '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', 'w', 'e', 'r', 'n', 's', 't', 'i', 'd', '</w>' -
基于上表完成 BPE 算法的步骤5、6:在上表的语料词频中,频率最高的单元对是 'e' 和 's',总共出现了9次,将其进行合并,并且将 'es' 添加到词表中。另外对于被合并的单元 'e' 和 's' 做检测,可以发现语料中已经不存在 's' 了,所以需要在词表中去除 's',最终得到的结果如下:
语料词频 词表 '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'l', 'o', 'w', 'e', 'r', 'n', 't', 'i', 'd', '</w>', 'es' -
继续基于上表完成 BPE 算法的步骤5、6:在上表的语料词频中,频率最高的单元对是 'es' 和 't',总共出现了9次,将其进行合并,并且将 'est' 添加到词表中。另外对于被合并的单元 'es' 和 't' 做检测,可以发现语料中 'es' 和 't' 都已经不存在了,所以需要在词表中去除 'es' 和 't',最终得到的结果如下:
语料词频 词表 'l o w </w>': 5
'l o w e r </w>': 2
'n e w est </w>': 6
'w i d est </w>': 3'l', 'o', 'w', 'e', 'r', 'n', 'i', 'd', '</w>', 'est' -
继续基于上表完成 BPE 算法的步骤5、6:在上表的语料词频中,频率最高的单元对是 'est' 和 '</w>',总共出现了9次,将其进行合并,并且将 'est</w>' 添加到词表中。另外对于被合并的单元 'est' 和 '</w>' 做检测,可以发现语料中 'est' 已经不存在了,所以需要在词表中去除 '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'l', 'o', 'w', 'e', 'r', 'n', 'i', 'd', '</w>', 'est</w>' -
一直持续上述步骤直到满足停止条件。
3.1.2 使用词表做编码#
使用上一小节的算法得到词表之后,将词表中所有的词按照长度进行降序排列。编码时,遍历排好序的词表,查找词表中是否有是当前单词的子字符串的子词。如果有,那么这个子词就作为表示该单词的 token 之一,原单词中除去该子词之后剩余的部分继续遍历排好序的词表,查找词表中是否有匹配的子词。一直重复该操作。
当所有的词表都遍历完了之后,如果依然有子字符串没有匹配上词表中的任何子词,那么就使用
下面举个例子说明:
# 给定输入文本
the highest mountain
# 转为单词序列
["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>"]
# 输入文本编码后的token序列
["the</w>", "high", "est</w>", "moun", "tain</w>"]
3.1.3 使用词表做解码#
解码的过程就比较简单了,如果 token 是以 </w> 结尾的,说明这是一个单词的结尾,如果 token 不是以 </w> 结尾的,说明这个 token 需要和其后面的 token 拼接起来构成单词。整体来看,就等同于把 </w> 替换成空格之后,直接把全部 token 拼接成一个字符串。
3.2 Byte-level BPE#
在上一章节中提出的算法里面,有一个操作是 将单词拆分为最小单元,至于如何将单词拆分为最小单元,并没有详细讨论,这里详细说明一下该操作。
如果是字母语言的话,比如英语、法语,他们的字符数量都是非常少的,数量多的是字符构成的单词。但是对于非字母语言的话,比如中文、日文、韩文,他们的字符数就已经非常多了。比如中文常用汉字三四千个左右,但是如果把不常用的也放进去就有几万之多。所以如果训练多语言模型,直接使用 unicode 编码,将每个 unicode 字符当做最小单元,那么这些最小单元的数据就已经极其多了。
所以关于拆分最小单元这个操作,就提出了另一种方案,按照 ascii 码进行拆分,这样最小单元总共就只有256个,拆分之后再使用 BPE。这种方法就称为 Byte-level BPE,其中模型 gpt2 就是采用的这种方法。当然这样拆分之后会有很多看起来是乱码的字符存在,比如使用gpt2-xl的 tokenizer 对 你好中国
按照 ascii 码进行拆分的代码和结果如下所示(你好中国
这四个汉字,每个字使用 unicode 编码之后占用3个字节,所以按照 ascii 拆分后总共12个字符):
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2-xl")
pre_tokenizer = tokenizer.backend_tokenizer.pre_tokenizer
s = "你好中国"
print(list(pre_tokenizer.pre_tokenize_str(s)[0][0]))
# 输出结果:['ä', '½', 'ł', 'å', '¥', '½', 'ä', '¸', 'Ń', 'å', 'Ľ', '½']
3.3 WordPiece#
相比于 BPE 算法,WordPiece 的区别主要是在选取待合并的两个单元时所采取的方法不同,其他部分的算法流程是相同的。
在 WordPiece 中使用点互信息表示两个子词之间的凝聚程度的高低。在选取待合并的两个单元时就选取点互信息最高的。
点互信息的公式如下,假设待合并的两个子词为 x 和 y,合并后的子词为 z:
WordPiece 算法获取词表的步骤如下:
-
准备一份足够大的语料;
-
确定期望的词表的大小;
-
将单词拆分为最小单元,比如英语的话就拆分为26个英文字母,并且每个单词的结尾处添加 "</w>" 标识这是一个单词的结尾;
-
统计出每个单词的概率;【到这初始化就完成了,此时的词表中全部都是单个字母】
-
统计所有单词中连续出现的两个单元之间的概率,并计算出这两个单元的点互信息,选取点互信息最高的单元对合并为一个新的单元,将这个新合并出来的单元添加到词表中;
-
检测被合并的那两个单元,如果某个单元在整个语料中都没有再出现过,那么就从词表中移除它;
-
不断重复上述第5步和第6步,直到词表的数量达到预设定的大小;
上述算法步骤中,相比于 BPE 的算法步骤,修改的有:
- 第4步中,BPE 算法是统计每个单词的频率,WordPiece 中是统计每个单词的概率;
- 第5步中,BPE 算法是选取频率最大的单元对进行合并,WordPiece 中是选取点互信息最大的单元对进行合并;
3.4 BPE 与 WordPiece 对比#
本文里的 BPE 和 WordPiece 与新词发现的算法极为相似,关于新词发现详情见: http://mingchao.wang/RrOtMm1s/。在新词发现那篇文章中详细解释了在那个任务下使用频率与使用点互信息的优劣。
但是在本文的 tokenize 任务中,BPE 与 WordPiece 并没有明显的优劣,各自有各自的特点,阐述如下:
- 使用频次的 BPE 算法能够获取到更高效的编码方式,类似于哈夫曼编码(Huffman Coding)的思想;
- 相比于 BPE,使用点互信息的 WordPiece 算法能够获取到更高质量的词根,详情见 http://mingchao.wang/RrOtMm1s/ 里的解释;
3.5 ULM(unigram language model)#
待补充...