首发于 文本分类技术
把隐马尔可夫模型彻底说清楚

把隐马尔可夫模型彻底说清楚

1. 掌握HMM的必要性

有关于隐马尔可夫模型,我去年整理过一次,最后应该是直接把PPT截图到CSDN里了,似乎那篇文章也没有什么人看,或许是因为做成PPT索引不到,或许是大家对HMM没啥兴趣。但其实隐马的优势有点类似于朴素贝叶斯,我们比较好做人工干预。比如在HanLP的人名识别中就是利用的隐马模型,如果你对隐马基本原理没有一个清晰认识,那么直接上隐马的人名识别时,会有很多并非人名的词语被识别出来,这时候,就要利用HMM的3个概率表,即发射概率表,状态转移概率表,初始状态表,通过修改调整概率表,来提高系统识别准确率。

2. HMM的缺陷

按照何晗的《自然语言处理入门》第4章所述,HMM应用于分词的效果并不是特别理想,特别是很多在词典中的词没有被正确识别,也就是说HMM对于分词消岐并不理想,作者的结论是HMM的分词效果甚至不如词典分词。作者同时也利用HanLP做了二阶HMM的分词,作者的结论是增加HMM的阶数并不能提高分词器的准确率,单纯靠提高转移概率矩阵的复杂度并不能提高模型的拟合能力。

3. HMM解决序列标注问题的过程

训练过程:

(1)统计状态的概率分布,也就是初始概率矩阵pi

以分词为例,就是统计状态B、M、E、S的出现频率。其中B表示分词后词语的首字,M表示分词后词语的非首非尾自(如果词语字数>=3),E表示词语的尾字,S表示单字成词。比如“北京/ 信息/ 科技/ 大学”,对应的状态序列为“BEBEBEBE”。我想统计符号B、M、E、S在训练语料中的出现频率这件事的公式我就不用再在这里写出了吧?

(2)统计状态转移概率矩阵A

还以分词为例,我们应该用训练语料统计出下边的转移概率矩阵

转移概率矩阵顾名思义,就是说从一种转态到另一种状态的概率。比如训练语料中的一个词语“大学”,那么该词语对应的状态序列为“BE”,此时由B到E的转移次数就应该+1,按照这样的统计方法,就可以统计出状态转移概率矩阵A。(当然注意概率指的是出现次数除以总次数,这里认为频率就等于概率)

(3)统计发射概率矩阵B

依然以分词为例,发射概率就是一个状态可能对应的汉字的概率分布。就是下边的概率矩阵。

还是假设训练语料中出现了“大学”“BE”的对应关系,那么由“B”状态发射出“大”这个字的次数应该+1,由“E”状态发射出“学”这个字的次数应该+1,按照这样的统计方法,就可以统计出发射概率矩阵B。

至此,我们已经利用训练语料统计出了隐马模型的所有参数,也就是通常所说的三要素,接下来我们介绍预测。

预测过程:

(4)维特比算法

预测过程比较简单,就是利用维特比算法。我们先以分词为例说明维特比要做的事情。给定一个观测的序列如“北京信息科技大学”,利用维特比算法,系统可以求出一个最佳的状态序列比如是“BEBEBEBE”。好,就到这里,你应该已经知道维特比要做的事情了。那么,维特比是如何做到的呢,下边我们贴出李航《统计学习方法》中的计算过程。

首先给出两个定义:

接下来给出维特比算法

好了,读到这里你可能不想看了,没有关系,我们在下一小结求解李航《统计学习方法》书中第10章的10.3题,通过求解你就明白维特比的计算过程了。如果求解了这道习题,还是不明白,没有关系,文章最后一节给出一个Python实现的简单分词器,看了这段代码我想你应该了解了HMM分词的训练和预测过程了。

(5)根据状态序列执行分词

有了状态序列,就可以根据状态值进行分词了。比如“北京信息科技大学”HMM模型预测出的状态序列为“BEBEBEBE”,显然分词结果就是“北京/信息/科技/大学”。当然,我们这个例子刚好HMM预测正确了,实践过程中HMM很有可能会预测错误,比如预测出“SE”这样的序列,显然这两个状态不可能连续出现,因此,我们需要设计一定的规则将HMM输出的状态序列转换为合理的分词结果。HanLP的规则是只要遇到B或S这两个状态就切出1个词。

4. 一道例题理解HMM的训练和预测过程

(1)HMM的两个基本假设

前边我们都是以分词这个任务来叙述HMM的,这里,我们再回归到HMM的一些数学原理。

假设1:隐马的任一时刻状态只依赖于前一状态,而与其他时刻状态无关。

