之前寫過一篇使用語言模型進行中文分詞的部落格,本篇在之前寫的語言模型的基礎上,通過隱馬爾科夫模型實現簡單的拼音輸入法,即輸入一組拼音,我們把它轉換成中文。
一、隱馬爾科夫模型
首先我們來說一下什麼是馬爾科夫鏈,簡單的說,任何一組我們可以觀察到的連續序列,比如連續一個星期的天氣狀況,都可以稱為馬爾科夫鏈。
馬爾科夫鏈的最主要特性是,假設當前節點的狀態(比如今天的天氣),只與之前N個連續狀態相關(比如之前一個星期的天氣狀況),通過這個性質和大量的統計,我們就可以根據最近幾天的天氣預測未來一天的天氣了。
那麼什麼是隱馬爾科夫模型呢?
比如你有一個朋友在美國,他會每天告訴你他今天做了什麼(打球、跑步或呆在家裡),你要通過他的這些活動推測過去一週內他們那邊的天氣。這裡你朋友的活動對你來說是可以觀察到的,即觀察序列,而過去一週的天氣是隱藏序列,那麼這個通過觀察序列推測隱藏序列的模型,就是隱馬爾科夫模型。
隱馬爾科夫模型的“隱”就體現在“隱藏序列”這個概念裡。
二、通過拼音推測漢字
很明顯,我們的拼音輸入法的觀察序列就是使用者的輸入拼音,比如”wo shi zhong guo ren”,我們要推測出使用者想要輸入的是“我 是 中 國 人”,這是個很典型的隱馬爾科夫模型:
如上圖所示,我們根據給定的觀察物件O,獲得一個概率最大的序列S*。我們所知道的資料有:
1, 所有觀察物件的值
2, 隱藏序列的馬爾科夫模型概率,這是通過統計獲得的
3, 隱藏狀態到觀察狀態的概率,比如 “晴天”(隱藏狀態) 到 “出去玩”(觀察狀態)的概率
我們要求的是S*各個狀態的連續概率最大的那個序列。
三、前向概率Viterbi演算法
我們把隱藏序列中的一個節點的每一種可能取值都畫出來的話,我們會發現這其實是一個有向圖尋找最優路徑的問題:
H、O、S都是當前節點可能的取值(比如hao拼音可能有好、號、耗等漢字)。而對於這種尋找最優路徑的問題,我們可以通過動態規劃的思想去考慮,假設σt(k)表示第t個位置對應的漢字是k,s表示隱藏序列,o表示觀察序列,M表示當前隱藏節點的可能取值,那麼:
上式中的表示從隱藏節點k到觀察序列t+1的發射概率,α_{l,k}表示l到k的馬爾科夫狀態轉移概率。
寫出了遞迴式,這個動態規劃程式就容易寫了。
四、實現
既然我們是一個有向圖,那麼閒來構造圖中的所有節點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class GraphNode(object): """有向圖中的節點""" def __init__(self, word, emission): # 當前節點所代表的漢字(即狀態) self.word = word # 當前狀態發射拼音的發射概率 self.emission = emission # 最優路徑時,從起點到該節點的最高分 self.max_score = 0.0 # 最優路徑時,該節點的前一個節點,用來輸出路徑的時候使用 self.prev_node = None class Graph(object): """有向圖結構構造器""" def __init__(self, pinyins, im): """根據拼音所對應的所有漢字組合,構造有向圖""" self.sequence = [] for py in pinyins: current_position = {} # 從拼音、漢字的對映表中讀取漢字及漢字到拼音的發射概率 for word,emission in im.emission[py].items(): node = GraphNode(word, emission) current_position[word] = node self.sequence.append(current_position) |
核心的viterbi演算法為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
def viterbi(self, t, k): """ 第t個位置出現k字元的概率,記為p_t(k) """ if self.get_key(t,k) in self.viterbi_cache: return self.viterbi_cache[self.get_key(t,k)] node = self.graph.sequence[t][k] if t == 0: # 從統計語言模型中獲得轉移概率 state_transfer = self.lm.get_init_prop(k) # 獲得發射概率 emission_prop = self.emission[self.pinyins[t]][k] # 計算當前節點的概率得分 node.max_score = 1.0 * state_transfer * emission_prop self.viterbi_cache[self.get_key(t,k)] = node.max_score return node.max_score # 獲得前一個狀態所有可能的漢字 pre_words = self.graph.sequence[t-1].keys() # 對當前節點分貝與前一個節點的所有可能漢字進行計算,獲得概率最大的那個 for l in pre_words: state_transfer = self.lm.get_trans_prop(k, l) emission_prop = self.emission[self.pinyins[t-1]][l] score = self.viterbi(t-1, l) * state_transfer * emission_prop if score > node.max_score: node.max_score = score node.prev_node = self.graph.sequence[t-1][l] self.viterbi_cache[self.get_key(t,k)] = node.max_score return node.max_score |
由於完整程式碼牽扯的東西太多,包括語言模型、漢字拼音對映表,這裡就不在全部貼出來了,不過有了上面兩部分程式碼,其實其他的也很容易謝了,就是載入進去而已。如有興趣可以聯絡我索取相關完整程式碼。
輸出效果:
1 2 3 4 5 6 7 8 9 10 11 |
ocalhost:HMM roy$ python InputMethod.py loading words hashing bi-gram keys hashing single word keys zhong wen shu ru fa 中 文 輸 入 發 ren min ri bao ri ren min 人 民 日 報 日 人 民 zhong hua ren min gong he guo 中 華 人 民 共 和 國 localhost:HMM roy$ |
第一組輸入的有一個錯字,主要是因為語料庫不夠完善,可能“入法”兩個字的概率小於“入發”兩個字了,不是大問題。
由於找我發郵件要程式碼的人太多…我覺得還是直接放在 這裡 讓大家下載吧,不過程式碼都是demo性質的,只為實現效果,切勿放到真實環境執行。