nlp基础—8.隐马尔科夫模型(HMM)分词实现

文章目录

      • 引言
      • HMM分词实现


理论部分传送门:

  • nlp基础—7.隐马尔可夫模型(HMM算法)
  • 数据代码链接见:https://gitee.com/lj857335332/hmm-for-word-segmentation

引言

  隐马尔可夫模型相比于神经网络分词来说,速度比较快。早期,在自然语言处理中,曾用于分词。它的效果受训练集的影响比较大,它只能处理过在训练集中见过的组合,隐马尔可夫模型的泛化能力是比较弱的。

HMM分词实现

  训练集采用人民日报1998年中文标注语料库;
nlp基础—8.隐马尔科夫模型(HMM)分词实现_第1张图片
代码部分如下:

import time
import numpy as np


def trainParameter(fileName):
    """
    依据数据集统计π,A,B这三个参数
    :param fileName: 训练文本地址
    :return: 三个参数
    """
    # 定义一个查询字典,用于映射四种标记在数组中对应位置,方便查询
    # B:词语开头;M:一个词语的中间词;E:一个词语的结果;S:非词语,单个词
    statuDict = {'B': 0, 'M': 1, 'E': 2, 'S': 3}

    # 每个字只有四种状态,所以下面各类初始化中大小参数为4
    # 初始化PAI的一维数组,涉及四种状态
    PAI = np.zeros(4)
    # 初始化状态转移概率矩阵A,涉及四种状态分别到四种状态的转移,大小为4*4
    A = np.zeros((4, 4))
    # 初始化观测概率矩阵,涉及到四种状态到每个字的发射概率
    # 这里采用一个65536的空间保证对于所有的汉字都能找到对应的位置进行存储
    B = np.zeros((4, 65536))

    # 读取训练文本
    fr = open(fileName, encoding='utf-8')
    # 文本中的每一行认为是一个训练样本,训练样本已经分词完毕,词语之间用间隔空开,统计时我们按照如下思路:
    """
        1.先将句子按照空格隔开,例如例句中5个词语,隔开后变成一个长度为5的列表,每个元素为一个词语
        2.对每个词语长度进行判断:
              如果为1认为该词语是S,即单个字
              如果为2则第一个是B,表开头,第二个为E,表结束
              如果大于2,则第一个为B,最后一个为E,中间全部标为M,表中间词
        3.统计PI:该句第一个字的词性对应的PI中位置加1
                  例如:PI = [0, 0, 0, 0],当本行第一个字是B,即表示开头时,PI中B对应位置为0,
                       则PI = [1, 0, 0, 0],全部统计结束后,按照计数值再除以总数得到概率
          统计A:对状态链中位置t和t-1的状态进行统计,在矩阵中相应位置加1,全部结束后生成概率
          统计B:对于每个字的状态以及字内容,生成状态到字的发射计数,全部结束后生成概率
    """
    for line in fr.readlines():
        # 对单行句子去掉结尾的"\n"并按空格进行切割
        curLine = line.strip().split()
        # 对词性的标记放在该列表中
        wordLabel = []
        # 对分割后的每一个单词进行遍历
        for i in range(len(curLine)):
            # 如果单词长度为1,那么将该词标为"S",即单个词
            if len(curLine[i]) == 1:
                label = "S"
            # 如果长度不为1,开头为B,最后为E,中间添加长度-2个M
            else:
                label = "B" + 'M' * (len(curLine[i]) - 2) + 'E'

            # 如果是单行开头的第一个字,PAI中对应位置加1
            if i == 0:
                PAI[statuDict[label[0]]] += 1

            # 对于单词中的每一个字,在生成的状态链中统计B
            for j in range(len(label)):
                # 遍历状态链中每一个状态,并找到对应的中文汉字,并在B对应的位置中加1
                # 因为是中文分词,使用ord(汉字)即可找到其对应汉字编码
                B[statuDict[label[j]]][ord(curLine[i][j])] += 1

            # 在整行的状态链中添加该单词的状态链
            wordLabel.extend(label)

        # 单行所有单词都结束后,统计状态转移A矩阵
        # 因为A涉及了前一个状态,所以需要等整条状态链都生成后再开始统计
        for i in range(1, len(wordLabel)):
            # 统计t时刻状态和t-1时刻状态的所有状态组合的出现次数
            A[statuDict[wordLabel[i - 1]]][statuDict[wordLabel[i]]] += 1

    # 将π,A,B转化为概率—归一化
    # 对PAI求和,概率生成中的分母
    sumPAI = np.sum(PAI)
    # 遍历PAI中的每一个元素,元素出现的次数/总次数等于概率
    for i in range(len(PAI)):
        # 为了防止结果下溢出,在概率上我们将其转换为log对数形式
        # 当单向概率为0的时候,log没有定义,因此需要单独判断,手动赋予一个极小值
        if PAI[i] == 0:
            PAI[i] = -3.14e+100
        else:
            PAI[i] = np.log(PAI[i] / sumPAI)

    # 对A求和,概率中的分母
    for i in range(len(A)):
        sumA = np.sum(A[i])
        for j in range(len(A[i])):
            if A[i][j] == 0:
                A[i][j] = -3.14e+100
            else:
                A[i][j] = np.log(A[i][j] / sumA)

    # 对B求和,概率中的分母
    for i in range(len(B)):
        sumB = np.sum(B[i])
        for j in range(len(B[i])):
            if B[i][j] == 0:
                B[i][j] = -3.14e+100
            else:
                B[i][j] = np.log(B[i][j] / sumB)

    return PAI, A, B


