HMM和viterbi算法初步实践-----中文分词

article/2025/9/12 9:24:47

马尔科夫性质:当一个随机过程在给定现在状态及所有过去状态情况下,其未来状态的条件概率分布仅依赖于当前状态。换句话说,在给定现在状态时,它与过去状态(即该过程的历史路径)是条件独立的(也就是没有任何的关系),那么此随机过程即具有马尔可夫性质。具有马尔可夫性质的过程通常称之为马尔可夫过程。

马尔科夫链:状态空间中经过从一个状态到另一个状态的转换的随机过程。该过程要求具备“无记忆”的性质:下一状态的概率分布只能由当前状态决定,在时间序列中它前面的事件均与之无关。也就是马尔可夫性质。在马尔可夫链的每一步,系统根据概率分布,可以从一个状态变到另一个状态,也可以保持当前状态。状态的改变叫做转移,与不同的状态改变相关的概率叫做转移概率。

隐马尔可夫模型:是统计模型,它用来描述一个含有隐含未知参数的马尔可夫过程。其难点是从可观察的参数中确定该过程的隐含参数,然后利用这些参数来作进一步的分析。在正常的马尔可夫模型中,状态对于观察者来说是直接可见的。这样状态的转移概率便是全部的参数。而在隐马尔可夫模型中,状态并不是直接可见的,但受状态影响的某些变量则是可见的。每一个状态在可能输出的符号上都有一概率分布。因此输出符号的序列能够透露出状态序列的一些信息。

三个问题
1、评估问题:概率计算问题:给定模型和观测序列,计算在模型下观测序列出现的概率。

前向、后向算法解决的是一个评估问题,即给定一个模型,求某特定观测序列的概率,用于评估该序列最匹配的模型。

2、模型学习问题:已知观测序列,估计模型中的参数,使得在该模型下观测序列概率最大,即用极大似然估计的方法估计参数。

Baum-Welch算法解决的是一个模型训练问题,即参数估计,是一种无监督的训练方法,主要通过EM迭代实现;即只有观测序列,无状态序列时训练模型。

极大似然估计:观测序列和相应的状态序列都存在的监督学习算法,用来估计参数。

3、解码问题/预测问题:已知模型和观测序列,给定观测序列,求最可能的对应的状态序列。

维特比算法解决的是给定一个模型和某个特定的输出序列,求最可能产生这个输出的状态序列。如通过海藻变化(输出序列)来观测天气(状态序列),是预测问题,通信中的解码问题。

中文分词实践
  代码主要来源于《Python自然语言处理实战:核心技术与算法》(github)一书,对其中一些地方略作修改。这里我们主要运用维特比算法将中文分词转化为HMM的解码问题。
  HMM主要理论概念参考后文链接,这里主要讲解下代码的大致思路:规定每个字最多只有四个构词位置:B(词首)、M(词中)、E(词尾)和S(单独成词)。在分词任务中,o为B、M、E、S这4中标记, λ \lambda λ为句子中的每个字符,例如"北" “京”(包括标点等非中文字符)。在HMM中,将 P ( λ k ∣ o k ) P(\lambda_k|o_k) P(λkok)称为发射概率, P ( o k ∣ o k − 1 ) P(o_k|o_{k-1}) P(okok1)称为转移概率。通过设置 P ( o k ∣ o k − 1 ) = 0 P(o_k|o_{k-1})=0 P(okok1)=0,可以排除类似BBB、EM等不合理的组合。

  在HMM中,求解 m a x P ( λ ∣ o ) P ( o ) maxP(\lambda|o)P(o) maxP(λo)P(o)的常用方法是 Veterbi 算法。 它是一种动态规划方法,核心思想是 : 如果最终的最优路径经过某个 o i o_i oi, 那么从初始节点到 o i − 1 o_{i-1} oi1点的路径必然也是一个最优路径一一因为每一个节点。 o i o_i oi 只会影响前后两个 P ( o i − 1 ∣ o i ) P(o_{i-1}|o_i) P(oi1oi) P ( o i ∣ o i + 1 ) P(o_i|o_{i+1}) P(oioi+1)

  根据这个思想,可以通过递推的方法,在考虑每个 o i o_i oi时只需要求出所有经过各 o i − 1 o_{i-1} oi1的候选点的最优路径, 然后再与当前的 o i o_i oi结合比较。 这样每步只需要算不超过 L 2 L^2 L2次,就可以逐步找出最优路径。 Viterbi 算法的效率是 O ( n ∗ L 2 ) O(n*L^2) O(nL2),L是候选数目最多的节点 o i o_i oi的候选数目,它正比于 n, 这是非常高效率的。 HMM 的状态转移图如下图所示:
