隐式马尔可夫模型(HMM)的Python实现

原理

请参考 《自然语言处理入门》 作者何晗 第129页

代码

隐式马尔可夫模型可由 λ = ( π , A , B ) \lambda=(\pi, A,B) λ=(π,A,B) 完全描述。
代码首先实现了接口HMM,该接口定义了隐式马尔可夫模型的一些公共操作。
其次定义了专门用于文本序列标注的隐式马尔可夫模型 TextSequence, 实现了对文本的操作。
最后实现了维特比算法(viterbi)对隐式马尔可夫模型进行解码,根据输入序列预测状态序列。

TextSequence 可用于分词、实体识别、事件抽取等自然语言处理任务。

from typing import *
import numpy as np

class HMM():

    def __init__(self, N: int, M: int):
        self.N = N
        self.M = M
        self.PI = np.zeros((N, ), dtype=np.float)
        self.A = np.zeros((N, N), dtype=np.float)
        self.B = np.zeros((N, M), dtype=np.float)

    def getPI(self) -> np.ndarray:
        """ 获取初始状态概率 R ^ N"""
        raise NotImplementedError()

    def getA(self) -> np.ndarray:
        """ 获取状态转移概率 R ^ (N * N)"""
        raise NotImplementedError()

    def getB(self) -> np.ndarray:
        """ 获取发射概率 R ^ (N * M) """
        raise NotImplementedError()


class TextSequence(HMM):
    def __init__(self, texts: List[List[str]], labels: List[List[int]], nClasses:int):
        self.texts = texts
        self.labels = labels

        # 构造词典
        dictSet = {"[OOV]": 0}
        dictArray = ["[OOV]"]
        for seq in texts:
            for word in seq:
                w = dictSet.get(word)
                if w == None:  # 如果当前词不在词典之中
                    dictSet[word] = len(dictArray)
                    dictArray.append(word)

        self.dict = (dictSet)
        self.dictArray = (dictArray)

        # 构造矩阵
        HMM.__init__(self, nClasses, len(dictArray))

    def getIndex(self, text :List[str]) -> List[int]:
        """把序列转换成索引"""
        indexes = []
        for word in text:
            index = self.dict.get(word)
            if(index == None):
                indexes.append(0)
            else:
                indexes.append(index)
        return indexes


    def getPI(self) -> np.ndarray:
        self.PI.fill(0)

        for labelSeq in self.labels:
            if(len(labelSeq) > 0):
                self.PI[labelSeq[0]] += 1

        self.PI /= self.PI.sum()

        return self.PI


    def getA(self) -> np.ndarray:
        self.A.fill(0)

        for labelSeq in self.labels:
            if(len(labelSeq) > 1):
                for i in range(1, len(labelSeq)):
                    labelPre = labelSeq[i - 1]
                    lableCur = labelSeq[i]
                    self.A[labelPre, lableCur] += 1

        #对每行进行归一化
        for i in range(self.N):
            maxValue = self.A[i,].sum()
            if(maxValue == 0):
                maxValue = 1
            self.A[i, ] /= maxValue

        return self.A


    def getB(self) -> np.ndarray:
        self.B.fill(0)
        if(len(self.texts) != len(self.labels)):
            raise RuntimeError("输入句子序列长度与输入标签序列长度不一致")

        for i in range(len(self.texts)):
            if (len(self.texts[i]) != len(self.labels[i])):
                raise RuntimeError("输入句子长度与标签长度不一致")
            for j in range(len(self.texts[i])) :
                state = self.labels[i][j]
                word = self.texts[i][j]
                wordIndex = self.dict[word]
                self.B[state, wordIndex] += 1

        for i in range(self.N):
            maxValue = self.B[i, ].sum()
            if(maxValue == 0):
                maxValue = 1
            self.B[i, ] /= maxValue

        return self.B



