zoukankan      html  css  js  c++  java
  • 中文分词——HMM算法

    上一篇文章中,我们讲述了如何用查词典的方法对中文语句分词,但这种方式不能百分百地解决中文分词问题,比如对于未登录词(在已有的词典中,或者训练语料里面没有出现过的词),无法用查词典的方式来切分,这时候可以用隐马尔可夫模型(HMM)来实现。在实际应用中,一般也是将词典匹配分词作为初分手段,再利用其他方法提高准确率。


    HMM介绍

    隐马尔可夫模型(Hidden Markov Model,HMM)是统计模型,是关于时序的概率图模型,它用来描述一个含有隐含未知参数的马尔可夫过程,即由一个隐藏的马尔可夫链随机生成不可观测的状态随机序列,再由各个状态生成一个观测而产生观测随机序列的过程。序列的每一个位置又可以看作是一个时刻,其结构见下图。其难点是从可观察的参数中确定该过程的隐含参数,然后利用这些参数来作进一步的分析,例如中文分词。

    HMM

    如上图所示,状态序列H可表示为:

    [H = H_{1},H_{2},...,H_{T} ]

    假设总共有n个状态,即每个状态序列必为状态集合之一,状态值集合Q为:

    [Q={ q_{1},q_{2},...,q_{n} } ]

    观测序列O表示为:

    [O=O_{1},O_{2},...,O_{T} ]

    假设观测值总共有m个,则观测值集合为:

    [V={ v_{1},v_{2},...,v_{m} } ]


    一个模型,两个假设,三个问题

    1、一个模型

    HMM的基本元素可以表示为

    [lambda = { Q, V, pi, A, B } ]

    Q:状态值集合

    V: 观测值集合

    π:初始概率分布

    A:([a_{ij}]) 状态转移矩阵

    B:([b_{j(k)}]) 给定状态下,观测值概率矩阵,即发射矩阵

    2、两个假设

    • 齐次Markov

    即假设观测序列中t时刻的状态,只跟上一时刻t-1有关,

    [P(h_{t+1}|h_{t},...,h_{1};o_{t},...,o_{1}) = P(h_{t+1}|h_{t}) ]

    • 观测独立

    即每个时刻的观测值只由该时刻的状态值决定

    [P(o_{t}|o_{t-1},...,o_{1};h_{t},...,h_{1})=P(o_{t}|h_{t}) ]

    3、三个问题

    HMM在实际应用中主要用来解决3类问题:

    • 评估问题(概率计算问题)
      即给定观测序列 (O=O_{1},O_{2},O_{3}…O_{t})和模型参数λ=(A,B,π),怎样有效计算这一观测序列出现的概率.
      (Forward-backward算法)

    • 解码问题(预测问题)
      即给定观测序列(O=O_{1},O_{2},O_{3}…O_{t})和模型参数λ=(A,B,π),怎样寻找满足这种观察序列意义上最优的隐含状态序列S。

      (viterbi算法,近似算法)

    • 学习问题
      即HMM的模型参数λ=(A,B,π)未知,如何求出这3个参数以使观测序列(O=O_{1},O_{2},O_{3}…O_{t})的概率尽可能的大.
      (即用极大似然估计的方法估计参数,Baum-Welch,EM算法)


    HMM模型在中文分词中的应用

    在中文分词中,我们经常用sbme的字标注法来标注语料,即通过给每个字打上标签来达到分词的目的,其中s表示single,代表单字成词;b表示begin,代表多字词的开头;m表示middle,代表三个及以上字词的中间部分;e表示end,表示多字词的结尾。这样我是中国人就可以表示为ssbme

    对于字标注的分词方法来说,输入就是n个字,输出就是n个标签。我们用(λ=λ_{1}λ_{2}…λ_{n})(表示输入的句子,)(o=o_{1}o_{2}…o_{n})表示输出。那最优的输出从概率的角度来看,就是求条件概率

    [P(o|lambda)=P(o_{1},...,O_{n}|lambda_{1},...,lambda_{n}) ]

    即要求上式概率最大,根据独立性假设,即每个字的输出只与当前字有关,上式可表示为:

    [P(o_{1},...,O_{n}|lambda_{1},...,lambda_{n}) = prod limits_{k}P(o_{k}|lambda_{k}), k=1,...,n ]

    由bayes定理,

    [P(o|lambda) = frac{P(lambda, o)}{P(lambda)}=frac{P(o)P(lambda/o)}{P(lambda)}sim P(o)P(lambda/o) ]

    同样地由独立性假设,

    [P(lambda|o) = prod limits_{k}P(lambda_{k}|o_{k}), k=1,...,n ]

    由HMM的markov假设:每个输出仅和上一个时刻相关,有

    [P(o) = P(o_{1})P(o_{2}|o_{1})P(o_{3}|o_{2})...P(o_{n}|o_{n-1}) ]

    因此,最初的求解可以转化为

    [P(o|lambda)sim P(o)P(lambda/o) = P(o_{1})P(lambda_{1}|o_{1}) P(o_{2}|o_{1})P(lambda_{2}|o_{2})P(o_{3}|o_{2})...P(o_{n}|o_{n-1})P(lambda_{n}|o_{n}) ]

    其中(P(lambda_{k}|o_{k}))(为发射概率,)(P(o_{k}|o_{k-1}))为转移概率


    代码实现

    用HMM模型来实现中文分词,状态值集合有四种,分别是 s, b, m, e;观测值集合,即所有汉子;转移状态集合有['ss','sb','bm','be','mm','me','es','eb'], 其他组合都为0,即不可能的情况,比如bs, ms这些状态不可能转移。这里从jieba分词的github下载了一份字典库dict.txt.small,需要的朋友可以去官方github下载。我们先看看该词典的格式

    [zhuzf@localhost test]$ head -n 5 dict.txt.small 
    的 3188252 uj
    了 883634 ul
    是 796991 v
    在 727915 p
    和 555815 c
    [zhuzf@localhost test]$ tail -n 5 dict.txt.small
    龙胜 10 nr
    龙蛇混杂 10 i
    龙骨山 10 nr
    龚松林 10 nr
    龟板 10 n
    

    代码实现1:

    我们根据这份词典统计每个字是s,b,m,e的发射概率, 计算方法为将词典中所有的一字词都计入s标签下,把多字词的首字都计入b标签下,最后一个字计入e标签下,其余的计入m标签中。此外计算过程中还使用了对数概率,防止溢出;

    hmm_model = {i: Counter(i) for i in 'sbme'}
    with open('data/dict.txt.small', encoding='utf-8') as f:
        for line in f:
            lines = line.strip('
    ').split(' ')
            if len(lines[0]) == 1:
                hmm_model['s'][lines[0]] += int(lines[1])
            else:
                hmm_model['b'][lines[0][0]] += int(lines[1])
                hmm_model['e'][lines[0][-1]] += int(lines[1])
            for m in lines[0][1:-1]:
                hmm_model['m'][m] += int(lines[1])
    log_total = {i: log(sum(hmm_model[i].values())) for i in 'sbme'}
    

    初始概率,第一个词只可能是b或者s

    start_p={'b': -0.26268660809250016,
     'e': -3.14e+100,
     'm': -3.14e+100,
     's': -1.4652633398537678}
    

    转移概率,根据结巴分词的状态转移矩阵prob_trans.p得到, value值为概率对数

    P={'B': {'E': -0.510825623765990, 'M': -0.916290731874155},
     'E': {'B': -0.5897149736854513, 'S': -0.8085250474669937},
     'M': {'E': -0.33344856811948514, 'M': -1.2603623820268226},
     'S': {'B': -0.7211965654669841, 'S': -0.6658631448798212}}
    trans = {}
    for key, values in P.items():
        for k,v in values.items():
            trans[(key+k).lower()] = v 
    print(trans)
    

    output

    {'be': -0.51082562376599,
     'bm': -0.916290731874155,
     'eb': -0.5897149736854513,
     'es': -0.8085250474669937,
     'me': -0.33344856811948514,
     'mm': -1.2603623820268226,
     'sb': -0.7211965654669841,
     'ss': -0.6658631448798212}
    

    viterbi算法, 动态规划算法实现,得到最优路径,即得到概率最大的sbme组合, 对于概率估算,简单采用了加1平滑法,没出现的单字都算1次。

    分词9.png

    viterbi算法的核心就是,计算如上图序列中每个时刻(此处为token)的最大概率路径,因为每个词都有可能是sbme中的一个,可以减少计算量。比如具有n个词的输入序列,所有可能路径有(4^{n})条,穷举法也可以得到所有路径中概率最大的一条,但是如果隐状态可能取值的个数m太大的话,(m^{n})会变得非常大,从而导致计算量太大。

    def viterbi(start_p, nodes, trans):
        # paths以字典的方式缓存状态序列,以及概率值,初始位置只有start_p
        paths = start_p  
        # 从第二个词开始计算可能的最大概率路径
        for l in range(1, len(nodes)): 
            paths_ = paths # paths_缓存上一刻的状态
            print(paths_)
            paths = {}
            # 在l时刻,即第l个词的位置,分别对s,b,m,e四种状态值分析,计算在该状态值中最大概率路径,并保存到缓存paths中
            for i in nodes[l]: 
                nows = {} # 当前时刻状态值为i的所有可能路径缓存
                # j为所有之前时刻缓存的路径
                for j in paths_: 
                    # j[-1]为j路径最后一个状态值,比如j为'bess',判断s+i是否是可能组合,比如se就不可能
                    if j[-1]+i in trans:  
                        # l时刻下状态值i的路径概率 = 之前路径概率 + token到i的发射概率 + j[-1]i的转移概率 
                        nows[j+i] = paths_[j]+nodes[l][i]+trans[j[-1]+i]
                # 选取nows中概率最大的路径
                prob_i, path_i = max((v, k) for k,v in nows.items())
                paths[path_i] = prob_i
        print(paths)
        # 求出最后一个字哪一种状态的对应概率最大,最后一个字只可能是两种情况:e(结尾)和s(独立词)
        prob, states = max((v, k) for k,v in paths.items() if k[-1] in 'es')
        return prob, states
    

    HMM模型,对输入句子做分词

    def hmm_cut(s):
        # nodes 为输入语句s中每个token分别为sbme的概率
        nodes = [{i: log(j[t]+1)-log_total[i] for i, j in hmm_model.items()} for t in s]
        _, tags = viterbi(start_p, nodes, trans)
        print(tags)
        words = [s[0]]
        for i in range(1, len(s)):
            if tags[i] in ['b', 's']:
                words.append(s[i])
            else:
                words[-1] += s[i]
        return words
    

    测试

    text = '华为手机深得大家的喜欢'
    print(' '.join(hmm_cut(text)))
    # '华为 手机 深得 大家 的 喜欢'
    text = '王五的老师经常夸奖他'
    print(' '.join(hmm_cut(text)))
    # '王五 的 老师 经常 夸奖 他'
    

    以第一个语句为例,我们把每个时刻保存的paths打印出来看一下

    0: {'b': -0.26268660809250016, 'e': -3.14e+100, 'm': -3.14e+100, 's': -1.4652633398537678}
    1: {'ss': -6.7477507587683565, 'sb': -8.514824192919416, 'bm': -7.803021856684376, 'be': -5.779768469237251}
    2: {'bes': -13.546372927012078, 'beb': -12.721481505265722, 'bmm': -16.329054443981185, 'bme': -14.38750591207887}
    3: {'bess': -22.58204745531191, 'besb': -20.100674738295005, 'bebm': -20.053773324661236, 'bebe': -19.39785648454214}
    4: {'bebes': -28.147928301644228, 'bebeb': -26.84139484673273, 'besbm': -29.56559469156299, 'bebme': -28.614871482566265}
    5: {'bebess': -34.21920088939125, 'bebesb': -35.64550863881571, 'bebebm': -33.966633148962686, 'bebebe': -33.14651295553467}
    6: {'bebebes': -39.29135543196375, 'bebebeb': -38.32672117128855, 'bebebmm': -38.95179717057812, 'bebebme': -39.73898048704186}
    7: {'bebebess': -46.54991062378507, 'bebebesb': -46.739964735908735, 'bebebebm': -44.26298344994734, 'bebebebe': -43.90148032305975}
    8: {'bebebebes': -46.94960190563267, 'bebebebeb': -52.88266343174185, 'bebebebmm': -52.64274104579346, 'bebebebme': -51.34076405809046}
    9: {'bebebebess': -55.923380851692144, 'bebebebesb': -55.24166000635461, 'bebebebebm': -62.67816509870802, 'bebebebmme': -61.51714578556312}
    10: {'bebebebesss': -66.66337116442051, 'bebebebessb': -64.60666567893038, 'bebebebesbm': -65.49347468402344, 'bebebebesbe': -63.772287447683404}
    tags:  bebebebesbe
    华为 手机 深得 大家 的 喜欢
    

    可见对于未登录词,王五,HMM模型可以发现这是个人名,分词时组合在一起,而查字典方法做不到。


    代码实现2

    以下是结巴分词的代码,其实和上面差不多:

    hmm_model = {i: Counter(i) for i in 'SBME'}
    with open('data/dict.txt.small', encoding='utf-8') as f:
        for line in f:
            lines = line.strip('
    ').split(' ')
            if len(lines[0]) == 1:
                hmm_model['S'][lines[0]] += int(lines[1])
            else:
                hmm_model['B'][lines[0][0]] += int(lines[1])
                hmm_model['E'][lines[0][-1]] += int(lines[1])
            for m in lines[0][1:-1]:
                hmm_model['M'][m] += int(lines[1])
    log_total = {i: log(sum(hmm_model[i].values())) for i in 'SBME'}
    # 转移矩阵
    trans_p={'B': {'E': -0.510825623765990, 'M': -0.916290731874155},
     'E': {'B': -0.5897149736854513, 'S': -0.8085250474669937},
     'M': {'E': -0.33344856811948514, 'M': -1.2603623820268226},
     'S': {'B': -0.7211965654669841, 'S': -0.6658631448798212}}
    # 发射矩阵
    emit_p = {i: {t: log(j[t]+1)-log_total[i] for t in j.keys()} for i, j in hmm_model.items()}
    # 初始概率
    start_p={'B': -0.26268660809250016,
     'E': -3.14e+100,
     'M': -3.14e+100,
     'S': -1.4652633398537678}
    # 状态转移集合,比如B状态前只可能是E或S状态
    PrevStatus = {
        'B': 'ES',
        'M': 'MB',
        'S': 'SE',
        'E': 'BM'
    }
    MIN_FLOAT = -3.14e100
    # HMM模型中文分词中,我们的输入是一个句子(也就是观察值序列),输出是这个句子中每个字的状态值
    # HMM的解码问题
    def jieba_viterbi(obs, states, start_p, trans_p, emit_p):
        V = [{}]  # 状态概率矩阵  
        path = {}
        for y in states:  # 初始化状态概率
            V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
            path[y] = [y] # 记录路径
        for t in range(1, len(obs)):
            V.append({})
            newpath = {}
            for y in states:
                em_p = emit_p[y].get(obs[t], MIN_FLOAT)
                # t时刻状态为y的最大概率(从t-1时刻中选择到达时刻t且状态为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
                newpath[y] = path[state] + [y] # 只保存概率最大的一种路径 
            path = newpath 
        # 求出最后一个字哪一种状态的对应概率最大,最后一个字只可能是两种情况:E(结尾)和S(独立词)  
        (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
    
        return (prob, path[state])
    
    def hmm_cut(s):
        # viterbi算法得到sentence 的切分
        prob, tags = jieba_viterbi(s, 'SBME', start_p, trans_p, emit_p)
        print(tags)
        words = [s[0]]
        for i in range(1, len(s)):
            if tags[i] in ['B', 'S']:
                words.append(s[i])
            else:
                words[-1] += s[i]
        return words
    

    测试:

    text = '小明硕士毕业于中国科学院计算所'
    print(' '.join(hmm_cut(text)))
    # >> ['B', 'E', 'B', 'E', 'B', 'M', 'E', 'B', 'E', 'B', 'M', 'E', 'B', 'E', 'S']
    # >> '小明 硕士 毕业于 中国 科学院 计算 所'
    

    文章参考:

    1、https://blog.csdn.net/sinat_33741547/article/details/78690440

    2、https://github.com/ustcdane/annotated_jieba/blob/master/jieba/finalseg/init.py

    3、https://spaces.ac.cn/archives/3922

  • 相关阅读:
    前端工程化浅学
    jQuery学习
    黄金票据和白银票据获取域控权限
    [转]0day零距离
    [转]走近0day
    [转]人生如解 -- 关于破解组织的乱弹
    [转]WAREZ无形帝国
    [转]BSD系统正在死亡?一些安全研究人员这样认为
    Solaris:你好奇的十件事
    Windows和Office激活汇总
  • 原文地址:https://www.cnblogs.com/zongfa/p/15153992.html
Copyright © 2011-2022 走看看