隐马尔科夫HMM应用于中文分词

下图是描述天气变化的隐马尔科夫模型 隐马尔科夫HMM应用于中文分词
文章图片

隐马尔科夫模型: 一般用(A,B,pi)来表示A是指状态转移矩阵,在这里是指Rainy与Sunny之间四组的状态转移概率,在分词中也就是标注(比如SBME),B是发射矩阵,在这里是指Rainy中的Walk,Shop,Clean三种观测值的概率以及Sunny中的Walk,Shop,Clean三种观测值的概率,在分词中是指一个个的字,PI是初始状态分布,在这里就是start上的到Rainy的概率为0.6,到Sunny的概率为0.4
如何理解将HMM应用到中文分词中的呢? 在知道模型(A,B,PI),也知道观测序列(比如某几天的观测序列是WalkCleanShopCleanWalkWalkShop),问题是在知道HMM模型和观测序列的情况下,求出可以使观测序列出现概率最大的状态序列(也就是知道那五天的活动,让你推出五天的天气,可以让五天活动出现的概率最大,在中文分词中,可以观测到的是文字,让推导出每个字的状态,根据标注的状态去进行分词,比如遇到S和E就进行分词)
基于HMM的分词过程:
①先训练好模型,因为语料是分好词来训练的,所以代码写起来还算简单,HMM的参数pi,A,B训练只是做一个简单的统计工作.
②写维特比算法实现分词
维特比算法的解释(Viterbi):维特比算法的思想就是按照句子的序列,从句首开始分析每个字,考虑每个字的所有可能状态,认为当前字的状态只与前一个字的状态有关,每次都将使当前字状态概率最大的上一个状态存起来(比如每个字有4种状态可能,那么对应的这四种状态都存了使状态概率最大的上一个字的状态)以上图的模型为例,观测序列为WalkCleanShopCleanWalk,求出现该观测序列概率最大的状态序列
【隐马尔科夫HMM应用于中文分词】隐马尔科夫HMM应用于中文分词
文章图片

具体HMM应用到分词中的代码实现:

# coding: utf-8import numpy as np import math #每一个字的4个隐藏状态 0123/start(该字是一个词的开头) middle(该字是一个词的中间部分) end(该字是一个词的结束) single(该字是单独就是一个词) #监督学习:用的语料库是分好词的来训练 #注:ord()函数获取一个字符的Unicode编码 infinite = float(-2.0**31)#负无穷#训练参数(已知O和I的情况下监督学习),只需要做一个统计就可以得到参数 #传入字符串语料数据,必须是处理好分词的,并且传入分隔符 def mle(train_data,split_char=""): tokens = train_data.split(split_char) #模型参数 pi = np.zeros(4)#状态概率pai A = np.zeros((4,4))#状态转移矩阵# B M E S B = np.zeros((4,65536))#发射矩阵,某个隐状态下的观测分布 last_token = tokens[0] for token in tokens: token = token.strip() token_len = len(token) last_token_state =3 if len(last_token)==1 else 2#若上一个token长度为1则为single转移到某个状态否则是end转移到某个状态 #不为空字符判断 if token_len ==0: continue #单字成词 if token_len == 1: pi[3]+=1 A[last_token_state][3]+=1#上一个状态转移到single #给出状态single下出现的观测字符 B[3][ord(token)]+=1 elif token_len==2: pi[0]+=1 pi[2]+=1 #start转移到end A[0][2]+=1 A[last_token_state][0]+=1 #给出状态start和end下出现的观测字符 B[0][ord(token[0])]+=1 B[2][ord(token[1])]+=1 else: pi[0]+=1 pi[2]+=1 pi[1]+=token_len -2 #start转移到middle,middle转移到middle,middle转移到end A[0][1]+=1 A[1][1]+=(token_len-3) A[1][2]+=1 A[last_token_state][0]+=1 #给出状态start,middle,end下出现的观测字符 B[0][ord(token[0])]+=1#start B[2][ord(token[token_len-1])]+=1#end for i in range(1,token_len-1):#middle B[1][ord(token[i])]+=1 last_token = token #取对数 sum1 = np.sum(pi) for i in range(len(pi)): pi[i] = math.log(pi[i] / sum1) log_val(A) log_val(B) return pi,A,B#对pi,A,B结果取对数,因为单个数值太小,会溢出 def log_val(data): #遍历矩阵每一行,每一行概率相加为1,做取对数处理 col_len = data.shape[1] for k,line in enumerate(data): sum1 = np.sum(line) log_sum = math.log(sum1) for i in range(col_len): if data[k][i] == 0: data[k][i] = infinite else: data[k][i] = math.log(data[k][i]) - log_sum#预测维特比算法 def viterbi(pi,A,B,O): O = O.strip() O_len = len(O) pi_len = len(pi) if O_len==0: return #保存所有状态的最大值是由哪一个状态产生的也就是计算δ[t](i)时,是由哪一个δ[t-1](q)产生的,q就是哪个状态 states = np.full(shape=(O_len,pi_len),fill_value=https://www.it610.com/article/0.0) #保存计算过所有的计算的δ deltas = np.full(shape=(O_len,pi_len),fill_value=0.0) #初始化计算最优P(I,O1) = max{P(O1|I)*p(I)} for j in range(0,pi_len): deltas[0][j] = pi[j] + B[j][ord(O[0])]#变加法是因为取了对数 #dp计算P(I|O1,O2,O3,...Ot,I1,I2...It-1) for t in range(1,O_len): for i in range(0,pi_len):#计算每一个δ[t](i=q1...q[pi_len]) = max{δt[j]*A[ji]*B[qi|Ot]},j是遍历所有状态 deltas[t][i] = deltas[t-1][0]+A[0][i] #寻找最大的δ[t](i) for j in range(1,pi_len): current = deltas[t-1][j]+A[j][i] if current> deltas[t][i]: deltas[t][i] = current #保存当前δ[t](i)取得最大值是是从上一个哪个状态来的 states[t][i] = j deltas[t][i] +=B[i][ord(O[t])] #回溯找到最优概率路径 max1 = deltas[O_len-1][0] best_state = np.zeros(O_len) #先找出最后一个观测的最可能状态是什么 for i in range(1,pi_len): if deltas[O_len-1][i] > max1: max1 = deltas[O_len-1][i] best_state[O_len-1] = i #由最后一个观测得到的最好状态往前回溯找出状态序列 for i in range(O_len-2, -1, -1): best_state[i] = states[i+1][int(best_state[i+1])] return best_statedef output_words(decode,O): T = len(O) for i in range(0,T): #如果预测当前字符最有可能的状态是end或者single就分词 if decode[i] ==2 or decode[i] == 3: print(O[i],"|",end='') else: print(O[i],end='')#开始训练 f = open("words.txt","r",encoding="utf-8") data = https://www.it610.com/article/f.read()[3:]; f.close() pi,A,B = mle(data)#训练结束 #测试 f2 = open("test.txt","r",encoding="utf-8") O = f2.read().strip() states = viterbi(pi, A, B, O) output_words(states, O)

    推荐阅读