def viterbi(hmm:HMM, sequence: List[int]) -> List[int]:
    """ 把每个状态当成一个结点,结点的权重就是发射概率,结点的边就是转移概率,
        目标是找出具有最大概率的状态序列。
        我们可以使用动态规划的思想求解,即该问题具有最优子解构:
            如果长度为N的状态序列y1y2...yn是最优的,那么长度为N-1的状态序列y1y2..y(n-1)也必然是最优的!
        :param hmm  隐式马尔可夫对象
        :param sequence 输入序列
        :return 状态序列
     """

    # 处理特殊情况
    if(hmm == None or len(sequence) == 0):
        return []

    # 算法需要的数据结构
    scores = np.zeros(hmm.N, dtype=float)
    path = np.zeros((len(sequence), hmm.N), dtype=np.int)
    PI = hmm.getPI()
    A = hmm.getA()
    B = hmm.getB()

    # 算法开始
    for i in range(len(scores)):
        scores[i] = PI[i] * B[i, sequence[0]]

    for i in range(1, len(sequence)):
        scoresTemp = np.zeros(scores.shape)
        for s2 in range(hmm.N):         #后一个状态
            for s1 in range(hmm.N):     #前一个状态
                scoreCur = scores[s1] *  A[s1, s2] * B[s2, sequence[i]]
                if(scoreCur >= scoresTemp[s2]):
                    scoresTemp[s2] = scoreCur
                    path[i, s2] = s1
        scores = scoresTemp

    # 解最优路径
    bestEndStateIndex = scores.argmax()
    bestPath = [bestEndStateIndex]
    for i in range(1, len(sequence)):
        lastState = path[len(sequence) - i][bestEndStateIndex]
        bestPath.append(lastState)
        bestEndStateIndex = lastState

    bestPath.reverse()

    return bestPath

测试

以下代码对句子中的英文和数字进行了自动标注从而生成训练样本 (x, y) 输入到HMM模型中。详情见代码中的注释。

from HMM import HMM, TextSequence, viterbi

def is_alphabet(uchar):
    """判断一个unicode是否是英文字母"""
    if (uchar >= u'\u0041' and uchar <= u'\u005a') or (uchar >= u'\u0061' and uchar <= u'\u007a'):
        return True
    else:
        return False

def is_number(uchar):
    """判断一个unicode是否是数字"""
    if uchar >= u'\u0030' and uchar <= u'\u0039':
        return True
    else:
        return False

if __name__ == '__main__':

    x = [
        list("维特比算法由安德鲁·维特比(AndrewViterbi)于1967年提出"),
        list("用于在数字通信链路中解卷积以消除噪音"),
        list("此算法被广泛应用于CDMA和GSM数字蜂窝网络"),
        list("拨号调制解调器、卫星、深空通信和802"),
        list("现今也被常常用于dsafdsaf语音识别、关键字识别、计算语言学和生物信息学中"),
        list("例如在语音(语音识别)"),
        list("中,声音信号作为456观察到的事件序列"),
        list("而文本字符串,被看作是asbs隐含的产生声音信号的原因"),
        list("因此可对声音AAAAA信号应用维特比算法寻找最有可能的文本字符串"),
    ]


    y = []

    """
        使用BIO标注法对数字英文数据标注
        0 = O  表示非数字英文
        1 = B  表示数字英文的开始
        2 = I  表示数字英文的中间部分
    """
    for i in range(len(x)):
        temp = []
        isBegin = True
        for j in range(len(x[i])):
            if is_alphabet(x[i][j]) or is_number(x[i][j]):
                if(isBegin):
                    temp.append(1)
                else:
                    temp.append(2)
                isBegin = False
            else:
                temp.append(0)
                isBegin = True
        y.append(temp)

    print("序列标签为:")
    print(y)

    ### 测试算法
    hmm = TextSequence(x, y, 3)
    states = viterbi(hmm, hmm.getIndex(list("法由安德鲁·维特比(Andreterbi)于1967年提出")))
    print("预测结果为:")
    print(states)

在这里插入图片描述
可以发现,算法预测的序列标签结果与实际是一致的。这里直接利用x中的数据进行测试了,是因为数据集样本数量太少,如果使用其它文本,则会有太多OOV(out of vocabulary) 从而导致预测不准确。

下面是我后来在真实数据集上跑的例子

首先原HMM代码需要进行小量修改:

  • 对A、B、 π \pi π统计完之后进行加1平滑防止零概率的情况
  • 进行viterbi算法解码时,使用对数化后的值进行比较,以防止小概率连乘导致数值计算越界
    修改后的代码如下:
from typing import *
import numpy as np

class HMM():

    def __init__(self, N: int, M: int):
        self.N = N
        self.M = M
        self.PI = np.ones((N, ), dtype=np.float)  #加1平滑
        self.A = np.ones((N, N), dtype=np.float)  #加1平滑
        self.B = np.ones((N, M), dtype=np.float)  #加1平滑

    def getPI(self) -> np.ndarray:
        """ 获取初始状态概率 R ^ N"""
        raise NotImplementedError()

    def getA(self) -> np.ndarray:
        """ 获取状态转移概率 R ^ (N * N)"""
        raise NotImplementedError()

    def getB(self) -> np.ndarray:
        """ 获取发射概率 R ^ (N * M) """
        raise NotImplementedError()