出自Python自然语言处理实战
HMM代码如下:

class HMM(object):# 初始化全局信息def __init__(self):# 主要是用于存取算法中间结果,不用每次都训练模型self.model_file = './chapter-3/data/hmm_model.pkl'# 状态值集合self.state_list = ['B', 'M', 'E', 'S']  # B:词首 M:词中 E:词尾 S:单独成词# 参数加载,用于判断是否需要重新加载model_fileself.load_para = False# 用于加载已计算的中间结果,当需要重新训练时,需初始化清空结果def try_load_model(self, trained):if trained:import picklewith open(self.model_file, 'rb') as f:  # pkl 二进制文件,需用rbself.A_dic = pickle.load(f)self.B_dic = pickle.load(f)self.Pi_dic = pickle.load(f)self.load_para = Trueelse:# 状态转移概率(状态->状态的条件概率)self.A_dic = {}# 发射概率(状态->词语的条件概率)self.B_dic = {}# 状态的初始概率self.Pi_dic = {}self.load_para = False# 计算转移概率、发射概率以及初始概率def train(self, path):# 重置几个概率矩阵self.try_load_model(False)# 统计状态出现次数,求p(o)Count_dic = {}# 初始化参数def init_parameters():for state in self.state_list:self.A_dic[state] = {s: 0.0 for s in self.state_list}self.Pi_dic[state] = 0.0self.B_dic[state] = {}Count_dic[state] = 0# 对词进行标注def makeLabel(text):out_text = []if len(text) == 1:out_text.append('S')else:out_text += ['B'] + ['M'] * (len(text) - 2) + ['E']return out_textinit_parameters()line_num = -1# 观察者集合,主要是字以及标点等words = set()with open(path, encoding='utf8') as fp:for line in fp:line_num += 1line = line.strip()if not line:continueword_list = [i for i in line if i != ' ']words |= set(word_list)  # 更新字的集合linelist = line.split()  # 默认空字符串分割line_state = []for w in linelist:line_state.extend(makeLabel(w))assert len(word_list) == len(line_state)  # 每个字对应一个状态for k, v in enumerate(line_state):Count_dic[v] += 1  # 各状态在句子中出现次数if k == 0:self.Pi_dic[v] += 1  # 每个句子的第一个字的状态,用于计算初始状态概率else:self.A_dic[line_state[k - 1]][v] += 1  # 记录前状态->现状态的次数self.B_dic[line_state[k]][word_list[k]] = \self.B_dic[line_state[k]].get(word_list[k], 0) + 1.0  # 记录状态->字的次数self.Pi_dic = {k: v * 1.0 / line_numfor k, v in self.Pi_dic.items()}  # 计算状态(B,S)的初始概率self.A_dic = {k: {k1: v1 / Count_dic[k]for k1, v1 in v.items()}for k, v in self.A_dic.items()  # 例:B->S =n(BS)/n(S)}  # 状态转移矩阵#加1平滑self.B_dic = {k: {k1: (v1 + 1) / Count_dic[k]for k1, v1 in v.items()}for k, v in self.B_dic.items()}#序列化import picklewith open(self.model_file, 'wb') as f:pickle.dump(self.A_dic, f)pickle.dump(self.B_dic, f)pickle.dump(self.Pi_dic, f)return self# 求最大概率的路径 text:切词文本 states:状态集合 start_p:初始概率 trans_p:转移矩阵 emit_p:发射概率def viterbi(self, text, states, start_p, trans_p, emit_p):V = [{}]path = {}# 分别计算出(B S E M)->text[0]各个概率for y in states:V[0][y] = start_p[y] * emit_p[y].get(text[0], 0)path[y] = [y]for t in range(1, len(text)):V.append({})newpath = {}# 检验训练的发射概率矩阵中是否存在该字neverSeen = text[t] not in emit_p['S'].keys() and \text[t] not in emit_p['M'].keys() and \text[t] not in emit_p['E'].keys() and \text[t] not in emit_p['B'].keys()for y in states:emitP = emit_p[y].get(text[t],0) if not neverSeen else 1.0  # 设置未知字单独成词# 文本过长,超过一定的迭代,会导致计算结果趋近于零,报错:ValueError: max() arg is an empty sequence(prob, state) = max([(V[t - 1][y0] * trans_p[y0].get(y, 0), y0)for y0 in states if V[t - 1][y0] > 0])  # 计算出V[t-1]各状态到当前状态概率的最大值   state:前一状态V[t][y] = prob * emitPnewpath[y] = path[state] + [y]path = newpathif emit_p['M'].get(text[-1], 0) > emit_p['S'].get(text[-1], 0):(prob, state) = max([(V[len(text) - 1][y], y) for y in ('E', 'M')])else:(prob, state) = max([(V[len(text) - 1][y], y) for y in states])return (prob, path[state])  # 返回概率最大的那条路径def cut(self, text):import osif not self.load_para:self.try_load_model(os.path.exists(self.model_file))  # 读取已保存模型参数prob, pos_list = self.viterbi(text, self.state_list, self.Pi_dic,self.A_dic, self.B_dic)begin, next = 0, 0for i, char in enumerate(text):pos = pos_list[i]if pos == 'B':begin = ielif pos == 'E':yield text[begin:i + 1]next = i + 1elif pos == 'S':yield charnext = i + 1if next < len(text):yield text[next:]hmm = HMM()
hmm.train('./data/trainCorpus.txt_utf8')text = '这里是北京时间八点整'
res = hmm.cut(text)
print(text)
print(str(list(res))) # ['这里', '是', '北京', '时间', '八点', '整']

