中文分词算法之HMM和Viterbi(维特比)算法理解

正文之前

这周二开博士沙龙,大老板对我想做的方向,很感兴趣。。我他么有点害怕,听同组师兄的女朋友,也是一个大老板门下的师姐说,在他们那一次博士沙龙,大老板对我大加褒奖,不吝溢美之词,让我更害怕了。这是一份沉甸甸的压力,我自觉我还是个小菜鸡,还不至于成为大老板手上的小红人,所以我怕自己让大老板失望,那样就不好了。不过既然都这样了,那就好好学吧。对吧,大老板还推荐大家都来看看《汉字》 这个纪录片。。也就是他想让我做的方向的一个很好地启蒙片。。。我就推荐下吧

漢字-Bilibili 1080p國語中字

正文

最近读了一个博客,里面简述了一些中文分词算法,现在正在深入研究维特比算法,链接如下 ,有兴趣的朋友可以去看看全文:

浅谈分词算法(3)基于字的分词方法(HMM)

具体的内容不多说,下面就简单讲下我对这里面的Viterbi算法的理解。


首先需要介绍下隐马尔科夫模型(Hidden Markov Model,HMM):

HMM包含如下的五元组:

  • 状态值集合Q={q1,q2,...,qN},其中N为可能的状态数;在本文的例子中,就是汉字有可能的四个状态(B,M,E,S),分别表示词的开始、结束、中间(begin、end、middle)及字符独立成词(single)

  • 观测值集合V={v1,v2,...,vM},其中M为可能的观测数;观测值就是文本中的字咯;

  • 转移概率矩阵A=[aij],其中aij表示从状态i转移到状态j的概率;这个在本中文是指从一个状态转移到另一个状态的概率;

  • 发射概率矩阵(也称之为观测概率矩阵)B=[bj(k)],其中bj(k)表示在状态j的条件下生成观测vk的概率;本文中指一个字在某一状态的可能性。这个是先验的(就是说通过统计方法得到的)

  • 初始状态分布π.(初始值,内部给定)

一般地,将HMM表示为模型λ=(A,B,π),状态序列为I,对应测观测序列为O。对于这三个基本参数,HMM有三个基本问题:

  • 概率计算问题,在模型λ下观测序列O出现的概率;

  • 学习问题,已知观测序列O,估计模型λ的参数,使得在该模型下观测序列P(O|λ)最大;

  • 解码(decoding)问题,已知模型λ与观测序列O,求解条件概率P(I|O)最大的状态序列I。

更详细的,简洁的说法请参见wiki吧,or有个博客讲的也还算清晰,主要看以天气和治病为例子的那些真实世界映射,骰子那个不是那么好理解wiki百科关于维特比 ||||||||||||| 一文搞懂HMM(隐马尔可夫模型) |||||||||||||

想象一个乡村诊所。村民有着非常理想化的特性,要么健康要么发烧。他们只有问诊所的医生的才能知道是否发烧。 聪明的医生通过询问病人的感觉诊断他们是否发烧。村民只回答他们感觉正常、头晕或冷。

假设一个病人每天来到诊所并告诉医生他的感觉。医生相信病人的健康状况如同一个离散马尔可夫链。病人的状态有两种“健康”和“发烧”,但医生不能直接观察到,这意味着状态对他是“隐含”的。每天病人会告诉医生自己有以下几种由他的健康状态决定的感觉的一种:正常、冷或头晕。这些是观察结果。 整个系统为一个隐马尔可夫模型(HMM)。

医生知道村民的总体健康状况,还知道发烧和没发烧的病人通常会抱怨什么症状。 换句话说,医生知道隐马尔可夫模型的参数。 这可以用Python语言表示如下:

states = ('Healthy', 'Fever')
 
observations = ('normal', 'cold', 'dizzy')
 
start_probability = {'Healthy': 0.6, 'Fever': 0.4}
 
transition_probability = {
   'Healthy' : {'Healthy': 0.7, 'Fever': 0.3},
   'Fever' : {'Healthy': 0.4, 'Fever': 0.6},
   }
 
emission_probability = {
   'Healthy' : {'normal': 0.5, 'cold': 0.4, 'dizzy': 0.1},
   'Fever' : {'normal': 0.1, 'cold': 0.3, 'dizzy': 0.6},
}

