First we try, then we trust

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  183 随笔 :: 111 文章 :: 2983 评论 :: 339 引用

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中比比皆是,看来我只能恨自己脑筋太简单了!

 

posted on 2007-02-21 00:24 吕震宇 阅读(7948) 评论(30)  编辑 收藏 所属分类: ICTCLAS

评论

#1楼  2007-02-21 10:03 U2U      
感觉这样不能说是丑陋吧
  回复  引用  查看    

从软件工程的角度来讲,确是丑陋。
这样的写法确实可以算是“高明”或者“高IQ”,但问题是,高IQ的人是少数。也许在中科院内不算是少数,但社会上确实如此。这么写的好处也许是运行效率比较高,但是开发效率和维护效率一定是非常低下的,团队协作效率也是低下的。因为别人要有足够的IQ去理解这样的代码,而且也需要足够的时间去消化这样的思路。
见仁见智啦
  回复  引用    

#3楼 [楼主] 2007-02-21 16:06 吕震宇      
@U2U
将“丑陋”改成“天书般的”了,确实“丑陋”两字有点伤人:-)
  回复  引用  查看    

只有一种可能,这是机器生成的或者处理过的代码
  回复  引用    

#5楼  2007-02-21 18:05 U2U      
@吕震宇
呵呵
  回复  引用  查看    

#6楼  2007-02-21 18:06 U2U      
其实我个人并不觉得这样的代码丑陋或者难以理解,对比起开发难度,我偏向于高性能的算法
  回复  引用  查看    

#7楼 [楼主] 2007-02-21 20:45 吕震宇      
@U2U
其实ICTCLAS中的代码并没有效率,如此复杂的while循环条件判断怎么能说有效率呢?完全可以将GetElement方法拆分成多个功能不同的方法以避免复杂的条件判断,这样程序执行起来才更有效率。
  回复  引用  查看    

#8楼  2007-02-22 14:16 gqh [未注册用户]
那就把他封装起来,些个公共函数提供调用,这样也不必考虑内部实现,就像一个黑盒子。估计微软的操作系统里面这样的代码不少把。虽然留给你的api很好调用。哈哈
  回复  引用    

#9楼 [楼主] 2007-02-22 22:06 吕震宇      
@gqh
希望不会造就出一个“驴粪蛋”式的系统来(转引“驴粪蛋,外面光”),哈哈。
  回复  引用  查看    

#10楼  2007-02-24 21:21 姓名 [未注册用户]
我想这个系统有价值的是分词思路,有了设计思路找谁来实现不可以啊。就好像买了一台电脑,不管包装箱内的电脑本身怎么,一群人偏在死扣那个外面透明胶带帖歪了,我吐……
  回复  引用    

#11楼  2007-02-25 12:03 巫云      
估计一开始应该不是这样的代码, 在后期优化的结果吧
  回复  引用  查看    

#12楼  2007-02-26 15:10 Kestin [未注册用户]
代码可读性太低
  回复  引用    

#13楼  2007-03-05 16:02 dy [未注册用户]
@Kestin
针对这个算法,你是否可以写出一个“可读性”很高的代码呢?

  回复  引用    

#14楼 [楼主] 2007-03-07 21:45 吕震宇      
@Kestin
@dy
我将ICTCLAS移植到了.NET平台上,对上面的代码进行了重写,分离出了几个不同功能的类。我会在随后的文章中介绍我的新设计的。
  回复  引用  查看    

#15楼 [楼主] 2007-03-09 09:16 吕震宇      
我的解决办法请参考:
http://www.cnblogs.com/zhenyulu/articles/668695.html

  回复  引用  查看    

#16楼  2007-03-15 16:06 sinboy [未注册用户]
文章写的不错,学习中。。。
  回复  引用    

#17楼  2007-03-19 09:46 kirc [未注册用户]
估计张华平看了,该哭笑不得了.
  回复  引用    

#18楼  2007-03-19 09:53 kirc [未注册用户]
ictclas的分词主要的方向是提高准确性,但是很多时候是要求分词速度的.比如搜索引擎. 分词的速度要比准确性稍微重要一些.
好像Free的ICTCLAS的速度不到几百K吧?这个肯定是不能用在搜索引擎上的.用在智能聊天机器人上好像还比较酷.


  回复  引用    

