ICTCLAS
分词系统是由中科院计算所的张华平、刘群所开发的一套获得广泛好评的分词系统,该版的
Free
版开放了源代码,为初学者提供了宝贵的学习材料。我们可以在
“http://sewm.pku.edu.cn/QA/”
找到
FreeICTCLASLinux.tar
的
C++
代码。
可是目前该版本的
ICTCLAS
并没有提供完善的文档,所以阅读起来有一定的难度,所幸网上可以找到一些对
ICTCLAS
进行代码分析的文章,对理解分词系统的内部运行机制提供了很大的帮助。这些文章包括:
按照上面这些文章的思路去读
ICTCLAS
的代码,可以比较容易的理顺思路。然而在我阅读代码的过程中,越来越对
ICTCLAS
天书般的代码感到厌烦。我不得不佩服中科院计算所的人思维缜密,头脑清晰,能写出滴水不漏而又让那些
“
头脑简单
”
的人百思不得其解的代码。将一件本来很简单的事情做得无比复杂
...
ICTCLAS
中有一个名为
CDynamicArray
的类,存放在
DynamicArray.cpp
与
DynamicArray.h
两个文件中,这个
DynamicArray
是干什么用的?经过一番研究后终于明白是一个经过排序的链表。为了表达的更明白些,我们不妨看下面这张图:
(图一)
上面这张图是一个按照
index
值进行了排序的链表,当插入新结点时必须确保
index
值的有序性。
DynamicArray
类完成的功能基本上与上面这个链表差不多,只是排序规则不是
index
,而是
row
和
col
两个数据,如下图:
(图二)
大家可以看到,这个有序链表的排序规则是先按
row
排序,
row
相同的按照
col
排序。当然排序规则是可以改变的,如果先按
col
排,再按
row
排,则上面的链表必须表述成:
(图三)
在了解了这些内容的基础上,不妨让我们看看
ICTCLAS
中
DynamicArray.cpp
中的代码实现(这里我们只看
GetElement
方法的实现,其基本功能为给出
row
与
col
,然后将对应的元素取出来)。
DynamicArray.cpp
ELEMENT_TYPE CDynamicArray::GetElement(
int
nRow,
int
nCol, PARRAY_CHAIN pStart,
PARRAY_CHAIN *pRet)
{
PARRAY_CHAIN pCur = pStart;
if (pStart == 0)
pCur = m_pHead;
if (pRet != 0)
*pRet = NULL;
if (nRow > ( int )m_nRow || nCol > ( int )m_nCol)
//Judge if the row and col is overflow
return INFINITE_VALUE;
if ( m_bRowFirst )
{
while ( pCur != NULL && (nRow != - 1 && ( int )pCur->row < nRow || (nCol !=
- 1 && ( int )pCur->row == nRow && ( int )pCur->col < nCol)) )
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
}
else
{
while ( pCur != NULL && (nCol != - 1 && ( int )pCur->col < nCol || (( int )pCur
->col == nCol && nRow != - 1 && ( int )pCur->row < nRow)) )
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
}
if ( pCur != NULL && (( int )pCur->row == nRow || nRow == - 1) && (( int )pCur
->col == nCol || nCol == - 1) )
//Find the same position
{
//Find it and return the value
if (pRet != 0)
*pRet = pCur;
return pCur-> value ;
}
return INFINITE_VALUE;
}
PARRAY_CHAIN *pRet)
{
PARRAY_CHAIN pCur = pStart;
if (pStart == 0)
pCur = m_pHead;
if (pRet != 0)
*pRet = NULL;
if (nRow > ( int )m_nRow || nCol > ( int )m_nCol)
//Judge if the row and col is overflow
return INFINITE_VALUE;
if ( m_bRowFirst )
{
while ( pCur != NULL && (nRow != - 1 && ( int )pCur->row < nRow || (nCol !=
- 1 && ( int )pCur->row == nRow && ( int )pCur->col < nCol)) )
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
}
else
{
while ( pCur != NULL && (nCol != - 1 && ( int )pCur->col < nCol || (( int )pCur
->col == nCol && nRow != - 1 && ( int )pCur->row < nRow)) )
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
}
if ( pCur != NULL && (( int )pCur->row == nRow || nRow == - 1) && (( int )pCur
->col == nCol || nCol == - 1) )
//Find the same position
{
//Find it and return the value
if (pRet != 0)
*pRet = pCur;
return pCur-> value ;
}
return INFINITE_VALUE;
}
这里我先要说明的是程序中的
m_bRowFirst
变量,它表示是先按
row
大小排列还是先按
col
大小排列。如果
m_bRowFirst
为逻辑真值,那么链表就如上面图二所示,如果为假,则如图三所示。
除了这个外,看到上面长长的条件表达式,你一定会吓坏了吧!更让人吓坏的是调用这段程序的代码:
对
GetElement
方法的调用
// 来自 NShortPath.cpp 中 ShortPath 方法
eWeight = m_apCost->GetElement( -1, nCurNode, 0, &pEdgeList);
// 来自 Segment.cpp 中 BiGraphGenerate 方法
aWord.GetElement(pCur->col, -1, pCur, &pNextWords); //Get next words which begin with pCur->col
·
先分析第一个调用
第一个调用给
GetElement
方法的
nRow
传递了
-1
,他想干什么呢?
假设这时候变量
m_bRowFirst
为
true
,并且传递过去的
nCol!=-1
,那么
while (pCur != NULL && (nRow != - 1 && (int)pCur->row < nRow || (nCol != -1 && (int)pCur->row == nRow && (int)pCur->col < nCol)))
等价于
while (pCur != NULL && (
(int)pCur->row == -1
&& (int)pCur->col < nCol)))
,注意红色部分在程序运行时永远为
false
(因为根本就不存在
row
为
-1
的结点),因此,上面的表达式等价于
while(false)
!
这对于该段程序没有任何意义!
因此我们可以得到这样一个结论:如果
GetElement
方法的
nRow
参数取
-1
,当且仅当
m_bRowFirst
为
false
时才有意义
。这时候,代码中第二个
while
得到执行,让我们分析一下:
while (pCur != NULL && (nCol != - 1 && (int)pCur->col < nCol || ((int)pCur->col == nCol && nRow != - 1 && (int)pCur->row < nRow)))
在
nRow
为
-1
时等价于
while (pCur != NULL && ((int)pCur->col < nCol )
,这就容易解释的多了:在如图三所示的链表中查找
col=nCol
的第一个结点。
My God!
·
再分析第二个调用
上面的第二个调用就更让人摸不着头脑了:将
pCur->col
传递给
GetElement
的
nRow
参数,并将
-1
传递给
nCol
参数,这想干什么呢?要想分析清楚这个问题,没有个把钟头恐怕不行(再次佩服这些中科院的牛人们)。
按照
“
分析第一个调用
”
中的结论可知,如果
GetElement
方法的
nCol
参数取
-1
,当且仅当
m_bRowFirst
为
true
时才有意义
。因此链表排序一定是先按照行排(如图二),此时对
DynamicArray
的
GetElement
方法的调用可以简化成:
对方法调用进行剥离和简化
//
来自
Segment.cpp
中
BiGraphGenerate
方法
aWord.GetElement(pCur->col, -1, pCur, &pNextWords);
//======================================================================
ELEMENT_TYPE CDynamicArray::GetElement( int nRow, int nCol, PARRAY_CHAIN pStart, PARRAY_CHAIN *pRet)
// 经过调用后,上面的形参对应的值分别是: nRow : pStart->col, nCol : -1, pStart, &pNextWords
// 注意,为了和下面代码中的 pCur 以示区分,这里用了 pStart 这个变量名。
{
......
while (pCur != NULL && ( ( int )pCur->row < pStart->col ))
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
if (pCur != NULL && ( ( int )pCur->row == pStart->col )
//Find the same position
{
//Find it and return the value
if (pRet != 0)
*pRet = pCur;
return pCur-> value ;
}
return INFINITE_VALUE;
}
aWord.GetElement(pCur->col, -1, pCur, &pNextWords);
//======================================================================
ELEMENT_TYPE CDynamicArray::GetElement( int nRow, int nCol, PARRAY_CHAIN pStart, PARRAY_CHAIN *pRet)
// 经过调用后,上面的形参对应的值分别是: nRow : pStart->col, nCol : -1, pStart, &pNextWords
// 注意,为了和下面代码中的 pCur 以示区分,这里用了 pStart 这个变量名。
{
......
while (pCur != NULL && ( ( int )pCur->row < pStart->col ))
{
if (pRet != 0)
*pRet = pCur;
pCur = pCur->next;
}
if (pCur != NULL && ( ( int )pCur->row == pStart->col )
//Find the same position
{
//Find it and return the value
if (pRet != 0)
*pRet = pCur;
return pCur-> value ;
}
return INFINITE_VALUE;
}
此时的意义就比较明显了,其实就是找
pCur->row == pStart->col
的那个结点。
如图四,这是
row
优先排列的一个链表:
图四
进行初步分词后的链表结构(
TagArrayChain
)实例
用二维表来表示图四中的链表结构如下图五所示:
图五
TagArrayChain
实例的二维表表示形式
然后找出相邻两个词的平滑值。例如
“
他
@
说
”
、
“
的
@
确
”
、
“
的
@
确实
”
、
“
的确
@
实
”
、
“
的确
@
实在
”
等。如果仔细观察的话,可以注意到以下特点:例如
“
的确
”
这个词,它的
col = 5
,需要和它计算平滑值的有两个,分别是
“
实
”
和
“
实在
”
,你会发现这两个词的
row = 5
。同样道理,
“
确
”
的
col = 5
,它也需要和
“
实
”
与
“
实在
”
(
row = 5
)分别计算平滑值。
其实,这就是为什么上面分析的找
pCur->row == pStart->col
的那个结点的原因了。最终得到的平滑值图可以表述成图六:
图六
进行初次分词后生成的二叉图表的二维图表表示形式
到此为止才明白代码作者的真正用意:
将该调用放到上下文中再次查看
//=========
来自
Segment.cpp
中
BiGraphGenerate
方法
===========
......
// 取得和当前结点列值 (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);
......
}
......
// 取得和当前结点列值 (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);
......
}
·
小结
想不到短短一个
GetElement
方法中竟然综合考虑了
1
)
row
优先排序的链表;
2
)
col
优先排序的链表;
3
)当
nRow
为
-1
时的行为(只有
m_bRowFirst
为
false
时才能这么做,代码中没有指,所以非常容易出错!);
4
)当
nCol
为
-1
时的行为;
5
)当
nRow
与
nCol
都不为
-1
时的行为。
这也难怪我们会看到诸如
while (pCur != NULL && (nRow != - 1 && (int)pCur->row < nRow || (nCol != -1 && (int)pCur->row == nRow && (int)pCur->col < nCol)))
这样的逻辑表达式了!我们也不得不佩服代码书写者复杂的逻辑思维能力(离散数学的谓词逻辑一定学得超级好)和给代码阅读者制造障碍的能力!类似代码在
ICTCLAS
中比比皆是,看来我只能恨自己脑筋太简单了!