分词系统研究完整版
但有一点不完美的是,该源代码没有配套的文档,阅读起来可能有一定的障碍,尤其是对C/C++不熟的人来说.本人就一直用Java/VB作为主要的开发语言,C/C++上大学时倒是学过,不过工作之后一直没有再使用过,语法什么的忘的几乎一干二净了.但语言这东西,基本的东西都相通的,况且Java也是在C/C++的基础上形成的,有一定的相似处.阅读一遍源代码,主要的语法都应该不成问题了.
虽然在ICTCLAS的系统中没有完整的文档说明,但是我们可以通过查阅张华平和刘群发表的一些相关论文资料,还是可以窥探出主要的思路.
该分词系统的主要是思想是先通过CHMM(层叠形马尔可夫模型)进行分词,通过分层,既增加了分词的准确性,又保证了分词的效率.共分五层,如下图一所示:
基本思路:先进行原子切分,然后在此基础上进行N-最短路径粗切分,找出前N个最符合的切分结果,生成二元分词表,然后生成分词结果,接着进行词性标注并完成主要分词步骤.
下面是对源代码的主要内容的研究:
1.首先,ICTCLAS分词程序首先调用CICTCLAS_WinDlg::OnBtnRun()开始程序的执行.并且可以从看出它的处理方法是把源字符串分段处理。并且在分词前,完成词典的加载过程,即生成m_ICTCLAS对象时调用构造函数完成词典库的加载。关于词典结构的分析,请参加分词系统研究(二)。
void CICTCLAS_WinDlg::OnBtnRun()
{
......
//在此处进行分词和词性标记
if(!m_ICTCLAS.ParagraphProcessing((char *)(LPCTSTR)m_sSource,sResult))
m_sResult.Format("错误:程序初始化异常!");
else
m_sResult.Format("%s",sResult);//输出最终分词结果
......
}
2.在OnBtnRun()方法里面调用分段分词处理方法bool CResult::ParagraphProcessing(char *sParagraph,char *sResult)完成分词的整个处理过程,包括分词的词性标注.其中第一个参数为源字符串,第二个参数为分词后的字符串.在这两个方法中即完成了整个分词处理过程,下面需要了解的是在此方法中,如何调用其它方法一步步按照上图所示的分析框架完成分词过程.为了简单起见,我们先不做未登录词的分析。
//Paragraph Segment and POS Tagging
bool CResult::ParagraphProcessing(char *sParagraph,char *sResult)
{
........
Processing(sSentence,1); //Processing and output the result of current sentence.
Output(m_pResult[0],sSentenceResult,bFirstIgnore); //Output to the imediate result
.......
}
3.主要的分词处理是在Processing()方法里面发生的,下面我们对它进行进一步的分析.
bool CResult::Processing(char *sSentence,unsigned int nCount)
{
......
//进行二叉分词
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的词典结构是理解分词的重要依据,通过这么一个数据结构设计合理访问速度高效的词典才能达到快速准备的分词的目的。
通过阅读和分析源代码,我们可以知道,是程序运行初,先把词典加载到内存中,以提高访问的速度。源代码在Result.cpp的构造函数CResult()内实现了词典和分词规则库的加载。如下代码所示:
CResult::CResult()
{
……
m_dictCore.Load("data\\coreDict.dct");
m_POSTagger.LoadContext("data\\lexical.ctx");
……
}
我们再跳进Load方法具体分析它是怎样读取数据词典的,看Load的源代码:
bool CDictionary::Load(char *sFilename,bool bReset)
{
FILE *fp;
int i,j,nBuffer[3];
//首先判断词典文件能否以二进制读取的方式打开
if((fp=fopen(sFilename,"rb"))==NULL)
return false;//fail while opening the file
//为新文件释放内存空间
for( i=0;i<CC_NUM;i++)
{//delete the memory of word item array in the dictionary
for( j=0;j<m_IndexTable[i].nCount;j++)
delete m_IndexTable[i].pWordItemHead[j].sWord;
delete [] m_IndexTable[i].pWordItemHead;
}
DelModified();//删除掉修改过的,可以先不管它
//CC_NUM:6768,应该是GB2312编码中常用汉字的数目6763个加上5个空位码
for(i=0;i<CC_NUM;i++)
{
//读取一个整形数字(词块的数目)
fread(&(m_IndexTable[i].nCount),sizeof(int),1,fp);
if(m_IndexTable[i].nCount>0)
m_IndexTable[i].pWordItemHead=new WORD_ITEM[m_IndexTable[i].nCount];
else
{
m_IndexTable[i].pWordItemHead=0;
continue;
}
j=0;
//根据前面读到的词块数目,循环读取一个个词块
while(j<m_IndexTable[i].nCount)
{
//读取三字整数,分别为频度(Frequency)/词内容长度(WordLen)/句柄(Handle)
fread(nBuffer,sizeof(int),3,fp);
m_IndexTable[i].pWordItemHead[j].sWord=new char[nBuffer[1]+1];
//读取词内容
if(nBuffer[1])//String length is more than 0
{
fread(m_IndexTable[i].pWordItemHead[j].sWord,sizeof(char),nBuffer[1],fp);
}
m_IndexTable[i].pWordItemHead[j].sWord[nBuffer[1]]=0;
if(bReset)//Reset the frequency
m_IndexTable[i].pWordItemHead[j].nFrequency=0;
else
m_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;//Get next item in the original table.
}
}
fclose(fp);
return true;
}
看完上面的源代码,词典的结构也应该基本清楚了,如下图一所示:
图一
修改表的数据结构和上图差不多,但是在词块数目后面多了一个nDelete数目,即删除的数目,数据结构如下图二所示:
图二
GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。词典库图一所示的6768个块即对应GB2312编码中的这个6768个区位.图一中每一个大块代表以该字开头的所有词组,括号内的字为区位码对应的汉字,词典表中并不存在,为了说明方便才添加上去的.如下所示:
块6759
count:5
wordLen:2 frequency:0 handle:24832 word:(黯)淡
wordLen:2 frequency:1 handle:24942 word:(黯)淡
wordLen:2 frequency:3 handle:31232 word:(黯)然
wordLen:6 frequency:0 handle:27648 word:(黯)然神伤
wordLen:6 frequency:0 handle:26880 word:(黯)然失色
块6760
count:1
wordLen:2 frequency:0 handle:28160 word:(鼢)鼠
块6761
count:2
wordLen:4 frequency:0 handle:28160 word:(鼬)鼠皮
wordLen:2 frequency:0 handle:28160 word:(鼬)獾
对修改后如何保存的源代码进行分析:
bool CDictionary::Save(char *sFilename)
{
FILE *fp;
int i,j,nCount,nBuffer[3];
PWORD_CHAIN pCur;
if((fp=fopen(sFilename,"wb"))==NULL)
return false;//fail while opening the file
//对图一中所示的6768个数据块进行遍历
for(i=0;i<CC_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&&j<m_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.nHandle<m_IndexTable[i].pWordItemHead[j].nHandle))
{//Output the modified data to the file
nBuffer[0]=pCur->data.nFrequency;
nBuffer[1]=pCur->data.nWordLen;
nBuffer[2]=pCur->data.nHandle;
fwrite(nBuffer,sizeof(int),3,fp);
if(nBuffer[1])//String length is more than 0
fwrite(pCur->data.sWord,sizeof(char),nBuffer[1],fp);
pCur=pCur->next;//Get next item in the modify table.
}
//频度nFrequecy等于-1说明该词已被删除,跳过它
else if(m_IndexTable[i].pWordItemHead[j].nFrequency==-1)
{
j+=1;
}
//如果修改表中的词长度比原表中的长度大或 长度相等但句柄值要多,就把原表的词写入的词典文件中
else 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.nHandle>m_IndexTable[i].pWordItemHead[j].nHandle))
{//Output the index table data to the file
nBuffer[0]=m_IndexTable[i].pWordItemHead[j].nFrequency;
nBuffer[1]=m_IndexTable[i].pWordItemHead[j].nWordLen;
nBuffer[2]=m_IndexTable[i].pWordItemHead[j].nHandle;
fwrite(nBuffer,sizeof(int),3,fp);
if(nBuffer[1])//String length is more than 0
fwrite(m_IndexTable[i].pWordItemHead[j].sWord,sizeof(char),nBuffer[1],fp);
j+=1;//Get next item in the original table.
}
}
//把原表中剩余的词写入的词典文件当中
if(j<m_IndexTable[i].nCount)
{
while(j<m_IndexTable[i].nCount)
{
if(m_IndexTable[i].pWordItemHead[j].nFrequency!=-1)
{//Has been deleted
nBuffer[0]=m_IndexTable[i].pWordItemHead[j].nFrequency;
nBuffer[1]=m_IndexTable[i].pWordItemHead[j].nWordLen;
nBuffer[2]=m_IndexTable[i].pWordItemHead[j].nHandle;
fwrite(nBuffer,sizeof(int),3,fp);
if(nBuffer[1])//String length is more than 0
fwrite(m_IndexTable[i].pWordItemHead[j].sWord,sizeof(char),nBuffer[1],fp);
}
j+=1;//Get next item in the original table.
}
}
else////原表已到尾部但修改表还没有遍历完,把修改表中剩余的词写入到词典文件当中
while(pCur!=NULL)//Add the rest data to the file.
{
nBuffer[0]=pCur->data.nFrequency;
nBuffer[1]=pCur->data.nWordLen;
nBuffer[2]=pCur->data.nHandle;
fwrite(nBuffer,sizeof(int),3,fp);
if(nBuffer[1])//String length is more than 0
fwrite(pCur->data.sWord,sizeof(char),nBuffer[1],fp);
pCur=pCur->next;//Get next item in the modify table.
}
}
//不是修改标记,则把原表的数据全部写入到词典文件当中
else
{
fwrite(&m_IndexTable[i].nCount,sizeof(int),1,fp);
//write to the file
j=0;
while(j<m_IndexTable[i].nCount)
{
nBuffer[0]=m_IndexTable[i].pWordItemHead[j].nFrequency;
nBuffer[1]=m_IndexTable[i].pWordItemHead[j].nWordLen;
nBuffer[2]=m_IndexTable[i].pWordItemHead[j].nHandle;
fwrite(nBuffer,sizeof(int),3,fp);
if(nBuffer[1])//String length is more than 0
fwrite(m_IndexTable[i].pWordItemHead[j].sWord,sizeof(char),nBuffer[1],fp);
j+=1;//Get next item in the original table.
}
}
}
fclose(fp);
return true;
}
增加一个词条目:
bool CDictionary::AddItem(char *sWord, int nHandle,int nFrequency)
{
char sWordAdd[WORD_MAXLENGTH-2];
int nPos,nFoundPos;
PWORD_CHAIN pRet,pTemp,pNext;
int i=0;
//预处理,去掉词的前后的空格
if(!PreProcessing(sWord, &nPos,sWordAdd,true))
return false;
//查找词典原表中该词是否存在
if(FindInOriginalTable(nPos,sWordAdd,nHandle,&nFoundPos))
{//The word exists in the original table, so add the frequency
//Operation in the index table and its items
if(m_IndexTable[nPos].pWordItemHead[nFoundPos].nFrequency==-1)
{//The word item has been removed
m_IndexTable[nPos].pWordItemHead[nFoundPos].nFrequency=nFrequency;
if(!m_pModifyTable)//Not prepare the buffer
{
m_pModifyTable=new MODIFY_TABLE[CC_NUM];
memset(m_pModifyTable,0,CC_NUM*sizeof(MODIFY_TABLE));
}
m_pModifyTable[nPos].nDelete-=1;
}
else
m_IndexTable[nPos].pWordItemHead[nFoundPos].nFrequency+=nFrequency;
return true;
}
//如果修改表为空,为它初始化空间
if(!m_pModifyTable)//Not prepare the buffer
{
m_pModifyTable=new MODIFY_TABLE[CC_NUM];
memset(m_pModifyTable,0,CC_NUM*sizeof(MODIFY_TABLE));
}
//在修改表中查询该词是否存在,如果存在增加该词的频度
if(FindInModifyTable(nPos,sWordAdd,nHandle,&pRet))
{
if(pRet!=NULL)
pRet=pRet->next;
else
pRet=m_pModifyTable[nPos].pWordItemHead;
pRet->data.nFrequency+=nFrequency;
return true;
}
//如果没有在修改表中找到,则添加进去
pTemp=new WORD_CHAIN;//Allocate the word chain node
if(pTemp==NULL)//Allocate memory failure
return false;
memset(pTemp,0,sizeof(WORD_CHAIN));//init it with 0
pTemp->data.nHandle=nHandle;//store the handle
pTemp->data.nWordLen=strlen(sWordAdd);
pTemp->data.sWord=new char[1+pTemp->data.nWordLen];
strcpy(pTemp->data.sWord,sWordAdd);
pTemp->data.nFrequency=nFrequency;
pTemp->next=NULL;
//插入到修改表中
if(pRet!=NULL)
{
pNext=pRet->next;//Get the next item before the current item
pRet->next=pTemp;//link the node to the chain
}
else
{
pNext=m_pModifyTable[nPos].pWordItemHead;
m_pModifyTable[nPos].pWordItemHead=pTemp;//Set the pAdd as the head node
}
pTemp->next=pNext;//Very important!!!! or else it will lose some node
//把词块数目加一
m_pModifyTable[nPos].nCount++;//the number increase by one
return true;
}
删除修改过的词条
bool CDictionary::DelModified()
{
PWORD_CHAIN pTemp,pCur;
if(!m_pModifyTable)
return true;
for(int i=0;i<CC_NUM;i++)
{
pCur=m_pModifyTable[i].pWordItemHead;
//删除链表上的节点
while(pCur!=NULL)
{
pTemp=pCur;
pCur=pCur->next;
delete pTemp->data.sWord;
delete pTemp;
}
}
delete [] m_pModifyTable;
m_pModifyTable=NULL;
return true;
}
//采用二分法进行查找
bool CDictionary::FindInOriginalTable(int nInnerCode,char *sWord,int nHandle,int *nPosRet)
{
PWORD_ITEM pItems=m_IndexTable[nInnerCode].pWordItemHead;
int nStart=0,nEnd=m_IndexTable[nInnerCode].nCount-1,nMid=(nStart+nEnd)/2,nCount=0,nCmpValue;
while(nStart<=nEnd)//Binary search
{
nCmpValue=strcmp(pItems[nMid].sWord,sWord);
//如果中间那个正好是要查找的
if(nCmpValue==0&&(pItems[nMid].nHandle==nHandle||nHandle==-1))
{
if(nPosRet)
{
if(nHandle==-1)//Not very strict match
{//Add in 2002-1-28
nMid-=1;
//Get the first item which match the current word
while(nMid>=0&&strcmp(pItems[nMid].sWord,sWord)==0)
nMid--;
if(nMid<0||strcmp(pItems[nMid].sWord,sWord)!=0)
nMid++;
}
*nPosRet=nMid;
return true;
}
if(nPosRet)
*nPosRet=nMid;
return true;//find it
}
else if(nCmpValue<0||(nCmpValue==0&&pItems[nMid].nHandle<nHandle&&nHandle!=-1))
{
nStart=nMid+1;
}
else if(nCmpValue>0||(nCmpValue==0&&pItems[nMid].nHandle>nHandle&&nHandle!=-1))
{
nEnd=nMid-1;
}
nMid=(nStart+nEnd)/2;
}
if(nPosRet)
{
//Get the previous position
*nPosRet=nMid-1;
}
return false;
}
//在修改表中查询
bool CDictionary::FindInModifyTable(int nInnerCode,char *sWord,int nHandle,PWORD_CHAIN *pFindRet)
{
PWORD_CHAIN pCur,pPre;
if(m_pModifyTable==NULL)//empty
return false;
pCur=m_pModifyTable[nInnerCode].pWordItemHead;
pPre=NULL;
//sWord相等且句柄(nHandle)相等
while(pCur!=NULL&&(_stricmp(pCur->data.sWord,sWord)<0||(_stricmp(pCur->data.sWord,sWord)==0&&pCur->data.nHandle<nHandle)))
//sort the link chain as alphabet
{
pPre=pCur;
pCur=pCur->next;
}
if(pFindRet)
*pFindRet=pPre;
if(pCur!=NULL && _stricmp(pCur->data.sWord,sWord)==0&&(pCur->data.nHandle==nHandle||nHandle<0))
{//The node exists, delete the node and return
return true;
}
return false;
}
得到词的类型,共三种汉字、分隔符和其他
int CDictionary::GetWordType(char *sWord)
{
int nType=charType((unsigned char *)sWord),nLen=strlen(sWord);
if(nLen>0&&nType==CT_CHINESE&&IsAllChinese((unsigned char *)sWord))
return WT_CHINESE;//Chinese word
else if(nLen>0&&nType==CT_DELIMITER)
return WT_DELIMITER;//Delimiter
else
return WT_OTHER;//other invalid
}
ICTCLAS分词的第一步就是原子分词。但在进行原子切分之前,首先要进行断句处理。所谓断句,就是根据分隔符、回车换行符等语句的分隔标志,把源字符串分隔成多个稍微简单一点的短句,再进行分词处理,最后再把各个分词结果合起来,形成最终的分词结果。
分成短句之后,即可进行原子分词,所谓原子,是指该短句中不可分割的最小语素单位。一个汉字、短句前后的开始结束标识字段、全角标点符号、连在一起的数字字母单字节字符等。最后一种情况可以举例说明,比如:三星SHX-132型号的手机1元钱,则SHX-132、1都是一个原子,其它的每个汉字是一个原子。
按照这种方式,通过简单的汉字分割就形成了原子分词的结果,并对每个原子单位进行词性标注。nPOS=1表示是开始标记,nPOS=4表示结束标记,nPOS=0表示未识别词。原子分割后的数据结构如下图一所示:
图一
原子分词后的实例如下图二所示:
图二
经过原子分词之后,下面即可进行初次分词。参见分词系统研究(四)。
=============================================================
经过原子分词后,源字符串成了一个个独立的最小语素单位。下面的初次切分,就是把原子之间所有可能的组合都先找出来。算法是用两个循环来实现,第一层遍历整个原子单位,第二层是当找到一个原子时,不断把后面相邻的原子和该原子组合到一起,访问词典库看它能否构成一个有意义有词组。
用数学方法可以做如下描述:
有一个原子序列:A(n)(0<=n<m)(其中m为原子序列A的长度)。当I=n时,判断AnAn+1..Ap是否为一个词组,其中n<p<m.
用伪码表示:
for(int I=0;I<m;I++){
String s=A[I];
for(int j=I+1;j<m;j++){
s+=A[j];
if(s是一个词组){
把s加入到初次切分的列表中;
记录该词组的词性;
记录该词组所在表中的坐标位置及其它信息;
}
else
break;
}
}
初次切分后的数据结构如下图一所示:
图一
分词用例”他说的确实在理”经过初次切分后的结果如下图二所示:
图二
用二维表来表示图一中的链表结构如下图二所示:
图三
从上图三可以看出,在二维表中,初次切分后的词组,第一次字相同的在同一行,最后一个字相同的在同一列,原来的原子在对称轴上.
对上述过程进行处理的参考源代码如下:
bool CSegment::BiSegment(char *sSentence, double dSmoothingPara, CDictionary &dictCore, CDictionary &dictBinary, unsigned int nResultCount)
{
......
//在此处完成上图一的处理结果,生成一个链表结构
m_graphSeg.GenerateWordNet(sSentence,dictCore,true);//Generate words array
......
在生成图二所示的表结构之后,进一步生成二叉图表.
....
//Generate the biword link net
BiGraphGenerate(m_graphSeg.m_segGraph,aBiwordsNet,dSmoothingPara,dictBinary,dictCore);
....
对该函数进行深入分析:
bool CSegment::BiGraphGenerate(CDynamicArray &aWord, CDynamicArray &aBinaryWordNet,double dSmoothingPara,CDictionary &DictBinary,CDictionary &DictCore)
{
......
//获取链表的长度
m_nWordCount=aWord.GetTail(&pTail);//Get tail element and return the words count
if(m_npWordPosMapTable)
{//free buffer
delete [] m_npWordPosMapTable;
m_npWordPosMapTable=0;
}
//分配一个数组,存贮图二中每个结点的词的位置,如下图四所示
if(m_nWordCount>0)//Word count is greater than 0
m_npWordPosMapTable=new int[m_nWordCount];//Record the position of possible words
//把指针指向当前链表的开头,并计算每个词的位置,然后把它放到数组中
pCur=aWord.GetHead();
while(pCur!=NULL)//Set the position map of words
{
m_npWordPosMapTable[nWordIndex++]=pCur->row*MAX_SENTENCE_LEN+pCur->col;
pCur=pCur->next;
}
//遍历所有的结点,并计算相临两个词之间的平滑值
pCur=aWord.GetHead();
while(pCur!=NULL)//
{
if(pCur->nPOS>=0)//It's not an unknown words
dCurFreqency=pCur->value;
else//Unknown words
dCurFreqency=DictCore.GetFrequency(pCur->sWord,2);
//取得和当前结点列值(col)相同的下个结点
aWord.GetElement(pCur->col,-1,pCur,&pNextWords);
while(pNextWords&&pNextWords->row==pCur->col)//Next words
{
//前后两个词用@分隔符连接起来
strcpy(sTwoWords,pCur->sWord);
strcat(sTwoWords,WORD_SEGMENTER);
strcat(sTwoWords,pNextWords->sWord);
//计算两个连接词的边长
nTwoWordsFreq=DictBinary.GetFrequency(sTwoWords,3);
//Two linked Words frequency
dTemp=(double)1/MAX_FREQUENCE;
//计算平滑值
dValue=-log(dSmoothingPara*(1+dCurFreqency)/(MAX_FREQUENCE+80000)+(1-dSmoothingPara)*((1-dTemp)*nTwoWordsFreq/(1+dCurFreqency)+dTemp));
//-log{a*P(Ci-1)+(1-a)P(Ci|Ci-1)} Note 0<a<1
if(pCur->nPOS<0)//Unknown words: P(Wi|Ci);while known words:1
dValue+=pCur->value;
//Get the position index of current word in the position map table
nCurWordIndex=BinarySearch(pCur->row*MAX_SENTENCE_LEN+pCur->col,m_npWordPosMapTable,m_nWordCount);
nNextWordIndex=BinarySearch(pNextWords->row*MAX_SENTENCE_LEN+pNextWords->col,m_npWordPosMapTable,m_nWordCount);
//把当前结点在位置表中的位置和下个结点在位置表中的位置及平滑值/词性插入到二叉链表中
aBinaryWordNet.SetElement(nCurWordIndex,nNextWordIndex,dValue,pCur->nPOS);
pNextWords=pNextWords->next;//Get next word
}
pCur=pCur->next;
}
return true;
}
图四
最终生成的键表结果如下图五所示:
图五
对应的二维图表表示形式如下图六所示:
图六
其中小数值代表了相临两个词之间的耦合成度,即构成更大长度词的可能性的机率,值越小说明两个词独立成词的可能性越大。
==============================================================ICTCLAS和别的分司系统不一样的地方就是于--N最短路径分词算法。所谓N最短路径其实就是最短路径和最大路径的折中,保留前N个最优路径。这样做的目的就是对这两种方法取长补短,既能达到一个比较理解的分词不达意效果,又能保证分词不达意速度。在此处,我们中国人的中庸思想被完美体现:)。
在源程序中,N最短路径是在CNShortPath类里里面实现的。
bool CSegment::BiSegment(char *sSentence, double dSmoothingPara, CDictionary &dictCore, CDictionary &dictBinary, unsigned int nResultCount)
{
......
//调用构造函数,生成一个二维链表,如下图一所示。每个链表节点是一个队列,数据结构如下图二所示
CNShortPath sp(&aBiwordsNet,nResultCount);
//最短路径算法实现
sp.ShortPath();
//输出最短路径
sp.Output(nSegRoute,false,&m_nSegmentCount);
.....
}
图一
图二
对NShortPath的构造函数分进一步分析:
CNShortPath::CNShortPath(CDynamicArray *apCost,unsigned int nValueKind)
{
//研究(四)中图五所示的链表
m_apCost=apCost;//Set the cost
m_nValueKind=nValueKind;//Set the value kind
//顶点数
m_nVertex=apCost->m_nCol+1;
if(m_nVertex<apCost->m_nRow+1)
m_nVertex=apCost->m_nRow+1;//Get the vertex numbers
m_pParent=new CQueue*[m_nVertex-1];//not including the first node
m_pWeight=new ELEMENT_TYPE *[m_nVertex-1];
for(unsigned int i=0;i<m_nVertex-1;i++)//The queue array for every node
{
m_pParent[i]=new CQueue[nValueKind];
m_pWeight[i]=new ELEMENT_TYPE[nValueKind];
}
}
int CNShortPath::ShortPath()
{
unsigned int nCurNode=1,nPreNode,i,nIndex;
ELEMENT_TYPE eWeight;
PARRAY_CHAIN pEdgeList;
for(;nCurNode<m_nVertex;nCurNode++)
{
CQueue queWork;
eWeight=m_apCost->GetElement(-1,nCurNode,0,&pEdgeList);//Get all the edges
//对研究(四)中的图六所示的表,按列优进行遍历,把每个列的数据放到一个临时工作队列中
while(pEdgeList!=0 && pEdgeList->col==nCurNode)
{
nPreNode=pEdgeList->row;
eWeight=pEdgeList->value;//Get the value of edges
for(i=0;i<m_nValueKind;i++)
{
if(nPreNode>0)//Push the weight and the pre node infomation
{
if(m_pWeight[nPreNode-1][i]==INFINITE_VALUE)
break;
//把行值(ROW)放入队列中,并且保证队列的排序是按Weight值从小到大排列
queWork.Push(nPreNode,i,eWeight+m_pWeight[nPreNode-1][i]);
}
else
{
queWork.Push(nPreNode,i,eWeight);
break;
}
}//end for
pEdgeList=pEdgeList->next;
}//end while
//Now get the result queue which sort as weight.
//Set the current node information
for(i=0;i<m_nValueKind;i++)
{
m_pWeight[nCurNode-1][i]=INFINITE_VALUE;
}
//memset((void *),(int),sizeof(ELEMENT_TYPE)*);
//init the weight
i=0;
//临时工作队列中取出前面的一个,并记路路径长度的和
while(i<m_nValueKind&&queWork.Pop(&nPreNode,&nIndex,&eWeight)!=-1)
{//Set the current node weight and parent
if(m_pWeight[nCurNode-1][i]==INFINITE_VALUE)
m_pWeight[nCurNode-1][i]=eWeight;
else if(m_pWeight[nCurNode-1][i]<eWeight)//Next queue
{
i++;//Go next queue and record next weight
if(i==m_nValueKind)//Get the last position
break;
m_pWeight[nCurNode-1][i]=eWeight;
}
m_pParent[nCurNode-1][i].Push(nPreNode,nIndex);
}
}//end for
return 1;
}
m_nParent的最终结果如下图三所示,它在当前位置记录该词的父节点位置(对应于研究(四)中的图四)
图三
int CNShortPath::Output(int **nResult,bool bBest,int *npCount)
{//sResult is a string array
......
GetPaths(m_nVertex-1,i,nResult,bBest);
}
//获取分词输出路径。指研究(四)中的图四
void CNShortPath::GetPaths(unsigned int nNode,unsigned int nIndex,int **nResult,bool bBest)
{
CQueue queResult;
unsigned int nCurNode,nCurIndex,nParentNode,nParentIndex,nResultIndex=0;
if(m_nResultCount>=MAX_SEGMENT_NUM)//Only need 10 result
return ;
nResult[m_nResultCount][nResultIndex]=-1;//Init the result
queResult.Push(nNode,nIndex);
nCurNode=nNode;
nCurIndex=nIndex;
bool bFirstGet;
while(!queResult.IsEmpty())
{
while(nCurNode>0)//
{//Get its parent and store them in nParentNode,nParentIndex
if(m_pParent[nCurNode-1][nCurIndex].Pop(&nParentNode,&nParentIndex,0,false,true)!=-1)
{
nCurNode=nParentNode;
nCurIndex=nParentIndex;
}
if(nCurNode>0)
queResult.Push(nCurNode,nCurIndex);
}
if(nCurNode==0)
{ //Get a path and output
nResult[m_nResultCount][nResultIndex++]=nCurNode;//Get the first node
bFirstGet=true;
nParentNode=nCurNode;
while(queResult.Pop(&nCurNode,&nCurIndex,0,false,bFirstGet)!=-1)
{
nResult[m_nResultCount][nResultIndex++]=nCurNode;
bFirstGet=false;
nParentNode=nCurNode;
}
nResult[m_nResultCount][nResultIndex]=-1;//Set the end
m_nResultCount+=1;//The number of result add by 1
if(m_nResultCount>=MAX_SEGMENT_NUM)//Only need 10 result
return ;
nResultIndex=0;
nResult[m_nResultCount][nResultIndex]=-1;//Init the result
if(bBest)//Return the best result, ignore others
return ;
}
queResult.Pop(&nCurNode,&nCurIndex,0,false,true);//Read the top node
while(queResult.IsEmpty()==false&&(m_pParent[nCurNode-1][nCurIndex].IsSingle()||m_pParent[nCurNode-1][nCurIndex].IsEmpty(true)))
{
queResult.Pop(&nCurNode,&nCurIndex,0);//Get rid of it
queResult.Pop(&nCurNode,&nCurIndex,0,false,true);//Read the top node
}
if(queResult.IsEmpty()==false&&m_pParent[nCurNode-1][nCurIndex].IsEmpty(true)==false)
{
m_pParent[nCurNode-1][nCurIndex].Pop(&nParentNode,&nParentIndex,0,false,false);
nCurNode=nParentNode;
nCurIndex=nParentIndex;
if(nCurNode>0)
queResult.Push(nCurNode,nCurIndex);
}
}
}
最终得到最短路么(0,1,2,3,6,9,11,12),里面的数值分别对应研究(四)中图四的下标,到此分词的第一大步就结束了,并形成最终结果:始##始/他/说/的/确实/在/理/末##末