隐马尔科夫模型HMM之前后向算法Python代码实现,包括2个优化版本

☕️ 本文系列文章汇总:

(1)HMM开篇:基本概念和几个要素

(2)HMM计算问题:前后向算法

(3)HMM学习问题:Baum-Welch算法

(4)  HMM预测问题:维特比算法
本篇算法原理分析及公式推导请参考: HMM计算问题:前后向算法

之前发布的四篇隐马尔科夫模型系列学习博文,将隐马尔科夫模型包含的各个算法的原理及公式都详细介绍了,所谓无代码实现无以成体系,那么接下来我会带大家把算法用代码实现一遍,语言主要为python。本篇先来对前后向算法进行coding~

1. 参数初始化:

        """
        初始化模型参数
        :param pi: 初始状态概率向量
        :param A: 已学习得到的状态转移概率矩阵,这里直接引用课本中的例子10.2
        :param B: 已学习得到的概率矩阵,这里直接引用课本中的例子10.2
        :param V: 已知的观测集合,同样使用例子中的值
        """
        self.pi = pi
        self.A = A
        self.B = B
        self.V = V

2. 根据原理公式,定义前向算法计算过程:

    def forward(self, O):
        """
        前向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        alpha_t_plus_1 = np.zeros((row, col), dtype=float)
        for t, o in enumerate(O):
            if t == 0:
                # 初值α 公式10.15
                for i, p in enumerate(self.pi):
                    obj_index = self.V.index(o)
                    alpha_t_plus_1[t][i] = p * self.B[i][obj_index]
            else:
                # 递推 公式10.16
                for i in range(self.A.shape[0]):
                    alpha_ji = 0.
                    # 公式10.16里中括号的内容
                    for j, a in enumerate(alpha_t_plus_1[t-1]):
                        alpha_ji += (a * self.A[j][i])
                    obj_index = self.V.index(o)
                    # 公式10.16
                    alpha_t_plus_1[t][i] = alpha_ji * self.B[i][obj_index]

        return alpha_t_plus_1

该方法主要输出的是前向算法中alpha的中间结果,即原理中提到的\alpha_{1}(i), \alpha_{2}(i), \alpha_{3}(i), ...,\alpha_{T}(i) 。

3. 定义后向算法计算过程:

    def backward(self, O):
        """
        后向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        betaT = np.zeros((row+1, col), dtype=float)

        for t, o in enumerate(O[::-1]):
            if t == 0:
                # 初值β 公式10.19
                betaT[t][:] = [1] * self.A.shape[0]
                continue
            else:
                # 反向递推 公式10.20
                for i in range(self.A.shape[0]):
                    beta_t = 0.
                    obj_index = self.V.index(O[t - 1])
                    for j, b in enumerate(betaT[t-1]):
                        beta_t += (self.A[i][j] * self.B[j][obj_index] * b)
                    betaT[t][i] = beta_t
        betaT[-1][:] = [self.pi[i] * self.B[i][self.V.index(O[0])] * betaT[-2][i] for i in range(self.A.shape[0])]
        return betaT

 该方法主要输出的是前向算法中beta的中间结果,即原理中提到的\beta_{1}(i), \beta_{2}(i), \beta_{3}(i), ...,\beta_{T}(i) 。

4. 定义概率计算函数:

    def cal_prob(self, O, opt):
        if opt == 'f':
            metrix = self.forward(O)
            # 计算前向算法P(O|λ) 公式10.17
            return sum(metrix[-1])
        elif opt == 'b':
            # 计算后向算法P(O|λ) 公式10.21
            metrix = self.backward(O)
            return sum(metrix[-1])

 将上述方法封装成一个类:

import numpy as np

