【hmm分词】
HMM分词
理论至https://blog.csdn.net/weixin_42498517/article/details/102659784
训练语料为人民日报
# -*- coding:utf-8 -*-
# @Time: 2019/12/5 14:30
# @Author: Ray.X
"""
Use HMM for Chinese word segmentation
训练语料为人民日报
"""
import os
import pickleclass HMM:
def __init__(self):
"""
定义模型保存位置
状态集合[S, B, E, M] S 单独成词 B词首E词中M词尾
加载参数,判断是否需要重新训练
"""
self.module_file = 'model/hmm_model.pkl'
self.states_list = ['S', 'B', 'E', 'M']
self.load_para = False# 是否训练
self.trans_dic = {}# 状态转移概率
self.emit_dic = {}# 发射概率
self.start_dic = {}# 初始概率
self.count_dic = {}# 统计状态出现次数
self.V = [{}]# 统计最大概率路径def load_model(self, trained):
"""
@param trained:判断是使用模型,还是训练模型。
@return: 训练模型则初始化清空初始概率、转移概率、发射概率
"""
if trained:
with open(self.module_file, 'rb') as f:
self.start_dic = pickle.load(f)
self.trans_dic = pickle.load(f)
self.emit_dic = pickle.load(f)
self.load_para = True
else:# 清空
self.load_para = False
self.trans_dic = {}
self.emit_dic = {}
self.start_dic = {}def init_parameters(self):
"""
初始化参数
@return:
"""
for state in self.states_list:
self.trans_dic[state] = {s: 0.0 for s in self.states_list}# 初始化 状态转移矩阵
# {'S': {'S': 0.0, 'B': 0.0, 'E': 0.0, 'M': 0.0},
# 'B': {'S': 0.0, 'B': 0.0, 'E': 0.0, 'M': 0.0},
# 'E': {'S': 0.0, 'B': 0.0, 'E': 0.0, 'M': 0.0},
# 'M': {'S': 0.0, 'B': 0.0, 'E': 0.0, 'M': 0.0}}
self.emit_dic[state] = {}# 初始化 状态发射矩阵
# {'S': {',': 3.0, '新': 1.0, '的': 2.0},
# 'B': {'中': 3.0, '儿': 1.0, '踏': 1.0},
# 'E': {'年': 1.0, '亿': 1.0, '华': 1.0},
# 'M': {'9': 1.0, '8': 1.0, '6': 1.0, '产': 1.0}}
self.start_dic[state] = 0.0# 初始化 初始概率
# {'S': 0, 'B': 1, 'E': 0, 'M'}
self.count_dic[state] = 0# 初始化每个状态出现次数
# {'S': 0, 'B': 1, 'E': 0, 'M'}def train(self, path):
"""
训练HMM模型,计算转移概率、发射概率、初始概率
@param path: 训练语料地址
"""
self.load_model(False)# 重置概率矩阵def make_label(text):
"""
生成标签
@param text: 训练语料
@return:
"""
out_text = []
if len(text) == 1:
out_text.append('S')# 单字标记为S
else:
out_text += ['B'] + ['M'] * (len(text) - 2) + ['E']# 多字标记为BE BME BMME...
return out_textself.init_parameters()
line_num = -1# 从0行开始words = set()# 构建无序无重的字、标点集合
with open(path, encoding='UTF-8') as f:
for line in f:
line_num += 1
line = line.strip()# 清除头尾字符 默认空格、换行
if not line:
continueword_list = [i for i in line if i != ' ']
words |= set(word_list)# 合并字符集line_list = line.split()# 按空格分割语料 语料为标注完成的 词语
line_state = []
for w in line_list:
line_state.extend(make_label(w))# 生成每个字的标签集assert len(word_list) == len(line_state)# 断言 条件为True正常执行
for i, v in enumerate(line_state):# 遍历Data 生成[(index, state)]
self.count_dic[v] += 1# 统计每个状态的次数
if i == 0:
self.start_dic[v] += 1# 统计每一个句子第一个字的状态,用于计算初始状态概率
else:
self.trans_dic[line_state[i - 1]][v] += 1# 统计状态间的转移次数,用于计算转移概率 状态——>状态
self.emit_dic[line_state[i]][word_list[i]] = \
self.emit_dic[line_state[i]].get(word_list[i], 0) + 1.0# 统计每个状态下出现的字次数,用于计算发射概率 状态->字
# 计算初始概率句子的第一个字属于 k 状态出现的次数/总的句子数
self.start_dic = {k: v * 1.0 / line_num for k, v in self.start_dic.items()}
# 计算转移概率k状态的前一个状态k1出现的次数v1/k状态出现的次数
self.trans_dic = {k: {k1: v1 / self.count_dic[k] for k1, v1 in v.items()} for k, v in self.trans_dic.items()}
# 计算发射概率 加1平滑防止某个字出现的次数为0 则分子为0
# k状态下 k1字出现的次数/k状态出现的次数
self.emit_dic = {k: {k1: (v1 + 1) / self.count_dic[k] for k1, v1 in v.items()} for k, v in self.emit_dic.items()}
# 储存模型为dump
with open(self.module_file, 'wb') as f:
pickle.dump(self.start_dic, f)
pickle.dump(self.trans_dic, f)
pickle.dump(self.emit_dic, f)return selfdef veterbi(self, obs, states, start_p, trans_p, emit_p):
"""
使用veterbi算法计算最大概率路径
@param obs:输入
@param states:状态集
@param start_p: 初始概率
@param trans_p: 转移概率矩阵
@param emit_p:发射概率矩阵
@return:
"""
self.V = [{}]# V[t][状态] = 路径概率
path = {}# 中间变量,表路径上的当前状态# 初始化初始状态
for k in states:
self.V[0][k] = start_p[k] * emit_p[k].get(obs[0], 0)# k状态初始概率 * k状态下 句子第一个字 出现的概率
path[k] = [k]# {'S':[...]}# 对t>1的节点计算最大概率路径
for t in range(1, len(obs)):
self.V.append({})
newpath = {}# 判断该字是否在发射矩阵中
neverSeen = obs[t] not in emit_p['S'].keys() and obs[t] not in emit_p['M'].keys() and obs[t] not in \
emit_p['E'].keys() and obs[t] not in emit_p['B'].keys()for k in states:
emitP = emit_p[k].get(obs[t], 0) if not neverSeen else 1.0# 未知字单独成词,不存的概率在设为 1.0
# (最大概率,概率最大的前一个状态) = max(前一个状态k0的概率 * k0到k的转移概率 * 这个字 出现在k状态下 概率)
(prob, state) = max([(self.V[t-1][k0] * trans_p[k0].get(k, 0) * emitP, k0) for k0 in states
if self.V[t-1][k0] > 0])self.V[t][k] = prob# 记录最大概率
newpath[k] = path[state] + [k]# 记录路径path = newpath# 初始化,不保留旧路径# 得到最大概率路径,计算最后第二个字的状态最大概率
if emit_p['M'].get(obs[-1], 0) > emit_p['S'].get(obs[-1], 0):
# 如果最后一个字是词尾的概率大于单词,最后第二个字的状态只可能是词中或者词尾(M > S 不能说一定是M)
(prob, state) = max([(self.V[len(obs) - 1][k], k) for k in ('E', 'M')])
else:
# 否则都有可能
(prob, state) = max([(self.V[len(obs) - 1][k], k) for k in states])
print(path, state)
print_dptable(self.V)
return prob, path[state]def cut(self, text):
"""
分词接口,调用veterbi
@param text: 输入
@return:
"""
if not self.load_para:
self.load_model(os.path.exists(self.module_file))
prob, max_path = self.veterbi(text, self.states_list, self.start_dic, self.trans_dic, self.emit_dic)
print(prob, max_path)
start, then = 0, 0
for i, char in enumerate(text):
pos = max_path[i]
if pos == 'B':
start = i
elif pos == 'E':
yield text[start: i+1]
print('1', text[start: i+1])
elif pos == 'M':
then = i+1
elif pos == 'S':
yield char
print('2', char)
then = i+1
if then < len(text):
yield text[then:]
print('3', text[then:])# 打印路径概率表
def print_dptable(V):
print("", end=' ')
for i in range(len(V)):
print("%7d" % i, end=' ')
print()for y in list(V[0].keys()):
print("%.5s: " % y, end=' ')
for t in range(len(V)):
print("%.7s" % ("%f" % V[t][y]), end=' ')
print()hmm = HMM()
hmm.train('../Data/CWS/trainCorpus.txt_utf8')# 训练后注释
res = hmm.cut('南京市长江大桥')
print(str(list(res)))
# V:[{'S': 0.00024068043618878063, 'B': 0.0008479575355477435, 'E': 0.0, 'M': 0.0},
#{'S': 2.597703368715881e-08, 'B': 5.829385508478131e-09, 'E': 1.8054004390160279e-06, 'M': 2.994752366629435e-07},
#{'S': 9.838524382946052e-10, 'B': 1.6974514448106187e-09, 'E': 5.794565751422634e-10, 'M': 3.5958846294198544e-11},
#{'S': 3.886964550765146e-13, 'B': 4.18999169959862e-13, 'E': 7.021903255143946e-12, 'M': 2.4633073463114406e-13},
#{'S': 1.1929121178105033e-15, 'B': 1.2449982203103038e-15, 'E': 1.6152119319628026e-16, 'M': 8.934275095138284e-17},
#{'S': 1.9171797519360457e-18, 'B': 2.9217898225125463e-18, 'E': 4.6433599609509474e-18, 'M': 1.3333437077827637e-18},
#{'S': 2.4660655016412213e-22, 'B': 9.879329881241226e-23, 'E': 2.1188370123793454e-22, 'M': 9.902992826341432e-24}]
# path:{'S': ['B', 'E', 'B', 'E', 'B', 'E', 'S'],
#'B': ['B', 'E', 'B', 'E', 'B', 'E', 'B'],
#'E': ['B', 'E', 'B', 'E', 'S', 'B', 'E'],
#'M': ['B', 'E', 'B', 'E', 'B', 'M', 'M']}
推荐阅读
- 人工智能|hugginface-introduction 案例介绍
- 深度学习|论文阅读(《Deep Interest Evolution Network for Click-Through Rate Prediction》)
- nlp|Keras(十一)梯度带(GradientTape)的基本使用方法,与tf.keras结合使用
- NER|[论文阅读笔记01]Neural Architectures for Nested NER through Linearization
- 深度学习|2019年CS224N课程笔记-Lecture 17:Multitask Learning
- 深度学习|[深度学习] 一篇文章理解 word2vec
- 论文|预训练模型综述2020年三月《Pre-trained Models for Natural Language Processing: A Survey》
- NLP|NLP预训练模型综述
- NLP之文本表示——二值文本表示
- 隐马尔科夫HMM应用于中文分词