前言
大家好,我是半虹,这篇文章来讲分词算法
正文
1 概述
所谓分词就是将文本段落分解成基本语言单位,这里的基本单位也可以称为词元
在上篇文章,我们主要从分词过程的角度出发,介绍了一些不同类型的分词算法
而本篇文章,我们将要从分词结果的角度出发,来介绍一些不同粒度的分词算法
2 按粒度划分
分词算法按照粒度可以分为以下三类:词粒度、字粒度、子词粒度,下面会逐一进行讨论
2.1 词粒度
基于词粒度的分词是最为直观的分词,这与人类理解自然语言的过程是相似的
对于英文来说,句子中的空格是天然的分隔符;对于中文来说,情况则变得更困难
在上篇文章中所介绍的多种分词方法,都属于词粒度分词方法,一个例子如下
例句:he is likely to be unfriendly to me 分词:['he', 'is', 'likely', 'to', 'be', 'unfriendly', 'to', 'me']
基于词粒度的分词有着一些优点:
词粒度的划分能够保留更多的语义信息
但是这种分词方法也有其他缺点:
无论对于中文还是英文,词的数量都是庞大的,因此需要维护一张巨大的词表(vocabulary )
即使拥有着巨大的词表,还是难以避免地遇到在词表中从来没有出现过的生词 (out of vocabulary )
而且特别对于英文来说,模型难以学习到同一个单词的不同形式和变化
2.2 字粒度
基于字粒度的分词是一种极端的分词,此类方法直接将文本分解为最小粒度
对于英文来说,就是将其分解为字母;对于中文来说,就是将其分解为汉字
由于操作简单,此类方法甚至无需建立模型,就按字符划分即可,一个例子如下
例句:he is likely to be unfriendly to me 分词:['h', 'e', 'i', 's', 'l', 'i', 'k', 'e', 'l', 'y', 't', 'o', 'b', 'e', 'u', 'n', 'f', 'r', 'i', 'e', 'n', 'd', 'l', 'y', 't', 'o', 'm', 'e']
基于字粒度的分词优点如下:
词表规模大大较少,这是因为英文字母就只有 26 2626 个,中文常用汉字也只有 3500 35003500 个
极少出现未知词汇,很多常用词都可以由字母或者汉字组合得到
从另一方面来说其缺点如下:
单个字母或者单个汉字并没有包含太多的语义信息
同样的文本会切分更多的词元,使输入的长度增加,从而让模型变得难训练
2.3 子词粒度
不难发现,词粒度和字粒度的优劣是互补的,那会不会有一种折中的方案,能够综合二者的优点呢
答案自然是肯定的,这种方案就是子词粒度,这是目前很多大规模预训练模型所采用的分词方法,
例子如下
例句:he is likely to be unfriendly to me 分词:['he', 'is', 'like', 'ly', 'to', 'be', 'un', 'friend', 'ly', 'to', 'me']
基于子词粒度分词,其核心为:用尽可能小的词表来覆盖尽可能多的词汇
这种方案介于词粒度和字粒度,能很好地平衡 out of vocabulary 的问题
怎么达到此效果呢?关键就是:用词表中的小词元来组成句子中的大词汇,类似于英文中的词缀组合
例如现给定词汇 unfriendly,可将其拆分为以下的词元:un、friend、ly
这些词元本身也是带有语义的,如 un 表示否定、friend 表示友好、ly 表示品质
而且词元之间也能相互组合来构成新词汇,例如,like 和 ly 组合成 likely
这有利于模型学习词汇之间的形式和变化,同时还具有更好的泛化性
学习 BPE \text{BPE}BPE 词表的步骤及例子如下:
准备语料库,定义词表大小和迭代次数,假设语料库中只有以下句子:
a tidy tiger tied a tie tighter to tidy her tiny tail
用预分词器将语料库的句子切分为单词,可以用空格或规则切分
拆分单词为字符序列,并且在每个单词的末尾加停止符用以表示结尾,停止符可以是 </w>
统计单词的出现频率,这些单词及其频率构成训练语料:
{ 'a </w>': 2, 't i d y </w>': 2, 't i g e r </w>': 1, 't i e d </w>': 1, 't i e </w>': 1, 't i g h t e r </w>': 1, 't o </w>': 1, 'h e r </w>': 1, 't i n y </w>': 1, 't a i l </w>': 1, }
统计字符的出现频率,这些字符作为初始子词构成词典:
{ '</w>': 12, 'a': 3, 't': 10, 'i': 8, 'd': 3, 'y': 3, 'g': 2, 'e': 5, 'r': 3, 'h': 2, 'o': 1, 'n': 1, 'l': 1, }
开始迭代,每次迭代完成以下操作,直至到达预设词表大小或到达迭代次数
统计语料中相邻子词对的出现频率,选取频率最高的子词对合并成新的子词加入词表,并更新词典
每次更新,词典可能出现三种变化:
词典的数量加一,这表明加入了合并后的子词,同时原来的两个子词被保留
词典的数量减一,这表明加入了合并后的子词,同时原来的两个子词被消解
词典的数量不变,这表明加入了合并后的子词,同时原来的两个子词一个被保留、一个被消解
随着迭代的进行,词典大小通常先增后减
第一次迭代如下:统计发现,子词 t
和 i
相邻出现的频率最高,将二者合并进词表,并更新词典
{ # 语料 'a </w>': 2, 'ti d y </w>': 2, 'ti g e r </w>': 1, 'ti e d </w>': 1, 'ti e </w>': 1, 'ti g h t e r </w>': 1, 't o </w>': 1, 'h e r </w>': 1, 'ti n y </w>': 1, 't a i l </w>': 1, }
{ # 词典 '</w>': 12, 'a': 3, 't': 3, # [修改] 'i': 1, # [修改] 'd': 3, 'y': 3, 'g': 2, 'e': 5, 'r': 3, 'h': 2, 'o': 1, 'n': 1, 'l': 1, 'ti': 7, # [增加] }
{ # 词表 'ti', }
第二次迭代如下:统计发现,子词 y
和 </w>
相邻出现的频率最高,将二者合并进词表,并更新词典
{ # 语料 'a </w>': 2, 'ti d y</w>': 2, 'ti g e r </w>': 1, 'ti e d </w>': 1, 'ti e </w>': 1, 'ti g h t e r </w>': 1, 't o </w>': 1, 'h e r </w>': 1, 'ti n y</w>': 1, 't a i l </w>': 1, }
{ # 词典 '</w>': 9, # [修改] 'a': 3, 't': 3, 'i': 1, 'd': 3, # 'y': 0, # [删减] 'g': 2, 'e': 5, 'r': 3, 'h': 2, 'o': 1, 'n': 1, 'l': 1, 'ti': 7, 'y</w>': 3, # [增加] }
{ # 词表 'ti', 'y</w>', }
第三次迭代如下:统计发现,子词 e
和 r
相邻出现的频率最高,将二者合并进词表,并更新词典
{ # 语料 'a </w>': 2, 'ti d y</w>': 2, 'ti g er </w>': 1, 'ti e d </w>': 1, 'ti e </w>': 1, 'ti g h t er </w>': 1, 't o </w>': 1, 'h er </w>': 1, 'ti n y</w>': 1, 't a i l </w>': 1, }
{ # 词典 '</w>': 9, 'a': 3, 't': 3, 'i': 1, 'd': 3, 'g': 2, 'e': 2, # [修改] # 'r': 0, # [删减] 'h': 2, 'o': 1, 'n': 1, 'l': 1, 'ti': 7, 'y</w>': 3, 'er': 3, # [增加] }
{ # 词表 'ti', 'y</w>', 'er', }
第四次迭代如下:统计发现,子词 er
和 </w>
相邻出现的频率最高,将二者合并进词表,并更新词典
{ # 语料 'a </w>': 2, 'ti d y</w>': 2, 'ti g er</w>': 1, 'ti e d </w>': 1, 'ti e </w>': 1, 'ti g h t er</w>': 1, 't o </w>': 1, 'h er</w>': 1, 'ti n y</w>': 1, 't a i l </w>': 1, }
{ # 词典 '</w>': 6, # [修改] 'a': 3, 't': 3, 'i': 1, 'd': 3, 'g': 2, 'e': 2, 'h': 2, 'o': 1, 'n': 1, 'l': 1, 'ti': 7, 'y</w>': 3, # 'er': 0, # [删减] 'er</w>': 3, # [新增] }
{ # 词表 'ti', 'y</w>', 'er', 'er</w>', }
最终,上述词表加上基础字符得到最终词表如下
{ # 上述词表 'ti', 'y</w>', 'er', 'er</w>', # 基础字符 '</w>', 'a', 't', 'i', 'd', 'y', 'g', 'e', 'r', 'h', 'o', 'n', 'l', }
应用 BPE \text{BPE}BPE 词表分词的步骤及例子如下:
设子词词表如下:
{ 'ti', 'y</w>', 'er', 'er</w>', '</w>', 'a', 't', 'i', 'd', 'y', 'g', 'e', 'r', 'h', 'o', 'n', 'l', }
且输入句子如下:
tiger is tidy
将子词词表中的子词按照长度从大到小进行排序(若长度相同,则按照上述词典对应词频排序)
{ 'er</w>', 'ti', 'y</w>', 'er', '</w>', 'a', 't', 'd', 'g', 'e', 'h', 'i', 'o', 'n', 'l', 'r', 'y', }
将输入句子预分词,并在每个单词后添加停止符 </w>
['tiger</w>', 'is</w>', 'tidy</w>']
对输入句子的每个单词,遍历排序的词典
若当前子词是这个单词的子字符串,则将其切分并对剩余部分继续匹配
若遍历完词典仍有子字符串无匹配,则将这些子字符串替换成未知标志,例如 <unk>
最终结果如下:
['ti', 'g', 'er</w>', 'i', '<unk>', '</w>', 'ti', 'd', 'y</w>']
上述过程亦可称为编码过程,通常发生在模型的输入部分,旨在将文本分解成词元输入到模型
另外与之对应的是解码过程,通常发生在模型的输出部分,旨在将模型输出的词元还原成文本
具体就是将上述的词元序列直接拼接起来即可,但要注意将 </w>
替换回空格
对应结果如下:
tiger i<unk> tidy