全文检索4(关于中文分词ICTCLAS分词系统)

 
ICTCLAS 分词系统是由中科院计算所的张华平、刘群所开发的一套获得广泛好评的分词系统,该版的 Free 版开放了源代码,为初学者提供了宝贵的学习材料。我们可以在 http://sewm.pku.edu.cn/QA/ 找到 FreeICTCLASLinux.tar C++ 代码。
可是目前该版本的 ICTCLAS 并没有提供完善的文档,所以阅读起来有一定的难度,所幸网上可以找到一些对 ICTCLAS 进行代码分析的文章,对理解分词系统的内部运行机制提供了很大的帮助。这些文章包括:
1 http://blog.csdn.net/group/ictclas4j/ ;《 ICTCLAS 分词系统研究(一)~(六)》作者: sinboy
2 http://qxred.yculblog.com/post.1204714.html ;《 ICTCLAS 中科院分词系统 代码 注释 中文分词 词性标注》作者:风暴红 QxRed
按照上面这些文章的思路去读 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;
}
这里我先要说明的是程序中的 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; 
}
 
此时的意义就比较明显了,其实就是找 pCur->row == pStart->col 的那个结点。
可有人会问,干吗把 row col 扯到一起呢?这又是一个非常复杂的问题。具体内容可以参考 sinboy 的《 ICTCLAS分词系统研究(四)--初次切分 》一文。这里简单解释如下:
如图四,这是 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);
  ......
}
·         小结
想不到短短一个 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 中比比皆是,看来我只能恨自己脑筋太简单了!
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值