结巴分词源码分析

闲来无事,在博客园的论坛里随意游荡,看到一个开源的python库,名字叫做结巴分词,一直很好奇这些自然语言的处理方式,但是网上的相关介绍却少的可怜,仅有的一些博客写介绍的比较浅。幸好代码量不多,花了两周的时间把代码和设计的算法仔细的梳理了一边,供大家参考,也希望能够抛砖引玉。

先看一下分词用到了哪些算法,文档里面如下介绍:

• 基于Trie树结构实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图(DAG)
• 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合
• 对于未登录词,采用了基于汉字成词能力的HMM模型,使用了Viterbi算法

估计是我水平比较菜,反正我看完之后不知所云。废话也不说了,下面好好来讲讲代码,结巴分词最顶层的目录jieba下有如下文件,dict.txt是一个词库,里面记录了大约350000个词语的词频和词性,结巴分词提供的功能接口都定义和实现在__init__.py中,finalseg文件夹中提供了隐马尔科夫维特比算法相关的代码,用于文本切词;analyse中提供了TF-IDF算法和textrank算法相关的实现,可以用于提取文章关键词以及文本摘要;唯一让我比较抑或的是posseg文件夹中的代码,和finalseg极其相似,实在猜测不出添加这么一个文件夹的意图。

image

概括的讲完结巴分词的文件结构后,再详细的讲一讲各个文件的内容。dict.txt的内容如下图所示,里面有每个词的统计次数和词性,在文档中提到的算法二中用到了词的统计次数,但是似乎所有的算法都没有用到词性,有想法的小伙伴可以尝试改进一下。

5

_compat.py文件是处理python2和python3之间差异的一个文件,有一个函数strdecode专门用来将字符串转换unicode, 感觉还蛮有用的

def strdecode(sentence):
    if not isinstance(sentence, text_type):
        try:
            sentence = sentence.decode('utf-8')
        except UnicodeDecodeError:
            sentence = sentence.decode('gbk', 'ignore')
    return sentence

__main__.py文件将底层的接口通过命令行的方式暴露给用户,用户可以设置自己的词典,需要处理的文件,是否使用隐马尔可夫模型,这个文件不涉及分词的算法,看起来没有什么难度,原来一直没有搞清楚__main__.py和__init__.py这两个文件的关系,通过万能的度娘总算搞清楚了,这里贴一个链接,http://www.tuicool.com/articles/iYRfe2 写的非常通俗易懂。

__init__.py这个文件是结巴分词的核心,里面提供了分词的接口:cut。它有三种模式:

1. 全模式,把句子中所有在词库中出现的词都找出来

2. 不使用隐马尔科夫模型的精确模式,基于最大概率路径, 找出基于词频的最大切分组合

3. 同时使用最大概率路径和隐马尔科夫模型,对于未登录词也有比较好的切分效果

这三种模式都会根据字典生成句子中汉字构成的有向无环图(DAG),实现的函数如下:

def get_DAG(sentence):
    global FREQ
    DAG = {}
    N = len(sentence)
    for k in xrange(N):
        tmplist = []
        i = k
        frag = sentence[k]
        while i < N and frag in FREQ:
            if FREQ[frag]:
                tmplist.append(i)
            i += 1
            frag = sentence[k:i + 1]
        if not tmplist:
            tmplist.append(k)
        DAG[k] = tmplist
    return DAG

以“但也并不是那么出乎意料或难以置信”这句话作为输入,生成的DAG如下,简单的讲就是把句子中词的位置标记出来

0 [0] 但
1 [1] 也
2 [2] 并
3 [3, 4] 不是
4 [4] 是
5 [5, 6] 那么
6 [6] 么
7 [7, 8, 10] 出乎意料
8 [8] 乎
9 [9, 10] 意料
10 [10] 料
11 [11] 或
12 [12, 13, 15] 难以置信
13 [13] 以
14 [14, 15] 置信
15 [15] 信

一、全模式 现在看一下全模式的代码:

def __cut_all(sentence):
    dag = get_DAG(sentence)
    old_j = -1
    for k, L in iteritems(dag):
        if len(L) == 1 and k > old_j:
            yield sentence[k:L[0] + 1]
            old_j = L[0]
        else:
            for j in L:
                if j > k:
                    yield sentence[k:j + 1]
                    old_j = j

就是把上面生成的DAG中的所有的组合显示出来,还是以上面的句子为例,全模式切分的结果如下,是不是觉得这么非常的easy。

但/也/并/不是/那么/出乎/出乎意料/意料/或/难以/难以置信/置信

二、 不使用隐马尔科夫模型的精确模式,用一个比较简单的句子为例,输入的句子是“难以置信”,按照全模式会输出:难以/置信/难以置信 三个组合。作为一个将汉语的人,我们明显知道最佳的分词结果就是“难以置信”这一种结果。下面用最大概率的方法解释为什么是这个结果。子啊字典中我们可以查询“难以置信”所有的组合以及它们出现的概率(除以所有词出现的总次数)如下:

次数 出现概率
18505 3.08*10-4
136136 2.27*10-3
11145 1.85*10-4
11188 1.86*10-4
难以 5681 9.45*10-5
置信 64 1.06*10-6
难以置信 169 2.81*10-6

根据上面各词的概率,可以算出“难以置信”所有分词方式的概率,而最大出现的可能就是“难以置信”,而且它的概率相比其他组合高的可不是一倍两倍。

分词方式 出现概率
难 以 置 信 1.37*10-14
难以 置 信 3.65*10-14
难 以 置信 7.41*10-13
难以 置信 1.01*10-10
难以置信 2.81*10-6

实现的代码如下,相信参考我的例子,看下面的代码也就不是什么大难题了,在计算概率的时候,结巴分词用的方式是log函数

def calc(sentence, DAG, route):
    N = len(sentence)
    route[N] = (0, 0)
    logtotal = log(total)
    for idx in xrange(N - 1, -1, -1):
        route[idx] = max((log(FREQ.get(sentence[idx:x + 1]) or 1) -
                          logtotal + route[x + 1][0], x) for x in DAG[idx])

def __cut_DAG_NO_HMM(sentence):
    DAG = get_DAG(sentence)
    route = {}
    calc(sentence, DAG, route)
    x = 0
    N = len(sentence)
    buf = ''
    while x < N:
        y = route[x][1] + 1
        l_word = sentence[x:y]
        if re_eng.match(l_word) and len(l_word) == 1:
            buf += l_word
            x = y
        else:
            if buf:
                yield buf
                buf = ''
            yield l_word
            x = y
    if buf:
        yield buf
        buf = ''
三、 同时使用最大概率路径和隐马尔科夫模型
由于这一部分设计到的理论知识比较多,建议先看一下相关的论文和博客。然后再通过代码验证对理论算法的知识,可以更深刻的理解隐马尔科夫模型。在这个地方中文句子作为可观测序列,对应的隐藏状态值集合为(B, M, E, S): {B:begin, M:middle, E:end, S:single}。分别代表每个状态代表的是该字在词语中的位置,B代表该字是词语中的起始字,M代表是词语中的中间字,E代表是词语中的结束字,S则代表是单字成词。
在HMM模型中文分词中,我们的输入是一个句子(也就是观察值序列),输出是这个句子中每个字的状态值。比如:“小明硕士毕业于中国科学院计算所”,隐藏序列为
BEBEBMEBEBMEBES,根据这个状态序列我们可以进行切词:BE/BE/BME/BE/BME/BE/S,所以切词结果如下:小明/硕士/毕业于/中国/科学院/计算/所。
finalseg中有BMES各个状态间的转移概率以及隐藏状态对应于各个中文的发射概率,再根据维特比算法,计算分词就相当的容易了。由于对隐马尔科夫理解有限,也就不瞎写了,有兴趣的可以看一下http://blog.csdn.net/likelet/article/details/7056068,写的非常的赞。
posted @ 2015-05-25 23:37  腩啵兔子  阅读(2231)  评论(0编辑  收藏  举报