上面关于HMM的叙述大部分来自原文,所以大家可以去看原文,结合我的看就好了

如何从HMM模型到维特比算法,还请大家移步原文看,我就不多赘述,还是上代码加注释会比较好,毕竟我主要的工作就是加了一些注释。

# -*- coding: utf-8 -*-
'''
start:初始概率分布,大概就是第一个字的状态的概率吧
tran :状态转移概率,从当前状态到下一个状态的转移的概率,
emit :发射概率,表示在某一状态下生成某个观测状态(在这一状态下,这个字是这个状态)的概率
'''
import sys
import re
import getopt

MIN_FLOAT = -3.14e100

PROB_START_P = "prob_start.p"
PROB_TRANS_P = "prob_trans.p"
PROB_EMIT_P = "prob_emit.p"
#某一个词的状态为key时,prevStatus表示前一个词的状态的框定范围
PrevStatus = {
    'B': 'ES',
    'M': 'MB',
    'S': 'SE',
    'E': 'BM'
}

Force_Split_Words = set([])
from prob_start import P as start_P
from prob_trans import P as trans_P
from prob_emit import P as emit_P


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 range(1, len(obs)):
        V.append({})
        newpath = {}
        # 考察当前字,对于上一个字的发射概率,取其中最大的那个
        for y in states:
            #获取当前词在y状态下的发射概率
            em_p = emit_p[y].get(obs[t], MIN_FLOAT)
            # y状态在prevStatus的限定后,前一个词的限定范围内某一状态y0的概率 +  y0对当前字的y状态的转移概率 + 当前词在y状态下的发射概率(其实就是这个词是某个状态的概率的意思)
            # state表示前一个字到当前字的y状态的最大概率,prob表示这个概率。
            (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
            # 更新路径,state表示当前字的前一个字到当前字的y状态的最大可能概率,所以是path[state],因为要取前一个字的最大概率路径
            newpath[y] = path[state] + [y]
        path = newpath
    # 最后一个要重新复盘,因为最后一个字只能是E or S
    (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
    for i in path:
        print((i,path[i]))
    for v in  V:
        print((v))
    return (prob, path[state])


def __cut(sentence):
    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:]


re_han = re.compile("([\u4E00-\u9FD5]+)")
re_skip = re.compile("([a-zA-Z0-9]+(?:\.\d+)?%?)")


def cut(sentence):
    sentence = sentence.strip().decode('utf-8')
    blocks = re_han.split(sentence)
    lseg = []
    for blk in blocks:
        if re_han.match(blk):
            for word in __cut(blk):
                if word not in Force_Split_Words:
                    lseg.append(word)
                else:
                    for c in word:
                        lseg.append(c)
        else:
            tmp = re_skip.split(blk)
            for x in tmp:
                if x:
                    lseg.append(x)
    return lseg


if __name__ == "__main__":
    ifile = 'input.txt'
    ofile = 'seg.txt'
    # try:
    #     opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["ifile=", "ofile="])
    # except getopt.GetoptError:
    #     print('seg_hmm.py -i <inputfile> -o <outputfile>')
    #     sys.exit(2)
    # for opt, arg in opts:
    #     if opt == '-h':
    #         print('seg_hmm.py -i <inputfile> -o <outputfile>')
    #         sys.exit()
    #     elif opt in ("-i", "--ifile"):
    #         ifile = arg
    #     elif opt in ("-o", "--ofile"):
    #         ofile = arg

    with open(ifile, 'rb') as inf:
        for line in inf:
            rs = cut(line)
            print(' '.join(rs))
            with open(ofile, 'a',encoding='utf8') as outf:
                outf.write(' '.join(rs) + "\n")

OK,该说的都在代码上了,想要我细细道来也别想了。。麻烦,好人做到底,我再附个图,这下应该简单明了了:

----------图片上传不了。。。---------去下面看吧----------

图片来源知乎:如何通俗地讲解 viterbi 算法?

正文之后

OK,溜了,在代码中还学习到了yield和enumerate的用法,开心`

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,230评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,261评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,089评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,542评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,542评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,544评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,922评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,578评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,816评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,576评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,658评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,359评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,920评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,859评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,381评论 2 342

推荐阅读更多精彩内容