用Python自己写一个分词器,python实现分词功能,隐马尔科夫模型预测问题之维特比算法(Viterbi Algorithm)的Python实现

news2024/9/25 11:20:35

 ☕️ 本文系列文章汇总:

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

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

         代码实现 

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

          代码实现
(4)  HMM预测问题:维特比算法
本篇算法原理分析及公式推导请参考: HMM预测问题:维特比算法

目录

1. 模型参数估计

2. 维特比实现

3. 完整代码Github

4. 实例


事实上维特比算法属于隐马尔科夫模型的“应用篇”,特别是在NLP的分词领域,维特比算法无处不在。我们先需要根据HMM的学习算法来学习得到一个模型λ=(π,A,B),然后再通过这个模型,利用维特比算法对数据进行预测。本篇基于维特比算法实现一个简单的分词器,有助于大家深入理解。


1. 模型参数估计

我们先通过训练集来估计出一个模型。训练集是一堆已经分好词的文本,一行一条训练样本。在训练集中,我们的观测数据是每一个字,我们的状态是每一个字对应的分词标志,一共有4种状态:S,表示单字成词;B,表示一个分出来的词的起始字;M,表示一个分出来的词的中间字;E,表示一个分出来的词的结尾字。例如:

|什么|难过|,|只不过||一次|错过

S|BE|BE|S|BME|S|BE|BE

注意,由于我们的训练集包含了事实上包含了观测值和状态值,因此我们不需要用无监督的Baum Welch算法来学习模型,只需要简单的有监督统计方法来估计模型参数即可,这个思想主要用到《统计学习方法》中10.3.1节中提到的方法。

class Model:
    def __init__(self, trainfile, N, M, Q):
        self.trainfile = trainfile
        self.N = N
        self.M = M
        self.Pi = np.zeros(N)
        self.A = np.zeros((N, N))
        self.B = np.zeros((N, M))
        self.Q2id = {x: i for i, x in enumerate(Q)}

    def cal_rate(self):
        reader = dataloader(self.trainfile)
        for i, line in enumerate(reader):
            line = line.strip().strip('\n')
            if not line:
                continue
            word_list = line.split(' ')
            status_sequence = []
            # 计算π和B中每个元素的频数
            for j, item in enumerate(word_list):
                if len(item) == 1:
                    flag = 'S'
                else:
                    flag = 'B' + 'M' * (len(item) - 2) + 'E'
                if j == 0:
                    self.Pi[self.Q2id[flag[0]]] += 1
                for t, s in enumerate(flag):
                    self.B[self.Q2id[s]][ord(item[t])] += 1
                status_sequence.extend(flag)
            # 计算A元素的频数
            for t, s in enumerate(status_sequence):
                prev = status_sequence[t - 1]
                self.A[self.Q2id[prev]][self.Q2id[s]] += 1

    def generate_model(self):
        """
        根据课本10.3.1介绍的方法,将频数参数矩阵π、A、B转换成频率参数矩阵,
        并对每一个元素取log,将后续的乘法运算转成加法运算,方便计算。
        """
        self.cal_rate()
        norm = -2.718e+16
        denominator = sum(self.Pi)
        # 处理π
        for i, pi in enumerate(self.Pi):
            if pi == 0.:
                self.Pi[i] = norm
            else:
                self.Pi[i] = np.log(pi / denominator)
        # 处理A
        for row in range(self.A.shape[0]):
            denominator = sum(self.A[row])
            for col, a in enumerate(self.A[row]):
                if a == 0.:
                    self.A[row][col] = norm
                else:
                    self.A[row][col] = np.log(a / denominator)
        # 处理B
        for row in range(self.B.shape[0]):
            denominator = sum(self.B[row])
            for col, b in enumerate(self.B[row]):
                if b == 0.:
                    self.B[row][col] = norm
                else:
                    self.B[row][col] = np.log(b / denominator)
        return AttrDict(
            pi=self.Pi,
            A=self.A,
            B=self.B
        )

2. 维特比实现

这一部分的代码完全是按照课本中算法流程【10.5】中的步骤来的,注意矩阵的运算正确即可。

