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中每個詞語的頻度得分,要在所有的路徑中找出一條路徑使頻度得分的總和最大,這同時也是動態規劃的一個典型應用。
作者實現的程式碼如下:
- def calc(sentence,DAG,idx,route):
- N = len(sentence)
- route[N] = (0.0,'')
- for idx in xrange(N-1,-1,-1):
-
candidates = [ ( FREQ.get(sentence[idx:x+1],min_freq) + route[x+
- route[idx] = max(candidates)
在這裡分享一個檔案,該檔案儲存了 用marshal序列化後預設的 trie,FREQ,total,min_freq。把這個檔案解壓後放到D盤根目錄下,然後執行下面這段程式碼就可以看到任意一個sentence的route了。
- # -*- coding: utf-8 -*-
- # python2.7
- import marshal
- def get_DAG(sentence):
- N = len(sentence)
- i,j=0,0
- p = trie
- DAG = {}
- while i<N:
- c = sentence[j]
- if c in p:
- p = p[c]
- if''in p:
- if i notin DAG:
- DAG[i]=[]
- DAG[i].append(j)
- j+=1
- if j>=N:
- i+=1
- j=i
- p=trie
- else:
- p = trie
- i+=1
- j=i
- for i in xrange(len(sentence)):
- if i notin DAG:
- DAG[i] =[i]
- return DAG
- def calc(sentence,DAG,idx,route):
- N = len(sentence)
- route[N] = (0.0,'')
- for idx in xrange(N-1,-1,-1):
- candidates = [ ( FREQ.get(sentence[idx:x+1],0.0) + route[x+1][0],x ) for x in DAG[idx] ]
- route[idx] = max(candidates)
- if __name__=='__main__':
- sentence=u'國慶節我在研究結巴分詞'
- trie,FREQ,total,min_freq = marshal.load(open(u'D:\jieba.cache','rb'))#使用快取載入重要變數
- rs=get_DAG(sentence)#獲取DAG
- route={}
- calc(sentence,rs,0,route)#根據得分進行初步分詞
- print route