class FB:
    def __init__(self, pi, A, B, V):
        """
        初始化模型参数
        :param pi: 初始状态概率向量
        :param A: 已学习得到的状态转移概率矩阵,这里直接引用课本中的例子10.2
        :param B: 已学习得到的概率矩阵,这里直接引用课本中的例子10.2
        :param V: 已知的观测集合,同样使用例子中的值
        """
        self.pi = pi
        self.A = A
        self.B = B
        self.V = V

    def cal_prob(self, O, opt):
        if opt == 'f':
            metrix = self.forward(O)
            # 计算P(O|λ) 公式10.17
            return sum(metrix[-1])
        elif opt == 'b':
            # 计算P(O|λ) 公式10.21
            metrix = self.backward(O)
            return sum(metrix[-1])


    def forward(self, O):
        """
        前向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        alpha_t_plus_1 = np.zeros((row, col), dtype=float)
        for t, o in enumerate(O):
            if t == 0:
                # 初值α 公式10.15
                for i, p in enumerate(self.pi):
                    obj_index = self.V.index(o)
                    alpha_t_plus_1[t][i] = p * self.B[i][obj_index]
            else:
                # 递推 公式10.16
                for i in range(self.A.shape[0]):
                    alpha_ji = 0.
                    # 公式10.16里中括号的内容
                    for j, a in enumerate(alpha_t_plus_1[t-1]):
                        alpha_ji += (a * self.A[j][i])
                    obj_index = self.V.index(o)
                    # 公式10.16
                    alpha_t_plus_1[t][i] = alpha_ji * self.B[i][obj_index]

        return alpha_t_plus_1

    def backward(self, O):
        """
        后向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        betaT = np.zeros((row+1, col), dtype=float)

        for t, o in enumerate(O[::-1]):
            if t == 0:
                # 初值β 公式10.19
                betaT[t][:] = [1] * self.A.shape[0]
                continue
            else:
                # 反向递推 公式10.20
                for i in range(self.A.shape[0]):
                    beta_t = 0.
                    obj_index = self.V.index(O[t - 1])
                    for j, b in enumerate(betaT[t-1]):
                        beta_t += (self.A[i][j] * self.B[j][obj_index] * b)
                    betaT[t][i] = beta_t
        betaT[-1][:] = [self.pi[i] * self.B[i][self.V.index(O[0])] * betaT[-2][i] for i in range(self.A.shape[0])]
        return betaT

 用书上的例子运行一下:

if __name__ == '__main__':
    from time import time
    # 课本例子10.2
    pi = [0.2, 0.4, 0.4]
    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]
    ])
    O = ['红', '白', '红']
    f = FB(pi=pi, A=a, B=b, V=['红', '白'])
    # start = time()
    resf = f.forward(O)
    resb = f.backward(O)
    # print(time()-start)
    print('α:{}\n前向算法的概率计算结果:{}'.format(resf, f.cal_prob(O, opt='f')))
    print('β:{}\n后向算法的概率计算结果:{}:'.format(resb, f.cal_prob(O, opt='b')))

 运行结果:

隐马尔科夫模型HMM之前后向算法Python代码实现,包括2个优化版本_第1张图片

另外我们注意到上面的代码中,嵌套了三层for循环,当数据量很大的时候,这样计算很耗时,那么我们来优化一下:

优化版一:

我们注意到,内层for循环主要是为了矩阵计算的,所以我们其实可以直接利用numpy来进行矩阵的运算。

import numpy as np

