jieba中文分词源码分析(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gfsfg8545/article/details/48195655

一、jieba的使用举例

  • jieba的简单使用
    我们根据作者的 github的例子来编写一个自己的例子,代码如下:
# encoding=utf-8
import jieba

seg_list = jieba.cut("去北京大学玩123", cut_all=True)
print("Full Mode: " + "/".join(seg_list))  # 全模式

seg_list = jieba.cut("去北京大学玩123", cut_all=False)
print("Default Mode: " + "/".join(seg_list))  # 精确模式

seg_list = jieba.cut("他来到了南七技校")  # 默认是精确模式
print("/".join(seg_list))

seg_list = jieba.cut_for_search("今天是2015年9月3号,去天安门广场庆祝抗战胜利70周年")  # 搜索引擎模式
print("/".join(seg_list))

运行前需要安装jieba,输出结果为:

Full Mode: 去/北京/北京大学/大学/玩/123
Default Mode: 去/北京大学/玩/123/来到//南七/技校
今天/是/2015//9//3//,//天安/广场/天安门/天安门广场/庆祝/抗战/胜利/70/周年

通过上面的例子可以看出,jieba分词具有三种模式:
1. 精确模式,试图将句子最精确地切开,适合文本分析;
2. 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;
3. 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。


童鞋们肯定比较好奇 jieba是什么实现这些功能的?
作者主要利用如下算法实现分词的:
1. 基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG);
作者这个版本中使用前缀字典实现了词库的存储(即dict.txt文件中的内容),而弃用之前版本的trie树存储词库,想想也是,python中实现的trie树是基于dict类型的数据结构而且dict中又嵌套dict 类型,这样嵌套很深,导致内存耗费严重,详情见作者把trie树改成前缀词典的 缘由, 具体实现见 gen_pfdict(self, f_name)。接着说DAG有向无环图, 生成句子中汉字所有可能成词情况所构成的有向无环图。DAG根据我们生成的前缀字典来构造一个这样的DAG,对一个sentence DAG是以{key:list[i,j…], …}的字典结构存储,其中key是词的在sentence中的位置,list存放的是在sentence中以key开始且词sentence[key:i+1]在我们的前缀词典中 的以key开始i结尾的词的末位置i的列表,即list存放的是sentence中以位置key开始的可能的词语的结束位置,这样通过查字典得到词, 开始位置+结束位置列表。例如:句子“抗日战争”生成的DAG中{0:[0,1,3]} 这样一个简单的DAG, 就是表示0位置开始, 在0,1,3位置都是词, 就是说0~0,0~1,0~3 即 “抗”,“抗日”,“抗日战争”这三个词 在dict.txt中是词。
2. 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合;
基于上面的DAG利用动态规划查找最大概率路径,这个理解DP算法的很容易就能明白了。根据动态规划查找最大概率路径的基本思路就是对句子从右往左反向计算最大概率,..依次类推, 最后得到最大概率路径, 得到最大概率的切分组合(这里满足最优子结构性质,可以利用反证法进行证明),这里代码实现中有个小trick,概率对数(可以让概率相乘的计算变成对数相加,防止相乘造成下溢,因为在语料、词库中每个词的出现概率平均下来还是很小的浮点数).
3. 对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法
未登录词(即jieba中文分词源码分析(一))中说的OOV, 其实就是词典 dict.txt 中没有记录的词。这里采用了HMM模型,HMM是个简单强大的模型,可以参考这个网络资源进行学习,HMM在实际应用中主要用来解决3类问题:

a. 评估问题(概率计算问题)
即给定观测序列 O=O1,O2,O3…Ot和模型参数λ=(A,B,π),怎样有效计算这一观测序列出现的概率.
(Forward-backward算法)
b. 解码问题(预测问题)
即给定观测序列 O=O1,O2,O3…Ot和模型参数λ=(A,B,π),怎样寻找满足这种观察序列意义上最优的隐含状态序列S。
(viterbi算法,近似算法)
c. 学习问题
即HMM的模型参数λ=(A,B,π)未知,如何求出这3个参数以使观测序列O=O1,O2,O3…Ot的概率尽可能的大.
(即用极大似然估计的方法估计参数,Baum-Welch,EM算法)

