结巴分词源代码解析(二)

来源:互联网 发布:xnview mac 破解 编辑:程序博客网 时间:2024/05/11 05:28
本篇分两部分,一、补充说明动态规划求最大概率路径的过程;二、使用viterbi算法处理未登录词。


一、动态规划求最大概率路径补充
从全模式中看出一句话有多种划分方式,那么哪一种是好的划分方式,最大概率路径认为,如果某个路径下词的联合概率最大,那么这个路径为最好的划分方式。
(个人认为这种思想是有缺陷的,我们知道每一个词的出现频率是一个较小的小数,小数相乘结果会受到小数的个数较大影响,即分词结果会偏向于划分为较长的词。)
具体处理方法,由于多个小数连乘会导致结果是一个很小的数,这里对概率做log处理,这样问题转换为求图的最长路径问题。在句子最后增加一个结束节点。那么动态
规划的初始状态为f(N)=0,f(i)表示从节点i到结束的最长路径。具体到jieba代码中使用了route[N]=(0,0)。等式右边为一个tuple,第一个元素为最大路径的值,第二
个元素为当前这个词的末尾坐标。
转移方程为f(i)=max(v(DAG[]i])+f(DAG[i])),jieba代码中为:
route[idx] = max((log(FREQ.get(sentence[idx:x + 1]) or 1) - logtotal + route[x + 1][0], x) for x in DAG[idx])
最终得到route[0]为结束条件,再根据route得到分词结果。
以“英语单词很难记忆”为例,终止为route为:
{"0": [-42.21707512440173, 3], "1": [-46.29273582292598, 1], "2": [-36.80691827609816, 3], "3": [-34.66134438350637, 3], "4": [-25.40413428531462, 4], "5": [-18.63593458171283, 5], "6": [-10.55017769877787, 7], "7": [-12.426756194264565, 7], "8": [0, 0]}
那么划分的结果是:英语单词/很/难/记忆
另:按照算法趋向于取长词,这里‘很难’没有被分在一起很奇怪,然后我去查了词典,词典里真没有“很难”这个词。当然也可能是P(‘很’)*P(‘难’)>P('很难')

二、viterbi算法处理未登录词
1、cut函数
上文中已经谈到未登录词的处理时调用finalseg.cut(buf)。
代码如下:
def __cut(sentence):    global emit_P    prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P)    begin, nexti = 0, 0    # print pos_list, sentence    for i, char in enumerate(sentence):        pos = pos_list[i]        if pos == 'B':            begin = i        elif pos == 'E':            yield sentence[begin:i + 1]            nexti = i + 1        elif pos == 'S':            yield char            nexti = i + 1    if nexti < len(sentence):        yield sentence[nexti:]
主要处理内容就一行prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P),后面内容为结果输出。所以cut函数的作用是调用viterbi函数然后输出。

理解viterbi函数需要对viterbi算法进行理解,参考《HMM模型之viterbi算法》。

2、viterbi函数

代码如下:

def viterbi(obs, states, start_p, trans_p, emit_p):    V = [{}]  # tabular    path = {}    for y in states:  # init        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)        path[y] = [y]    for t in xrange(1, len(obs)):        V.append({})        newpath = {}        for y in states:            em_p = emit_p[y].get(obs[t], MIN_FLOAT)            (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            newpath[y] = path[state] + [y]        path = newpath    (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')    return (prob, path[state])

参数明说:obs是待分词的序列,states为隐藏状态集,start_p为初始向量,trans_p是状态转移矩阵,emit_p是混合矩阵。

变量V是一个list,list每一个元素是一个字典,list的长度为obs的长度。字典的keys为隐藏状态集,values为局部概率。

变量path是局部最佳路径,keys为隐藏状态集,values为list即为局部最佳路径。

第一个循环即为求t=1时刻的局部概率和局部路径,相关概念请参考《HMM模型之viterbi算法》。get()函数为取值,第二个参数MIN_FLOAT为全部变量,值为:-3.14e100,表示一个极小的概率,函数表示如果在能够取到第一个参数的值,则返回相应的值,否则返回MIN_FLOAT。

第二个循环求t>1时刻,局部概率和局部路径。

            (prob, state) = max(                [(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]])
PrevStatus[y]表示隐藏状态y的前一个时刻可能的隐藏状态集。这个计算含义为通过t-1时刻的局部概率计算t时刻隐藏状态为y的局部概率和反向指针。其中返回值是一个元组,prob是局部概率,state是反向指针。

<span style="white-space:pre"></span>newpath[y] = path[state] + [y]
这是得到t时刻的局部最佳路径,即反向指针指向的局部最佳路径+新的状态y。

<span style="white-space:pre"></span>(prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
计算到这里已经得到末尾元素的各个隐藏状态的局部概率和局部最佳路径,因为最后一个元素的隐藏状态只可能是E或者S,所以只需要比较这两个状态的局部概率即可。较大者即为全局的最佳概率和最佳路径。




0 0