词是中文表达语义的最小单位,自然语言处理的基础步骤就是分词,分词的结果对中文信息处理至为关键。
本文先对中文分词方法进行一下概述,然后简单讲解一下结巴分词背后的原理。
中文分词概述
简单来说,中文分词根据实现特点大致可分为两个类别:
基于词典的分词方法、基于统计的分词方法。
基于词典的分词方法
基于词典的分词方法首先会建立一个充分大的词典,然后依据一定的策略扫描句子,若句子中的某个子串与词典中的某个词匹配,则分词成功。
常见的扫描策略有:正向最大匹配、逆向最大匹配、双向最大匹配和最少词数分词。
正向最大匹配
对输入的句子从左至右,以贪心的方式切分出当前位置上长度最大的词,组不了词的字单独划开。其分词原理是:词的颗粒度越大,所能表示的含义越精确。
逆向最大匹配
原理与正向最大匹配相同,但顺序不是从首字开始,而是从末字开始,而且它使用的分词词典是逆序词典,其中每个词条都按逆序方式存放。在实际处理时,先将句子进行倒排处理,生成逆序句子,然后根据逆序词典,对逆序句子用正向最大匹配。
双向最大匹配
将正向最大匹配与逆向最大匹配组合起来,对句子使用这两种方式进行扫描切分,如果两种分词方法得到的匹配结果相同,则认为分词正确,否则,按最小集处理。
最少词数分词
即一句话应该分成数量最少的词串,该方法首先会查找词典中最长的词,看是不是所要分词的句子的子串,如果是则切分,然后不断迭代以上步骤,每次都会在剩余的字符串中取最长的词进行分词,最后就可以得到最少的词数。
总结:基于词典的分词方法简单、速度快,效果也还可以,但对歧义和新词的处理不是很好,对词典中未登录的词没法进行处理。
基于统计的分词方法
基于统计的分词方法是从大量已经分词的文本中,利用统计学习方法来学习词的切分规律,从而实现对未知文本的切分。随着大规模语料库的建立,基于统计的分词方法不断受到研究和发展,渐渐成为了主流。
常用的统计学习方法有:隐马尔可夫模型(HMM)、条件随机场(CRF)和基于深度学习的方法。
HMM和CRF
这两种方法实质上是对序列进行标注,将分词问题转化为字的分类问题,每个字有4种词位(类别):词首(B)、词中(M)、词尾(E)和单字成词(S)。由字构词的方法并不依赖于事先编制好的词典,只需对分好词的语料进行训练即可。当模型训练好后,就可对新句子进行预测,预测时会针对每个字生成不同的词位。其中HMM属于生成式模型,CRF属于判别式模型。
基于深度学习的方法
神经网络的序列标注算法在词性标注、命名实体识别等问题上取得了优秀的进展,这些端到端的方法也可以迁移到分词问题上。与所有深度学习的方法一样,该方法需要较大的训练语料才能体现优势,代表为BiLSTM-CRF。
总结:基于统计的分词方法能很好地处理歧义和新词问题,效果比基于词典的要好,但该方法需要有大量人工标注分好词的语料作为支撑,训练开销大,就分词速度而言不如前一种。
在实际应用中一般是将词典与统计学习方法结合起来,既发挥词典分词切分速度快的特点,又利用了统计分词结合上下文识别生词、自动消除歧义的优点。结巴分词正是这一类的代表,下面简要介绍一下它的实现算法。
结巴分词原理
官方Github上对所用算法的描述为:
基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG);
采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合;
对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法。
下面逐一介绍:
构造前缀词典
结巴分词首先会依照统计词典dict.txt构造前缀词典。dict.txt含有近35万的词条,每个词条占用一行,其中每一行有3列,第一列为词条,第二列为对应的词频,第三列为词性,构造前缀词典需要用到前两列。
具体做法为:首先定义一个空的python字典,然后遍历dict.txt的每一行,取词条作为字典的键,词频作为对应的键值,然后遍历该词条的前缀,如果前缀对应的键不在字典里,就把该前缀设为字典新的键,对应的键值设为0,如果前缀在字典里,则什么都不做。
这样等遍历完dict.txt后,前缀词典就构造好了。在构造前缀词典时,会对统计词典里所有词条的词频做一下累加,累加值等计算最大概率路径时会用到。
生成有向无环图(DAG)
用正则表达式分割句子后,对每一个单独的子句会生成一个有向无环图。
具体方式为:先定义一个空的python字典,然后遍历子句,当前子句元素的索引会作为字典的一个键,对应的键值为一个python列表(初始为空),然后会以当前索引作为子串的起始索引,不断向后遍历生成不同的子串,如果子串在前缀词典里且键值不为0的话,则把子串的终止索引添加到列表中。
这样等遍历完子句的所有字后,对应的DAG就生成好了。(子串的键值如果是0,则说明它不是一个词条)
计算最大概率路径
DAG的起点到终点会有很多路径,需要找到一条概率最大的路径,然后据此进行分词。可以采用动态规划来求解最大概率路径。
具体来说就是:从子句的最后一个字开始,倒序遍历子句的每个字,取当前字对应索引在DAG字典中的键值(一个python列表),然后遍历该列表,当前字会和列表中每个字两两组合成一个词条,然后基于词频计算出当前字到句尾的概率,以python元组的方式保存最大概率,元祖第一个元素是最大概率的对数,第二个元素为最大概率对应词条的终止索引。
词频可看作DAG中边的权重,之所以取概率的对数是为了防止数值下溢。有了最大概率路径,分词结果也就随之确定。
对未登录词采用HMM模型进行分词
当出现没有在前缀词典里收录的词时,会采用HMM模型进行分词。HMM模型有5个基本组成:观测序列、状态序列、状态初始概率、状态转移概率和状态发射概率。分词属于HMM的预测问题,即已知观测序列、状态初始概率、状态转移概率和状态发射概率的条件下,求状态序列。结巴分词已经内置了训练好的状态初始概率、状态转移概率和状态发射概率。
句子会作为观测序列,当有新句子进来时,具体做法为:先通过Viterbi算法求出概率最大的状态序列,然后基于状态序列输出分词结果(每个字的状态为B、M、E、S之一)。
至此,结巴分词的原理就简单介绍完了。
最后举一个简单的例子:
假如待分词的句子为: “这几天都在学自然语言处理”。
首先依据前缀词典生成DAG:
{ 0: [0],
1: [1, 2],
2: [2, 3],
3: [3],
4: [4],
5: [5],
6: [6, 7, 9],
7: [7],
8: [8, 9],
9: [9],
10: [10, 11],
11: [11] }
句子元素对应的索引会作为字典的键,对应键值的第一项与当前索引相同,其余项会与当前索引组成词条,这个词条在前缀词典里且对应键值不为0。
生成的DAG如下:
然后采用动态规划求出的最大概率路径为:
{12: (0, 0),
11: (-9.073726763747516, 11),
10: (-8.620554852761583, 11),
9: (-17.35315508178225, 9),
8: (-17.590039287472578, 9),
7: (-27.280113467960604, 7),
6: (-22.70346658402771, 9),
5: (-30.846092652642497, 5),
4: (-35.25970621827743, 4),
3: (-40.95138241952608, 3),
2: (-48.372244833381465, 2),
1: (-50.4870755319817, 2),
0: (-55.92332690525722, 0)}
最大概率路径按句子索引倒序排列,但分词时会从索引0开始顺序遍历句子。
具体做法为:
首先遍历0,0对应的键值最后一项为0,即词的长度为1,遇到长度为1的词时(即单字)先不分,继续往后看,然后遍历1,1对应的键值最后一项为2,即词的长度为2,这时会把索引为0的单字作为词分割出来,然后接着把索引1、2对应的词分割出来,然后遍历3,3对应的键值最后一项为3,属于单字,先不分,索引4、5同理,然后遍历6,6对应的键值最后一项为9,即词的长度为4,注意,这里索引3、4、5对应的单字序列(即“都在学”)如果不在前缀词典中或者在前缀词典中但键值为0,则会对单字序列采用HMM模型进行分词,否则的话,会对单字序列每个字进行分词,分好之后把索引6、7、8、9对应的词分割出去,然后遍历10,10对应的键值最后一项为11,即词的长度为2,直接把索引10、11对应的词分割出去,至此分词结束。
总结一下:
在遇到长度>=2的词之前会把它前面出现的所有单字保存下来。
如果保存下来的单字序列长度为0,则直接把当前词分割出去;
如果保存下来的单字序列长度为1,则直接把单字作为词分割出去,然后把后面词分割出去;
如果保存下来的单字序列长度>1,会分两种情况:假如单字序列不在前缀词典中或者在前缀词典中但键值为0,则会对单字序列采用HMM模型进行分词,否则的话,会对单字序列每个字进行分词。
最后分好的词为:['这', '几天', '都', '在', '学', '自然语言', '处理']。