class Viterbi:
    def __init__(self, model: dict):
        self.pi = model.pi
        self.A = model.A
        self.B = model.B

    def predict(self, datapath):
        """
        根据算法10.5中的流程计算δ和ψ
        """
        reader = dataloader(datapath)
        self.O = [line.strip().strip('\n') for line in reader]
        N = self.pi.shape[0]
        self.segs = []
        for o in self.O:
            o = [w for w in o if w]
            if not o:
                self.segs.append([])
                continue
            T = len(o)
            delta_t = np.zeros((T, N))
            psi_t = np.zeros((T, N))
            for t in range(T):
                if not t:
                    delta_t[t][:] = self.pi + self.B.T[:][ord(o[0])]  # 由于log转换,所以原先的*变成+
                    psi_t[t][:] = np.zeros((1, N))
                else:
                    deltaTemp = delta_t[t - 1] + self.A.T
                    for i in range(N):
                        delta_t[t][i] = max(deltaTemp[:][i]) + self.B[i][ord(o[t])]
                        psi_t[t][i] = np.argmax(deltaTemp[:][i])
            I = []
            """
            这里是回溯的过程,目的在于找最优路径,记得最后的路径需要反转,因为我们是从T时刻往前
            找的。
            """
            maxNode = np.argmax(delta_t[-1][:])
            I.append(int(maxNode))
            for t in range(T - 1, 0, -1):
                maxNode = int(psi_t[t][maxNode])
                I.append(maxNode)
            I.reverse()
            self.segs.append(I)

    def segment(self):
        """
        得到最优路径状态序列后,我们就可以根据状态序列对句子进行分割了
        """
        segments = []
        for i, line in enumerate(self.segs):
            curText = ""
            temp = []
            for j, w in enumerate(line):
                if w == 0:
                    temp.append(self.O[i][j])
                else:
                    if w != 3:
                        curText += self.O[i][j]
                    else:
                        curText += self.O[i][j]
                        temp.append(curText)
                        curText = ''
            segments.append(temp)
        return segments

3. 完整代码Github

import numpy as np


class AttrDict(dict):
    # 一个小trick,将结果返回成一个字典格式
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self


def dataloader(datapath):
    with open(datapath, 'r') as reader:
        for line in reader:
            yield line


class Model:
    def __init__(self, trainfile, N, M, Q):
        self.trainfile = trainfile
        self.N = N
        self.M = M
        self.Pi = np.zeros(N)
        self.A = np.zeros((N, N))
        self.B = np.zeros((N, M))
        self.Q2id = {x: i for i, x in enumerate(Q)}

    def cal_rate(self):
        reader = dataloader(self.trainfile)
        for i, line in enumerate(reader):
            line = line.strip().strip('\n')
            if not line:
                continue
            word_list = line.split(' ')
            status_sequence = []
            # 计算π和B中每个元素的频数
            for j, item in enumerate(word_list):
                if len(item) == 1:
                    flag = 'S'
                else:
                    flag = 'B' + 'M' * (len(item) - 2) + 'E'
                if j == 0:
                    self.Pi[self.Q2id[flag[0]]] += 1
                for t, s in enumerate(flag):
                    self.B[self.Q2id[s]][ord(item[t])] += 1
                status_sequence.extend(flag)
            # 计算A元素的频数
            for t, s in enumerate(status_sequence):
                prev = status_sequence[t - 1]
                self.A[self.Q2id[prev]][self.Q2id[s]] += 1

    def generate_model(self):
        self.cal_rate()
        norm = -2.718e+16
        denominator = sum(self.Pi)
        for i, pi in enumerate(self.Pi):
            if pi == 0.:
                self.Pi[i] = norm
            else:
                self.Pi[i] = np.log(pi / denominator)

        for row in range(self.A.shape[0]):
            denominator = sum(self.A[row])
            for col, a in enumerate(self.A[row]):
                if a == 0.:
                    self.A[row][col] = norm
                else:
                    self.A[row][col] = np.log(a / denominator)

        for row in range(self.B.shape[0]):
            denominator = sum(self.B[row])
            for col, b in enumerate(self.B[row]):
                if b == 0.:
                    self.B[row][col] = norm
                else:
                    self.B[row][col] = np.log(b / denominator)
        return AttrDict(
            pi=self.Pi,
            A=self.A,
            B=self.B
        )


