笔记转载于GitHub项目:https://github.com/NLP-LOVE/Introduction-NLP
第3章的n元语法模型从词语接续的流畅度出发,为全切分词网中的二元接续打分,进而利用维特比算法求解似然概率最大的路径。这种词语级别的模型无法应对 OOV(Out of Vocabulary,即未登录词) 问题: 00V在最初的全切分阶段就已经不可能进人词网了,更何谈召回。
例如下面一句:
头上戴着束发嵌宝紫金冠,齐眉勒着二龙抢珠金抹额
加粗的就是相对陌生的新词,之前的分词算法识别不出,但人类确可以,是因为读者能够识别“戴着”,这些构词法能让人类拥有动态组词的能力。我们需要更细粒度的模型,比词语更细粒度的就是字符。
具体说来,只要将每个汉字组词时所处的位置(首尾等)作为标签,则中文分词就转化为给定汉字序列找出标签序列的问题。一般而言,由字构词是序列标注模型的一种应用。 在所有“序列标注”模型中,隐马尔可夫模型是最基础的一种。
序列标注指的是给定一个序列 x=x1x2...xnx=x_1x_2...x_nx=x1x2...xn,找出序列中每个元素对应标签 y=y1y2...yny=y_1y_2...y_ny=y1y2...yn 的问题。其中,y 所有可能的取值集合称为标注集。比如,输入一个自然数序列,输出它们的奇偶性。
求解序列标注问题的模型一般称为序列标注器,通常由模型从一个标注数据集 {X,Y}={(x(i),y(i))},i=1,...,K\{X,Y\}=\{(x^{(i)},y^{(i)})\},i=1,...,K{X,Y}={(x(i),y(i))},i=1,...,K 中学习相关知识后再进行预测。再NLP问题中,x 通常是字符或词语,而 y 则是待预测的组词角色或词性等标签。中文分词、词性标注以及命名实体识别,都可以转化为序列标注问题。
分词标注集并非只有一种,为了捕捉汉字分别作为词语收尾(Begin、End)、词中(Middle)以及单字成词(Single)时不同的成词概率,人们提出了{B,M,E,S}这种最流行的标注集。
总之,序列标注问题是NLP中最常见的问题之一。许多应用任务都可以变换思路,转化为序列标注来解决。所以一个准确的序列标注模型非常重要,直接关系到NLP系统的准确率。机器学习领域为NLP提供了许多标注模型,本着循序渐进的原则,本章介绍其中最基础的一个隐马尔可夫模型。
隐马尔可夫模型( Hidden Markov Model, HMM)是描述两个时序序列联合分布 p(x,y) 的概率模型: x 序列外界可见(外界指的是观测者),称为观测序列(obsevation sequence); y 序列外界不可见,称为状态序列(state sequence)。比如观测 x 为单词,状态 y 为词性,我们需要根据单词序列去猜测它们的词性。隐马尔可夫模型之所以称为“隐”,是因为从外界来看,状 态序列(例如词性)隐藏不可见,是待求的因变量。从这个角度来讲,人们也称状态为隐状态(hidden state),而称观测为显状态( visible state)。隐马尔可夫模型之所以称为“马尔可夫模型”,”是因为它满足马尔可夫假设。
状态与观测之间的依赖关系确定之后,隐马尔可夫模型利用三个要素来模拟时序序列的发生过程----即初始状态概率向量、状态转移概率矩阵和发射概率矩阵。
给定 π ,初始状态 Y1 的取值分布就确定了,比如采用{B,M,E,S}标注集时概率如下: p(y1=B)=0.7p(y1=M)=0p(y1=E)=0p(y1=S)=0.3 p(y_1=B)=0.7\\ p(y_1=M)=0\\ p(y_1=E)=0\\ p(y_1=S)=0.3 p(y1=B)=0.7p(y1=M)=0p(y1=E)=0p(y1=S)=0.3 那么此时隐马尔可夫模型的初始状态概率向量为 π=[0.7,0,0,0.3],注意,句子第一个词是单字的可能性要小一些。
前两个问题是模式识别的问题:1) 根据隐马尔科夫模型得到一个可观察状态序列的概率(评价);2) 找到一个隐藏状态的序列使得这个序列产生一个可观察状态序列的概率最大(解码)。第三个问题就是根据一个可以观察到的状态序列集产生一个隐马尔科夫模型(学习)。
根据已知条件①②,病情状态(健康、发烧)可作为隐马尔可夫模型的隐状态(上图蓝色状态),而身体感受(头晕、体寒或正常)可作为隐马尔可夫模型的显状态(图中白色状态)。条件③符合隐马尔可夫模型假设一,条件④符 合隐马尔可夫模型假设二。这个案例其实描述了一个隐马尔可夫模型, 并且参数已经给定。构造模型代码见: 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 states_label_index, states_index_label = generate_index_map(states) observations_label_index, observations_index_label = generate_index_map(observations) def convert_map_to_matrix(map, label_index1, label_index2): m = np.empty((len(label_index1), len(label_index2)), dtype=float) for line in map: for col in map[line]: m[label_index1[line]][label_index2[col]] = map[line][col] return JArray(JFloat, m.ndim)(m.tolist()) def convert_observations_to_index(observations, label_index): list = [] for o in observations: list.append(label_index[o]) return list def convert_map_to_vector(map, label_index): v = np.empty(len(map), dtype=float) for e in map: v[label_index[e]] = map[e] return JArray(JFloat, v.ndim)(v.tolist()) # 将numpy数组转为Java数组 ## pi:初始状态概率向量 ## A:状态转移概率矩阵 ## B:发射概率矩阵 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) observations_index = convert_observations_to_index(observations, observations_label_index) pi = convert_map_to_vector(start_probability, states_label_index) FirstOrderHiddenMarkovModel = JClass('com.hankcs.hanlp.model.hmm.FirstOrderHiddenMarkovModel') given_model = FirstOrderHiddenMarkovModel(pi, A, B)
代码如下(接上),直接通过模型进行生成: ## 第一个参数:序列最低长度 ## 第二个参数:序列最高长度 ## 第三个参数:需要生成的样本数 for O, S in given_model.generate(3, 5, 2): print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in zip(O, S)))
隐马尔可夫模型最具实际意义的问题当属序列标注了:给定观测序列,求解最可能的状态序列及其概率。
将其中的每个 Xt、Yt 对应上实际发生序列的 Si、Oj,就能带入(π,A,B)中的相应元素,从而计算出任意序列的概率,最后找出这些概率的最大值就得到预测结果。找出概率最大值要用到维特比算法。
上图从左往右时序递增,虚线由初始状态概率决定,实线则是转移概率。由于受到观测序列的约束,不同状态发射观测的概率不同,所以每个节点本身也必须计算自己的花费,由发射概率决定。又由于 Yt+1 仅依赖于 Yt,所以网状图可以动态规划的搜索,也就是维特比算法:
预测代码如下(接上面代码): 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 观察该结果,“/”隔开观测和状态,最后的 0.015 是序列的联合概率。
HanLP 已经实现了基于隐马尔可夫模型的中文分词器 HMMSegmenter,并且实现了训练接口。代码详见:
hmm_cws.py:https://github.com/NLP-LOVE/Introduction-NLP/tree/master/code/ch04/hmm_cws.py
如果隐马尔可夫模型中每个状态仅依赖于前一个状态, 则称为一阶;如果依赖于前两个状态,则称为二阶。既然一阶隐马尔可夫模型过于简单,是否可以切换到二阶来提高分数呢?
答案是可以的,跟一阶类似,这里不再详细介绍二阶隐马尔可夫模型,详细请看原书。
这里我们使用 MSR语料库进行评测,结果如下表所示:
算法 | P | R | F1 | R(oov) | R(IV) |
---|---|---|---|---|---|
最长匹配 | 89.41 | 94.64 | 91.95 | 2.58 | 97.14 |
二元语法 | 92.38 | 96.70 | 94.49 | 2.58 | 99.26 |
一阶HHM | 78.49 | 80.38 | 79.42 | 41.11 | 81.44 |
二阶HHM | 78.34 | 80.01 | 79.16 | 42.06 | 81.04 |
可以看到,二阶隐马尔可夫模型的 Roov 有少许提升,但综合 F1 反而下降了。这说明增加隐马尔可夫模型的阶数并不能提高分词器的准确率,单靠提高转移概率矩阵的复杂度并不能提高模型的拟合能力,我们需要从别的方面想办法。目前市面上一些开源分词器仍然停留在一阶隐马尔可夫模型的水平,比如著名的结巴分词,它们的准确率也只能达到80%左右。
这一章我们想解决的问题是新词识别,为此从词语级模型切换到字符级模型,将中文分词任务转换为序列标注问题。作为新手起步,我们尝试了最简单的序列标注模型----隐马尔可夫模型。隐马尔可夫模型的基本问题有三个:样本生成、参数估计、序列预测。
然而隐马尔可夫模型用于中文分词的效果并不理想,虽然召回了一半的 OOV,但综合 F1 甚至低于词典分词。哪怕升级到二阶隐马尔可夫模型, F1 值依然没有提升。 看来朴素的隐马尔可夫模型不适合中文分词,我们需要更高级的模型。
话说回来,隐马尔可夫模型作为入门模型,比较容易上手,同时也是许多高级模型的基础。打好基础,我们才能挑战高级模型。
HanLP何晗–《自然语言处理入门》笔记:
https://github.com/NLP-LOVE/Introduction-NLP
项目持续更新中…
目录
章节 |
---|
第 1 章:新手上路 |
第 2 章:词典分词 |
第 3 章:二元语法与中文分词 |
第 4 章:隐马尔可夫模型与序列标注 |
第 5 章:感知机分类与序列标注 |
第 6 章:条件随机场与序列标注 |
第 7 章:词性标注 |
第 8 章:命名实体识别 |
第 9 章:信息抽取 |
第 10 章:文本聚类 |
第 11 章:文本分类 |
第 12 章:依存句法分析 |
第 13 章:深度学习与自然语言处理 |