class FB:
    def __init__(self, pi, A, B, V):
        """
        初始化模型参数
        :param pi: 初始状态概率向量
        :param A: 已学习得到的状态转移概率矩阵,这里直接引用课本中的例子10.2
        :param B: 已学习得到的概率矩阵,这里直接引用课本中的例子10.2
        :param V: 已知的观测集合,同样使用例子中的值
        """
        self.pi = np.array(pi)
        self.A = np.array(A)
        self.B = np.array(B)
        self.V = V

    def cal_prob(self, O, opt):
        if opt == 'f':
            metrix = self.forward(O)
            # 计算P(O|λ) 公式10.17
            return sum(metrix[-1])
        elif opt == 'b':
            # 计算P(O|λ) 公式10.21
            metrix = self.backward(O)
            return sum(metrix[-1])


    def forward(self, O):
        """
        前向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        alpha_t_plus_1 = np.zeros((row, col), dtype=float)
        obj_index = self.V.index(O[0])
        # 初值α 公式10.15
        alpha_t_plus_1[0][:] = self.pi * self.B[:].T[obj_index]
        for t, o in enumerate(O[1:]):
            t += 1
            # 递推 公式10.16
            obj_index = self.V.index(o)
            for i in range(self.A.shape[0]):
                # 公式10.16
                alpha_ji = alpha_t_plus_1[t-1][:] @ self.A[:].T[i]
                alpha_t_plus_1[t][i] = alpha_ji * self.B[i][obj_index]

        return alpha_t_plus_1

    def backward(self, O):
        """
        后向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        betaT = np.zeros((row+1, col), dtype=float)
        # 初值β 公式10.19
        betaT[0][:] = [1] * self.A.shape[0]
        for t, o in enumerate(O[::-1][1:]):
            t += 1
            # 反向递推 公式10.20
            obj_index = self.V.index(O[t-1])
            for i in range(self.A.shape[0]):
                beta_t = self.A[i][:] * self.B[:].T[obj_index] @ betaT[t-1][:].T
                betaT[t][i] = beta_t
        betaT[-1][:] = [self.pi[i] * self.B[i][self.V.index(O[0])] * betaT[-2][i] for i in range(self.A.shape[0])]
        return betaT

优化版二:

优化版一中,只省略了最内层循环,即每个状态的每一列。其实近一步观察发现,对于每一行的for循环也可以直接用矩阵计算的方式省略

import numpy as np

class FB:
    def __init__(self, pi, A, B, V):
        """
        初始化模型参数
        :param pi: 初始状态概率向量
        :param A: 已学习得到的状态转移概率矩阵,这里直接引用课本中的例子10.2
        :param B: 已学习得到的概率矩阵,这里直接引用课本中的例子10.2
        :param V: 已知的观测集合,同样使用例子中的值
        """
        self.pi = np.array(pi)
        self.A = np.array(A)
        self.B = np.array(B)
        self.V = V

    def cal_prob(self, O, opt):
        if opt == 'f':
            metrix = self.forward(O)
            # 计算P(O|λ) 公式10.17
            return sum(metrix[-1])
        elif opt == 'b':
            # 计算P(O|λ) 公式10.21
            metrix = self.backward(O)
            return sum(metrix[-1])


    def forward(self, O):
        """
        前向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        alpha_t_plus_1 = np.zeros((row, col), dtype=float)
        obj_index = self.V.index(O[0])
        # 初值α 公式10.15
        alpha_t_plus_1[0][:] = self.pi * self.B[:].T[obj_index]
        for t, o in enumerate(O[1:]):
            t += 1
            # 递推 公式10.16
            obj_index = self.V.index(o)
            alpha_ji = alpha_t_plus_1[t - 1][:].T @ self.A
            alpha_t_plus_1[t][:] = alpha_ji * self.B[:].T[obj_index]

        return alpha_t_plus_1

    def backward(self, O):
        """
        后向算法
        :param O: 已知的观测序列
        :return: P(O|λ)
        """
        row, col = len(O), self.A.shape[0]
        betaT = np.zeros((row+1, col), dtype=float)
        # 初值β 公式10.19
        betaT[0][:] = [1] * self.A.shape[0]
        for t, o in enumerate(O[::-1][1:]):
            t += 1
            # 反向递推 公式10.20
            obj_index = self.V.index(O[t-1])
            beta_t = self.A * self.B[:].T[obj_index] @ betaT[t-1][:].T
            betaT[t][:] = beta_t
        betaT[-1][:] = [self.pi[i] * self.B[i][self.V.index(O[0])] * betaT[-2][i] for i in range(self.A.shape[0])]
        return betaT

好啦,就是这么一步步递进优化,方法论就是,先用基本的代码将原理实现,帮助理解,然后发现竟然有3个for循环,复杂度且不说,代码首先就不美观,看看有没有可以优化的地方,发现for循环的服务对象是矩阵计算,所以,很自然的想到直接利用矩阵计算的方法一步到位。代码已经放到GitHub上了,我将会持续更新其它算法的实现。

你可能感兴趣的:(大道至简系列,#,机器学习算法系列,技术实战,算法,人工智能,机器学习,自然语言处理,python)