HMM隱馬爾科夫模型
①通俗的理解
首先舉一個例子,扔骰子,有三種骰子,第一個是比較常見的6個面,每一個面的概率都是1/6。第二個只有4個面,,每一個面的概率是1/4。第三個有8個面,,每一個面的概率是1/8。
首先先選擇一個骰子,挑到每一個骰子的概率1/3,然後投擲,可以得到。最後會得到一堆的序列,比如會得到等等,這種序列叫可見狀態序列,但在HMM裏面,還存在一個隱含狀態鏈,比如這個狀態鏈可能是。從8個面出來的,6個面投出來的。所以隱馬爾科夫模型裏面有兩串序列,可觀測序列和隱含狀態序列。
一般來講,說到隱馬爾科夫鏈其實就是隱含的狀態序列了,markov鏈各個元素直接是存在了轉化關係的,比如一開始是D6下一個狀態是D4,D6,D8的概率都是1/3,這樣設置平均值只是爲了初始化而已,一般這種值其實是可以隨意改變的,比如這裏下一個狀態序列是D8,那麼就可以設置轉化到D6的概率是0.1,D4的概率是0.1,D8的概率是0.8。這樣就又是一個很新的HMM了。
同樣的,儘管可見的狀態之間沒有轉換概率,但是隱含狀態和可見狀態之間有一個概率叫做輸出概率,也叫發射概率,就這個例子來說,D6輸出1的概率就是6了,產生23456的概率都是1/6。
所以,HMM首先是有兩個序列,可觀測序列,隱含序列。每一個隱含序列裏面的元素直接是存在着轉化概率的,也就是用概率來描述轉化到下一個狀態的可能性;而隱含序列和觀測序列之間也有一個概率,發射概率,也叫輸出概率。
隱含狀態關係圖:
每一個箭頭都會對應着一個概率,轉換概率。
如果知道了轉換概率和隱含概率那麼求解觀測序列會很簡單,但如果是這樣那麼這個模型就沒有什麼可以研究的了,所以肯定是會缺失一點值的,比如知道觀測序列和參數,但是不知道隱含序列是什麼,或者是知道了 觀測序列想求參數,又或者是知道了這個參數和隱含序列求這個觀測序列出現的參數是什麼。其實這些就對應了後面的三個問題。HMM的核心其實也就是這三個問題。
②三個基本問題通俗解釋
1.知道骰子有幾種(知道隱含序列有多少種),也就是幾種骰子,上面就有三種,每種骰子是什麼(轉換概率),根據骰子扔出的結果(可觀測狀態鏈),想知道每次投的是哪種骰子(隱含狀態序列)?
這問題其實就是解碼問題了,知道觀測序列求隱含序列。其實有兩種解法,一種就是暴力求解:其實就是maximum likelihood全部乘起來一起算即可。另一種就厲害帶了,遞推的方法,前向算法,後向算法,前向-後向算法。
2.還是知道骰子有幾種隱含狀態(隱含狀態的數量),知道骰子是什麼(知道轉換概率),根據骰子投出的結果我想知道投出這個結果的概率是多少?
這個問題看起來貌似沒有什麼太大的作用,但是實際上是用於對於模型的檢測,如果我們投出來的結果都對應了很小的概率,那麼模型可能就是錯誤的,有人換了我們的骰子。
3.知道骰子有幾種(知道隱含序列的種類),不知道每種骰子是什麼(不知道轉換概率),實驗得到多次骰子的結果(可觀測序列),現在想知道每種骰子是什麼(轉換概率)
這很重要,後面會用到來求解分詞的狀態。
到這裏先引出一個比較簡單的問題,0號問題:
0.知道骰子的種類,骰子是什麼,每次扔什麼骰子,根據這個結果,求投出這個結果的概率是多少。
其實就是該知道的都知道了,直接求概率。
求解無非就是概率相乘了:
③三個問題的簡單解答
1.看見了觀測序列,求隱藏序列
這裏解釋的就是第一種解法了,最大似然估計,也就是暴力解法。首先我知道我有3個骰子,六面骰子,八面骰子,四面骰子,同時結果我也知道了(1 6 3 5 2 7 3 5 2 4),現在想知道我每一次投的哪一個骰子,是六面的還是四面的還是八面的呢?
最簡單的方法就是窮舉了,從零個一直到第最後一個一次把每一個概率算出來,鏈不長還行,鏈要是長了就算不了了,窮舉的數量太大。
接下來是要討論另一個比較牛逼的算法,viterbi algorithm。
首先只扔一次骰子:
結果是1的話那麼概率最大的就是四面骰子了,1/4,其他的都是1/6和1/8。接着再扔一次:這個時候我們就要計算三個值了,分別是D6,D8,D4的概率。要取到最大的概率,那麼第一次肯定就是取到D4了,所以取到D6的最大概率:
取到D8的最大概率:取到D4這輩子都不可能了,四面骰子沒有6。所以最大的序列就是D4,D6
繼續拓展一下,投多一個:這個時候又要計算三種情況了,分別計算爲D4,D6,D8的概率是多少:
可以看到,概率最大的就是D4了,所以三次投骰子概率最大的隱含序列就是D4,D6,D4。所以無論這個序列多長,直接遞推計算可以算出來的,算到最後一位的時候,直接反着找就好了。
####2.求可觀測序列的概率
如果你的骰子有問題,要怎麼檢測出來?首先,可以先算一下正常骰子投出一段序列的概率,再算算不正常的投出來序列的概率即可。如果小於正常的,那麼就有可能是錯的了。
比如結果如上,我們這個時候就不是要求隱含序列了,而是求出現這個觀測序列的總概率是多少。首先如果是隻投一個骰子:這時候三種骰子都有可能:
再投一次:
同樣是要計算總的分數。
再投一次:
就是這樣按照套路計算一遍再比較結果即可。
3.只是知道觀測序列,想知道骰子是怎麼樣的?
這個算法在後面會細講,Baum-welch算法。
④HMM的公式角度
下面正式開始講解,上面只是過一個印象。
1.馬爾科夫模型
要看隱馬爾科夫自然先動馬爾科夫是什麼。已知現在有N個有序的隨機變量,根據貝葉斯他們的聯合分佈可以寫成條件連乘:
再繼續拆:
馬爾科夫鏈就是指,序列中的任何一個隨機變量在給定它的前一個變量的分佈於更早的變量無關。也就是說當前的變量只與前一個變量有關,與更早的變量是沒有關係的。用公式表示:
在這個假設的前提下:這個式子表示的就是一階馬爾科夫模型:
上圖表示的是一階的馬爾科夫模型,一階的意思是隻和前一個variable相關,二階那自然就是和前兩個variable相關了,所以M階的:但是這樣帶來的問題就是指數爆炸的問題,雖然達到了關聯更早變量的能力,但是計算能力很大。每一個變量等於是指數級的計算量非常大。那麼有沒有一種方法可以使得當前變量和更早的變量關聯起來呢?又不需要這麼多參數?
2.隱馬爾科夫模型
於是我們引入了隱變量,做到了一個變量可以和更早的變量關聯起來。使用隱變量構成一階馬爾科夫模型,可觀測變量和隱變量關聯,這樣就可以得到一類模型,也就是狀態空間模型。
就是隱變量,爲可觀測變量。那麼的聯合概率:
於是就被分解成了三個部分:,這三個東西分別是,初始概率,轉移概率和發射概率。這個時候,每一個變量之間已經不存在有馬爾科夫性了,因爲每一個變量x都會通過對應的隱變量和之前的變量相關,所以無法再從中拿到任何一個變量了。其實隱變量其實就像是一個媒介,關聯起所有的向量。當是離散的時候,這個模型就是隱馬爾科夫模型了。
3.隱馬爾科夫模型的定義
根據上面的描述,已經知道了隱馬爾科夫模型是由三個部分構成,初始概率,轉移概率,發射概率。那自然就對應着三個參數了:。π是初始矩陣,也就是一個Nx1的,N就是隱變量的數量;A是狀態轉移矩陣,,因爲這是代表轉移到每一個隱變量的概率,包括自己的。B就是發射概率了。
使用來代表隱含狀態序列,使用
隱狀態的種類:,有N種,每一種分別是。
所有可能觀測到的集合:
最後就是隱馬爾科夫模型的兩個條件了,這兩個條件前面可是提過的,這兩個條件在後面大有作爲:
①齊次假設:
②觀測獨立性假設:
4.三個問題的公式求解
問題1:給定模型的參數和觀測序列,計算在λ參數下的O觀測序列出現的概率是多少?
這個問題,暴力求解,前向後向算法。首先要介紹的就是暴力求解。
暴力求解
這個算法沒有什麼卵用,只是用於理解這個模型而已。
按照概率公式,列舉所有可能的長度爲T的狀態序列,求各個狀態序列I和觀測序列的聯合概率。
還是常規操作,分解了再說:
那麼現在就是要求解。
這上面用到了齊次假設。
這裏就用到了觀測獨立假設。
所以,
所以這個就是暴力求解的過程了,沒有什麼太抽象的東西,都是直接公式的推導代入即可。如果馬爾科夫鏈很長,一般都是做不完的,所以這個算法也就是助於理解而已。
前向算法
在給定時刻,求出現了觀測序列。假設表示的就是在觀測到同時最後一個狀態是的時候的概率。
求初值:
對於t = 1,2,3…T:。這個其實蠻好理解的,等於就是:(上一個節點轉移的i個狀態的和)*i狀態到下一個狀態的發送概率
最後:
前向算法很直觀,比較好理解。
後向算法
後向算法,顧名思義就是往前推的了。所以使用β來表示。
初值:
對於t=T-1,T-2,…1:
最終:
直觀理解:這個東西有點騷,你得倒着看。首先既然是得到了未來的一個概率,那麼要求之前的,肯定要回退到隱狀態啊。怎麼回退呢?怎麼來的就怎麼退,隱狀態到觀測序列用的是B矩陣發射概率,那自然是用B矩陣裏面的來回退了,於是乘上回退到狀態轉移矩陣,然後再用A矩陣回到的上一個節點。但是這樣只是做一條路,總共可不止這麼多條,於是再加和即可。
這樣就應該很直觀了。
公式理解:
這個更直觀,公式一推就都出來了。這個推導忘了是再哪看到的了。當時我和我的小夥伴們都驚呆了,所以記得比較清楚。
額外補充一點,對於前向概率和後向概率之間的關係:
####前向概率和後向概率的關係
我們定義一個概率表達,在參數給定了的情況下,求解出現了觀測序列且當前狀態是的概率是多少。分解並推導一下這個公式:
記:
表示的就是在給定了觀測序列和參數的情況下,當前狀態是的概率是多少。
還有另外一個比較重要的:給定了觀測值和參數,求在時刻狀態是,時刻狀態是的概率,記爲:
推導一下,這個在後面baum-welch算法大有作爲。
主要就是怎麼求。這個和前向算法有點像,直接畫圖理解:
對比圖片其實很明顯了:
所以
期望:
在觀測狀態已知,參數已知的情況下,狀態出現的期望:
在觀測狀態已知,參數已知的情況下,狀態轉移到狀態的期望:
這樣的話那麼第一個問題就解決了。
問題2:已知觀測序列,估計模型的參數的參數,使得在該模型下的最大。
baum-welch算法求解-EM思想
求極大,自然就是EM了。這裏直接就使用Q函數:
還是再提一下,這裏直接使用Q函數,這是李航老師裏面的正式理解,和通俗理解就差log裏面除一個隱變量的分佈了。雖然式子不一樣,但是求導之後下面這個常數Q分佈是可以去掉的,沒有任何影響,因爲參數使用的是前一次迭代求出來的,所以兩者沒有區別。
稍微化簡一下:
由於是已知的,所以是已知的,那麼
雖然是可以求出來的,但是它是在加和裏面,不可以提出來,所以是不能去掉的,而是和加和沒有關係的,所以可以正比去掉,反正對整個函數的趨勢沒有影響的。
前面推導過:
代進去:
爲什麼要分成三個部分呢?因爲求導一下就沒一邊了,開心。
求:
有一個條件:。既然是有條件了,拉格朗日乘子法就用上了。化簡一下上式:拉格朗日乘子法:
求導:
上面那幾條式子全部加起來:
所以
求A:
這裏的條件就是,還是延用上面的方法,拉格朗日求導,其實就是改一下上面的公式就好了。所以有:
求B:
這裏的條件就是,於是:
這樣就是整個更新算法了。
有監督學習
如果已經有了一堆標註好的數據,那就沒有必要再去玩baum-welch算法了。這個算法就很簡單了,多的不說,直接上圖:
問題3:如果有了參數和觀測序列,求的概率最大的隱含序列是什麼。
上面骰子的例子已經舉過了,再講一個下雨天的例子:
day | rain | sun |
---|---|---|
rain | 0.7 | 0.3 |
sun | 0.4 | 0.6 |
轉移概率如上
day | walk | shop | clean |
---|---|---|---|
rain | 0.1 | 0.4 | 0.5 |
sun | 0.6 | 0.3 | 0.1 |
發射概率入上
初始概率
已知模型參數和觀測三天行爲(walk,shop,clean),求天氣。
①首先是初始化:
②第二天:
③最後一天:
$ξ_2 = max[0.0384P(rain|sun),0.0432P(sun|sun)]*P(sun|clean) = 0.002592 $
接着就是回溯了,查看最後一天哪個最大,倒着找,最後就是2,1,1了,也就是(sun,rain,rain)。
⑤Hidden Markov Model代碼實現
1.工具類的實現
class Tool(object):
infinite = float(-2**31)
@staticmethod
def log_normalize(a):
s = 0
for x in a:
s += x
if s == 0:
print('Normalize error,value equal zero')
return
s = np.log(s)
for i in range(len(a)):
if a[i] == 0:
a[i] = Tool.infinite
else:
a[i] = np.log(a[i]) - s
return a
@staticmethod
def log_sum(a):
if not a:
return Tool.infinite
m = max(a)
s = 0
for t in a:
s += np.exp(t - m)
return m + np.log(s)
@staticmethod
def saveParameter(pi,A, B, catalog):
np.savetxt(catalog + '/' + 'pi.txt', pi)
np.savetxt(catalog + '/' + 'A.txt', A)
np.savetxt(catalog + '/' + 'B.txt', B)
準備了幾個靜態方法,一個就是log標準化,也就是把所有的數組都歸一化操作,變成一個概率,log算加和,保存矩陣。所有的操作都是使用log表示,如果單單是隻用數值表示的話,因爲涉及到很多的概率相乘,很多時候就變很小,根本檢測不出。而用log之後下限會大很多。
2.有監督算法的實現
其實就是根據上面的公式統計即可。
def supervised(filename):
'''
The number of types of lastest variable is four,0B(begin)|1M(meddle)|2E(end)|3S(sigle)
:param filename:learning fron this file
:return: pi A B matrix
'''
pi = [0]*4
a = [[0] * 4 for x in range(4)]
b = [[0] * 65535 for x in range(4)]
f = open(filename,encoding='UTF-8')
data = f.read()[3:]
f.close()
tokens = data.split(' ')
#start training
last_q = 2
old_process = 0
allToken = len(tokens)
print('schedule : ')
for k, token in enumerate(tokens):
process = float(k) /float(allToken)
if process > old_process + 0.1:
print('%.3f%%' % (process * 100))
old_process = process
token = token.strip()
n = len(token)
#empty we will choose another
if n <= 0:
continue
#if just only one
if n == 1:
pi[3] += 1
a[last_q][3] += 1
b[3][ord(token[0])] += 1
last_q = 3
continue
#if not
pi[0] += 1
pi[2] += 1
pi[1] += (n-2)
#transfer matrix
a[last_q][0] += 1
last_q = 2
if n == 2:
a[0][2] += 1
else:
a[0][1] += 1
a[1][1] += (n-3)
a[1][2] += 1
#launch matrix
b[0][ord(token[0])] += 1
b[2][ord(token[n-1])] += 1
for i in range(1, n-1):
b[1][ord(token[i])] += 1
pi = Tool.log_normalize(pi)
for i in range(4):
a[i] = Tool.log_normalize(a[i])
b[i] = Tool.log_normalize(b[i])
return pi, a, b
按照公式來,一個一個單詞判斷,如果是單個的那自然就是single,所以當的時候直接就是。初始狀態是因爲一開始肯定是結束之後才接着開始的,所以自然就是2,end。之後都是比較常規了。
3.viterbi算法的實現
def viterbi(pi, A, B, o):
'''
viterbi algorithm
:param pi:initial matrix
:param A:transfer matrox
:param B:launch matrix
:param o:observation sequence
:return:I
'''
T = len(o)
delta = [[0 for i in range(4)] for t in range(T)]
pre = [[0 for i in range(4)] for t in range(T)]
for i in range(4):
#first iteration
delta[0][i] = pi[i] + B[i][ord(o[0])]
for t in range(1, T):
for i in range(4):
delta[t][i] = delta[t-1][0] + A[0][i]
for j in range(1, 4):
vj = delta[t-1][j] + A[0][j]
if delta[t][i] < vj:
delta[t][i] = vj
pre[t][i] = j
delta[t][i] += B[i][ord(o[t])]
decode = [-1 for t in range(T)]
q = 0
for i in range(1, 4):
if delta[T-1][i] > delta[T-1][q]:
q = i
decode[T-1] = q
for t in range(T-2, -1, -1):
q = pre[t+1][q]
decode[t] = q
return decode
根據上面的例子來就好,先找到轉移概率最大的,迭代到最後找到概率最大的序列之後倒着回來找即可。最後就得到一串編碼,然後使用這段編碼來進行劃分。
def segment(sentence, decode):
N = len(sentence)
i = 0
while i < N:
if decode[i] == 0 or decode[i] == 1:
j = i+1
while j < N:
if decode[j] == 2:
break
j += 1
print(sentence[i:j+1],"|",end=' ')
i = j+1
elif decode[i] == 3 or decode[i] == 2: # single
print (sentence[i:i + 1], "|", end=' ')
i += 1
else:
print ('Error:', i, decode[i] , end=' ')
i += 1
這個就是根據編碼劃分句子的函數了。首先是通過有監督的學習得到參數,然後用viterbi算法得到編碼序列,再用segment函數做劃分即可。
if __name__ == '__main__':
pi = np.loadtxt('supervisedParam/pi.txt')
A = np.loadtxt('supervisedParam/A.txt')
B = np.loadtxt('supervisedParam/B.txt')
f = open("../Data/novel.txt" , encoding='UTF-8')
data = f.read()[3:]
f.close()
decode = viterbi(pi, A, B, data)
segment(data, decode)
執行代碼。
效果當然可以了,畢竟是有監督。無監督的就沒有這麼秀了。
4.baum-welch算法的實現
參考上面三個公式,除了B有點難更新之外其他的都很簡單。
def cal_alpha(pi, A, B, o, alpha):
print('start calculating alpha......')
for i in range(4):
alpha[0][i] = pi[i] + B[i][ord(o[0])]
T = len(o)
temp = [0 for i in range(4)]
del i
for t in range(1, T):
for i in range(4):
for j in range(4):
temp[j] = (alpha[t-1][j] + A[j][i])
alpha[t][i] = Tool.log_sum(temp)
alpha[t][i] += B[i][ord(o[t])]
print('The calculation of alpha have been finished......')
def cal_beta(pi, A, B, o, beta):
print('start calculating beta......')
T = len(o)
for i in range(4):
beta[T-1][i] = 1
temp = [0 for i in range(4)]
del i
for t in range(T-2, -1, -1):
for i in range(4):
beta[t][i] = 0
for j in range(4):
temp[j] = A[i][j] + B[j][ord(o[t + 1])] + beta[t + 1][j]
beta[t][i] += Tool.log_sum(temp)
print('The calculation of beta have been finished......')
def cal_gamma(alpha, beta, gamma):
print('start calculating gamma......')
for t in range(len(alpha)):
for i in range(4):
gamma[t][i] = alpha[t][i] + beta[t][i]
s = Tool.log_sum(gamma[t])
for i in range(4):
gamma[t][i] -= s
print('The calculation of gamma have been finished......')
def cal_kesi(alpha, beta, A, B, o, ksi):
print('start calculating ksi......')
T = len(o)
temp = [0 for i in range(16)]
for t in range(T - 1):
k = 0
for i in range(4):
for j in range(4):
ksi[t][i][j] = alpha[t][i] + A[i][j] + B[j][ord(o[t+1])] + beta[t+1][j]
temp[k] = ksi[t][i][j]
k += 1
s = Tool.log_sum(temp)
for i in range(4):
for j in range(4):
ksi[t][i][j] -= s
print('The calculation of kesi have been finished......')
的更新按照公式即可。
def update(pi, A, B, alpha, beta, gamma, ksi, o):
print('start updating......')
T = len(o)
for i in range(4):
pi[i] = gamma[0][i]
s1 = [0 for x in range(T-1)]
s2 = [0 for x in range(T-1)]
for i in range(4):
for j in range(4):
for t in range(T-1):
s1[t] = ksi[t][i][j]
s2[t] = gamma[t][i]
A[i][j] = Tool.log_sum(s1) - Tool.log_sum(s2)
s1 = [0 for x in range(T)]
s2 = [0 for x in range(T)]
for i in range(4):
for k in range(65536):
if k%5000 == 0:
print(i, k)
valid = 0
for t in range(T):
if ord(o[t]) == k:
s1[valid] = gamma[t][i]
valid += 1
s2[t] = gamma[t][i]
if valid == 0:
B[i][k] = -Tool.log_sum(s2)
else:
B[i][k] = Tool.log_sum(s1[:valid]) - Tool.log_sum(s2)
print('baum-welch algorithm have been finished......')
最後再封裝一把:
def baum_welch(pi, A, B, filename):
f = open(filename , encoding='UTF-8')
sentences = f.read()[3:]
f.close()
T = len(sentences) # 觀測序列
alpha = [[0 for i in range(4)] for t in range(T)]
beta = [[0 for i in range(4)] for t in range(T)]
gamma = [[0 for i in range(4)] for t in range(T)]
ksi = [[[0 for j in range(4)] for i in range(4)] for t in range(T-1)]
for time in range(1000):
print('Time : ', time)
sentence = sentences
cal_alpha(pi, A, B, sentence, alpha)
cal_beta(pi, A, B, sentence, beta)
cal_gamma(alpha, beta, gamma)
cal_kesi(alpha, beta, A, B, sentence, ksi)
update(pi, A, B, alpha, beta, gamma, ksi, sentence)
Tool.saveParameter(pi, A, B, 'unsupervisedParam')
print('Save matrix successfully!')
初始化矩陣:
def inite():
pi = [random.random() for x in range(4)]
Tool.log_normalize(pi)
A = [[random.random() for y in range(4)] for x in range(4)]
A[0][0] = A[0][3] = A[1][0] = A[1][3] = A[2][1] = A[2][2] = A[3][1] = A[3][2] = 0
B = [[random.random() for y in range(65536)] for x in range(4)]
for i in range(4):
A[i] = Tool.log_normalize(A[i])
B[i] = Tool.log_normalize(B[i])
return pi , A , B
最後跑一遍就OK了。
無監督算法使用的是EM框架,肯定會存在局部最大的問題和初始值敏感,跑了56次,用的谷歌GPU跑,代碼沒有經過優化,跑的賊慢。
最後的效果也是慘不忍睹。
總結
概率圖模型大多都是圍繞着三個問題展開的,求觀測序列的概率最大,求隱含序列的概率最大,求參數。MEMM,RCF大多都會圍繞這幾個問題展開。求觀測序列的概率,暴力求解是爲了理解模型,前向後向算法纔是真正有用的;概率最大的隱含序列viterbi算法,動態規劃的思想最後回溯回來查找;求參數就是套用EM框架求解。
HMM是屬於生成模型,首先求出,轉移概率和表現概率直接建模,也就是對樣本密度建模,然後才進行推理預測。事實上有時候很多NLP問題是和前後相關,而不是隻是和前面的相關,HMM這裏明顯是隻和前面的隱變量有關,所以還是存在侷限性的。對於優化和模型優缺點等寫了MEMM和RCF再一起總結。
最後附上GitHub代碼:
https://github.com/GreenArrow2017/MachineLearning/tree/master/MachineLearning/HMM