jieba分词未登录词的Viterbi算法源码解析(二)

上篇文章已经介绍过如何DAG构建路由,那么接下来说一下对于未登陆词,结巴分词是如何实现分词的,这里就要用到以前说的HMM隐马模型,不知道的话可以看下我的文章:https://blog.csdn.net/jameslvt/article/details/81087649 ,这篇文章说的很清楚,也介绍了基本的Viterbi算法,另外在上篇文章不是说过一个为什么采用从后往前这种方式计算呢?因为,我们这个有向无环图的方向是从前向后指向,对于一个节点,我们只知道这个节点会指向后面哪些节点,但是我们很难直接知道有哪些前面的节点会指向这个节点。

废话不多说,直接上代码:里面的解释已经很明确了,这段代码是在jieba模块下的finalseg 模块中的__init__方法中,其次在posseg模块下也有Viterbi算法的详细代码

#状态转移矩阵,比如B状态前只可能是E或S状态  
PrevStatus = {  
    'B':('E','S'),  
    'M':('M','B'),  
    'S':('S','E'),  
    'E':('B','M')  
}  

def viterbi(obs, states, start_p, trans_p, emit_p):
    '''
    :param obs: 观测序列 obs = sentence
    :param states: 隐藏状态集合 states = 'BMES'
    :param start_p: 初始状态概率向量
    :param trans_p: 状态转移矩阵
    :param emit_p: 观测概率矩阵(发射矩阵)
    :return:
    '''
    V = [{}]  # tabular,字典列表
    path = {}
    #初始化概率
    for y in states:  # init 初始化,初始概率*观测概率 states = 'BMES'
        # V[0][y]为V列表第一个元素:一个key为y的字典,y为'BMES',V列表只有一个元素,有4个Key的字典
        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT) # 初始概率*观测概率,已经求过np.log了,乘法变为加法,类似于这种[{'B': 0.8, 'M': 0.7, 'S': 0.9}]
        path[y] = [y] # path为4个key的字典,path['B'] = ['B'], path['E'] = ['E'], path['M'] = ['M'], path['S'] = ['S']
    # 递推步
    for t in xrange(1, len(obs)):
        V.append({}) # 给V添加一个字典,这个句子有长度多少就添加几个字典
        newpath = {}

        #真正循环句子计算最优路径
        for y in states: # states = 'BMES'
            em_p = emit_p[y].get(obs[t], MIN_FLOAT) # y状态下,观测到第t个字的概率,b(o),即当前字观测概率,即发射概率
            # 转移概率,乘法转加法,V[t-1][y0]前一个字,状态为y0的概率,trans_p[y0].get(y):y0状态转移到y状态的概率,
            # 求出了由y0的四个状态转到当前y状态的最大概率,及当时y0的状态,动态规划
            (prob, state) = max([(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]])
            V[t][y] = prob # 记录下y0的四个状态转到每个y状态的最大概率
            newpath[y] = path[state] + [y] # 记录下前面最大概率路径和y的状态,并存到字典newpath[y]中,newpath[y]有4个key
        path = newpath # 更新path,path还是为4个key的字典,value为当前key状态的最大概率路径

    # 终止步,最后一个元素,状态为E或S,取概率大的那个
    (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')

    return (prob, path[state])

简单的Viterbi算法完成后,我们来看一下状态是语料库中状态的入参,真正的Viterbi算法,即posseg目录下的Viterbi算法,注释已经标注好了

import sys
import operator
MIN_FLOAT = -3.14e100
MIN_INF = float("-inf")



#python 中获取版本号的方法
if sys.version_info[0] > 2:
    xrange = range


def get_top_states(t_state_v, K=4):
    return sorted(t_state_v, key=t_state_v.__getitem__, reverse=True)[:K]


def viterbi(obs, states, start_p, trans_p, emit_p):
	 '''
    :param obs: 观测序列 obs = sentence
    :param states: 隐藏状态集合 states = 'BMES'
    :param start_p: 初始状态概率向量
    :param trans_p: 状态转移矩阵
    :param emit_p: 观测概率矩阵(发射矩阵)
    :return:
    '''
    V = [{}]  # tabular 状态转移矩阵
    mem_path = [{}]
    all_states = trans_p.keys() #所有的状态[('B', 'y'), ('E', 'h'), ('E', 'jn'), ('B', 'k'), ('B', 'qe'), ('E', 'vn'),
    #初始化概率,只获取第一个字的初始概率
    for y in states.get(obs[0], all_states):  # it 获取输入字的第一个单词的状态
        # V[0][y]为V列表第一个元素:一个key为y的字典,y为'BMES',V列表只有一个元素,有4个Key的字典
        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)# 初始概率*观测概率,已经求过np.log了,乘法变为加法,即{('B', 'y'): -3.14e+100}不断往后面叠加
        mem_path[0][y] = '' #{('B', 'y'): '', ('E', 'h'): '', ('E', 'jn'): '', ('B', 'k'): '', ('B', 'qe'): ''.........}
    # 递推步
    for t in xrange(1, len(obs)):
        V.append({})# 给V添加一个字典,这个句子有长度多少就添加几个字典
        mem_path.append({})
        #prev_states  即是所有转移概率中存在的状态对应的值{('B', 'a'): {('E', 'a'): -0.0050648453069648755,('M', 'a'): -5.287963037107507},...} 中的key
        prev_states = [
            x for x in mem_path[t - 1].keys() if len(trans_p[x]) > 0]

        #prev_states_expect_next {('B', 'a'): {('E', 'a'): -0.0050648453069648755,('M', 'a'): -5.287963037107507},...} 中的key中key即('B', 'a')对应的('E', 'a')和('M', 'a')并去重
        prev_states_expect_next = set(
            (y for x in prev_states for y in trans_p[x].keys()))

        #这里set取的是第一个和第二个参数的交集,其实放的是初始到下一个可能的状态
        obs_states = set(
            states.get(obs[t], all_states)) & prev_states_expect_next
        #这里做一个非空的判断
        if not obs_states:
            obs_states = prev_states_expect_next if prev_states_expect_next else all_states
         #真正循环句子计算最优路径
        for y in obs_states:
        	# 转移概率,乘法转加法,V[t-1][y0]前一个字,状态为y0的概率,trans_p[y0].get(y):y0状态转移到y状态的概率,
            # 求出了由y0的状态转到当前y状态的最大概率,及当时y0的状态,动态规划
            prob, state = max((V[t - 1][y0] + trans_p[y0].get(y, MIN_INF) +
                               emit_p[y].get(obs[t], MIN_FLOAT), y0) for y0 in prev_states)
            V[t][y] = prob# 记录下y0的四个状态转到每个y状态的最大概率
            mem_path[t][y] = state  # 记录下前面最大概率路径和y的状态,并存到字典mem_path[y]中,mem_path[y]有4个key
    #取出mem_path中第二个字典的所有的key开始循环,取出最后一个字状态的概率和状态
    last = [(V[-1][y], y) for y in mem_path[-1].keys()] 
    # if len(last)==0:
    #     print obs
    prob, state = max(last)
    #以汽车为例,route=[None, None, None, None, None, None],python中把一个汉字是3个字长度
    route = [None] * len(obs)
    i = len(obs) - 1
    while i >= 0:
        route[i] = state
        #mem_path放的是y对应的y0的状态
        state = mem_path[i][state] 
        i -= 1
    return (prob, route)

猜你喜欢

转载自blog.csdn.net/Jameslvt/article/details/81129406