HMM:隱式馬爾可夫鏈
HMM的典型介紹就是這個模型是一個五元組:
觀測序列(observations):實際觀測到的現象序列
隱含狀態(states):所有的可能的隱含狀態
初始概率(start_probability):每個隱含狀態的初始概率
轉移概率(transition_probability):從一個隱含狀態轉移到另一個隱含狀態的概率
發射概率(emission_probability):某種隱含狀態產生某種觀測現象的概率
HMM模型可以用來解決三種問題:
- 參數(StatusSet, TransProbMatrix, EmitRobMatrix, InitStatus)已知的情況下,求解觀察值序列。(Forward-backward算法)
- 參數(ObservedSet, TransProbMatrix, EmitRobMatrix, InitStatus)已知的情況下,求解狀態值序列。(viterbi算法)
- 參數(ObservedSet)已知的情況下,求解(TransProbMatrix, EmitRobMatrix, InitStatus)。(Baum-Welch算法)
維特比算法:
算法:
例子
想象一個鄉村診所。村民有着非常理想化的特性,要么健康要么發燒。他們只有問診所的醫生的才能知道是否發燒。 聰明的醫生通過詢問病人的感覺診斷他們是否發燒。村民只回答他們感覺正常、頭暈或冷。
假設一個病人每天來到診所並告訴醫生他的感覺。醫生相信病人的健康狀況如同一個離散
馬爾可夫鏈。病人的狀態有兩種“健康”和“發燒”,但醫生不能直接觀察到,這意味着狀態對他是“隱含”的。每天病人會告訴醫生自己有以下幾種由他的健康狀態決定的感覺的一種:正常、冷或頭暈。這些是觀察結果。 整個系統為一個隱馬爾可夫模型(HMM)。
醫生知道村民的總體健康狀況,還知道發燒和沒發燒的病人通常會抱怨什么症狀。 換句話說,醫生知道隱馬爾可夫模型的參數。 這可以用
Python語言表示如下:
states = ('Healthy', 'Fever')
observations = ('normal', 'cold', 'dizzy')
start_probability = {'Healthy': 0.6, 'Fever': 0.4}
transition_probability = {
'Healthy' : {'Healthy': 0.7, 'Fever': 0.3},
'Fever' : {'Healthy': 0.4, 'Fever': 0.6},
}
emission_probability = {
'Healthy' : {'normal': 0.5, 'cold': 0.4, 'dizzy': 0.1},
'Fever' : {'normal': 0.1, 'cold': 0.3, 'dizzy': 0.6},
}
在這段代碼中, 起始概率start_probability 表示病人第一次到訪時醫生認為其所處的HMM狀態,他唯一知道的是病人傾向於是健康的。這里用到的特定概率分布不是均衡的,如轉移概率大約是{'Healthy': 0.57, 'Fever': 0.43}。 轉移概率transition_probability表示潛在的馬爾可夫鏈中健康狀態的變化。在這個例子中,當天健康的病人僅有30%的機會第二天會發燒。放射概率emission_probability表示每天病人感覺的可能性。假如他是健康的,50%會感覺正常。如果他發燒了,有60%的可能感覺到頭暈。
病人連續三天看醫生,醫生發現第一天他感覺正常,第二天感覺冷,第三天感覺頭暈。 於是醫生產生了一個問題:怎樣的健康狀態序列最能夠解釋這些觀察結果。維特比算法解答了這個問題。
# Helps visualize the steps of Viterbi.
def print_dptable(V):
print " ",
for i in range(len(V)): print "%7d" % i,
print
for y in V[0].keys():
print "%.5s: " % y,
for t in range(len(V)):
print "%.7s" % ("%f" % V[t][y]),
print
def viterbi(obs, states, start_p, trans_p, emit_p):
V = [{}]
path = {}
# Initialize base cases (t == 0)
for y in states:
V[0][y] = start_p[y] * emit_p[y][obs[0]]
path[y] = [y]
# Run Viterbi for t > 0
for t in range(1,len(obs)):
V.append({})
newpath = {}
for y in states:
(prob, state) = max([(V[t-1][y0] * trans_p[y0][y] * emit_p[y][obs[t]], y0) for y0 in states])
V[t][y] = prob
newpath[y] = path[state] + [y]
# Don't need to remember the old paths
path = newpath
print_dptable(V)
(prob, state) = max([(V[len(obs) - 1][y], y) for y in states])
return (prob, path[state])
函數viterbi 具有以下參數: obs 為觀察結果序列, 例如 ['normal', 'cold', 'dizzy']; states 為一組隱含狀態; start_p 為起始狀態概率; trans_p 為轉移概率; 而 emit_p 為放射概率。 為了簡化代碼,我們假設觀察序列 obs 非空且 trans_p[i][j] 和 emit_p[i][j] 對所有狀態 i,j 有定義。
在運行的例子中正向/維特比算法使用如下:
def example():
return viterbi(observations,
states,
start_probability,
transition_probability,
emission_probability)
print example()
維特比算法揭示了觀察結果 ['normal', 'cold', 'dizzy'] 最有可能由狀態序列 ['Healthy', 'Healthy', 'Fever']產生。 換句話說,對於觀察到的活動, 病人第一天感到正常,第二天感到冷時都是健康的,而第三天發燒了。
維特比算法的計算過程可以直觀地由
格圖表示。 維特比路徑本質上是穿過格式結構的最長路徑。 診所例子的格式結構如下, 黑色加粗的是維特比路徑:
在實現維特比算法時需注意許多編程語言使用
浮點數計算,當 p 很小時可能會導致結果
下溢。 避免這一問題的常用技巧是在整個計算過程中使用
對數概率,在
對數系統中也使用了同樣的技巧。 當算法結叢時,可以通過適當的冪運算獲得精確結果。
中文分詞的例子:
五元組參數在中文分詞中的具體含義
接下來我們講實的,不講虛的,針對中文分詞應用,直接給五元組參數賦予具體含義:
StatusSet & ObservedSet
狀態值集合為(B, M, E, S): {B:begin, M:middle, E:end, S:single}。分別代表每個狀態代表的是該字在詞語中的位置,B代表該字是詞語中的起始字,M代表是詞語中的中間字,E代表是詞語中的結束字,S則代表是單字成詞。
觀察值集合為就是所有漢字(東南西北你我他…),甚至包括標點符號所組成的集合。
狀態值也就是我們要求的值,在HMM模型中文分詞中,我們的輸入是一個句子(也就是觀察值序列),輸出是這個句子中每個字的狀態值。 比如:
小明碩士畢業於中國科學院計算所
輸出的狀態序列為
BEBEBMEBEBMEBES
根據這個狀態序列我們可以進行切詞:
BE/BE/BME/BE/BME/BE/S
所以切詞結果如下:
小明/碩士/畢業於/中國/科學院/計算/所
同時我們可以注意到:
B后面只可能接(M or E),不可能接(B or S)。而M后面也只可能接(M or E),不可能接(B, S)。
沒錯,就是這么簡單,現在輸入輸出都明確了,下文講講輸入和輸出之間的具體過程,里面究竟發生了什么不可告人的秘密,請看下文:
上文只介紹了五元組中的兩元【StatusSet, ObservedSet】,下文介紹剩下的三元【InitStatus, TransProbMatrix, EmitProbMatrix】。
這五元的關系是通過一個叫Viterbi的算法串接起來, ObservedSet序列值是Viterbi的輸入, 而StatusSet序列值是Viterbi的輸出, 輸入和輸出之間Viterbi算法還需要借助三個模型參數, 分別是InitStatus, TransProbMatrix, EmitProbMatrix, 接下來一一講解:
InitStatus
初始狀態概率分布是最好理解的,可以示例如下:
#B
-0.26268660809250016
#E
-3.14e+100
#M
-3.14e+100
#S
-1.4652633398537678
示例數值是對概率值取對數之后的結果(可以讓概率相乘的計算變成對數相加),其中-3.14e+100作為負無窮,也就是對應的概率值是0。下同。
也就是句子的第一個字屬於{B,E,M,S}這四種狀態的概率,如上可以看出,E和M的概率都是0,這和實際相符合,開頭的第一個字只可能是詞語的首字(B),或者是單字成詞(S)。
TransProbMatrix
轉移概率是馬爾科夫鏈很重要的一個知識點,大學里面學過概率論的人都知道,馬爾科夫鏈最大的特點就是當前T=i時刻的狀態Status(i),只和T=i時刻之前的n個狀態有關。也就是:
{Status(i-1), Status(i-2), Status(i-3), ... Status(i - n)}
更進一步的說,HMM模型有三個基本假設(具體哪三個請看文末備注)作為模型的前提,其中有個【有限歷史性假設】,也就是馬爾科夫鏈的n=1。即Status(i)只和Status(i-1)相關,這個假設能大大簡化問題。
回過頭看TransProbMatrix,其實就是一個4x4(4就是狀態值集合的大小)的二維矩陣,示例如下:
矩陣的橫坐標和縱坐標順序是BEMS x BEMS。(數值是概率求對數后的值,別忘了。)
-3.14e+100 -0.510825623765990 -0.916290731874155 -3.14e+100
-0.5897149736854513 -3.14e+100 -3.14e+100 -0.8085250474669937
-3.14e+100 -0.33344856811948514 -1.2603623820268226 -3.14e+100
-0.7211965654669841 -3.14e+100 -3.14e+100 -0.6658631448798212
比如TransProbMatrix[0][0]代表的含義就是從狀態B轉移到狀態B的概率,由
TransProbMatrix[0][0] = -3.14e+100
可知,這個轉移概率是0,這符合常理。由狀態各自的含義可知,狀態B的下一個狀態只可能是ME,不可能是BS,所以不可能的轉移對應的概率都是0,也就是對數值負無窮,在此記為-3.14e+100。
由上TransProbMatrix矩陣可知,對於各個狀態可能轉移的下一狀態,且轉移概率對應如下:
#B
#E:-0.510825623765990,M:-0.916290731874155
#E
#B:-0.5897149736854513,S:-0.8085250474669937
#M
#E:-0.33344856811948514,M:-1.2603623820268226
#S
#B:-0.7211965654669841,S:-0.6658631448798212
EmitProbMatrix
這里的發射概率(EmitProb)其實也是一個條件概率而已,根據HMM模型三個基本假設(哪三個請看文末備注)里的【觀察值獨立性假設】,觀察值只取決於當前狀態值,也就是:
P(Observed[i], Status[j]) = P(Status[j]) * P(Observed[i]|Status[j])
其中P(Observed[i]|Status[j])這個值就是從EmitProbMatrix中獲取。
EmitProbMatrix示例如下:
#B
耀:-10.460283,涉:-8.766406,談:-8.039065,伊:-7.682602,洞:-8.668696,...
#E
耀:-9.266706,涉:-9.096474,談:-8.435707,伊:-10.223786,洞:-8.366213,...
#M
耀:-8.47651,涉:-10.560093,談:-8.345223,伊:-8.021847,洞:-9.547990,....
#S
蘄:-10.005820,涉:-10.523076,唎:-15.269250,禑:-17.215160,洞:-8.369527...
雖然EmitProbMatrix也稱為矩陣,這個矩陣太稀疏了,實際工程中一般是將上面四行發射轉移概率存儲為4個Map,詳見代碼
HMMSegment。
到此,已經介紹完HMM模型的五元參數,假設現在手頭上已經有這些參數的具體概率值,並且已經加載進來,(也就是有該模型的字典了,詳見
HMMDict里面的hmm_model.utf8),那么我們只剩下Viterbi這個算法函數,這個模型就算可以開始使用了。所以接下來講講Viterbi算法。
HMM中文分詞之Viterbi算法
輸入樣例:
小明碩士畢業於中國科學院計算所
Viterbi算法計算過程如下:
定義變量
二維數組 weight[4][15],4是狀態數(0:B,1:E,2:M,3:S),15是輸入句子的字數。比如 weight[0][2] 代表 狀態B的條件下,出現'碩'這個字的可能性。
二維數組 path[4][15],4是狀態數(0:B,1:E,2:M,3:S),15是輸入句子的字數。比如 path[0][2] 代表 weight[0][2]取到最大時,前一個字的狀態,比如 path[0][2] = 1, 則代表 weight[0][2]取到最大時,前一個字(也就是明)的狀態是E。記錄前一個字的狀態是為了使用viterbi算法計算完整個 weight[4][15] 之后,能對輸入句子從右向左地回溯回來,找出對應的狀態序列。
使用InitStatus對weight二維數組進行初始化
已知InitStatus如下:
#B
-0.26268660809250016
#E
-3.14e+100
#M
-3.14e+100
#S
-1.4652633398537678
且由EmitProbMatrix可以得出
Status(B) -> Observed(小) : -5.79545
Status(E) -> Observed(小) : -7.36797
Status(M) -> Observed(小) : -5.09518
Status(S) -> Observed(小) : -6.2475
所以可以初始化 weight[i][0] 的值如下:
weight[0][0] = -0.26268660809250016 + -5.79545 = -6.05814
weight[1][0] = -3.14e+100 + -7.36797 = -3.14e+100
weight[2][0] = -3.14e+100 + -5.09518 = -3.14e+100
weight[3][0] = -1.4652633398537678 + -6.2475 = -7.71276
注意上式計算的時候是相加而不是相乘,因為之前取過對數的原因。
遍歷句子計算整個weight二維數組
//遍歷句子,下標i從1開始是因為剛才初始化的時候已經對0初始化結束了
for(size_t i = 1; i < 15; i++)
{
// 遍歷可能的狀態
for(size_t j = 0; j < 4; j++)
{
weight[j][i] = MIN_DOUBLE;
path[j][i] = -1;
//遍歷前一個字可能的狀態
for(size_t k = 0; k < 4; k++)
{
double tmp = weight[k][i-1] + _transProb[k][j] + _emitProb[j][sentence[i]];
if(tmp > weight[j][i]) // 找出最大的weight[j][i]值
{
weight[j][i] = tmp;
path[j][i] = k;
}
}
}
}
如此遍歷下來,weight[4][15] 和 path[4][15] 就都計算完畢。
確定邊界條件和路徑回溯
邊界條件如下:
對於每個句子,最后一個字的狀態只可能是 E 或者 S,不可能是 M 或者 B。
所以在本文的例子中我們只需要比較 weight[1(E)][14] 和 weight[3(S)][14] 的大小即可。
在本例中:
weight[1][14] = -102.492;
weight[3][14] = -101.632;
所以 S > E,也就是對於路徑回溯的起點是 path[3][14]。
回溯的路徑是:
SEBEMBEBEMBEBEB
倒序一下就是:
BE/BE/BME/BE/BME/BE/S
所以切詞結果就是:
小明/碩士/畢業於/中國/科學院/計算/所
到此,一個HMM模型中文分詞算法過程就闡述完畢了。
也就是給定我們一個模型,我們對模型進行載入完畢之后,只要運行一遍Viterbi算法,就可以找出每個字對應的狀態,根據狀態也就可以對句子進行分詞。
參考資料:
http://wulc.me/2017/03/02/%E7%BB%B4%E7%89%B9%E6%AF%94%E7%AE%97%E6%B3%95/