分词系统研究完整版ICTClAS分词系统是由中科院计算所的张华平、刘群所开发的一套获得广泛好评的分词系统,难能可贵的是该版的Free版开放了源代码,为我们很多初学者提供了宝贵的学习材料。但有一点不完美的是,该源代码没有配套的文档,阅读起来可能有一定的障碍,尤其是对C/C++不熟的人来说.本人就一直用Java/VB作为主要的开发语言,C/C++上大学时倒是学过,不过工作之后一直没有再使用过,语法什么的忘的几乎一干二净了.但语言这东西,基本的东西都相通的,况且Java也是在C/C++的基础上形成的,有一定的相似处.阅读一遍源代码,主要的语法都应该不成问题了.虽然在ICTCLAS的系统中没有完整的文档说明,但是我们可以通过查阅张华平和刘群发表的一些相关论文资料,还是可以窥探出主要的思路.该分词系统的主要是思想是先通过CHMM(层叠形马尔可夫模型)进行分词,通过分层,既增加了分词的准确性,又保证了分词的效率.共分五层,如下图一所示:基本思路:先进行原子切分,然后在此基础上进行N-最短路径粗切分,找出前N个最符合的切分结果,生成二元分词表,然后生成分词结果,接着进行词性标注并完成主要分词步骤.下面是对源代码的主要内容的研究:1.首先,ICTCLAS分词程序首先调用CICTCLAS_WinDlg::OnBtnRun()开始程序的执行.并且可以从看出它的处理方法是把源字符串分段处理。并且在分词前,完成词典的加载过程,即生成m_ICTCLAS对象时调用构造函数完成词典库的加载。关于词典结构的分析,请参加分词系统研究(二)。voidCICTCLAS_WinDlg::OnBtnRun(){......//在此处进行分词和词性标记if(!m_ICTCLAS.ParagraphProcessing((char*)(LPCTSTR)m_sSource,sResult))m_sResult.Format(错误:程序初始化异常!);elsem_sResult.Format(%s,sResult);//输出最终分词结果......}2.在OnBtnRun()方法里面调用分段分词处理方法boolCResult::ParagraphProcessing(char*sParagraph,char*sResult)完成分词的整个处理过程,包括分词的词性标注.其中第一个参数为源字符串,第二个参数为分词后的字符串.在这两个方法中即完成了整个分词处理过程,下面需要了解的是在此方法中,如何调用其它方法一步步按照上图所示的分析框架完成分词过程.为了简单起见,我们先不做未登录词的分析。//ParagraphSegmentandPOSTaggingboolCResult::ParagraphProcessing(char*sParagraph,char*sResult){........Processing(sSentence,1);//Processingandoutputtheresultofcurrentsentence.Output(m_pResult[0],sSentenceResult,bFirstIgnore);//Outputtotheimediateresult.......}3.主要的分词处理是在Processing()方法里面发生的,下面我们对它进行进一步的分析.boolCResult::Processing(char*sSentence,unsignedintnCount){......//进行二叉分词m_Seg.BiSegment(sSentence,m_dSmoothingPara,m_dictCore,m_dictBigram,nCount);......//在此处进行词性标注m_POSTagger.POSTagging(m_Seg.m_pWordSeg[nIndex],m_dictCore,m_dictCore);......}4.现在我们先不管词性标注,把注意力集中在二叉分词上,因为这个是分词的两大关键步骤的第一步.参考文章:1.基于层叠隐马模型的汉语词法分析,刘群张华平等2.基于N-最短路径的中文词语粗分模型,张华平刘群===========================================================================ICTCLAS分词系统研究(二)--词典结构ICTCLAS的词典结构是理解分词的重要依据,通过这么一个数据结构设计合理访问速度高效的词典才能达到快速准备的分词的目的。通过阅读和分析源代码,我们可以知道,是程序运行初,先把词典加载到内存中,以提高访问的速度。源代码在Result.cpp的构造函数CResult()内实现了词典和分词规则库的加载。如下代码所示:CResult::CResult(){……m_dictCore.Load(data\\coreDict.dct);m_POSTagger.LoadContext(data\\lexical.ctx);……}我们再跳进Load方法具体分析它是怎样读取数据词典的,看Load的源代码:boolCDictionary::Load(char*sFilename,boolbReset){FILE*fp;inti,j,nBuffer[3];//首先判断词典文件能否以二进制读取的方式打开if((fp=fopen(sFilename,rb))==NULL)returnfalse;//failwhileopeningthefile//为新文件释放内存空间for(i=0;iCC_NUM;i++){//deletethememoryofworditemarrayinthedictionaryfor(j=0;jm_IndexTable[i].nCount;j++)deletem_IndexTable[i].pWordItemHead[j].sWord;delete[]m_IndexTable[i].pWordItemHead;}DelModified();//删除掉修改过的,可以先不管它//CC_NUM:6768,应该是GB2312编码中常用汉字的数目6763个加上5个空位码for(i=0;iCC_NUM;i++){//读取一个整形数字(词块的数目)fread(&(m_IndexTable[i].nCount),sizeof(int),1,fp);if(m_IndexTable[i].nCount0)m_IndexTable[i].pWordItemHead=newWORD_ITEM[m_IndexTable[i].nCount];else{m_IndexTable[i].pWordItemHead=0;continue;}j=0;//根据前面读到的词块数目,循环读取一个个词块while(jm_IndexTable[i].nCount){//读取三字整数,分别为频度(Frequency)/词内容长度(WordLen)/句柄(Handle)fread(nBuffer,sizeof(int),3,fp);m_IndexTable[i].pWordItemHead[j].sWord=newchar[nBuffer[1]+1];//读取词内容if(nBuffer[1])//Stringlengthismorethan0{fread(m_IndexTable[i].pWordItemHead[j].sWord,sizeof(char),nBuffer[1],fp);}m_IndexTable[i].pWordItemHead[j].sWord[nBuffer[1]]=0;if(bReset)//Resetthefrequencym_IndexTable[i].pWordItemHead[j].nFrequency=0;elsem_IndexTable[i].pWordItemHead[j].nFrequency=nBuffer[0];m_IndexTable[i].pWordItemHead[j].nWordLen=nBuffer[1];m_IndexTable[i].pWordItemHead[j].nHandle=nBuffer[2];j+=1;//Getnextitemintheoriginaltable.}}fclose(fp);returntrue;}看完上面的源代码,词典的结构也应该基本清楚了,如下图一所示:图一修改表的数据结构和上图差不多,但是在词块数目后面多了一个nDelete数目,即删除的数目,数据结构如下图二所示:图二GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。词典库图一所示的6768个块即对应GB2312编码中的这个6768个区位.图一中每一个大块代表以该字开头的所有词组,括号内的字为区位码对应的汉字,词典表中并不存在,为了说明方便才添加上去的.如下所示:块6759count:5wordLen:2frequency:0handle:24832word:(黯)淡wordLen:2frequency:1handle:24942word:(黯)淡wordLen:2frequency:3handle:31232word:(黯)然wordLen:6frequency:0handle:27648word:(黯)然神伤wordLen:6frequency:0handle:26880word:(黯)然失色块6760count:1wordLen:2frequency:0handle:28160word:(鼢)鼠块6761count:2wordLen:4frequency:0handle:28160word:(鼬)鼠皮wordLen:2frequency:0handle:28160word:(鼬)獾对修改后如何保存的源代码进行分析:boolCDictionary::Save(char*sFilename){FILE*fp;inti,j,nCount,nBuffer[3];PWORD_CHAINpCur;if((fp=fopen(sFilename,wb))==NULL)returnfalse;//failwhileopeningthefile//对图一中所示的6768个数据块进行遍历for(i=0;iCC_NUM;i++){pCur=NULL;if(m_pModifyTable){//计算修改后有效词块的数目nCount=m_IndexTable[i].nCount+m_pModifyTable[i].nCount-m_pModifyTable[i].nDelete;fwrite(&nCount,sizeof(int),1,fp);pCur=m_pModifyTable[i].pWordItemHead;j=0;//对原表中的词块和修改表中的词块进行遍历,并把修改后的添加到原表中while(pCur!=NULL&&jm_IndexTable[i].nCount){//如果修改表中的词长度小于原表中对应位置的词的长度或者长度相等但nHandle值比原表中的小,则把修改表中的写入到词典文件当中.if(strcmp(pCur-data.sWord,m_IndexTable[i].pWordItemHead[j].sWord)0||(strcmp(pCur-data.sWord,m_IndexTable[i].pWordItemHead[j].sWord)==0&&pCur-data.nHandlem_IndexTable[i].pWordItemHead[j].nHandle)){//OutputthemodifieddatatothefilenBuffer[0]=pCur-data.nFrequency;nBuffer[1]=pCur-d