假设2:隐马的任意时刻观测值只依赖于对应该时刻的状态,而与其他时刻状态无关。

个人认为假设1在分词场景还是可以接受的,假设2忽略了两个以上汉字成词的概率这个特征。

(2)HMM中“隐”指什么?

实际上,前边我们都是说的状态,实际上对应于观测序列,可以将状态序列称为隐含状态,因为我们看到的只是汉字序列而没有看到对应于词的状态序列。

(3)例题:HMM的训练过程

这道例题是李航《统计学习方法》例10.1,通过这个例子,再次熟悉一下HMM的术语。

你能根据题目的3个叙述,写出3个概率矩阵和右侧的图吗?如果可以写出,那么你也就明白了HMM的训练过程。

(4)例题:HMM的预测过程

显然,最优状态序列为3 3 3,也就是3号盒子 3号盒子 3号盒子。

5. 基于HMM分词的Python示例

下边的代码是我用Python写的一个示例代码,代码长度在200行左右,希望通过该代码小伙伴们能彻底明白HMM的训练和预测过程。

#coding:utf-8
"""
基于隐马尔可夫模型的分词
"""
import codecs
import pickle
import numpy as np

idx_to_state = ['b', 'm', 'e', 's']
state_to_idx = {'b':0, 'm':1, 'e':2, 's':3}
# 极大似然估计法训练模型参数
def train():
    # 构建字表
    character_set = set()
    with codecs.open('RenMinData.txt_utf8', 'rb', 'utf-8', 'ignore') as infile:
        for line in infile:
            line = line.strip()
            if line:
                word_li = line.split()
                if word_li:
                    for word in word_li:
                        if word:
                            character_set.update(word)
    # 输出字表
    print('输出字表...')
    with open('character_list.txt', 'wb') as outfile:
        idx = 0
        for character in character_set:
            out_str = u'%d\t%s\n' % (idx, character)
            outfile.write(out_str.encode('utf-8', 'ignore'))
            idx += 1

    # 加载字索引以及索引字
    character_to_idx = dict()
    idx_to_character = []
    with codecs.open('character_list.txt', 'rb', 'utf-8', 'ignore') as infile:
        for line in infile:
            line = line.strip()
            if line:
                idx, character = line.split(u'\t')
                character_to_idx[character] = int(idx)
                idx_to_character.append(character)
    print('加载字索引表 长度=%d' % len(character_to_idx))
    print('加载索引字表 长度=%d' % len(idx_to_character))


    global state_li
    # 矩阵初始化
    print('初始化模型矩阵')
    A = np.zeros((len(idx_to_state), len(idx_to_state)))
    print('A=\n', A)
    B = np.zeros((len(idx_to_state), len(idx_to_character)))
    print('B.shape', B.shape)
    PI = np.zeros(len(idx_to_state))
    print('PI=', PI)

    # 训练
    with codecs.open('RenMinData.txt_utf8', 'rb', 'utf-8', 'ignore') as infile:
        for line_ser, line in enumerate(infile):
            line = line.strip()
            if line:
                # 对句子中的每个字打状态标记
                word_li = line.split()
                character_li = []
                if word_li:
                    for word in word_li:
                        if len(word) == 0:
                            continue
                        elif len(word) == 1:
                            character_li.append((word[0],'s'))
                        elif len(word) == 2:
                            character_li.append((word[0],'b'))
                            character_li.append((word[1], 'e'))
                        else:
                            character_li.append((word[0], 'b'))
                            character_li.extend([(w, 'm') for w in word[1:-1]])
                            character_li.append((word[-1], 'e'))
                # 统计相关次数
                # 更新PI列表
                PI[state_to_idx[character_li[0][1]]] += 1
                # 更新B字典
                for character, state in character_li:
                    B[state_to_idx[state], character_to_idx[character]] += 1
                # 更新A字典
                if len(character_li) >= 2:
                    for ser_num, cur_state in enumerate([w[1] for w in character_li[:-1]]):
                        next_state = character_li[ser_num+1][1]
                        cur_state_idx = state_to_idx[cur_state]
                        next_state_idx = state_to_idx[next_state]
                        A[cur_state_idx][next_state_idx] += 1

    # 计算PI
    PI = PI/sum(PI)
    # 计算A
    for row_ser in range(A.shape[0]):
        A[row_ser,::] /= sum(A[row_ser,::])
    # 计算B
    for row_ser in range(B.shape[0]):
        B[row_ser,::] /= sum(B[row_ser, ::])

    # 输出PI
    print("输出PI矩阵...")
    with open('model_PI', 'wb') as outfile:
        pickle.dump(PI, outfile)

    # 输出A
    print("输出A矩阵...")
    with open('model_A', 'wb') as outfile:
        pickle.dump(A, outfile)

    # 输出B
    print("输出B矩阵...")
    with open('model_B', 'wb') as outfile:
        pickle.dump(B, outfile)

