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

下图是描述天气变化的隐马尔科夫模型

隐马尔科夫HMM应用于中文分词_第1张图片

隐马尔科夫模型:

一般用(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应用于中文分词_第2张图片

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

# coding: utf-8

import 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=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_state


def 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 = 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)

你可能感兴趣的:(nlp)