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

总结

尝试了最简单的序列标注模型--隐马尔可夫模型
隐马尔可夫模型的基本问题有三个:样本生成,参数估计,序列预测
然而隐马尔可夫模型用于中文分词的效果并不理想
posted @ 2023-05-02 21:03  idazhi  阅读(177)  评论(0编辑  收藏  举报