class Viterbi:
    def __init__(self, model: dict):
        self.pi = model.pi
        self.A = model.A
        self.B = model.B

    def predict(self, datapath):
        reader = dataloader(datapath)
        self.O = [line.strip().strip('\n') for line in reader]
        N = self.pi.shape[0]
        self.segs = []
        for o in self.O:
            o = [w for w in o if w]
            if not o:
                self.segs.append([])
                continue
            T = len(o)
            delta_t = np.zeros((T, N))
            psi_t = np.zeros((T, N))
            for t in range(T):
                if not t:
                    delta_t[t][:] = self.pi + self.B.T[:][ord(o[0])]  # 由于log转换,所以原先的*变成+
                    psi_t[t][:] = np.zeros((1, N))
                else:
                    deltaTemp = delta_t[t - 1] + self.A.T
                    for i in range(N):
                        delta_t[t][i] = max(deltaTemp[:][i]) + self.B[i][ord(o[t])]
                        psi_t[t][i] = np.argmax(deltaTemp[:][i])
            I = []
            maxNode = np.argmax(delta_t[-1][:])
            I.append(int(maxNode))
            for t in range(T - 1, 0, -1):
                maxNode = int(psi_t[t][maxNode])
                I.append(maxNode)
            I.reverse()
            self.segs.append(I)

    def segment(self):
        segments = []
        for i, line in enumerate(self.segs):
            curText = ""
            temp = []
            for j, w in enumerate(line):
                if w == 0:
                    temp.append(self.O[i][j])
                else:
                    if w != 3:
                        curText += self.O[i][j]
                    else:
                        curText += self.O[i][j]
                        temp.append(curText)
                        curText = ''
            segments.append(temp)
        return segments

4. 实例

if __name__ == '__main__':
    # 因为我们的观测值是用编码来表示汉字的,这里设置观测值为65536就是为了能最大限度覆盖所有可能出
    # 现的汉字。
    trainer = Model(N=4, M=65536, Q=['S', 'B', 'M', 'E'], trainfile='train.txt')
    model = trainer.generate_model()
    segment = Viterbi(model)
    segment.predict('test.txt')
    print(segment.segment())

我们的训练集大概长这样:

 给一条测试数据:

分词后:

[['他', '强调', ',', '党校', '始终', '不', '变', '的', '初心', '就', '是', '为', '党育', '才', '、', '为', '党', '献策', '。', '各级', '党校', '要', '坚守', '这个', '初心', ',锐', '意', '进', '取', '、', '奋发', '有', '为', ',', '为', '全', '面建', '设社', '会', '主义现', '代化国', '家', '、', '全面', '推进', '中华', '民族', '伟大', '复兴', '作', '出', '新', '的', '贡献', '。']]

可以看出,这是一般非常粗糙的分词器,虽然有些词分的不准,但是总体上还是可以的。由于我们的模型参数估计方法不是自发的学习过程,所以对于语料的依赖特别强,语料中没见过的词,就可能分错。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/382467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】关于二叉树你所应该知道的数学秘密

目录 1.什么是二叉树(可以跳过 目录跳转) 2.特殊的二叉树(满二叉树/完全二叉树) 2.1 基础知识 2.2 满二叉树 2.3 完全二叉树 3.二叉树的数学奥秘(主体) 3.1 高度与节点个数 3.2* 度 4.运用二叉树的…

算法拾遗二十五之暴力递归到动态规划五

算法拾遗二十七之暴力递归到动态规划七题目一【数组累加和最小的】题目二什么暴力递归可以继续优化暴力递归和动态规划的关系面试题和动态规划的关系如何找到某个问题的动态规划方式面试中设计暴力递归的原则知道了暴力递归的原则 然后设计常见的四种尝试模型如何分析有没有重复…

力扣-丢失信息的雇员

大家好,我是空空star,本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目:1965. 丢失信息的雇员二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.正确示范④提交SQL运行结果5.其他…

SpringBoot高级-Condition相关操作

01-SpringBoot高级-今日内容 SpringBoot自定配置SpringBoot事件监听SpringBoot流程分析SpringBoot监控SpringBoot部署 02-SpringBoot自动配置-Condition-1 Condition是Spring4.0后引入的条件化配置接口,通过实现Condition接口可以完成有条件的加载相应的Bean Co…

移动架构43_什么是Jetpack

Android移动架构汇总​​​​​​​ 文章目录一 Android 开发框架演变1 MVC2 MVP3 MVVM二 什么是JetPack三 如何构建支持Jetpack项目一 Android 开发框架演变 1 MVC Model-View-Controller,模型-视图-控制器,Model负责数据管理,View负责UI显…

创建Vite+Vue3+TS基础项目

前言: 本篇内容不涉及插件的安装以及配置,具体安装及配置篇可以看下面目录,本篇只涉及创建ViteVue3TS基础项目相关内容。不讲废话,简单直接直接开撸。 目录 npm create vite vue3练习2 -- --template vue-ts npm i vue-rout…

线性表 顺序表数组