#19楼  2007-03-20 20:50 U2U      
我觉得如果一个人没有能力理解代码的话,就没必要去搞什么分词了。你既然有了理论,也就能看出代码用了什么算法之类的。
  回复  引用  查看    

#20楼 [楼主] 2007-03-20 23:18 吕震宇      
@U2U
即使有了足够的理论知识,我想也读不懂经过代码混淆器处理过的代码吧。况且消化吸收别人的技术不仅仅是学习理论这么简单吧,要不然我国干吗还买回国外的机器拆开来消化吸收一番呢,光学理论不就行了吗。
  回复  引用  查看    

#21楼  2007-04-25 11:00 hold [未注册用户]
对您的理论水平和编码技巧非常敬佩!而终日研究复杂理论不用出产品的专家,写写论文就行了,用不着抽象设计,更不必象微软那样做MSDN
  回复  引用    

#22楼  2007-05-09 20:06 张华平 [未注册用户]
我是ICTCLAS的作者张华平,楼上有人说我会苦笑不得,或者伤害,其实不至于了。我跟大家解释一下,那是5年前开放的代码,纯粹是实验室研究结果,还没有经过后来的工程磨练。到现在我刚进入科学院的学生还在同样的幼稚过,难看过,凡事都是一个成长过程。
看到网上有很多的爱好者为当年我开发的那个婴儿期的系统付诸如此大的热情和厚爱,我非常感到。感谢各位对我工作的厚爱。

我大致把一些关于ICTCLAS的进展跟大家汇报一下,也希望更多的朋友能从中获益。
ICTCLAS开源版本属于2002年我读硕士学位时候的一个工作,当时还是Beta版本,纯粹是实验室的研究加上一些产品的概念,里面还有不少错误与不足。后来我综合各位朋友和企业的需求和建议,花了差不多3年的时间不断的升级维护,可以说是脱胎换骨6次,每次大的更新都是完全重新写程序。当前已经升级到了ICTCLAS3.0,根据多年在业界和研究方面的体会,我个人认为:汉语分词之类的自然语言处理技术不应该片面地走语言学的路子,片面的追求准确率,片面的测试一些极其怪异的歧义和未登录词问题,做研究可以这样,但是做实用系统必须是好用,易用,能用的产品,能不能适应互联网大规模处理的需要,不从个别生僻的特例而从统计特性来看是否达到应用的需要,这个理念,我会在5月20日在杭州举行的首届全国网络侠客行大会的主题报告中进行阐述。
ICTCLAS3.0在准确率方面没有大的提升,我们更多的是提升其速度,目前,我测试的结果是在准确率基本和ICTCLAS1.0相当的情况下,速度提升了10倍,接近1MB/s,内存消耗在20M以内,系统中没有采用任何一个字符串操作函数。
欢迎大家访问http://www.i3s.ac.cn">http://www.i3s.ac.cn,了解我们最新的工作动态,这是ICTCLAS的官方发布网站,希望更多的朋友参与。

张华平 博士
中科计算技术转移中心信息智能与信息安全部 总经理
中国科学院计算技术研究所 副研究员
地址:北京市海淀区中关村东路18号财智大厦A座707室 邮编:100083
电话:+86-10-82601206
Email:zhanghp@software.ict.ac.cn
Homepage: http://www.i3s.ac.cn">http://www.i3s.ac.cn

  回复  引用    

#23楼 [楼主] 2007-05-23 21:44 吕震宇      
@张华平
如果因为这篇文章伤害了您的话,实在是抱歉。其实感觉FreeICTCLAS确实有不少地方可以改进,尤其是代码方面。相信几年的光景,ICTCLAS3.0的改进不小,只可惜没有开源:-)
  回复  引用  查看    

呵呵,现在成了商业应用了.
要赚钱了.
  回复  引用    

#25楼  2008-03-01 15:49 kissxrl [未注册用户]


支持开源的分词系统...


支持 张华平 ......


我目前在写一个简单的分词系统.....从开源的分词系统内借鉴呢好多东西....

多谢呢
  回复  引用    

做个标记以后学习,中文分词对搜索引擎以及数据库优化学习应该很有帮助,是吧
  回复  引用    


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-02-26 13:30 编辑过
Google站内搜索


相关链接: