本篇博文將詳細總結隱馬模型相關知識,理解該模型有一定的難度,在此淺薄的談下自己的理解。參考的實現代碼:HMM_EM(無監督式),HMM有監督式
文章目錄
概率計算問題
是關於時序的概率模型,描述由一個隱藏的馬爾科夫鏈生成不可觀測的狀態隨機序列,再由各個狀態生成觀測隨機序列的過程。
隱馬爾科夫模型隨機生成的狀態隨機序列,稱爲狀態序列;每個狀態生成一個觀測,由此產生的觀測隨機序列,稱爲觀測序列。序列的每個位置可看做是一個時刻。
上圖中的 表示狀態序列, 表示觀測序列。假設每個時刻的狀態可能有 種可能,每個時刻的觀測可能有 種可能。
由初始概率分佈、狀態轉移概率分佈 以及觀測概率分佈 確定。
這個初始概率分佈 是一個大小爲 的向量, 爲大小爲 的矩陣, 爲大小爲 的矩陣。
- 是長度爲 的狀態序列, 是對應的觀測序列,則有:
- 是狀態轉移概率矩陣,則有:
其中 表示在時刻 處於狀態 的條件下時刻 轉移到狀態 的概率。
HMM的兩個基本性質
齊次假設:
也即是當前時刻的隱狀態只與前一時刻的隱狀態有關。
觀測獨立性假設:
也即是當前時刻的觀測狀態只與當前時刻的隱狀態有關。
HMM的三個問題
問題一:給定模型 和觀測序列,計算模型 下觀測序列O出現的概率
我們先看看代碼中,是如何做的,參考的代碼中,數據的格式爲 ,假設了這是由三個高斯模型混合而成的樣本集,每個樣本有兩個特徵,共900個樣本。注意:樣本個數也即是上面所說的步長。形如:
5.8045294e-01 9.6570931e-01
1.0101383e+00 3.9152260e-01
-4.1251308e-01 9.6435345e-01
-2.0477262e+00 1.5029133e+00
-1.3130763e+00 1.6049016e-01
-6.2352642e-01 3.7779862e-01
1.8841870e+00 1.1210070e+00
2.5608726e+00 -1.4935448e+00
3.2966895e-01 -1.1184212e+00
-1.1982074e+00 7.4402510e-01
7.1916729e-01 1.2625977e+00
-3.6946331e-01 1.4573214e+00
-7.3522039e-01 7.0942551e-02
-4.8153116e-01 1.3661593e+00
然後需要初始化出上面所講的模型的三個要素。
def initForwardBackward(X,K,d,N):##X爲數據集,K爲隱狀態數,在這裏爲3,d爲觀測狀態數,N爲時刻總數,共N步。
# Initialize the state transition matrix, A. A is a KxK matrix where
# element A_{jk} = p(Z_n = k | Z_{n-1} = j)
# Therefore, the matrix will be row-wise normalized. IOW, Sum(Row) = 1
# State transition probability is time independent.
A = np.ones((K,K))##隱狀態轉移矩陣初始爲1
A = A/np.sum(A,1)[None].T ##需要保證每列之和爲1
# Initialize the marginal probability for the first hidden variable
# It is a Kx1 vector
PI = np.ones((K,1))/K## 初始pi,即爲第0時刻轉移到某個隱狀態的概率
## 這裏我們假設發射矩陣服從高斯分佈,所以我們只需要定義均值和方差即可。
## 顯然對於不同的隱狀態,會得到不同的觀測序列。即對於不同的隱狀態有不同的高斯分佈
## 並且這裏數據集每個樣本有d(d=2)個feature,相當於有多維度隨機變量,每個隨機變量的分佈都有不同的均值
## 所以MU的shape爲[d,k],即每個隱狀態對應d個均值
## 每個隱狀態對應有不同的協方差矩陣
MU = np.random.rand(d,K)
SIGMA = [np.eye(d) for i in xrange(K)]
return A, PI, MU, SIGMA
這樣我們就得到初始的 ,狀態轉移矩陣,發射矩陣。
前向後向算法—動態規劃
給定模型 和觀測序列 ,計算模型 下觀測序列 出現的概率。
我們首先嚐試直接用暴力求解:
- 狀態序列 的概率是:
- 對固定的狀態序列 ,測序列 的概率是:
- 和 同時出現的聯合概率是:
- 對所有可能的狀態序列 求和,得到觀測序列 的概率:
我們可以試想,在每一個時刻,隱狀態都有 個選擇,一共有 個時刻,故 ,而求和裏面共有 個因子,故時間複雜度爲。
顯然直接暴力計算 時間複雜度過高。
前向概率
定義:給定,定義到時刻 部分觀測序列爲 且狀態爲 的概率稱爲前向概率,記做:
-
初值:
-
遞推:對於(注意這是一個從前向後的遞推過程)
需要注意上一步,在第 步時,位於隱狀態 的概率轉移到第 步的隱狀態 ,這裏面的 有 種情況, 也有 種情況,在第 步的隱狀態 對應有前一步的 種情況求和(這也是爲什麼前向計算能得出),然後再乘以發射概率。作爲第 步處於該種隱狀態的概率值。注意 的意義,爲當前隱狀態 生成特定 的概率。 ** -
最終可得:
對 積分,其結果即爲生成指定的 的觀測序列的概率。
前向算法的時間複雜度是。
那麼在代碼中是如何實現前向計算的呢?
def buildAlpha(X,PI,A,MU,SIGMA):## X.shape[feature,N]
# We build up Alpha here using dynamic programming. It is a KxN matrix
# where the element ALPHA_{ij} represents the forward probability
# for jth timestep (j = 1...N) and ith state. The columns of ALPHA are
# normalized for preventing underflow problem as discussed in secion
# 13.2.4 in Bishop's PRML book. So,sum(column) = 1
# c_t is the normalizing costant
N = np.size(X,1)
K = np.size(PI,0)
## 這裏需要注意Alpha的shape爲[K,N],表示在某個時刻爲某個特定隱藏狀態,其生成從開始時刻到當前時刻觀測序列的概率
Alpha = np.zeros((K,N))
c = np.zeros(N)
# Base case: build the first column of ALPHA
for i in xrange(K):
## PI[i]表示選擇該隱狀態的概率值。
## normPDF(X[:,0],MU[:,i],SIGMA[i])表示該樣本在該隱狀態下的高斯分佈下對應的概率值
┆ Alpha[i,0] = PI[i]*normPDF(X[:,0],MU[:,i],SIGMA[i])##也就是當前時刻的發射概率
c[0] = np.sum(Alpha[:,0])## 每列求和
Alpha[:,0] = Alpha[:,0]/c[0]## 歸一
# 以下就是上面所講的從前往後的遞推過程
for t in xrange(1,N):
┆ for i in xrange(K):
┆ ┆ for j in xrange(K):
┆ ┆ ┆ Alpha[i,t] += Alpha[j,t-1]*A[j,i] # sum part of recursion
┆ ┆ Alpha[i,t] *= normPDF(X[:,t],MU[:,i],SIGMA[i]) # product with emission prob
┆ c[t] = np.sum(Alpha[:,t])
┆ Alpha[:,t] = Alpha[:,t]/c[t] # for scaling factors
return Alpha, c ##注意函數返回的Alpha的shape爲[K,N]
特別需要注意上面所求矩陣 的意義:其 爲,表示在某個時刻爲某個特定隱藏狀態,其生成從開始時刻到當前時刻觀測序列的概率。
後向計算(同前向計算同理,只不過是從後向前計算)
定義:給定,定義到時刻 狀態爲 的前提下,從 到 的部分觀測序列爲 的概率爲後向概率,記做:
- 初值:
此時, 觀測值爲。故爲 - 遞推:對於(注意:這是一個從後向前的遞推過程)
這裏需要注意 是可以得到 而不知道,所以對於不同的 ,都要乘以 。 - 最終:
看看代碼中是如何實現後向計算的:
def buildBeta(X,c,PI,A,MU,SIGMA):## X.shape[features, N]
# Beta is KxN matrix where Beta_{ij} represents the backward probability
# for jth timestamp and ith state. Columns of Beta are normalized using
# the element of vector c.
N = np.size(X,1)
K = np.size(PI,0)
Beta = np.zeros((K,N))## 同上,shape也是[K,N],表示某一時刻的隱狀態爲某一隱狀態從最後生成到當前時刻序列的概率。
# Base case: build the last column of Beta
for i in xrange(K):
┆ Beta[i,N-1]=1.## 此時,觀測值爲$O_{N}$。故爲1
┆
# 按照上面所說的從後向前進行遞推。直到t==0時。
for t in xrange(N-2,-1,-1):
┆ for i in xrange(K):
┆ ┆ for j in xrange(K):
┆ ┆ ┆ Beta[i,t] += Beta[j,t+1]*A[i,j]*normPDF(X[:,t+1],MU[:,j],SIGMA[j])
┆ Beta[:,t] /= c[t+1]
return Beta
一定要注意上面代碼中,矩陣 的意義:shape也是[K,N],表示某一時刻的隱狀態爲某一隱狀態生成從最後時刻到當前時刻指定序列的概率。
前向後向概率的關係
這種前向後向計算,每次都是在上一層的基礎上進行遞推,相對於暴力計算方法,避免了大量的重複計算,降低了複雜度,計算只存在於相鄰的時間點內。
單個狀態的概率
求給定模型 和觀測,在時刻t處於狀態 的概率。記:
可計算得:
Gamma = Alpha*Beta ## 矩陣點乘,shape爲[k,N],這裏沒有除以分母,分母即所求gamma矩陣所在列之和。
這個Gamma主要是在M步更新PI的,故在M步再除分母一樣
這就意味着只要我們知道 個觀測序列,和模型(初始狀態,狀態矩陣,狀態轉移矩陣),就可以計算每個時刻的隱狀態。即:在每個時刻 選擇在該時刻最有可能出現的狀態 ,從而得到一個狀態序列,將它作爲預測的結果。
兩個狀態的聯合概率
求給定模型 和觀測,在時刻 處於狀態 並且時刻 處於狀態 的概率。
那麼在代碼中如何實現 呢?首先我們可以試想, 共有三種狀態, 也有三種狀態,故每相鄰的狀態轉移共有 種轉移方式。而代碼中共有 個時刻。故 的。
i = np.zeros((K,K,N))
for t in xrange(1,N):
## 每個時刻都是3*3 的矩陣,\alpha_{T}(i)*a_{ij}*\Beta_{t+1}(j),i,j都有k種可能,故a_{ij}就是轉移矩陣A。
Xi[:,:,t] = (1/c[t])*Alpha[:,t-1][None].T.dot(Beta[:,t][None])*A
# Now columnwise multiply the emission prob
for col in xrange(K):
## 因爲還需要乘以b_{jO_{t+1}},而3*3矩陣第二維即表示轉移的j
┆ Xi[:,col,t] *= normPDF(trainSet[:,t],MU[:,col],SIGMA[col])
第二個問題:學習問題,給出觀測序列,估計模型參數,使得 最大,顯然用MLE的方式來估計。
監督學習
若訓練數據包括觀測序列和狀態序列,則 的學習非常簡單,是監督學習。利用大數定理的結論 “頻率的極限是概率”,給出 的參數估計。
- 初始概率
- 轉移概率
- 觀測概率
有監督式的學習比較簡單,就是統計每個句子裏的每個詞的狀態而已,大概的講下思路:
- 獲取已經分詞好的語料庫,類似這樣
1986年 , 十億 中華 兒女 踏上 新 的 徵 程 。 過去 的 一 年 , 是 全國 各族 人民 在 中國 共產黨 領導 下 , 在 建設 有 中國 特色 的 社會主義 道路 上 , 堅持 改革 、 開放 , 團結 奮鬥 、 勝利 前進 的 一 年 。
- 每個詞即爲一個觀測狀態,再定義詞的隱狀態,例如參考代碼中的 B(開頭),M(中間), E(結尾), S(獨立成詞)作爲四種隱狀態,則可得到語料庫中每句話的隱狀態序列。由隱狀態序列求得 轉移概率。
- 爲初始狀態,可從語料庫中每句開頭第一個詞對應的隱狀態得出
- 由語料庫中每個隱狀態對應的詞得出 觀測概率
- 由此可以從語料庫中學習到參數矩陣 ,然後可以利用學習到的參數矩陣,對要預測的句子進行分詞(Viterbi算法),可根據預測得到的每個詞隱狀態,決定是否進行分詞。
循環遍歷語料庫中每個句子,統計句子中每個詞的隱狀態(已經定義好每個詞對應的隱狀態)。得到該句的 隱狀態列表。
for i in range(len(line_state)):## 不同的句子,其line_state不同,可以理解爲不同的時刻
if i == 0:
┆ Pi_dic[line_state[0]] += 1## 該句第一個詞的隱狀態
┆ Count_dic[line_state[0]] += 1## 後面做歸一化用的
else:
┆ A_dic[line_state[i-1]][line_state[i]] += 1## 統計轉移概率
┆ Count_dic[line_state[i]] += 1
## 統計發射概率,第i個隱狀態對應第i個觀測狀態
┆ if not B_dic[line_state[i]].has_key(word_list[i]):
┆ ┆ B_dic[line_state[i]][word_list[i]] = 0.0
┆ else:
┆ ┆ B_dic[line_state[i]][word_list[i]] += 1
這樣統計完語料庫中的每個句子後,做完歸一化後得到最終的 。
無監督學習(Baum-Welch算法)
若訓練數據只有觀測序列,則 的學習,需要使用 算法,是非監督學習。
算法整體框架:
所有觀測數據寫成,所有隱數據寫成,完全數據是,完全數據的對數似然函數是 。
在 中,上面公式中的 就是觀測,隱隨機變量就是隱狀態 。則其 公式中的 在 中爲 ,這其實就是 步。
假設 是 參數的當前估計值(也就是上一輪中得出的最優的參數)
這個 步在代碼中如何實現呢?實際上由後面的 步中爲了得到 需要先知道 的值,故在 步時先更新得到:
def Estep(trainSet, PI,A,MU,SIGMA):## PI,A,MU,SIGMA 爲上一輪M步迭代更新出的\lambda
## 即在E步利用上一輪更新後的PI,A,MU,SIGMA來計算gamma等
# The goal of E step is to evaluate Gamma(Z_{n}) and Xi(Z_{n-1},Z_{n})
# First, create the forward and backward probability matrices
Alpha, c = buildAlpha(trainSet, PI,A,MU,SIGMA)
Beta = buildBeta(trainSet,c,PI,A,MU,SIGMA)
# Dimension of Gamma is equal to Alpha and Beta where nth column represents
# posterior density of nth latent variable. Each row represents a state
# value of all the latent variables. IOW, (i,j)th element represents
# p(Z_j = i | X,MU,SIGMA)
Gamma = Alpha*Beta
#pdb.set_trace()
# Xi is a KxKx(N-1) matrix (N is the length of data seq)
# Xi(:,:,t) = Xi(Z_{t-1},Z_{t})
N = np.size(trainSet,1)
K = np.size(PI,0)
Xi = np.zeros((K,K,N))
for t in xrange(1,N):
┆ Xi[:,:,t] = (1/c[t])*Alpha[:,t-1][None].T.dot(Beta[:,t][None])*A
┆ # Now columnwise multiply the emission prob
┆ for col in xrange(K):
┆ ┆ Xi[:,col,t] *= normPDF(trainSet[:,t],MU[:,col],SIGMA[col])
return Gamma, Xi, c
爲待求 的參數。則有:
我們就是要最求上面 取極值時對應的 ,其實就是 ,這就是 步。
根據上面的暴力計算的結論:
函數可寫成:
- 極大化,獲得 參數
注意到 加和爲1,利用拉格朗日乘子法得:
對上式中的 求導,可得:
對 求和,得到:
從而得到:
PI = (Gamma[:,0]/np.sum(Gamma[:,0]))[None].T
同理可以用拉格朗日乘子法求得:
代碼中是如何實現 步呢?
def Mstep(X, Gamma, Xi):
# Goal of M step is to calculate PI, A, MU, and SIGMA while treating
# Gamma and Xi as constant
K = np.size(Gamma,0)
d = np.size(X,0)
PI = (Gamma[:,0]/np.sum(Gamma[:,0]))[None].T
tempSum = np.sum(Xi[:,:,1:],axis=2)
A = tempSum/np.sum(tempSum,axis=1)[None].T ## 轉移矩陣A
MU = np.zeros((d,K))
GamSUM = np.sum(Gamma,axis=1)[None].T
SIGMA = []
for k in xrange(K):
┆ MU[:,k] = np.sum(Gamma[k,:]*X,axis=1)/GamSUM[k]
┆ X_MU = X - MU[:,k][None].T
┆ SIGMA.append(X_MU.dot(((X_MU*(Gamma[k,:][None])).T))/GamSUM[k])
return PI,A,MU,SIGMA
問題三:預測算法
在每個時刻 選擇在該時刻最有可能出現的狀態 ,從而得到一個狀態序列,將它作爲預測的結果。
Viterbi算法
算法實際是用 動態規劃 解 預測問題,用 求概率最大的路徑(最優路徑),這是一條路徑對應一個狀態序列。其實就是我們知道了模型參數 後,從時刻 遞推到時刻 的最大概率路徑。
定義變量:在時刻 狀態爲 的所有路徑中,概率的最大值。
- 遞推
- 終止
那麼在代碼中時如何實現 算法呢?
def viterbi(obs, states, start_p, trans_p, emit_p):
"""
obs: 需要切分的sentence
states: 狀態種類序列,例如每個詞可能有四個狀態[B, M, E, S]
start_p: 就是上面所講的\PI
trans_p: 狀態轉移矩陣
emit_p: 發射矩陣
"""
V = [{}] #tabular V[t][state]:t表示時刻,state表示該時刻的隱狀態
path = {}
for y in states: #init
## emit_p[y].get(obs[0],0)表示在y隱狀態下,觀測狀態爲obs[0] 的發射概率。
## 在t=0 時刻時,觀測狀態即爲obs[0]
┆ V[0][y] = start_p[y] * emit_p[y].get(obs[0],0)
┆ path[y] = [y] ## 記錄當前的狀態路徑
for t in range(1,len(obs)):
┆ V.append({})
┆ newpath = {}
┆ for y in states:
## 在t時刻時,遍歷t-1時刻所有可能的隱狀態state與當前y隱狀態的連接概率,獲取最大時對於的state和對應的概率prob
┆ ┆ (prob,state ) = max([(V[t-1][y0] * trans_p[y0].get(y,0) * emit_p[y].get(obs[t],0) ,y0) for y0 in states if V[t-1][y0]>0])
┆ ┆ V[t][y] =prob## 將最大概率prob作爲V[t][y]
┆ ┆ newpath[y] = path[state] + [y]## path[state] 表示t-1時刻最大概率對應的隱狀態序列,再加上當前時刻的y
┆ path = newpath
## 取最後概率最大的對應的序列作爲最後結果。
(prob, state) = max([(V[len(obs) - 1][y], y) for y in states])
return (prob, path[state])
def cut(sentence):
#pdb.set_trace()
prob, pos_list = viterbi(sentence,('B','M','E','S'), prob_start, prob_trans, prob_emit)
return (prob,pos_list)
算法其實就是多步驟、每步多選擇模型的最優選擇問題,其在每一步的所有選擇都保存了從第一步到當前步的的最小或最大代價,以及當前情況下前進步驟的選擇。並且記下在每一步每一個隱狀態與上一步對應代價最小的隱狀態節點,如果有 個隱狀態,就形成 個不同的鏈路,並且保證了每個節點對應的鏈路都是該節點的最小代價。
個人總結
我個人覺得前向、後向計算、 算法、 這幾個算法有幾分相似,又有幾分區別之處,值得思考一下:
- 前向、後向計算是利用動態規劃的思想,每一步的計算都是在前一步計算結果的基礎上,大大降低了計算量。
- 算法 同樣也是利用了動態規劃的思想,能保證找到最優的路徑。
- 利用的是貪心的思想,每一步只能找到當前時刻最優的 個不同的,而只是在當前時刻最優,卻不能保證整體最優,故最後結果可能不是最優結果。那麼有人可能會問,爲啥 不用 的那種動態規劃的思想,而用貪心?這個問題其實很簡單,試想一下,在 中,最後一個時刻每個狀態都有對應的最優鏈路,因此我們可以找到最優的,而在 中呢,你不可能在最後一步遍歷所有的隱狀態(詞表一般很大),你只能採取貪心的方式每一步選擇當前最優的 個狀態。