DAG(有向無環圖)
有向無環圖,directed acyclic graphs,簡稱DAG,是一種圖的資料結構,其實很naive,就是沒有環的有向圖_(:з」∠)_
DAG在分詞中的應用很廣,無論是最大概率路徑,還是後面套NN的做法,DAG都廣泛存在於分詞中。
因為DAG本身也是有向圖,所以用鄰接矩陣來表示是可行的,但是jieba採用了python的dict,更方便地表示DAG,其表示方法為:
{prior1:[next1,next2...,nextN],prior2:[next1`,next2`...nextN`]...}
以句子 “國慶節我在研究結巴分詞”為例,其生成的DAG的dict表示為:
{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]}
其中,
國[0] 慶[1] 節[2] 我[3] 在[4] 研[5] 究[6] 結[7] 巴[8] 分[9] 詞[10]
get_DAG()函式程式碼如下:
def get_DAG(self, sentence):
self.check_initialized()
DAG = {}
N = len(sentence)
for k in xrange(N):
tmplist = []
i = k
frag = sentence[k]
while i < N and frag in self.FREQ:
if self.FREQ[frag]:
tmplist.append(i)
i += 1
frag = sentence[k:i + 1]
if not tmplist:
tmplist.append(k)
DAG[k] = tmplist
return DAG
frag即fragment,可以看到程式碼迴圈切片句子,FREQ即是詞典的{word:frequency}的dict
因為在載入詞典的時候已經將word和word的所有字首加入了詞典,所以一旦frag not in FREQ,即可以斷定frag和以frag為字首的詞不在詞典裡,可以跳出迴圈。
由此得到了DAG,下一步就是使用dp動態規劃對最大概率路徑進行求解。
最大概率路徑
值得注意的是,DAG的每個結點,都是帶權的,對於在詞典裡面的詞語,其權重為其詞頻,即FREQ[word]。我們要求得route = (w1, w2, w3 ,.., wn),使得Σweight(wi)最大。
動態規劃求解法
滿足dp的條件有兩個
-
重複子問題
-
最優子結構
我們來分析最大概率路徑問題。
重複子問題
對於結點Wi和其可能存在的多個後繼Wj和Wk,有:
任意通過Wi到達Wj的路徑的權重為該路徑通過Wi的路徑權重加上Wj的權重{Ri->j} = {Ri + weight(j)} ;
任意通過Wi到達Wk的路徑的權重為該路徑通過Wi的路徑權重加上Wk的權重{Ri->k} = {Ri + weight(k)} ;
即對於擁有公共前驅Wi的節點Wj和Wk,需要重複計算到達Wi的路徑。
最優子結構
對於整個句子的最優路徑Rmax和一個末端節點Wx,對於其可能存在的多個前驅Wi,Wj,Wk…,設到達Wi,Wj,Wk的最大路徑分別為Rmaxi,Rmaxj,Rmaxk,有:
Rmax = max(Rmaxi,Rmaxj,Rmaxk…) + weight(Wx)
於是問題轉化為
求Rmaxi, Rmaxj, Rmaxk…
組成了最優子結構,子結構裡面的最優解是全域性的最優解的一部分。
狀態轉移方程
由上一節,很容易寫出其狀態轉移方程
Rmax = max{(Rmaxi,Rmaxj,Rmaxk…) + weight(Wx)}
程式碼
上面理解了,程式碼很簡單,注意一點total的值在載入詞典的時候求出來的,為詞頻之和,然後有一些諸如求對數的trick,程式碼是典型的dp求解程式碼。
def calc(self, sentence, DAG, route):
N = len(sentence)
route[N] = (0, 0)
logtotal = log(self.total)
for idx in xrange(N - 1, -1, -1):
route[idx] = max((log(self.FREQ.get(sentence[idx:x + 1]) or 1) -
logtotal + route[x + 1][0], x) for x in DAG[idx])