def loadArtical(fileName):
    """
    加载文章
    :param fileName: 文件路径
    :return: 文章内容
    """
    # 初始化文章列表
    artical = []
    # 打开文件
    fr = open(fileName, encoding='utf-8')
    # 遍历读取文章每一行
    for line in fr.readlines():
        # 去掉每一行末尾的"\n"
        curLine = line.strip()
        # 将当前行添加到文章列表中
        artical.append(curLine)

    return artical


def participle(artical, PAI, A, B):
    """
    基于维特比算法实现的分词
    :param artical: 要分词的文章列表
    :param PAI: 初始状态概率向量PAI
    :param A:状态转移矩阵
    :param B:观测概率矩阵
    :return:分词后的文章
    """
    # 初始化分词后的文章列表
    retArtical = []
    # 对文章按行读取
    for line in artical:
        # 初始化δ,δ存放四种状态的概率值,因为状态链中每个状态都有四个概率值
        # 因此,大小为(文本长度*四种状态)
        delta = [[0 for i in range(4)] for i in range(len(line))]

        # 第一步:初始化
        for i in range(4):
            # 初始化δ状态链中第一个状态的四种状态概率
            delta[0][i] = PAI[i] + B[i][ord(line[0])]
        # 初始化ψ,初始时为0
        psi = [[0 for i in range(4)] for i in range(len(line))]

        # 第二步:递推,依次处理整条链
        for t in range(1, len(line)):
            # 对于链中的四种状态,求四种状态概率
            for i in range(4):
                # 初始化一个临时列表,用于存放四种概率
                tmpDelta = [0] * 4
                for j in range(4):
                    tmpDelta[j] = delta[t - 1][j] + A[j][i]

                # 找到最大的那个δ*a
                maxDelta = max(tmpDelta)
                # 记录最大值对应的状态
                maxDeltaIndex = tmpDelta.index(maxDelta)
                # 将找到的最大值*b放入delta中
                delta[t][i] = maxDelta + B[i][ord(line[t])]
                # 在ψ中记录对应的最大状态索引
                psi[t][i] = maxDeltaIndex
        # 建立一个状态链列表,开始生成状态链
        sequence = []

        # 第三步:终止;获取最后一个状态概率对应的索引
        i_opt = delta[len(line) - 1].index(max(delta[len(line) - 1]))
        # 在状态链中添加索引
        sequence.append(i_opt)

        # 第四步:最优路径回溯
        # 从后往前遍历整条链
        for t in range(len(line) - 1, 0, -1):
            # 不断从当前时刻t的ψ列表中读取到t-1的最优状态
            i_opt = psi[t][i_opt]
            # 将状态放入列表中
            sequence.append(i_opt)
        # 因为是从后往前将状态放入列表中,这里需要翻转一下
        sequence.reverse()

        # 基于预测出来的状态开始进行分词
        curLine = ''
        # 遍历该行的每一个字
        for i in range(len(line)):
            # 在字符串中加入该字
            curLine += line[i]
            # 如果该字是3:S->单个词  或  2:E->结尾词 ,并且不是这句话的最后一个字,则在该字后面加上分隔符
            if (sequence[i] == 3 or sequence[i] == 2) and i != (len(line) - 1):
                curLine += '|'
        # 在返回的列表中添加分词后的该行
        retArtical.append(curLine)
    # 返回分词结果
    return retArtical