模型的关键相应参数λ=(A,B,π),经过作者对大量语料的训练, 得到了finalseg目录下的三个文件(初始化状态概率(π)即词语以某种状态开头的概率,其实只有两种,要么是B,要么是S。这个就是起始向量, 就是HMM系统的最初模型状态,对应文件prob_start.py;隐含状态概率转移矩A 即字的几种位置状态(BEMS四个状态来标记, B是开始begin位置, E是end, 是结束位置, M是middle, 是中间位置, S是single, 单独成词的位置)的转换概率,对应文件prob_trans.py;观测状态发射概率矩阵B 即位置状态到单字的发射概率,比如P(“狗”|M)表示一个词的中间出现”狗”这个字的概率,对应文件prob_emit.py)。这几个参数怎么得到的,具体方法见作者详述

二、jieba分词步骤

通过上面的举例即分析,想必大家对jieba分词应该有个大概的了解了。在上面的例子中我们注意到了,分词都是调用jieba.cut 这个函数,cut函数即是分词的入口,这个函数在文件jieba/__init__.py ,代码如下:

   #jieba分词的主函数,返回结果是一个可迭代的 generator
    def cut(self, sentence, cut_all=False, HMM=True):
        '''
        The main function that segments an entire sentence that contains
        Chinese characters into seperated words.
        Parameter:
            - sentence: The str(unicode) to be segmented.
            - cut_all: Model type. True for full pattern, False for accurate pattern.
            - HMM: Whether to use the Hidden Markov Model.
        '''
        sentence = strdecode(sentence) # 解码为unicode
        # 不同模式下的正则
        if cut_all:
            re_han = re_han_cut_all
            re_skip = re_skip_cut_all
        else:
            re_han = re_han_default
            re_skip = re_skip_default

         # 设置不同模式下的cut_block分词方法
        if cut_all:
            cut_block = self.__cut_all
        elif HMM:
            cut_block = self.__cut_DAG
        else:
            cut_block = self.__cut_DAG_NO_HMM
        # 先用正则对句子进行切分
        blocks = re_han.split(sentence)
        for blk in blocks:
            if not blk:
                continue
            if re_han.match(blk): # re_han匹配的串
                for word in cut_block(blk):# 根据不同模式的方法进行分词
                    yield word
            else:# 按照re_skip正则表对blk进行重新切分
                tmp = re_skip.split(blk)# 返回list
                for x in tmp:
                    if re_skip.match(x):
                        yield x
                    elif not cut_all: # 精准模式下逐个字符输出
                        for xx in x:
                            yield xx
                    else: 
                        yield x

其中参数sentence是需要分词的句子样本;cut_all是分词的模式,精确模式,全模式,默认使用HMM模型。下面根据cut函数来绘制出相应的流程图:
jieba.cut流程图
从图中可以看出,sentence先利用正则表达式切分,得到的词语列表blocks(re_han正则表达式使用了捕获括号,那么匹配的字符串也会被列入到list中返回),然后对切分后的每一个re_han匹配项blk词语利用cut_block方法进行具体的分词行为。


具体的分词流程概括起来如下:
1. 给定待分词的句子, 使用正则(re_han)获取匹配的中文字符(和英文字符)切分成的短语列表;
2. 利用get_DAG(sentence)函数获得待切分句子的DAG,首先检测(check_initialized)进程是否已经加载词库,若未初始化词库则调用initialize函数进行初始化,initialize中判断有无已经缓存的前缀词典cache_file文件,若有相应的cache文件则直接使用 marshal.load 方法加载前缀词典,若无则通过gen_pfdict对指定的词库dict.txt进行计算生成前缀词典,到jieba进程的初始化工作完成后就调用get_DAG获得句子的DAG;
3. 根据cut_block指定具体的方法(__cut_all,__cut_DAG,__cut_DAG_NO_HMM)对每个短语使用DAG进行分词 ,如cut_block=__cut_DAG时则使用DAG(查字典)和动态规划, 得到最大概率路径, 对DAG中那些没有在字典中查到的字, 组合成一个新的片段短语, 使用HMM模型进行分词, 也就是作者说的识别新词, 即识别字典外的新词;
4. 使用python的yield 语法生成一个词语生成器, 逐词语返回;

具体执行流程总结为下图:
cut_block具体分词行为

这一节的具体源码注释见github jieba\__init__.py,接下来的几节将对源码进行进一步的说明。


参考:

  1. 《统计学习方法》 李航
  2. 52nlpHMM系列
  3. http://www.pythontip.com/blog/post/9462/

猜你喜欢

转载自blog.csdn.net/gfsfg8545/article/details/48195655