def viterbe(O, PI, A, B, str=u''):
    """
    viterbi算法计算HMM问题2
    :param O:    观测序列
    :param PI:   初始状态分布
    :param A:    状态转移矩阵
    :param B:    发射矩阵
    :return:
    """

    # viterbi算法中初始化delta_1
    delta_1 = PI * B[:, 0]
    # viterbi算法中初始化kesi_1
    kesi_1 = np.zeros(PI.size, dtype=np.int)
    # 最优路径记录初始化
    kesi = np.array(kesi_1.T)

    # 递推
    delta_tplusone = delta_1.copy()
    for t in range(1, len(O)):
        max_delta_tminus_aji = np.tile(delta_tplusone, PI.size).reshape(A.shape).T * A
        delta_t = np.max(max_delta_tminus_aji, 0) * B[:, O[t]]
        kesi_t = np.argmax(max_delta_tminus_aji, 0)
        kesi = np.column_stack((kesi, kesi_t.T))
        delta_tplusone = delta_t.copy()

    # 终止
    P_star = np.max(delta_t)
    i_T_star = np.argmax(kesi_t)

    # 最优路径回溯
    I_star = [i_T_star]
    i_tplus_star = i_T_star
    for t in range(kesi.shape[1] - 1, 0, -1):
        i_t_star = kesi[:, t][i_tplus_star]
        I_star.append(i_t_star)
        i_tplus_star = i_t_star
    I_star = I_star[::-1]

    # 输出分词结果
    if str:
        out_str = u""
        state_li = [idx_to_state[w] for w in I_star]
        i = 0
        while i<len(state_li):
            if state_li[i] == 'b':
                j = i+1
                while j<len(state_li):
                    if state_li[j] not in ['m', 'e']:
                        break
                    j += 1
                out_str += str[i:j] + u'/'
                i = j
            else:
                out_str += str[i] + u'/'
                i+=1
        print(out_str)


if __name__ == '__main__':
    # 训练模型
    train()
    # 加载模型
    PI = pickle.load(open('model_PI', 'rb'))
    A = pickle.load(open('model_A', 'rb'))
    B = pickle.load(open('model_B', 'rb'))
    # 加载字索引以及索引字
    character_to_idx = dict()
    idx_to_character = []
    with codecs.open('character_list.txt', 'rb', 'utf-8', 'ignore') as infile:
        for line in infile:
            line = line.strip()
            if line:
                idx, character = line.split(u'\t')
                character_to_idx[character] = int(idx)
                idx_to_character.append(character)
    print('加载字索引表 长度=%d' % len(character_to_idx))
    print('加载索引字表 长度=%d' % len(idx_to_character))
    # 测试
    # A = np.array([[0.5, 0.2, 0.3],
    #               [0.3, 0.5, 0.2],
    #               [0.2, 0.3, 0.5]])
    # B = np.array([[0.5, 0.5],
    #               [0.4, 0.6],
    #               [0.7, 0.3]])
    # PI = np.array([0.2, 0.4, 0.4])
    # O = [0, 1, 0]
    str = u'小明硕士毕业于北京信息科技大学'
    try:
        O = [character_to_idx[w] for w in str]
    except:
        print('有未登录字')
        exit(1)
    print(str)
    viterbe(O, PI, A, B, str)

这里我们给出训练语料的格式,每行1个句子,词语之间以空格分隔即可。


下边给出实现代码的github链接,如有问题请大家留言。

至此,专栏中有关自然语言处理基本机器学习算法就全部介绍完毕了。我们介绍了HMM, baiziyu:最大熵模型 , baiziyu:条件随机场 。

天下网标王随州关键词按天收费内江网站开发推荐海北百度标王多少钱云浮百姓网标王推广公司广东网站排名优化价格昆明SEO按天扣费多少钱湘潭网站制作设计哪家好永州SEO按天计费报价鹤壁网站制作设计报价营口网站改版公司郑州阿里店铺运营报价达州企业网站设计哪家好山南seo网站推广价格贺州SEO按天收费推荐衡阳建设网站哪家好乐山网站推广工具公司大庆网站建设设计多少钱广东网站优化报价商洛网站优化按天收费价格淮安网络推广咸阳网站优化按天扣费公司黔南模板网站建设淮北百度标王价格坪山百度竞价塔城SEO按天扣费推荐运城模板制作价格南山百度竞价价格陇南网站推广宝鸡网站排名优化公司苏州百度标王哪家好香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

天下网标王 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化