初识线性表 文章目录初识线性表线性表的类型定义基本操作(一)init,destory,clear基本操作(二) 判空 ,求长基本操作(三)取值,取位置基本操作(四&am…

图解LeetCode——剑指 Offer 12. 矩阵中的路径

一、题目 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相…

Solon2 的应用生命周期

Solon 框架的应用生命周期包括:一个初始化函数时机点 六个事件时机点 两个插件生命时机点 两个容器生命时机点(v2.2.0 版本的状态): 提醒: 启动过程完成后,项目才能正常运行(启动过程中&…

基于麻雀算法改进的BP神经网络客流量预测,SSA-BP

目录 背影 BP神经网络的原理 BP神经网络的定义 BP神经网络的基本结构 BP神经网络的神经元 BP神经网络的激活函数, BP神经网络的传递函数 麻雀算法原理 麻雀算法主要参数 麻雀算法流程图 麻雀算法优化测试函数代码 基于麻雀算法改进的BP神经网络坑基监测 数据 matlab…

Windows 11 安装 Docker Desktop

Windows 环境安装 WSL2 WSL 简介 WSL 全称是 Windows Subsystem for Linux ,适用于 Linux 的 Windows 子系统,可让开发人员按原样运行 GNU/Linux 环境,包括大多数命令行工具、实用工具和应用程序,且不会产生传统虚拟机或双启动设…

低线城市外卖市场逐渐下沉,创业者如何有效开展本地外卖平台

近年来,我国网上外卖营业额不断上升,使我国餐饮业的比重越来越高。 随着外卖市场的下沉,低线城市的用户开始使用外卖平台 由于我国在线外卖行业逐渐成熟,一、二线主流市场逐渐饱和,外卖行业逐渐开始向低线城市发展&…

非标自动化设备远程监控解决方案

为了实现企业自动化生产,提高工作效率和稳定性,需对整个工厂进行远程监控和管理。 在工厂建立了一个远程监控系统,可以实现对工业自动化的设备状态进行远程实时监控,同时可以利用无线网络技术来实现对设备的数据采集和远程管理。 …

Guava ——Joiner和Splitter

大家好,这里是一口八宝周👏欢迎来到我的博客❤️一起交流学习 文章中有需要改进的地方请大佬们多多指点 谢谢🙏在本篇文章中,我将用实例展示,如何使用Joiner将集合转换为 String ,以及使用Splitter将 Strin…

如何使用EvilTree在文件中搜索正则或关键字匹配的内容

关于EvilTree EvilTree是一款功能强大的文件内容搜索工具,该工具基于经典的“tree”命令实现其功能,本质上来说它就是“tree”命令的一个独立Python 3重制版。但EvilTree还增加了在文件中搜索用户提供的关键字或正则表达式的额外功能,而且还…

Android性能优化系列篇:弱网优化

弱网优化1、Serializable原理通常我们使用Java的序列化与反序列化时,只需要将类实现Serializable接口即可,剩下的事情就交给了jdk。今天我们就来探究一下,Java序列化是怎么实现的,然后探讨一下几个常见的集合类,他们是…

sklearn中的数据预处理和特征工程

目录 一.数据挖掘的五大流程 1. 获取数据 2. 数据预处理 3. 特征工程: 4. 建模,测试模型并预测出结果 5. 上线,验证模型效果 二.数据预处理 1.数据无量纲化 2.preprocessing.MinMaxScaler(数据归一化) 3.preprocessing.Standard…

Linux编译器——gcc/g++(预处理、编译、汇编、链接)

目录 0.程序实现的两大环境 1.gcc如何完成 预处理 编译 汇编 链接 2.动态库与静态库 对比二者生成的文件大小 3. gcc常用选项 0.程序实现的两大环境 任何一个C程序的实现都要经过翻译环境与执行环境。 在翻译环境中又分为4个部分,预编译、编译、汇编与链…

Spring Cloud配置application.yml与bootstrap.yml区别及多profile配置 | Spring Cloud 6

一、前言 Spring Cloud 构建于 Spring Boot 之上,在 Spring Boot 中有两种上下文,一种是 bootstrap,另外一种是 application。 二、bootstrap与application (.yml/.properties) 2.1 两者区别 bootstrap.yml/bootstrap.properties 和 appl…

CHAPTER 3 Web HA集群部署 - Keepalived

Web HA集群部署 - Keepalived1. Keepalived概述1.1 工作原理1.2 核心功能1.3 拓扑图2. KeepAlived安装方式2.1 yum源安装2.2 源码包编译3. KeepAlived安装3.1 环境依赖3.2 安装nginx3.3 安装Keepalived4. Keepalived部署4.1 主备模式1. 节点配置2. 主节点配置文件3. 从节点配置…