HMM在自然语言处理中的应用一:词性标注
词性标注(Part-of-Speech tagging 或 POS tagging)是指对于句子中的每个词都指派一个合适的词性,也就是要确定每个词是名词、动词、形容词或其他词性的过程,又称词类标注或者简称标注。
对于输入句子:
The Fulton County Grand Jury said Friday an investigation of Atlanta’s recent primary election produced “ no evidence ” that any irregularities took place .
需要为句子中的每个单词标上一个合适的词性标记,既输出含有词性标记句子:
The/at-tl Fulton/np-tl County/nn-tl Grand/jj-tl Jury/nn-tl said/vbd Friday/nr an/at investigation/nn of/in Atlanta’s/np$ recent/jj primary/jj election/nn produced/vbn “/“ no/at evidence/nn ”/” that/cs any/dti irregularities/nns took/vbd place/nn ./.
在进行词性标注时,前提条件之一便是选择什么样的标记集?Brown语料库标记集有87个,而英语中其他标记集多数是从Brown语料库中的标记集发展而来的,如最常用的Penn Treebank标记集,包含45个标记,是小标记集。汉语标记集中常用的有北大《人民日报》语料库词性标记集、计算所汉语词性标记集等。
在确定使用某个标记集之后,下一步便是如何进行词性标注了!如果每个单词仅仅对应一个词性标记,那么词性标注就非常容易了。但是语言本身的复杂性导致了并非每一个单词只有一个词性标记,而存在一部分单词有多个词性标记可以选择,如book这个单词,既可以是动词(book that flight),也可以是名词(hand me that book),因此,词性标注的关键问题就是消解这样的歧义,也就是对于句子中的每一个单词在一定的上下文中选择恰如其分的标记。
关于词性标注歧义问题,对Brown语料库进行统计,按歧义程度排列的词型数目(The number of word types in Brown corpus by degree of ambiguity)DeRose(1988)给出了如下的标记歧义表:
无歧义(Unambiguous)只有1个标记: 35,340
歧义(Ambiguous) 有2-7个标记: 4,100
2个标记:3,764
3个标记:264
4个标记:61
5个标记:12
6个标记:2
7个标记:1
可见英语中的大多数单词都是没有歧义的,也就是这些单词只有一个单独的标记。但是,英语中的最常用单词很多都是有歧义的,因此,任何一个词性标注算法的关键归根结底还是如何解决词性标注中的歧义消解问题。
大多数的标注算法可以归纳为三类:
- 一类是基于规则的标注算法(rule-based tagger),
- 一类是随机标注算法(stochastic tagger),
- 最后一类是混合型的标注算法。
基于规则的标注算法一般都包括一个手工制作的歧义消解规则库;
随机标注算法一般会使用一个训练语料库来计算在给定的上下文中某一给定单词具有某一给定标记的概率,如基于HMM的标注算法;
而混合型标注算法具有上述两种算法的特点,如TBL标注算法。
1、对词性标注问题进行提炼:词性标注本质上是一个分类问题,对于句子中的每一个单词W,找到一个合适的词类类别T,也就是词性标记,不过词性标注考虑的是整体标记的好坏,既整个句子的序列标记问题;
2、抽象为数学模型:对于分类问题,有很多现成的数学模型和框架可以套用,譬如HMM、最大熵模型、条件随机场、SVM等等;
3、求出模型的解:上述模型和框架一旦可以套用,如何求解就基本确定好了,就像HMM中不仅描述了三大基本问题,并相应的给出了求解方案一样;
4、验证模型的合理性:就是词性标注的准确率等评测指标了,在自然语言处理中属于必不可少的评测环节;
5、解释现实问题:如果词性标注的各项指标够好,就可以利用该数学模型构造一个词性标注器来解决某种语言的标注问题了
如何建立一个与词性标注问题相关联的HMM模型?
首先必须确定HMM模型中的隐藏状态和观察符号(观察状态),由于我们是根据输入句子输出词性序列,因此可以将词性标记序列作为隐藏状态,而把句子中的单词作为观察符号,那么对于Brown语料库来说,就有87个隐藏状态(标记集)和将近4万多个观察符号(词型)。
确定了隐藏状态和观察符号,我们就可以根据训练语料库的性质来学习HMM的各项参数了。
如果训练语料已经做好了标注,那么学习这个HMM模型的问题就比较简单,只需要计数就可以完成HMM各个模型参数的统计,如标记间的状态转移概率可以通过如下公式求出:
P(Ti|Tj) = C(Tj,Ti)/C(Tj)
在已知T_J状态下 后面的词性是T_I的数目
而每个状态(标记)随对应的符号(单词)的发射概率可由下式求出:
P(Wm|Tj) = C(Wm,Tj)/C(Tj)
这个单词W_m标记为T_j的数目/T_j的个数
其中符号C代表的是其括号内因子在语料库中的计数。
如果训练语料库没有标注,那么HMM的第三大基本问题“学习”就可以派上用处了,通过一些辅助资源,如词典等,利用前向-后向算法也可以学习一个HMM模型,不过这个模型比之有标注语料库训练出来的模型要差一些。
总之,我们已经训练了一个与语料库对应的HMM词性标注模型,
那么如何利用这个模型来解决词性标注问题呢?当然是采用维特比算法解码了, HMM模型第二大基本问题就是专门来解决这个问题的。
已知HMM 求出现观察序列的状态序列:已知单词序列 求状态序列(词性)
原计划这一节讲解如何利用UMDHMM这个HMM工具包来实现一个toy版本的HMM词性标注器,自己也写了几个相关的小脚本,不过由于处理过程中需要借用Philip Resnik教授写的另外几个小脚本,所以这里先介绍一下他的工作。
Resnik教授利用UMDHMM写了一个关于如何使用隐马尔科夫模型的介绍和练习,主要目标包括以下四个方面:
1、 在一个“似英语”的文本上训练一个HMM模型(Train an HMM on a sample of English-like text );
2、对于训练好的模型进行检测(Inspect the resulting model);
3、根据训练好的模型随机生成句子(Generate sentences at random from the model);
4、对于测试句子寻找最可能隐藏状态序列(Create test sentences and find the most likely hidden state sequence)。
我的工作和Resnik教授的主要区别再于,他的训练集没有进行词性标注,利用了前向-后向算法生成HMM模型,并且需要读者有一定想象能力来作虚拟词性标注;
而我所用的训练集是有标注的,主要是通过统计的方法生成HMM模型,并且对于测试集标注是直观的。
但是,殊途同归,都是为了建立一个HMM模型,都是为了能利用UMDHMM。
主要包括几个perl脚本,UMDHMM编译后生成的几个可执行程序以及两个样例文件夹,需要注意的是,几个perl脚本需要根据你所使用的环境修改perl的执行路径,另外UMDHMM的几个可执行程序是Resnik教授在Solaris 5.5系统下编译的,并不适用于其他操作系统,因此最好自己单独编译一下UMDHMM,
对于这几个perl脚本,需要作一点预处理,
第一使其可执行:chmod u+x *.pl 或 chmod 755 *.pl;
第二,修改每个脚本的perl解释器目录,由于我用的是ubuntu9.04,所以处理的方法是,注释掉第一行,将第二行”/usr/local/bin/perl5“修改为“/usr/bin/perl”。
修改完毕perl脚本使其可执行之后,就可以进入example0目录进行练习了:cd example0
example0目录下有一个example0.train文件,只有一行,但是包含了一百句英语句子,这一百句英语句子只用了11个单词和两个标点符号”.”和“?”生成,是一个“似英语”句子生成器生成的,主目录下有这个程序,是lisp程序写的,我不明白怎么使用。如下所示部分句子:
the plane can fly . the typical plane can see the plane . a typical fly can see . who might see ? the large can might see a can . the can can destroy a large can …
对于这个训练集,Resnik教授建议读者写一个简单的词性列表,并尝试为每一个单词分配一个词性标记,并且同一个单词可以有不同的标记。
注意这个练习并不是要在这个文件中进行,可以在别的地方,譬如纸上或者心里都可以,不做也行的。
我就偷懒了,因为不知道如何标记,并且手工标记的工作量较大,我用了一个基于Brown语料库训练的词性标注器标注了一下,这个之后再详细说明。
由于UMDHMM这个工具包处理的都是数字而非符号,所以需要先将这个训练集转换为数字序列,由create_key.pl这个脚本完成:
../create_key.pl example0.key < example0.train > example0.seq
这一步在example0里生成两个文件:example0.key及example0.seq,前者主要将训练集中出现的单词符号映射为数字编号,如:
1 the
2 plane
8 a
4 fly
3 can
7 see
12 large
11 ?
10 might
9 who
6 typical
5 .
13 destroy
后者主要根据example0.key中的编号对训练集进行转换,并且形式为UMDHH中的训练集输入形式,如:
T= 590
1 2 3 4 5 1 6 2 3 7 1 2 5 8 6 4 3 7 5 9 10 7 11 1 12 3 10 7 8 3 5 1 3 3 13 8 12 3 5 9 10 7 11 9 10 4 11 9 3 4 11 1 3 10 7 5 1 2 3 4 8 6 4 5 9 3 4 11 1 12 4 3 4 5 9 3 7 11 9 3 7 8 3 11…
其中T代表了训练集中的单词符号数目。
就是相当于把应为序列转成数字序列。
在将训练集转换成UMDHMM需要的形式后,就可以利用UMDHMM中编译好的可执行程序esthmm来训练HMM模型了
首先我们利用genseq来随机生成句数字
../genseq -T 10 example0.hmm > example0.sen.seq
把数字转成英文
../ints2words.pl example0.key < example0.sen.seq > example0.sen
上面的是把数字转成英文
注意这两步可以利用管道命令合并在一起:
../genseq -T 10 example0.hmm | ../ints2words.pl example0.key
< 后面跟着的是输入的文本 >后面跟着的是输出的文本
最后一个练习也是最重要的一个:对于一个测试句子寻找其最可能的隐藏状态序列(Finding the Hidden State Sequence for a Test Sentence),对于本文来说,也就是词性序列了。我们使用testvit来完成这个任务,当然,前提是先准备好测试句子。可以根据exampl0.key中的单词和标点自己组织句子,也可以利用上一个练习随机生成一个句子,不过我选择了训练集中的第91句,比较典型:
the can can destroy the typical fly .
虽然违背了自然语言处理中实验的训练集与测试集分离的原则,不过考虑到这只是一个练习,另外也是为下一节做个小准备,我们就以此句话为例建立一个文件example0.test.words。不过UMDHMM还是只认数字,所以Resnik教授有为我们写了一个words2seq.pl处理此事:
../words2seq.pl example0.key < example0.test.words > example0.test
example0.test就是UMDHMM可以使用的测试集了,如下所示:
T= 8
1 3 3 13 1 6 4 5
现在就可以使用testvit,这次Resnik教授没有写错:
../testvit example0.hmm example0.test
看到结果了吗?我们得到了一个隐藏状态序列:
…
Optimal state sequence:
T= 8
6 1 5 2 6 3 1 7
…
如果之前你已经建立好了隐藏状态与词性标记的一一映射,那么就可以把它们所对应的词性标记一个一个写出来了!这个词性标注结果是否与你的期望一样?
如果你还没有建立这个映射,那么就可以好好发挥一下想象力了!
虽然这一过程展示了自然语言处理中EM算法在无监督学习任务中的重要作用,但是这类方法的标注准确性还相对较低,在实际应用中多是那些建立在有词性标注训练集基础上的机器学习算法,如最大熵模型、决策树等,所学习的词性标注器能获得较高的标注准确率。
本节我们就以一个标注好的训练集为基础,来学习一个最简单的HMM词性标注器。
首先就是准备训练集,作为一个练习,52nlp也本着尽量简单的原则,所以这里仍然选用Resnik教授所使用的example0.train,这个训练集虽然包含了一百句“似英语”的句子,但是只有一行,所以我们首先做一个断句处理,不过这些句子只采用了“.”和“?”作为句尾标志,因此断句相对简单。不过实际处理中英文断句问题比较麻烦,也有很多学者这方面做了很多研究工作。这里52nlp写了一个简单的sentsplit.pl脚本来处理这个训练集:
./sentsplit.pl example0.train example0.sentences
example0.sentences就成了每一句为一行的训练集,如下所示:
the plane can fly .
the typical plane can see the plane .
a typical fly can see .
who might see ?
the large can might see a can .
the can can destroy a large can .
…
但是,这个训练集只包含纯粹的单词句子,因此需要做一下词性标注,当然人工标注并检查是最好的了,但是我不懂,于是找了一个开源的词性标注工具对这些句子进行了标注,关于这个词性标注器的细节,下一节我会具体介绍,先来看标注后得到的包含词性标记的训练集example0.all,部分示例如下:
the/at plane/nn can/md fly/vb ./.
the/at typical/jj plane/nn can/md see/vb the/at plane/nn ./.
a/at typical/jj fly/nn can/md see/vb ./.
who/wps might/md see/vb ?/.
the/at large/jj can/nn might/md see/vb a/at can/nn ./.
…
无论什么方法,建立HMM词性标注器的关键就是根据这个训练集来学习一个合适的HMM模型了。我们先来确定HMM模型中的隐藏状态(词性标记)和观察符号(词型),这里只考察能从训练集中观察的到的词性标记和词型,因此上一节用到的create_key.pl这个脚本就可以派上用处了。对于确定训练集中的词型,利用example0.sentences就可以:
../create_key.pl words.key < example0.sentences > example0.seq
所得到的words.key就包含了训练集中的词型及其数字编号:
1 the
2 plane
8 a
4 fly
3 can
7 see
12 large
11 ?
10 might
9 who
6 typical
5 .
13 destroy
注意另一个副产品example0.seq在这一节里并不需要。同样我们也需要利用create_key.pl来确定训练集中的词性标记及其编号,不过这里我们需要先将example0.all中的词性标记序列抽取出来。这里52nlp写了一个简单的脚本extractpos.pl来处理此事:
./extractpos.pl example0.all example0.pos
所得到的example0.pos文件部分示例如下:
at nn md vb .
at jj nn md vb at nn .
at jj nn md vb .
wps md vb .
at jj nn md vb at nn .
at nn md vb at jj nn .
…
有了这个文件,就可以再次利用create_key.pl了:
../create_key.pl pos.key < example0.pos > example0.posseq
所得到的pos.key就包含了训练集中的词性标记及其数字编号:
4 vb
6 jj
3 md
2 nn
7 wps
5 .
1 at
同样,另一个副产品example0.posseq这里也不需要。
确定好了该HMM模型中的隐藏状态(词性标记)和观察符号(词型)后,下一步便是要计算HMM模型中其他三个基本要素了,包括初始概率向量pi, 状态转移矩阵A,混淆矩阵B。
我们先预处理一下语料库,主要的目标是对一元词性、二元词性及词型与词性的组合进行计数,这里52nlp写了一个脚本pretrain.pl来处理此事:
./pretrain.pl example0.all lex ngram
所得到的lex文件主要是统计词型及其词性标记的组合在训练集中出现的次数:
typical jj 25
large jj 22
might md 42
fly nn 20
a at 58
? . 57
plane nn 34
the at 35
who wps 57
can nn 39
see vb 45
destroy vb 9
fly vb 46
. . 43
can md 58
ngram文件主要包含的是一元词性及二元词性在训练集中的出现次数:
vb 100
jj 47
md 100
nn 93
wps 57
. 100
at 93
vb . 50
md vb 100
vb at 50
at jj 47
wps md 57
nn . 50
at nn 46
jj nn 47
nn md 43
有了这几个预处理文件,我们就可以训练一个简单的HMM词性标注模型了,这里52nlp写了一个约100行的脚本hmmtrain.pl来处理此事:
./hmmtrain.pl words.key pos.key ngram lex example.hmm
其中前四个是输入(准备)文件,最后一个example.hmm是输出文件,也就是本节的核心目标:一个合适的HMM词性标注模型,我们来简单看一下example.hmm:
M= 13
N= 7
A:
0.0100 0.4700 0.0100 0.0100 0.0100 0.4800 0.0100
…
B:
0.3396 0.0094 0.0094 0.0094 0.0094 0.0094 0.0094 0.5566 0.0094 0.0094 0.0094 0.0094 0.0094
…
pi:
0.1576 0.1576 0.1695 0.1695 0.1695 0.0797 0.0966
有兴趣的读者,可以对比一下上一节利用BaumWelch算法(前向-后向算法)所学习的HMM词性标注模型example0.hmm。
关于这个脚本,其中对于状态转移矩阵A,混淆矩阵B的计算采用了最简单的加一平滑来处理那些在训练集中的未出现事件, 关于加一平滑,不清楚读者可以在“MIT自然语言处理第三讲:概率语言模型(第四部分)” 中找到参考,或者任何一本自然语言处理书中关于ngram语言模型的章节都会介绍的。
加一平滑:(ii. 数据稀疏问题(Sparsity)
1. 未知事件的总计概率构成了测试数据的很大一部分(The aggregate probability of unseen events constitutes a large fraction of the test data)
2. Brown et al (1992): 考虑一个3.5亿词的英语语料库,14%的三元词是未知的)
c) 加一(Laplace)平滑(Add-One (Laplace) Smoothing)
i. 最简单的打折方法(Simplest discounting technique):
{P(w_{i}/w_{i-1})} = {C(w_{i-1},w_{i})+1}/{C(w_{i-1})+V}
这里V是词汇表的数目——语料库的“型”(where |ν| is a vocabulary size
现在我们就可以作上一节最后一个词性标注的练习了,仍然选择训练集中的第91句:
the can can destroy the typical fly .
可以利用Resnik教授的words2seq.pl来对此句进行转换,或者利用上一节已经处理好的UMDHMM可读的example0.test:
T= 8
1 3 3 13 1 6 4 5
现在就可以使用testvit及刚刚训练好的example.hmm来作词性标注了:
../testvit example.hmm example0.test
同样得到了一个隐藏状态序列:
…
Optimal state sequence:
T= 8
1 2 3 4 1 6 2 5
…
不过这次我们已经有了词性标记序列及其数字编号,可以对应着把它们写出来:
at nn md vb at jj nn .
与测试句子合在一起即是:
the/at can/nn can/md destroy/vb the/at typical/jj fly/nn ./.
对照example.all里的第91句:
the/at can/nn can/md destroy/vb the/at typical/jj fly/nn ./.
二者是一样的,不过这个绝不能说明此HMM词性标注器是100%正确的。
好了,本节就到此为止了,这一节的相关例子及小脚本可以单独按链接下载,也可以打包在这里供下载:52nlpexample.tar.gz
不过这套小工具还不足以处理实际问题中的词性标注问题,下一节我将介绍一个更加健壮的HMM词性标注开源工具。
介绍一个开源的HMM词性标注工具(citar)并且利用Brown语料库构造一个英文词性标注器。
上一节借用umdhmm构造的HMM词性标注工具是二元语法(bigram)标注器,因为我们只考虑了前一个词性标记和当前词性标记,算的上是最基本的马尔科夫模型标注器。这个HMM词性标注器可以通过好几种方式进行扩展,一种方式就是考虑更多的上下文,不只考虑前面一个词性标记,而是考虑前面两个词性标记,这样的标注器称之为三元语法(trigram)标注器,是非常经典的一种词性标注方法,在《自然语言处理综论》及《统计自然语言处理基础》中被拿来介绍。
正因为经典, 所以老外已经做足了功课,包括paper以及开源工具,我查了一下,其中比较有名的一个是TnT,作者既写了一篇“TnT — Statistical Part-of-Speech Tagging”,被引用869次,又开发了一套开源工具(http://www.coli.uni-saarland.de/~thorsten/tnt/),可谓“知行合一”。但是要获得这个工具必须填一个表,并且传真给对方,比较麻烦。不过幸好在英文维基百科关于词性标注的介绍页面上有替代品:Part-of-speech_tagging.
在这个页面的“External links(外部链接)”的最后一行,提到了一个名叫Citar的利用C++开发的隐马尔科夫模型(HMM)三元语法词性标注器:
“Citar LGPL C++ Hidden Markov Model trigram POS tagger, a Java port named Jitar is also available”
Citar is a simple part-of-speech tagger, based on a trigram Hidden Markov Model (HMM). It (partly) implements the ideas set forth in [1]. Citaris written in C++. There is also a Java/JDK counterpart named Jitar,
which is available at: http://code.google.com/p/jitar/
其中[1]指的是“TnT — Statistical Part-of-Speech Tagging”,其具体的实现思想在这篇文章里描述的很细致,我觉得主要需要注意的几个地方是trigram的平滑算法,未登录词的处理方法(主要是针对英文的),以及柱搜索(beam search)解码算法。先cmake . 之后make
编译citar直接make就可以了,生成三个可执行文件:train,tag,evaluate。
顾名思义,“train”是用来一些必要的文件的,tag则是进行标注的,而evaluate则是用来评价标注结果的。下面我们以Brown语料库为例来演示如何使用这三个可执行程序。
关于Brown语料库,我是从NLTK的包中得到的,NLTK提供了两个版本的语料库,一个是纯文本格式,另外一个是XML格式,都进行了词性标注,如果你对NLTK不熟悉,可以从下面两个链接直接下载这两个语料库:
1、XML格式的brown语料库,带词性标注;
2、普通文本格式的brown语料库,带词性标注;
至于Brown语料库的具体介绍,大家可以参考这个页面:BROWN CORPUS MANUAL。在这个练习中,我采用的是纯文本格式的brown语料库,但是这些文件是按照类别分布在很多小文件里,并且包含很多空行,所以我处理了一下这个文件,把它们合并到一个大的文件中,并且去除了行首的空格及空行,共得到57340个带词性标注的句子(brown.corpus)。我们首先对这个语料库进行划分,从中选取前55340句作为训练集(brown.train),选取剩余的2000句作为测试集(brown.test),现在我们就来运行这三个命令。
首先利用train来训练:
../train brown.train brown.lex brown.ngram
其中输入文件是训练集brown.train,而输出文件是brown.lex及brown.ngram,如果大家还记着上一节里我也生成了两个前处理文件lex和ngram的话,那么就不难理解这两个文件的内容含义了。事实上,我当时就是模仿citar的这个预处理思想做得,只是结果文件里的格式稍有不同而已。
有了上述两个文件,就可以利用tag进行词性标注了,我们拿citar里的一个示例句子来实验一下:
echo “The cat is on the mat .” | ../tag brown.lex brown.ngram
得到如下的结果:
The/at cat/nn is/bez on/in the/at mat/nn ./.
如果对一个没有标注的文件进行标注,可以利用如下的命令:
../tag brown.lex brown.ngram < input > output
最后,我利用evaluate来验证一下基于brown.train训练出来的词性标注器的准确率,在测试集brown.test上进行测试:
../evaluate brown.lex brown.ngram brown.test
得到如下的结果:
Accuracy (known): 0.964621
Accuracy (unknown): 0.740937
Accuracy (overall): 0.956389
说明这个词性标注器对于语料库中已存在的词的标注准确率是96.46%,对于未登录词的标注准确率是74.09%,而整体标注准确虑是95.63%。
好了,关于Citar我们就到此为止,有兴趣的读者可以找一些标注好的语料库来试试,包括中文的词性标注语料库,只不过它用于英文的未登录词处理方法对于中文并不合适而已。上面所提到的几个文件,包括处理好的brown.corpus,训练集brown.train,测试集brown.test及中间生成的brown.lex,brown.ngram我已经打包放在了网络硬盘里,