jieba分词源码解读二

上一篇文章说到结巴分词用了包装器实现了在 get_DAG 函数执行器生成了 trie 树。在这篇文章中我们要研究一下jieba分词中的 DAG(有向无环图,全称:directed acyclic graphs )。

在 cut 函数使用正则表达式把文本切分成一个一个短语和句子后,再用 __cut_DAG 函数对其进行分词。这些句子和短语就是 所谓的 sentence。每一个sentence都会生成一个DAG。作者用来表达DAG的数据结构是dict + list 。举一个例子,比如 sentence 是 "国庆节我在研究结巴分词",对应生成的DAG是这样的:

{0: [0, 1, 2], 1: [1], 2: [2], 3: [3], 4: [4], 5: [5, 6], 6: [6], 7: [7, 8], 8: [8], 9: [9, 10], 10: [10]}

其中的数字表示每个汉字在sentence中的位置,所以0:[0,1,2] 表示 在trie 树中,"国"开头的词语中对应于该 sentence 有三种匹配情况:国,国庆,国庆节;分别对应3条有向图的路径:0->1->2->3,0->2->3,0->3。结巴分词使用的是正向匹配法,这一点从字典中也可以看出。

此外补充一点在上一篇中提到的 initialize 函数除了生成了trie树外还返回了两个重要的值。在代码中分别叫 total 和 FREQ。total 是dict.txt中所有词语的词频之和。而FREQ是一个dict类型的变量,它用来保存dict.txt中每个词语的频度打分,打分的公式是 log(float(v)/total),其中v就是被打分词语的频度值。

那么剩下的目标就很明确了:我们已经有了sentence的DAG和sentence中每个词语的频度得分,要在所有的路径中找出一条路径使频度得分的总和最大,这同时也是动态规划的一个典型应用。

作者实现的代码如下:

[python]  view plain  copy
  1. def calc(sentence,DAG,idx,route):  
  2.     N = len(sentence)  
  3.     route[N] = (0.0,'')  
  4.     for idx in xrange(N-1,-1,-1):  
  5.         candidates = [ ( FREQ.get(sentence[idx:x+1],min_freq) + route[x+1][0],x ) for x in DAG[idx] ]  
  6.         route[idx] = max(candidates)  
这是一个自底向上的动态规划,从sentence的最后一个字开始倒序遍历每个分词方式的得分(必须是倒序,正序不行,大家可以思考下为什么)。然后将得分最高的情况以(频度得分值,词语最后一个字的位置)这样的tuple保存在route中。(注:从代码上看idx是一个无用的参数~)

在这里分享一个文件,该文件保存了 用marshal序列化后默认的 trie,FREQ,total,min_freq。把这个文件解压后放到D盘根目录下,然后运行下面这段代码就可以看到任意一个sentence的route了。

[python]  view plain  copy
  1. # -*- coding: utf-8 -*-  
  2. # python2.7  
  3. import marshal  
  4.   
  5. def get_DAG(sentence):  
  6.   
  7.     N = len(sentence)  
  8.     i,j=0,0  
  9.     p = trie  
  10.     DAG = {}  
  11.     while i<N:  
  12.         c = sentence[j]  
  13.         if c in p:  
  14.             p = p[c]  
  15.             if '' in p:  
  16.                 if i not in DAG:  
  17.                     DAG[i]=[]  
  18.                 DAG[i].append(j)  
  19.             j+=1  
  20.             if j>=N:  
  21.                 i+=1  
  22.                 j=i  
  23.                 p=trie  
  24.         else:  
  25.             p = trie  
  26.             i+=1  
  27.             j=i  
  28.     for i in xrange(len(sentence)):  
  29.         if i not in DAG:  
  30.             DAG[i] =[i]  
  31.     return DAG  
  32.   
  33. def calc(sentence,DAG,idx,route):  
  34.     N = len(sentence)  
  35.     route[N] = (0.0,'')  
  36.     for idx in xrange(N-1,-1,-1):  
  37.         candidates = [ ( FREQ.get(sentence[idx:x+1],0.0) + route[x+1][0],x ) for x in DAG[idx] ]  
  38.         route[idx] = max(candidates)  
  39.   
  40. if __name__=='__main__':  
  41.     sentence=u'国庆节我在研究结巴分词'  
  42.     trie,FREQ,total,min_freq = marshal.load(open(u'D:\jieba.cache','rb'))#使用缓存载入重要变量  
  43.     rs=get_DAG(sentence)#获取DAG  
  44.     route={}  
  45.     calc(sentence,rs,0,route)#根据得分进行初步分词  
  46.     print route  

附赠一篇讲如何判断有向图是否存在环的文章: http://blog.csdn.net/memray/article/details/8021865 。

你可能感兴趣的:(jieba分词源码解读二)