参考文章:
隐马尔可夫(HMM)、前/后向算法、Viterbi算法
《Python自然语言处理实战:核心技术与算法》
viterbi算法通俗理解
如何通俗地讲解 viterbi 算法
如何用简单易懂的例子解释隐马尔可夫模型?
隐马尔可夫模型(一)——马尔可夫模型
隐马尔可夫(HMM)、前/后向算法、Viterbi算法 再次总结


http://chatgpt.dhexx.cn/article/W3clCN3r.shtml

相关文章

HMM和Viterbi算法

一、隐马尔可夫模型&#xff08;Hidden Markov Model&#xff09; 1、简介 隐含马尔可夫模型并不是俄罗斯数学家马尔可夫发明的&#xff0c;而是美国数学家鲍姆提出的&#xff0c;隐含马尔可夫模型的训练方法&#xff08;鲍姆-韦尔奇算法&#xff09;也是以他名字命名的。隐含马…

基于Hmm模型和Viterbi算法的中文分词和词性标注

使用 python 实现基于Hmm模型和Viterbi算法的中文分词及词性标注&#xff1b;使用 最大概率算法 进行优化。最终效果&#xff1a;人民日报语料&#xff1a;分词(F1:96.189%)&#xff1b;词性标注(F1:97.934%) 完整代码和数据&#xff0c;参见本实验的 github地址&#xff1a;h…

【生信算法】利用HMM纠正测序错误(Viterbi算法的python实现)

利用HMM纠正测序错误&#xff08;Viterbi算法的python实现&#xff09; 问题背景 对两个纯系个体M和Z的二倍体后代进行约~0.05x的低覆盖度测序&#xff0c;以期获得后代个体的基因型&#xff0c;即后代中哪些片段分别来源于M和Z。已知&#xff1a; 后代中基因型为MM、MZ&…

Viterbi算法实现中文分词和词性标注

Viterbi算法 目标过程词典分词统计分词词性标注 附录附录二附录三 源码地址 目标 实现基于词典的分词方法和统计分词方法对分词结果进行词性标注对分词及词性标注结果进行评价&#xff0c;包括4个指标&#xff1a;正确率、召回率、F1值和效率 过程 词典分词 基于词典的分词…

viterbi 算法与python实现

Viterbi算法 &#xff08;部分内容转自知乎&#xff1a;《如何通俗地讲解 viterbi 算法&#xff1f;》&#xff09; 1、问题描述 如下如所示&#xff0c;如何快速找到从 S 到 E 的最短路径&#xff1f; 一&#xff1a;遍历穷举法&#xff0c;可行&#xff0c;但速度太慢&am…

维特比算法(viterbi)原理以及简单实现

维特比算法 看一下维基百科的解释&#xff0c;维特比算法&#xff08;Viterbi algorithm&#xff09;是一种动态规划算法。它用于寻找最有可能产生观测事件序列的维特比路径——隐含状态序列&#xff0c;特别是在马尔可夫信息源上下文和隐马尔可夫模型中。 通俗易懂的解释知乎…

flask中jsonify遇到的坑

1.jsonify可以将字典转换成json对象传入前端 data {"movie": movie_list,"page": page,"dic_list": dic,"total_page": total_page}>>坑1 字典的值不能为range(x,x)&#xff0c;上图dic就是像range(x,x)&#xff0c;会报错 …

flask中的jsonify返回的是乱码