if __name__ == '__main__':
    # 记录开始时间
    start = time.time()
    # 依据数据集统计π,A,B这三个参数
    PAI, A, B = trainParameter("data/HMMTrainSet.txt")
    # 根据训练得到的模型做测试
    artical = loadArtical("data/testArtical.txt")
    # 打印原文
    print('---------------------------------------原文------------------------------------------------')
    for line in artical:
        print(line)
    # 进行分词
    partiArtical = participle(artical, PAI, A, B)
    # 打印分词结果
    print('--------------------------------------分词-------------------------------------------------')
    for line in partiArtical:
        print(line)

    end = time.time()
    print('time span =', end - start, 's')

分词结果为:

---------------------------------------原文------------------------------------------------
深圳有个打工者阅览室
去年12月,我在广东深圳市出差,听说南山区工商分局为打工者建了个免费图书阅览室,这件新鲜事引起了我的兴趣。
12月18日下午,我来到了这个阅览室。阅览室位于桂庙,临南油大道,是一间轻体房,面积约有40平方米,内部装修得整洁干净,四周的书架上摆满了书,并按政治、哲学、法律法规、文化教育、经济、科技、艺术、中国文学、外国文学等分类,屋中央有两排书架,上面也摆满了图书和杂志。一些打工青年或站或蹲,认真地阅读,不时有人到借阅台前办理借书或还书手续。南山区在深圳市西边,地处城乡结合部,外来打工者较多。去年2月,南山区工商分局局长王安全发现分局对面的公园里常有不少打工者业余时间闲逛,有时还滋扰生事。为了给这些打工者提供一个充实自己的场所,他提议由全分局工作人员捐款,兴建一个免费阅览室。领导带头,群众响应,大家捐款1.4万元,购买了近千册图书。3月6日,建在南头繁华的南新路和金鸡路交叉口的阅览室开放了。从此,这里每天都吸引了众多借书、看书的人们,其中不仅有打工者,还有机关干部、公司职员和个体户。到了夏天,由于阅览室所在地被工程征用,南山区工商分局便把阅览室迁到了桂庙。阅览室的管理人员是两名青年,男的叫张攀,女的叫赵阳。张攀自己就是湖北来的打工者,听说南山区工商分局办免费阅览室,便主动应聘来服务。阅览室每天从早9时开到晚10时,夜里张攀就住在这里。他谈起阅览室里的图书,翻着一本本的借阅名册,如数家珍,对图书和工作的挚爱之情溢于言表。我在这里碰到南山区华英大厦一位叫聂煜的女青年,她说她也是个打工者,由于春节探家回来后就要去市内工作,很留恋这里的这个免费阅览室,想抓紧时间多看些书,她还把自己买的几本杂志捐给了阅览室。在阅览室的捐书登记簿上,记录着这样的数字:工商系统内部捐书3550册,社会各界捐书250册。我在阅览室读到了这样几封感谢信:深圳瑞兴光学厂的王志明写道:“我们这些年轻人远离了家乡,来到繁华紧张的都市打工,辛劳之余,能有机会看书读报,感到特别充实。”深圳文光灯泡厂的江虹说:“南山区工商分局的干部职工捐款、捐书,给我们打工者提供良好的学习环境,鼓励我们求知上进,真是办了一件大好事,他们是我们打工者的知音。”(本报记者罗华)
--------------------------------------分词-------------------------------------------------
深圳|有个||工者|阅览室
去年|12月||||广东|深圳|市出|||||南山区|工商|分局|||工者|建了||免费|图书|阅览室||这件||鲜事|引起||||兴趣|。
12月|18日|下午||我来|||这个|阅览室||阅览室|||桂庙||临南油|大道|||一间||体房||面积||有40平|方米|||部装|修得|整洁|干净|||||书架||摆满||||||政治||哲学||法律|法规||文化|教育||经济||科技||艺术||中国|文学||外国|文学||分类|||中央||两排|书架||上面||摆满||图书||杂志||一些|打工|青年|或站||||认真|地阅|||不时||人到|借阅|台前|办理|借书||还书|手续||南山区||深圳|市西边||地处|城乡|结合部||外来||工者||||去年|2月||南山区|工商|分局|局长||安全|发现|分局|对面||公园||常有|不少||工者|业余|时间|闲逛||有时||滋扰|生事|||||这些||工者|提供|一个|充实|自己||||||提议||全分局|工作|人员|捐款||兴建|一个|免费|阅览室||领导|带头||群众|响应||大家|捐款|1.4万|||购买|||千册|图书||3月|6日||建在|南头|繁华|||新路||金鸡路|交叉口||阅览室|开放||||||这里|每天||吸引||众多|借书||看书||人们||其中|不仅|||工者||||机关|干部||公司|职员|||体户||||夏天||由于|阅览室|||||工程|征用||南山区|工商|分局|便||阅览室|迁到||桂庙||阅览室||管理|人员||||青年|||||||||||赵阳||||自己|||湖北来|||工者||||南山区|工商|分局||免费|阅览室||便|主动||聘来|服务||阅览室|每天||早9时|开到|晚10时||夜里||||||这里||他谈||阅览室|||图书||翻着||本本||借阅|名册||如数|家珍|||图书||工作||挚爱||情溢||言表||||这里|碰到|南山区|华英|大厦|一位||||||青年|||||||||工者||由于|春节|探家|回来||||去市||工作|||留恋|这里||这个|免费|阅览室|||抓紧|时间||看些||||||自己|||几本|杂志|捐给||阅览室|||阅览室||捐书||记簿|||记录||这样||数字||工商|系统|内部|捐书3550册||社会|各界|捐书250册||||阅览室|读到||这样||封感|谢信||深圳|瑞兴|光学厂|||志明|写道|||我们|这些||轻人|远离||家乡||||繁华紧|||||打工||辛劳||||||机会|看书|读报||感到|特别|充实|||深圳|文光||泡厂||江虹||||南山区|工商|分局||干部|职工|捐款||捐书|||我们||工者|提供|良好||学习|环境||鼓励|我们|求知|上进||真是|||一件|大好|||他们||我们||工者||知音||||本报|记者|罗华|)
time span = 4.005795955657959 s

代码部分参考于GitHub,会发现有些部分分词不合理,之后讲到的条件随机场模型正是对这些不合理部分的修正。


如果对您有帮助,麻烦点赞关注,这真的对我很重要!!!如果需要互关,请评论或者私信!
在这里插入图片描述


你可能感兴趣的:(#,nlp基础知识,隐马尔科夫模型,HMM,分词)