class TextSequence(HMM):
    def __init__(self, texts: List[List[str]], labels: List[List[int]], nClasses:int):
        self.texts = texts
        self.labels = labels

        # 构造词典
        dictSet = {"[OOV]": 0}
        dictArray = ["[OOV]"]
        for seq in texts:
            for word in seq:
                w = dictSet.get(word)
                if w == None:  # 如果当前词不在词典之中
                    dictSet[word] = len(dictArray)
                    dictArray.append(word)

        self.dict = (dictSet)
        self.dictArray = (dictArray)

        # 构造矩阵
        HMM.__init__(self, nClasses, len(dictArray))

    def getIndex(self, text :List[str]) -> List[int]:
        """把序列转换成索引"""
        indexes = []
        for word in text:
            index = self.dict.get(word)
            if(index == None):
                indexes.append(0)
            else:
                indexes.append(index)
        return indexes


    def getPI(self) -> np.ndarray:
        self.PI.fill(1)

        for labelSeq in self.labels:
            if(len(labelSeq) > 0):
                self.PI[labelSeq[0]] += 1

        self.PI /= self.PI.sum()

        return self.PI


    def getA(self) -> np.ndarray:
        self.A.fill(1)

        for labelSeq in self.labels:
            if(len(labelSeq) > 1):
                for i in range(1, len(labelSeq)):
                    labelPre = labelSeq[i - 1]
                    lableCur = labelSeq[i]
                    self.A[labelPre, lableCur] += 1

        #对每行进行归一化
        for i in range(self.N):
            maxValue = self.A[i,].sum()
            if(maxValue == 0):
                maxValue = 1
            self.A[i, ] /= maxValue

        return self.A


    def getB(self) -> np.ndarray:
        self.B.fill(1)
        if(len(self.texts) != len(self.labels)):
            raise RuntimeError("输入句子序列长度与输入标签序列长度不一致")

        for i in range(len(self.texts)):
            if (len(self.texts[i]) != len(self.labels[i])):
                raise RuntimeError("输入句子长度与标签长度不一致")
            for j in range(len(self.texts[i])) :
                state = self.labels[i][j]
                word = self.texts[i][j]
                wordIndex = self.dict[word]
                self.B[state, wordIndex] += 1

        for i in range(self.N):
            maxValue = self.B[i, ].sum()
            if(maxValue == 0):
                maxValue = 1
            self.B[i, ] /= maxValue

        return self.B



def viterbi(hmm:HMM, sequence: List[int]) -> List[int]:
    """ 把每个状态当成一个结点,结点的权重就是发射概率,结点的边就是转移概率,
        目标是找出具有最大概率的状态序列。
        我们可以使用动态规划的思想求解,即该问题具有最优子解构:
            如果长度为N的状态序列y1y2...yn是最优的,那么长度为N-1的状态序列y1y2..y(n-1)也必然是最优的!
        :param hmm  隐式马尔可夫对象
        :param sequence 输入序列
        :return 状态序列
     """

    # 处理特殊情况
    if(hmm == None or len(sequence) == 0):
        return []

    # 算法需要的数据结构
    scores = np.zeros(hmm.N, dtype=float)
    path = np.zeros((len(sequence), hmm.N), dtype=np.int)
    PI = hmm.getPI()
    A = hmm.getA()
    B = hmm.getB()

    # 算法开始
    # 使用对数,防止连乘越界
    for i in range(len(scores)):
        scores[i] = np.log(PI[i]) + np.log(B[i, sequence[0]])

    for i in range(1, len(sequence)):
        scoresTemp = np.ones(scores.shape) * (- np.inf)   #初始化为负无穷
        for s2 in range(hmm.N):         #后一个状态
            for s1 in range(hmm.N):     #前一个状态
                scoreCur = scores[s1] + np.log(A[s1, s2])  + np.log(B[s2, sequence[i]])
                if(scoreCur >= scoresTemp[s2]):
                    scoresTemp[s2] = scoreCur
                    path[i, s2] = s1
        scores = scoresTemp

    # 解最优路径
    bestEndStateIndex = scores.argmax()
    bestPath = [bestEndStateIndex]
    for i in range(1, len(sequence)):
        lastState = path[len(sequence) - i][bestEndStateIndex]
        bestPath.append(lastState)
        bestEndStateIndex = lastState

    bestPath.reverse()

    return bestPath

在真实中文词性标注语料上的测试结果:

在这里插入图片描述

  • 7
    点赞
  • 95
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值