1. 程式人生 > >jieba分詞原始碼解讀二

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中每個詞語的頻度得分,要在所有的路徑中找出一條路徑使頻度得分的總和最大,這同時也是動態規劃的一個典型應用。

作者實現的程式碼如下:

  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了。

  1. # -*- coding: utf-8 -*-
  2. # python2.7
  3. import marshal  
  4. def get_DAG(sentence):  
  5.     N = len(sentence)  
  6.     i,j=0,0
  7.     p = trie  
  8.     DAG = {}  
  9.     while i<N:  
  10.         c = sentence[j]  
  11.         if c in p:  
  12.             p = p[c]  
  13.             if''in p:  
  14.                 if i notin DAG:  
  15.                     DAG[i]=[]  
  16.                 DAG[i].append(j)  
  17.             j+=1
  18.             if j>=N:  
  19.                 i+=1
  20.                 j=i  
  21.                 p=trie  
  22.         else:  
  23.             p = trie  
  24.             i+=1
  25.             j=i  
  26.     for i in xrange(len(sentence)):  
  27.         if i notin DAG:  
  28.             DAG[i] =[i]  
  29.     return DAG  
  30. def calc(sentence,DAG,idx,route):  
  31.     N = len(sentence)  
  32.     route[N] = (0.0,'')  
  33.     for idx in xrange(N-1,-1,-1):  
  34.         candidates = [ ( FREQ.get(sentence[idx:x+1],0.0) + route[x+1][0],x ) for x in DAG[idx] ]  
  35.         route[idx] = max(candidates)  
  36. if __name__=='__main__':  
  37.     sentence=u'國慶節我在研究結巴分詞'
  38.     trie,FREQ,total,min_freq = marshal.load(open(u'D:\jieba.cache','rb'))#使用快取載入重要變數
  39.     rs=get_DAG(sentence)#獲取DAG
  40.     route={}  
  41.     calc(sentence,rs,0,route)#根據得分進行初步分詞
  42.     print route