4隐马尔可夫模型与序列标注
4隐马尔可夫模型与序列标注
序列标注问题
•序列标注(tagging)指的是给定一个序列x=x_1 x_2…x_n,找出序列中每个元素对应标签y=y_1 y_2…y_n的问题
其中,y所有可能的取值集合称为标注集(tagset)
序列标注与中文分词
考虑一个字符序列x,想象切词器真的是拿刀切割字符串。那么每个字符在分词时充当两种角色,要么在i后切开,要么跳过不切 中文分词转化为标注集为{切,过}的序列标注问题
为了捕捉汉字分别作为词语首尾(Begin、End)、词中(Middle)以及单字成词(Single),人们提出{B,M,E,S}这种最流行的标注集
分词器将最近的两个BE标签对应区间内的所有字符合并为一个词语,S标签对应字符作为单字词语,按顺序输出即完成分词过程
序列标注与词性标注
序列标注与命名实体识别
命名实体:显示存在的实体,如人名,地名和机构名
构成地名的单词标注为“B/M/E/S-地名”,不构成命名实体的单词则统一标注为O,即复合词之外
将”北京“和“天安门”作为首尾组合成词,并且标注为地名
隐马尔可夫模型
•隐马尔可夫模型(Hidden Markov Model,HMM)是描述两个时序序列联合分布p(x,y)的概率模型
•x序列外界可见(外界指的是观测者),称为观测序列(observation sequence)
•观测x为单词
•y序列外界不可见,称为状态序列(state sequence)
•状态y为词性
•人们也称状态为隐状态(hidden state),而称观测为显状态(visible state)
比如观测x为单词,状态为y词性,我们需要根据单词序列去猜测他们的词性
隐马尔可夫模型之所以称为”隐“ 是因为从外界来看状态序列(列如词性)隐藏不可见,是待求的因变量
从马尔可夫假设到隐马尔可夫模型
马尔可夫假设:每个事件的发生概率只取决于前一个事件 ,将满足该假设的连续多个事件串联在一起,就构成了马尔可夫链
在此基础上 隐马尔可夫模型理解起来就并不复杂了:它的马尔可夫假设作用于状态序列
•假设①当前状态y_t仅仅依赖于前一个状态y_(t-1),连续多个状态构成隐马尔可夫链y
•假设②任意时刻的观测x_t只依赖于该时刻的状态y_t,与其他时刻的状态或观测独立无关
隐马尔可夫模型状态序列与观测序列的依赖关系如下图()
用箭头表示事件的依赖关系(箭头终点是结果,依赖于起点的因缘)
•隐马尔可夫模型利用三个要素来模拟时序序列的发生过程
•初始状态概率向量 (π)
•状态转移概率矩阵 (A)
•发射概率矩阵(也称作观测概率矩阵) (B)
初始状态概率向量
样本生成
案例-医疗诊断
import numpy as np
from pyhanlp import *
from jpype import JArray, JFloat, JInt
to_str = JClass('java.util.Arrays').toString
###病人状态 健康 或 发烧
states = ('Healthy', 'Fever')
###第一天可能的状态
start_probability = {'Healthy': 0.6, 'Fever': 0.4}
###
transition_probability = {
'Healthy': {'Healthy': 0.7, 'Fever': 0.3}, ###接下来一天可能的状态
'Fever': {'Healthy': 0.4, 'Fever': 0.6},
}
emission_probability = {
'Healthy': {'normal': 0.5, 'cold': 0.4, 'dizzy': 0.1}, ###不同状况对应可能的症状
'Fever': {'normal': 0.1, 'cold': 0.3, 'dizzy': 0.6},
}
###身体感受 正常 体寒 头晕
observations = ('normal', 'cold', 'dizzy')
###该函数接受一个列表作为输入参数,并返回每个疾病的标签 和 标签的索引
def generate_index_map(lables):
index_label = {}
label_index = {}
i = 0
for l in lables:
index_label[i] = l
label_index[l] = i
i += 1
##返回疾病的标签 和 标签的索引
return label_index, index_label
###调用generate_index_map函数获取标签和标签的索引
states_label_index, states_index_label = generate_index_map(states)
observations_label_index, observations_index_label = generate_index_map(observations)
##该函数用于将观察值和标签的对应关系存储在一个列表中,并返回一个列表
##输入值是观察值和标签对应关系 输出是一个列表 包含每个观察值对应的标签
def convert_observations_to_index(observations, label_index):
list = []
###变量观察值列表中每一个元素
for o in observations:
###将其添加到标签的对应关系列表中
list.append(label_index[o])
return list
###该函数映射一个对象mao转换为一个以为数组v 其中每个元素的值是mao中相应键的值
def convert_map_to_vector(map, label_index):
##创建一个长度为len(map)的一维数组v,并将其所有元素的值都设置为map中相应键的值
v = np.empty(len(map), dtype=float)
#print(v) ###[0.6 0.4]
#print(map) ###{'Healthy': 0.6, 'Fever': 0.4}
###遍历映射列表中每一个元素
for e in map:
#print(map[e]) ###0.6 0.4
###将其添加到v中相应的位置
v[label_index[e]] = map[e]
# print(v[label_index[e]]) ###0.6 0.4
##使用了NumPy库中的JArray类来创建一个一维数组v
return JArray(JFloat, v.ndim)(v.tolist()) # 将numpy数组转为Java数组
def convert_map_to_matrix(map, label_index1, label_index2):
###创建行为len(label_index1)列为len(label_index2)的二维数组
m = np.empty((len(label_index1), len(label_index2)), dtype=float)
for line in map:
for col in map[line]:
###将遍历得到map中的值赋值给对应m
m[label_index1[line]][label_index2[col]] = map[line][col]
return JArray(JFloat, m.ndim)(m.tolist())
###调用convert_map_to_matrix函数
A = convert_map_to_matrix(transition_probability, states_label_index, states_label_index)
B = convert_map_to_matrix(emission_probability, states_label_index, observations_label_index)
###调用convert_observations_to_index函数
observations_index = convert_observations_to_index(observations, observations_label_index)
###调用convert_map_to_vector函数
pi = convert_map_to_vector(start_probability, states_label_index)
FirstOrderHiddenMarkovModel = JClass('com.hankcs.hanlp.model.hmm.FirstOrderHiddenMarkovModel')
###将得到的值传入FirstOrderHiddenMarkovModel构造隐马尔可夫模型
given_model = FirstOrderHiddenMarkovModel(pi, A, B)
###python没有enum类型 所有概率向量或矩阵都用dict来模拟 转换后的A,B,pi就是java兼容的数组了
###可以直接传入FirstOrderHiddenMarkovModel构造隐马尔可夫模型
样本生成算法
###生成2个样本序列,长度介于3和5之间
for O, S in given_model.generate(3, 5, 2):
###使用join函数将他们连接成一个字符串
print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in zip(O, S)))
----
cold/Fever dizzy/Healthy cold/Healthy cold/Healthy
dizzy/Fever normal/Healthy dizzy/Healthy
###由于随机数原因每次运行结果都是随机的
隐马尔可夫模型的训练
根据训练数据是否有标注(是否记录了隐状态y),数据可分为完全数据和非完全数据,相应的训练算法分为监督学习和无监督学习。
监督学习中,利用极大似然法来估计隐马尔可夫模型的参数。隐马尔可夫模型参数指的就是三元组(π,A,B)
利用给定的隐马尔可夫模型 P生成十万个样本,在这十万个样本上训练新模型Q,比较新旧模型参数是否一致。
trained_model = FirstOrderHiddenMarkovModel()
###3:序列最低长度 10:序列最高长度 100000:需要生成的样本数
trained_model.train(given_model.generate(3, 10, 100000))
##for O, S in given_model.generate(3, 10, 100):
###使用join函数将他们连接成一个字符串
## print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in zip(O, S)))
print("新模型与旧模型是否相同",trained_model.similar(given_model))
trained_model.unLog() # 将对数形式的概率还原回来
----
新模型与旧模型是否相同 True
隐马尔可夫模型的预测
隐马尔可夫模型最具实际意义的问题当属序列标注了:给定观测序列,求解最可能的状态序列及其概率。
概率计算向前算法
搜索状态序列的维特比算法
###创建java整型数组,用来存储预测路径
###使用JArray函数创建了一个包含0值的JArray对象pred,并将其转换为一个JInt类型的一维数组。
pred = JArray(JInt, 1)([0, 0, 0])
prob = given_model.predict(observations_index, pred)
print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in
zip(observations_index, pred)) + " {:.3f}".format(np.math.exp(prob)))
----
normal/Healthy cold/Healthy dizzy/Fever 0.015
隐马尔可夫模型应用于中文分词
标注集
HanLP已经实现了{B,M,E,S}标签到整型的映射,称为CWSTagSet。
HanLP还实现了用于词性标注的POSTagSet
用于命名实体识别的NERTagSet。
对其他标注任务,读者还可以实现自己的标注集。通过替换标注集,用户可以无缝地利用HanLP实现的众多序列标注模型,包括隐马尔可夫模型在内。
字符映射
字符作为观测变量,必须是整型才可以被隐马尔可夫模型接受。
HanLP实现了从字符串形式到整型的映射,称为Vocabulary词表
预料转换
将语料转换为(x,y)二元组才能训练隐马尔可夫模型
训练
HMMSegmenter提供train接口,接受《人民日报》格式语料库的路径,训练出的模型存储在HiddenMarkovModel类型的成员变量model中
from pyhanlp import *
from tests.book.ch03.eval_bigram_cws import CWSEvaluator
from tests.book.ch03.msr import msr_dict, msr_train, msr_model, msr_test, msr_output, msr_gold
###一阶
FirstOrderHiddenMarkovModel = JClass('com.hankcs.hanlp.model.hmm.FirstOrderHiddenMarkovModel')
###二阶
#SecondOrderHiddenMarkovModel = JClass('com.hankcs.hanlp.model.hmm.SecondOrderHiddenMarkovModel')
HMMSegmenter = JClass('com.hankcs.hanlp.model.hmm.HMMSegmenter')
def train(corpus, model):
segmenter = HMMSegmenter(model)
segmenter.train(corpus)
print(segmenter.segment('商品和服务'))
return segmenter.toSegment()
if __name__ == '__main__':
segment = train(msr_train, FirstOrderHiddenMarkovModel())
----
[商品, 和, 服务]
预测
训练完隐马尔可夫模型后,模型的预测结果是{B,M,E,S}标签序列。分词器必须根据标签序列的指示,将字符序列转换为单词序列
1)初始化单词链表L与单词缓冲区W为空白,即[]与""。
2)逐个读入字符x与标签y,W+=c。若 y=B or S,则切断,L+=W,W=[]。
3)检查W是否为空白,若非空白则L+=W
###测试
print(segmenter.segment('商品和服务'))
---
结果是[商品,和,服务]。
评测
result = CWSEvaluator.evaluate(segment, msr_test, msr_output, msr_gold, msr_dict)
print(result)
----
P:78.49 R:80.38 F1:79.42 OOV-R:41.11 IV-R:81.44
•F_1值只有79.42%,甚至不如词典分词
•但R_OOV的确提高到了41.11%
•说明几乎有一半的OOV都被正确地召回了,拖累成绩的反而是IV的大量错误
二阶隐马尔可夫模型
如果隐马尔可夫模型中每个状态仅依赖于前一个状态,则称为一阶,如果依赖于前两个状态,则称为二阶
基于HanLP的模块化设计思想,我们并不需要从零开始。而是编写HiddenMarkovModel的子类SecondOrderHiddenMarkovModel 与上一节的FirstOrderHiddenMarkovModel放在一起的UML图如上图所示
比较一阶和二阶隐马尔可夫模型,在内部数据成员上有两个区别:
二阶隐马尔可夫模型中,y_t依赖于y_t-1和y_t-2,所有二阶转移概率是三维的张量,而不在是二维的矩阵。所以SecondOrderHiddenMarkovModel多了一个三维数组transition_probability2。其定义为:transition_probability2[i][j][k]
二阶隐马尔可夫模型应用于中文分词
SecondOrderHiddenMarkovModel = JClass('com.hankcs.hanlp.model.hmm.SecondOrderHiddenMarkovModel')
HMMSegmenter = JClass('com.hankcs.hanlp.model.hmm.HMMSegmenter')
def train(corpus, model):
segmenter = HMMSegmenter(model)
segmenter.train(corpus)
# print(segmenter.segment('商品和服务'))
return segmenter.toSegment()
segment2 = train(msr_train, SecondOrderHiddenMarkovModel())
result = CWSEvaluator.evaluate(segment2, msr_test, msr_output, msr_gold, msr_dict)
print(result)
----
P:78.34 R:80.01 F1:79.16 OOV-R:42.06 IV-R:81.04
总结
尝试了最简单的序列标注模型--隐马尔可夫模型
隐马尔可夫模型的基本问题有三个:样本生成,参数估计,序列预测
然而隐马尔可夫模型用于中文分词的效果并不理想