一种快速分词系统的设计与实现
(***计算机学院)
摘 要: 通过对已有分词算法的分析,一方面用hash加tire树的结构来改进词典,从而提高了分词速度,另一方面,在已有模型的基础上,通过增加规则来保证分词准确率。实验表明整个系统的分词速度和准确率都得到了一定程度的提高。
关键词:分词;hash;Trie树;规则
1 概述
分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。在英文的行文中,单词之间是以空格作为自然分界符的,而中文只有在句与句之间才通过标点或段落来简单划界,词与词之间则没有这样的分界符,因此,计算机上对于中文的处理就要比英文复杂得多。
词是最小的、能独立活动的、有意义的语言成分。计算机的所有语言知识都来自机器词典(给出词的各项信息)、句法规则(以词类的各种组合方式来描述词的聚合现象)以及有关词和句子的语义、语境、语用知识库。汉语信息处理系统只要涉及句法、语义(如检索、翻译、文摘、校对等应用),就需要以词为基本单位。当汉字由句转化为词之后,才能使得句法分析、语句理解、自动文摘、自动分类和机器翻译等文本处理具有可行性。可以说,分词是机器语言学的基础。
在实现分词算法的过程中,有两方面必须考虑:分词的正确率和分词的速度。由于无论哪种分词方法都需要将大量的时间用于计算出待切分语句的可能词,然后通过对切分出的这些词依据统计或语法方面的规则,得到一种最有可能的正确切分结果,来提高分词的正确率。如果能加快初始切分的速度,对于提高整个分词算法的速度也会有很大帮助。
本文在前人基础上设计了一种高效的字典组织结构来提高查找而提高分词速度,又加上二条规则的分词方法来保证分词的正确率。从而从整体上保证了分词的速度和正确率。
2 字典结构介绍
为了解决速度的问题,先介绍两个数据结构:
1.Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
那么在查找汉字的时候,那怎么用hash确定一个汉字位置呢?这就和每种编码的排列有关了,这里主要给出一种hash函数的策略。
对于GB2312编码,设输入的汉字为GBword,我们可以采用公式(C1-176)*94 + (C2-161)确定GBindex。其中,C1表示第一字节,C2表示第二字节。具体如下:
GBindex =((unsigned char)GBword.at(0)-176)*94 + (unsigned char)GBword.at(1) - 161;
2.Trie树
Trie,又称字典树、单词查找树,是一种树形结构,用于保存大量的字符串。它的优点是:利用字符串的公共前缀来节约存储空间。
其基本性质可以归纳为:
1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3. 每个节点的所有子节点包含的字符都不相同。
其基本操作有:查找 插入和删除,当然删除操作比较少见.我在这里只是实现了对整个树的删除操作,至于单个word的删除操作也很简单.
搜索字典项目的方法为:
(1) 从根结点开始一次搜索;
(2) 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;
(3) 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。
(4) 迭代过程……
(5) 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找。
其他操作类似处理.
假设有b,abc,abd,bcd,abcd,efg,hii这6个单词,我们构建的树就是这样的。
图1 Trie树结构图
3.将hash和trie树结合起来则会得到我们所要的字典结构:
图2 hash_Trie树字典结构图
经分析hash的查找时间复杂度为O(1),同理:Trie在查找的时间复杂度也为O(1).那么查询整个词的查找时间复杂度也为O(1)。
3 基于规则的分词算法:
本分词系统也是基于字典的改进型最大匹配算法。
分词算法,目前有很多。比较常用的是正向最大匹配和反向最大匹配算法。但这两种算法对于一些存在多元歧异的句子缺乏较好的支持。以“长春市长春节致词”和“长春市长春药店”这个两个句子为例:
“长春市长春节致词”可以依次拆分为 长春、长春市、市长、长春、春节、致词 这几个词,按照正向最大匹配算法,分词结果是长春市/长/春节/致词,按照反向最大匹配算法,分词结果是长春/市长/春节/致词。
“长春市长春药店”可以依次拆分为长春、长春市、市长、长春、春药、春药店、药店 这几个词,按照正向最大匹配算法,分词结果是 长春市/长春/药店,按照反向最大匹配算法,分词结果是“长春/市长/春药店”。
可见无论是正向还是反向最大匹配,都存在产生歧异的情况。
本系统的算法在正向匹配算法的基础上做了一些改进,用一句话描述就是找到句子中第一个未被匹配的字数最少的单词组合,如果多个组合未被匹配的字数都是最少则找到其中匹配的单词个数最少的组合。匹配顺序是从左至右。还是以上面两个句子为例:
“长春市长春节致词” 按正向最大匹配扫描顺序可以出现如下匹配的单词组合:
1) 长春/市长/春节/致词 匹配单词数4,未匹配字数0
2) 长春市/长春/致词 匹配单词数3,未匹配字数0
不难看出,第一中组合未匹配的字数最少,所以取组合1。
“长春市长春药店”按正向最大匹配扫描顺序可以出现如下匹配的单词组合
1) 长春市/长春/药店 匹配单词数3,未匹配字数0
2) 长春市/春药店 匹配单词数2,未匹配字数1
3) 长春市/春药 匹配单词数2,未匹配字数2
4) 长春市/药店 匹配单词数2,未匹配字数2
5) 长春/市长/春药店 匹配单词数3,未匹配字数0
6) 长春/市长/春药 匹配单词数3,未匹配字数1
7) 长春/市长/药店 匹配单词数3,未匹配字数1
8) 长春/市长/药店 匹配单词数3,未匹配字数1
9) 长春/长春/药店 匹配单词数3,未匹配字数1
可见组合1和组合5 未匹配数最小,匹配单词数相等,但组合1匹配顺序靠前,所以取组合1。
4 实现
以下是其分词的整个流程图:
图3 系统流程图
图中可以看出整个算法的核心由两个一个是它将字典组织成入口为hash结构而存储用trie树的结构,有了这个结构就可以快速的找到处理的句子中可能含有的所有词。这个结构就保证了此算法的快速。
另一个则是用博弈树来找到最佳匹配,而找的核心则是这个系统自定义的规则来找:一个句子中词数最少且未匹配字数最少的为最佳匹配。
下面是是博弈树来找到最佳匹配的流程图:
图4 寻找最佳匹配流程图
参考文献
[1] 张小欢.中文分词系统的设计和实现[D].电子科技大学学位论文 .2010-05-01.
[2] 吴晶晶,荆继武,聂晓峰,王平建.一种快速中文分词词典机制[J].中国科学院研究生完学报.2008-10-16
[3] 阵桂林,王永成,韩客松,王 刚.一种改进的快速分词算法.计算机研究与发展[J].1999-08-20.
[4] 陈小荷.现代汉语自动分析----Visual c++实现[M].北京语言文化大学出版社.1999.
[5] 王骐.博弈树搜索算法的研究及改进[D].浙江大学硕士论文 20060201
[6] 黄昌宁,赵海. 中文分词十年回顾[J] 中文信息学报 2007-03-22
[7] 翟伟斌,周振柳,蒋卓明,许榕生. 汉语分词词典设计[J] 计算机工程与应用2007
最后附上寻找最优匹配的代码:
//得到最优组合
//sentword表示全切分的词,pbegin表示当前
unsigned int getbestcomb(unsigned char first,sentwords* preinfo,strword*pbegin)
{
sentwords *pcurs;
strword *pcurw;
strword *prew;
strword *ptmp;
unsigned int i;
//如果是第一次,则进行下列初始化
if (first==1||pbegin==NULL)
{
pbegin=g_words.head;
first=0;
}
//其它情况时需进行下面的初始化
pcurs=(sentwords*)malloc(sizeof(sentwords));
if (pcurs==NULL)
{
return NULL;
}
initcomsent(pcurs);
if (preinfo!=NULL)
{
for (i=0;i<preinfo->wordcnt-1;i++)
{
pcurs->words[i]=preinfo->words[i];
}
pcurs->wordcnt=preinfo->wordcnt;
pcurs->blanks=preinfo->blanks+(pbegin->pos-preinfo->words[preinfo->wordcnt-1]->pos);
pcurs->words[pcurs->wordcnt-1]=pbegin;
}
else if (preinfo==NULL)
{
pcurs->blanks+=pbegin->pos;
pcurs->words[pcurs->wordcnt]=pbegin;
pcurs->wordcnt++;
}
prew=pbegin;
pcurw=prew->next;
//进入循环
while (pcurw!=NULL)
{
ptmp=prew;
if ((prew->pos+strlen(prew->word))<=pcurw->pos)
{
pcurs->blanks=pcurs->blanks+(pcurw->pos-(prew->pos+strlen(prew->word)));
pcurs->words[pcurs->wordcnt]=pcurw;
pcurs->wordcnt++;
prew=pcurw;
pcurw=pcurw->next;
}
else
{
//当有分支,则进行递归调用
getbestcomb(first,pcurs,pcurw);
//prew=pcurw;因为此行代码调了2个小时,
//而这个得到最优组合的函数共写了3个小时。
pcurw=pcurw->next;
}
// prew=pcurw;
// pcurw=pcurw->next;
}
if (minblank>pcurs->blanks||
minblank==pcurs->blanks&&mincnt>pcurs->wordcnt)
{
if (g_segresult!=NULL)
{
free((void *)g_segresult);
}
mincnt=pcurs->wordcnt;
minblank=pcurs->blanks;
g_segresult=pcurs;
}
else
{
free((void *)pcurs);
}
return SYS_OK;
}