查看更多知识: www.daydayxuexi.com
由于前面写的朴素bayes分类器,针对英文文本进行统计分析的,现在要想用于中文文本,则需要对中文文本进行分词。找了好几个分词系统,比如张华平老师的ICTCLAS、吕震宇老师用c#改写的ICTCLAS版本、KTDictSeg分词系统V1.3.01和清华王小飞写的双数组trie树中文分词程序。这里,比较后(衡量指标主要是速度和大小),我选择了双数组trie树的中文分词程序,并把它改成了c#版本。
CoreDict.txt是核心的词库,要用程序把它生成双数组格式词典,则需先生成。若未生成双数组格式词典则需要生成双数组格式的词典,若已有双数组格式的词典则直接加载词典即可。
至于双数组trie树的理论,可以在网上搜索看。这里我提供两篇论文:基于双数组Trie树中文分词研究和双数组Trie树算法优化及其应用研究。
我的改进的地方:改错的地方主要在program.cs里,而改进性能(时间)主要在Segment.cs里。其实原作者写得很粗糙,如
// String temp = gbk.GetString(gbk.GetBytes(m_lpBaseAddress), pIter, gbk.GetByteCount(m_lpBaseAddress) - pIter);
String temp = gbk.GetString(bytes, pIter, 2);//下一个最多为2个字节, modified by kosko
很明显,不需要每次循环都取出gbk.GetBytes(m_lpBaseAddress),我直接在循环外取出了bytes,这节省了约0.4s的时间;其次,我把每次取出内容的长度 由 (gbk.GetByteCount(m_lpBaseAddress) - pIter)改为2,因为每次取一个字符,明显,如果是汉字,便是双字节,需2,如果是英文或者字符集(如\n、\r)则只需1个字节。故我设置为每次取出长度为2,这也节省了约0.4s的时间。
然后,我还改进了字符串追加操作m_cSegTxt.Append(gbk.GetString(bytes, lpszCur, nWordLen)); 这里我把(gbk.GetBytes(m_lpBaseAddress)换成了bytes,而且把gbk.GetByteCount()换成了nWordLen,即在循环外面直接算出gbk.GetByteCount()这节省了0.25s+0.15s的时间。
Note:gbk是Encoding类型。
从而我得出一结论,字符串操作是非常耗费时间的,很多时候尽量避多次免取或存长字符串。当然,IO操作也非常耗费时间,只是我还没掌握里面的精华。
核心算法还是双数组trie树,但经过我改后,原来分一篇文章需要1.37s,现在只需约0.19s。但这对于大型应用来说,效果还太差了。
下面细讲下双数组trie树的算法:
构造双数组trie(base、check):
从总体上来看,都如网上所说:状态s,延续字有c1,c2....cn,那么何如确定状态s的base值呢,这样确定:找一个i值使得base[i+N(c1)]、base[i+N(c2)]、base[i+N(c3)]、base[i+N(c4)].....base[i+N(cn)]均为0,这样便可以确定base[s]=i。*
其实,这只是说了一部分,下面我把自己看了《双数组Trie树算法优化及其应用研究》[1]这篇论文后的做法介绍下。
N(ch)为字符ch的innercode码
struct StateInfo {
public int nStateSub;//当前状态的base和check数组下标,代表着一个字
public int nNextStateCount;//所有下一状态数(当前状态延续字的个数)
public int[] pNextID;//下一状态ID数组,其值为当前状态延续字的内码
} ;
step1. 把字典里的词装入数组words[],并用数组wordSize[]存取每个词的长度,初始化base、check为0
step2.按单词里字的序数(当前字在单词里处于第i位)循环(假设一个单词最大长为30)
step3.把所有第i个字的相关信息提取出来放入结构体数组pStateInfo。
step4.把这个结构体数组pStateInfo按照下一状态数进行排序
step5.按当前状态数组pStateInfo的序数(pStateInfo)进行循环(即第k个状态)
step6.按照*处所讲规则寻找当前状态s的base值i,然后对该当前状态的所有延续字c1、c2、c3...cn进行check赋值, check[i+N(c1)]=s,check[i+N(c2)]=s......check[i+N(cn)]=s。
Note:step6里,进行base值赋值时要注意,如果当前状态已经是最后一个状态(即单词的最后一个字),则base值为-i,如果当前状态时一个单词的最后一个字,但是还可以有延续字组成其他词,然后base值为-s。
通过上面的步骤,应该就可以构建好双数组trie了。
下面再介绍查询算法。
原理很简单:
步1 假设当前状态为s , 输入字符为c , N( c) 为序列码.
步2 循环过程:
t = base[ s ] + N ( c) ;
If ( t > = DA - S IZE)
then 根据s 和字符c 生成的hash 码hash ( s ,ch) 得到t 值.
If ( check [ t ] = s) then
s = t ;
else f ail ;
endif
步3 若base[ t ]不为负, 重复步骤1. 否则, t为一个可结束状态。
下面我打算给出如何有这种算法构造双数组trie的例子,这样看起来更清晰。还是用一般网上利用的那个例子:
词库:啊、阿根廷、阿胶、阿拉伯人、埃及、阿拉、阿根
现列出这些字的内码:啊-1 阿-2 埃-3 根-4 胶-5 拉-6 及-7 廷-8 伯-9 人-10
开始构造
step1.照做,check、base初始化为0
step2. for(j=1;j<=4;j++)//因为最大字长为4
step3.比如,当i为1时,讨论首字,有 啊、阿、埃这三个
啊(0)、阿(5)、埃(1)
step4.排序后,阿(5)、埃(1)、啊(0)
step5. for(k=0;k<3;k++)//因为第1轮有三个状态
step6.先讨论 阿 这个状态,设i=1,即设base[阿]=1,base[i+N(根)]=base[5]==0、base[i+N(胶)]=base[6]==0、base[i+N(拉)]=base[7]==0,故base[阿]=base[2]=1是可以的,故取base[2]=1,现在设置check值,check[5]=s=2,check[6]=s=2,check[7]=s=2.
然后讨论k=1时,也就是 埃 这个状态,设i=1,即设base[埃]=1,base[i+N(及)]=base[8]==0,故可取base[埃]=base[3]=1.
最后讨k=2时,也就是 啊 这个状态,设i=1,即设base[啊]=1,而 啊 没有延续字了,所以是一个词了,故去base[啊]=base[1]=-1*i=-1,check[1]=0.
开始讨论第二轮(也就是所有单词第二个字)//j=2
根(1)、胶(0)、拉(1)、及(0)
排序后:根(1)、拉(1)、胶(0)、及(0)
for (k=0;k<4;k++)
讨论 根(1) 这个状态 ,设i=1,即设base[根]=1, base[i+N(廷)]=base[9]==0,因此可取base[4]=1,check[9]=s=4.一直这样做下去。。。最后得到
base[ ] = { 0 , - 1 , 1 , 1 , - 4 , 1 , - 6 , 1 , - 8 , - 9 ,- 1} ,
check [ ] = { 0 ,0 ,0 ,0 ,10 ,2 ,2 ,2 ,3 ,5 ,7} .
查询就是个简单的事情了,照查询算法做便是了。
出自: http://hi.baidu.com/christole/blog/item/fdfdd496d58f2e7854fb965d.html