用flask返回json时遇到了返回字符串支持中文显示的问题&#xff0c;在web端显示的是utf-8的编码如图; 虽然不影响接口的读取&#xff0c;但是可读性太差&#xff0c;于是研究了一下怎么直接显示成中文。最后找到了解决方案如下&#xff0c;在配置中加入下面一行代码就OK了。 …

request jsonify

python的flask框架为用户提供了直接返回包含json格式数据响应的方法&#xff0c;即jsonify&#xff0c;在开发中会经常用到。如下一段简单的flask后端代码&#xff0c;服务端视图函数根据请求参数返回json格式的数据到客户端。 转载于:https://www.cnblogs.com/daqingzi/p/9018…

Flask使用json或jsonify返回响应的数据

前言 在做网站前后端数据交互的时候&#xff0c;我们经常需要使用Ajax等方法向后端发送请求来获取响应的数据&#xff0c;而我们经常需要的就是json格式的响应数据&#xff0c;它实际上就是一个字符串。要知道Flask如何返回json响应数据&#xff0c;首先就需要知道如何将字典di…

Flask 学习-8. jsonify返回中文没正常显示问题

前言 Flask 接口返回的json 格式数据有中文的时候&#xff0c;默认是以ASCII码 返回的&#xff0c;没正常显示中文。 jsonify 返回 json 数据 函数直接返回dict 数据 或返回jsonfy() 函数处理的数据&#xff0c;都是以json格式返回的 from flask import Flask, jsonify fro…

flask jsonify之序列化时的default函数、jsonify序列化自定义对象

目录 1.看源码 2、重写默认的default函数&#xff0c;实现自己的序列化机制 3、把对象转化成字典 3.1 __dict__的方式 3.2、定义keys和__getitem__的方式 4、最终的代码实现 5、关于default函数的其他知识 1.看源码 打开site-package&#xff0c;flask&#xff0c;json…

Flask | 解决jsonify返回中文乱码问题

在采用 return jsonify(data) 返回内容中含有中文时&#xff0c;前端接收数据出现中文乱码问题&#xff0c;乱码格式如下&#xff08;仅中文为ASCII码&#xff09;&#xff1a; 故在此记录下该问题的解决方式&#xff0c;以作后期参考&#xff1a; 在定义Flask app时&#xff…

jsonify返回中文编码的问题

app.config[JSON_AS_ASCII]Flase 在它下面加上上面的代码就欧克了 没加之前或者为True&#xff1a; 加了之后&#xff1a;

flask学习二(jsonify)

示例一&#xff1a; 实现动态路由&#xff0c;代码如下 # coding:utf-8 from flask import Flask from flask import jsonify # 创建对象 app Flask(__name__)users_list {"1001":["123","张三",19],"1002":["234","…

flask中jsonify和json区别

一 JSON数据结构 要把json与字典区分开来 dumps(字典转换成Json) loads(Json转换成字典) Python 的字典是一种数据结构&#xff0c;JSON 是一种数据格式。 json 就是一个根据某种约定格式编写的纯字符串&#xff0c;不具备任何数据结构的特征。而 python 的字典…

关于flask入门教程-记录集转jsonify

Flask 框架里&#xff0c;可以用 jsonify 返回 json 数据&#xff0c;但是为什么不用 Python 自带的 json 模块返回 JSON 数据呢&#xff1f; 其实两者是差不多的&#xff0c;jsonify指明了Content-Type 是 application/json &#xff0c; 这样做是符合 HTTP 协议的规定的&…

Flask 的 jsonify 理解

文章目录 python 代码解决原因Content-Type的区别 python 代码 # -*- coding:utf-8 -*- from flask import Flask, jsonifyapp Flask(__name__)urls [{id: 1,title: python,description: https://www.python.org/},{id: 2,title: flask,description: https://flask.palletsp…

Flask中jsonify和json.dumps用法以及区别(简单案例)

环境&#xff1a;python3.6, Flask1.0.3 flask提供了jsonify函数供用户处理返回的序列化json数据&#xff0c; 而python自带的json库中也有dumps方法可以序列化json对象. 其二者的区别&#xff0c;写个简单的案例实测一下便见分晓。 from flask import Flask from flask im…

const常量函数详解

在类中&#xff0c;如果你不希望某些数据被修改&#xff0c;可以使用const关键字加以限定。const 可以用来修饰成员变量和成员函数。 const常量与指针 const int *p1与const int *p2的顺序不同&#xff0c;但是他们指向的值都不能改变&#xff0c;上述代码说明虽然指向的值不能…