利用统计进行中文分词与词性分析
今天,翻出了我以前在本科阶段写的一些论文,虽然有几篇没有发表。突然发现很多还是比较实用,虽然学术价值并不是很大,于是我重新整理了下,用最简单的方式,摘要了部分出来拼成此文,当然拼的原料都是自己的,本文适合初学者,如若转载,请著名版权。
中文分词已经是老调重弹的话题了,传统的基于词库的分词技术应该是目前最基本的分词技术,在这里我不去深入挖掘,什么好什么不好的问题,今天我只想根据我自己的经验,来设计和实现一套中文分词与词性分析的一套系统,系统其实已经实现与Iveely Search Engine中。
我们采用隐马尔可夫模型(HMM)来实现中文分词和词性分析。在使用HMM之前,我们先了解下HMM的基本内容,然后在编码上我们才知道为什么要这样去写(这对读编码非常重要)。当然会和code对比起来。
利用学院派的讲解是这样:隐马尔可夫模型是马尔可夫链的一种,它的状态不能直接观察到,但能通过观测向量序列观察到,每个观测向量都是通过某些概率密度分布表现为各种状态,每一个观测向量是由一个具有相应概率密度分布的状态序列产生。也许看完这个,你一头雾水,因为在N年前,我最开始接触HMM的时候,也是领悟了一段时间,当然如果你很熟悉HMM,直接跳过。从工程的角度:
HMM的五元素(学术名) |
统计中含义(工程含义) |
隐含状态S |
词有的4种隐含状态:单字成词、词头、词中、词尾 |
观察状态O |
所有语料库中的汉字 |
初始状态概率矩阵Pi |
词中各种隐含状态的初始概率 |
隐含状态转移概率矩阵A |
4种隐含状态的转移概率,例如:词头到词尾的转移概率,是一个4*4的矩阵 |
观察状态转移概率矩阵B |
语料库中每一个汉字到4种隐含状态的概率。例如:汉字“我”到词头的概率,到词尾的概率。 |
第一步:统计HMM五种元素值
通过上图的表,你似乎可以开始工作了,但是你知道,没有Corpus,这件事是做不好的,所以,Corpus的选择,是非常好的,我们选用这个Corpus,下一步工作,我们就是利用工程学统计出上述五个元素的。
语料库中的形式如下:
“...废/B除/E 存/B在/E 的/S 所/B有/M制/E 关/B系/E 并/S 不/S 是/S 共/B产/M主/M义/E 所/S 独/B具/E 的/S 特/B征/E...”
B表示:begin(词头),M表示:Middle(词中),S表示Single(单字成词),E表示End(词尾),我想给你这样一个语料库,让你统计上面五个元素的值,应该不是什么问题了。当然我也会附加上主要的代码:
private void Train(string corpus) { string[] states = new string[] { "单字成词", "词头", "词中", "词尾" }; //添加隐含状态 this.AddStates(states); string[] sentences = corpus.Split(this.Delimiter.ToArray(), StringSplitOptions.RemoveEmptyEntries); string lastwordType = states[0]; string currentWordType = string.Empty; foreach (var sentence in sentences) { char[] list = sentence.ToArray(); for (int i = 0; i < list.Length; i = i + 3) { string word = list[i].ToString(CultureInfo.InvariantCulture); string wordType = list[i + 2].ToString(CultureInfo.InvariantCulture).ToLower(); //添加观察状态元素 this.AddObserver(word); if (wordType.Equals("s")) { currentWordType = states[0]; } else if (wordType.Equals("e")) { currentWordType = states[3]; } else if (wordType.Equals("m")) { currentWordType = states[2]; } else if (wordType.Equals("b")) { currentWordType = states[1]; } //初始状态转移矩阵 AddInitialStateProbability(currentWordType, 1); //观察状态转移矩阵 AddComplexProbability(currentWordType, word); //隐含状态转移矩阵 AddTransferProbability(lastwordType, currentWordType, 1); lastwordType = currentWordType; } } }
第二步:维特比最佳路径
维特比是什么?维特比算法是一种动态规划算法用于寻找最有可能产生观测事件序列的-维特比路径-隐含状态序列,特别是在马尔可夫信息源上下文和隐马尔可夫模型中。术语“维特比路径”和“维特比算法”也被用于寻找观察结果最有可能解释相关的动态规划算法。例如在统计句法分析中动态规划算法可以被用于发现最可能的上下文无关的派生(解析)的字符串,有时被称为“维比特分析”。
有了上面维特比的基础知识以及统计完HMM的基本五种元素之后,你会很纳闷,这五种元素计算出来后,意义是什么?如何利用它解决分词问题?下面的问题,我们以一个实例来解说:用户输入了关键字“Iveely 是一款开源的搜索引擎。” 我们该如何分词(解码过程)?
首先,将输入词组分为一个Array:
Iveely
|
是
|
一
|
款
|
开
|
源
|
搜
|
索
|
引
|
擎
|
S
|
S
|
S
|
S
|
S
|
S
|
S
|
S
|
S
|
S
|
B
|
B
|
B
|
B
|
B
|
B
|
B
|
B
|
B
|
B
|
M
|
M
|
M
|
M
|
M
|
M
|
M
|
M
|
M
|
M
|
E
|
E
|
E
|
E
|
E
|
E
|
E
|
E
|
E
|
E
|
那么分词的作用就是,在每一个词有不同的状态的时候,怎么使得他们的概率最大?这就变成了一个路径选择问题。我们可以不利用维特比算法来解决,因为暴力方法一定是一个解决思路,但是算法效率太低,每个字有4种状态,假设10个字,就有4的10次方计算量,然后取出概率最大的那一组就是我们所求的最佳路径。但是我们需要高效率解决这些问题,所以维特比是一种常用的方式。维比特怎么解决这个问题的呢?参考这里 (我开始写了很多,但是都不如这位兄弟专业,偷个小懒啦,哈哈 ^_^)
当然,我不会吝啬的保存自己的代码,Share 给大家:
public int[] Decode(string[] input, out double probability) { int inputLength = input.Length; int stateCount = this.state.Length; int minState; double minWeight; int[,] s = new int[stateCount,inputLength]; double[,] a = new double[stateCount,inputLength]; for(int i = 0; i < stateCount; i++) { object obj = complex.Table[this.state[i]][input[0]]; if(obj != null) { a[i, 0] = (1.0*Math.Log(initialState[this.state[i]])) - Math.Log(double.Parse(obj.ToString())) ; } } for(int t = 1; t < inputLength; t++) { for(int j = 0; j < stateCount; j++) { minState = 0; minWeight = a[0, t - 1] - Math.Log(double.Parse(this.transition.Table[this.state[0]][this.state[j]].ToString())); for(int i = 0; i < stateCount; i++) { double weight = a[i, t - 1] - Math.Log(double.Parse(this.transition.Table[this.state[i]][this.state[j]].ToString())); if(weight < minWeight) { minState = i; minWeight = weight; } } object obj = complex.Table[this.state[j]][input[t]]; if(obj != null) { a[j, t] = minWeight - Math.Log(double.Parse(obj.ToString())); s[j, t] = minState; } } } minState = 0; minWeight = a[0, inputLength - 1]; for(int i = 1; i < stateCount; i++) { if(a[i, inputLength - 1] < minWeight) { minState = i; minWeight = a[i, inputLength - 1]; } } int[] path = new int[input.Length]; path[inputLength - 1] = minState; for(int m = inputLength - 2; m >= 0; m--) { path[m] = s[path[m + 1], m + 1]; } probability = Math.Exp(-minWeight); return path; }
利用维特比算法,它返回了一个路径数组,数组是一个长度跟我们输入的字符串的长度一样,数组值是0-3的隐含状态,表示该次更属于哪一个状态,通过维特比算法处理后,将词与状态路径合并,我们可以得到这样的一张表:
Iveely |
是 |
一 |
款 |
开 |
源 |
搜 |
索 |
引 |
擎 |
S
|
S
|
S |
S |
S |
S |
S |
S |
S |
S |
B |
B |
B
|
B |
B
|
B |
B
|
B |
B |
B |
M |
M |
M |
M |
M |
M |
M |
M
|
M
|
M |
E |
E |
E |
E
|
E |
E
|
E |
E |
E |
E
|
我想看到上面的表,你已经知道具体的含义了,不用我多说吧?呵呵,让我们看看程序跑完的截图吧:
上面,讲完了基本的分词后,你一定想知道,我们怎么做中文词性分析,在自然语言处理(NLP)中,词性分析难度是很大的,目前据我所知,哈工大NLP实验室的,感觉还挺不错,而且是开源。好了,继续我们的词性分析。
第一步:分词
幸运的是,我们上面已经做好了分词,那么用户给定的输入,我们已经切分好词了:”iveely/是/一款/开源/搜索引擎/“。
第二步:统计学习
对,我们再次进行统计学习,依然利用HMM,不过语料库变了,我们利用1998年01月份人民日报的语料库,大致形式如下:
"...迈向/v 充满/v 希望/n 的/u 新/a 世纪/n ——/w 一九九八年/t 新年/t 讲话/n (/w 附/v 图片/n 1/m 张/q )/w ..."
每一个词的后面都有一个词性,词性表?查看这里
是不是感觉跟分词的时候统计类似?看看下表你就知道了:
HMM的五元素(学院名) |
统计中含义(工程含义) |
隐含状态S |
词有的N种隐含状态:名词、动词、介词… |
观察状态O |
所有语料库中的词语 |
初始状态概率矩阵Pi |
词中各种隐含状态的初始概率 |
隐含状态转移概率矩阵A |
N种隐含状态的转移概率,例如:动词到到名词的转移概率。 |
观察状态转移概率矩阵B |
语料库中每一个词语到N种隐含状态的概率。例如:汉字“我”是名词的概率。 |
我想看到这里,大家都应该很清楚了。后面利用维特比算法解码,算出给定的路径,然后将词性附加给我们的词组。
作者说:HMM并不是一种很完美的方法,只是从某种角度,它可以做分词,可以做词性分析,所有的code,点击这里查看,当然效果需要不断调整和修复。还有很多网友发邮件问我,为何IveelySE 0.4.0还没有发布,原因是很多方面的,最重要的是准确率上不去,不忍心将效果很差的智能搜索给大家,但是在继续整改之后,我也期待早日和大家分享。谢谢一直以来对IveelySE支持的朋友。
------------------------------------------------------------
我的微博:weibo.com/liufanping
世界上最快乐的事情,莫过于为理想而奋斗