#HMM隐马尔科夫模型 ###①通俗的理解 首先举一个例子,扔骰子,有三种骰子,第一个是比较常见的6个面,每个面的几率都是1/6。第二个只有4个面,
,每个面的几率是1/4。第三个有8个面,
,每个面的几率是1/8。git
###③三个问题的简单解答 ####1.看见了观测序列,求隐藏序列 这里解释的就是第一种解法了,最大似然估计,也就是暴力解法。首先我知道我有3个骰子,六面骰子,八面骰子,四面骰子,同时结果我也知道了(1 6 3 5 2 7 3 5 2 4),如今想知道我每一次投的哪个骰子,是六面的仍是四面的仍是八面的呢? 最简单的方法就是穷举了,从零个一直到第最后一个一次把每个几率算出来,链不长还行,链要是长了就算不了了,穷举的数量太大。 接下来是要讨论另外一个比较牛逼的算法,viterbi algorithm。 首先只扔一次骰子:github
能够看到,几率最大的就是D4了,因此三次投骰子几率最大的隐含序列就是D4,D6,D4。因此不管这个序列多长,直接递推计算能够算出来的,算到最后一位的时候,直接反着找就行了。 ####2.求可观测序列的几率 若是你的骰子有问题,要怎么检测出来?首先,能够先算一下正常骰子投出一段序列的几率,再算算不正常的投出来序列的几率便可。若是小于正常的,那么就有多是错的了。算法
再继续拆:$$p(x_1,x_2,x_3...x_N) = \prod_{i=1}^{n}p(x_i|x_{i-1},...,x_1)$$ 马尔科夫链就是指,序列中的任何一个随机变量在给定它的前一个变量的分布于更早的变量无关。也就是说当前的变量只与前一个变量有关,与更早的变量是没有关系的。用公式表示:数组
代进去:$$Q(\lambda,\hat\lambda) = \sum_Iln\pi_{i_1}P(O,I|\hat\lambda) + \sum_I(\sum_{t=1}^{T-1}lna_{i_ti_{t+1}})P(O,I|\hat\lambda)+\sum_I(\sum_{t=1}^{T}lnb_{i_to_t})P(O,I|\hat\lambda)$$ 为何要分红三个部分呢?由于求导一下就没一边了,开心。 求:
有一个条件:
。既然是有条件了,拉格朗日乘子法就用上了。化简一下上式:$$ \sum_Iln\pi_{i_1}P(O,I|\hat\lambda) = \sum_Iln\pi_{i_1}P(O,i_1=i|\hat\lambda) $$拉格朗日乘子法:bash
求导:
上面那几条式子所有加起来:
因此
求A: 这里的条件就是
,仍是延用上面的方法,拉格朗日求导,其实就是改一下上面的公式就行了。因此有:框架
求B: 这里的条件就是,因而:dom
这样就是整个更新算法了。 ######有监督学习 若是已经有了一堆标注好的数据,那就没有必要再去玩baum-welch算法了。这个算法就很简单了,多的不说,直接上图: 函数
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,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)
复制代码
执行代码。
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代码: github.com/GreenArrow2…