兼济天下则达 独善其身则穷
分词在搜索引擎当中是极为基础,分词的准确度,对于搜索结果影响很大。可是要写一个分词程序并没有那么容易,一般都是用开源的分词工具,公司使用的分词工具是SCWS。年前,我还不知道这个分词工具是存在一些问题的,直到组长回家之后,产品那边的同事和我说了分词存在问题后,我才注意到这个问题。
例如它会将”情歌接龙大串烧“分成“情歌/n 接/v 龙大/nz 串烧/v“,将”武松杀嫂雕塑是艺术“分成”武松杀/nr 嫂/ng 雕塑/n 是/v 艺术/n“。可是将这两个例子放在命令行中,用scws的命令行工具去分词时,得到的结果竟然是正确的,即它会分成“情歌/n 接龙/n 大/a 串烧/v”,“武松/nr 杀/v 嫂/ng 雕塑/n 是/v 艺术/n”。
刚开始我是一头雾水,根本不知道在哪里错了,比较了之后,才发现在命令行工具中,没有启用rules.ini这个规则表,而在程序中则启用了。于是去查看rules.ini的内容,查看之后,我发现其中只有词性语法规则表对这个分词结果有影响,也就是将v(1) + n = 5这条规则注释掉之后,“情歌接龙大串烧”这个句子分正确了。可是对于“武松杀嫂雕塑是艺术?"这一句,无论怎么修改,都没有办法解决,那么这样只有去修改scws的源码了。
曾一度想放弃修改源码,可是问题无法解决,于是只好去修改它,所幸的是,其关键部分都在scws.c这个文件中,总共也才1000多行,所以还是有希望解决这个问题。对程序所用到的数据结构有所了解后,跟着分词的步骤 一步一步走,有疑问的地方就printf。终于发现问题的症结。问题出在scws.c的885行这里。因为”武“是一个姓,所以它可以做为前缀,而一个姓氏的后面,一般可以跟一到两个字,于是它将"武松杀”合并成一个名字,于是我在885行后面加了个规则判断,如果“武”出现在一个词语的开头,就不用使用前缀规则,修改如下:
if (r1->attr[0] == 'n' && r1->attr[1] == 'r') {//这个是一个姓 if (wmap[i][i]->flag == SCWS_ZFLAG_WHEAD)//是词的开头,跳过 continue;
最终解决了这个问题。这里刚开始没考虑周全,后来又出现姓氏在词语中就会出现类似的问题,于是进行了如下修改:
if (r1->attr[0] == 'n' && r1->attr[1] == 'r') {//这个是一个姓 if (wmap[i][i]->flag == SCWS_ZFLAG_WHEAD || wmap[i][i]->flag == SCWS_ZFLAG_WPART)//是一个词的一部分,跳过 continue; }
之后不久产品那边又反应了一个问题。“如果有一张纸写着自己的死亡日期”分成了“如果/c 有/v 一/m 张纸写/nr 着/v 自己/r 的/uj 死亡/v 日期/n",有了上次的修改经验,一看就知道是类似的问题,于是在同样的地方进行了修改,修改如下:
if(r1->attr[0] == 'n'&& r1->attr[1] == 'r' ) { //这个字是个姓氏 if (wmap[i][i]->flag & SCWS_ZFLAG_WHEAD || wmap[i][i]->flag & SCWS_ZFLAG_WPART) //是一个词的一部分,跳过 continue; if (i > 0) { rule_item_t r2 = scws_rule_get(s->r, txt + zmap[i - 1].start, zmap[i - 1].end - zmap[i - 1].start); if (r2 != NULL && r2->attr[0] == 'm') //前面是一个量词,跳过 continue; } }
最终也解决了这个问题。 之后又有一个问题。“一天都坐在办公室”分成了“一 /m 天都 /ns 坐在 /v 办公室 /n”,这里我无能为力了,只好将“天都"从词典中删去。
之后又有一个问题,而这次我无法解决了。”求陈明真的个人介绍“分成了”求 /v 陈明 /nr 真的 /d 个人 /n 介绍 /v“。因为分词的过程中,它会将“陈明真的"这个四个字利用词性语法规则,再加上每个词的权重进行分词,而单从这个四个字来看,“陈明 + 真的"胜出。
在我看了代码后,渐渐理解scws的运作机理,它先将句子中的每个字拿到词典里查询,看看是词的头部还是部分,之后在用一些前缀规则和后缀规则进行处理,之后在用词性语法规则进行分词。而词性语法规则对结果的影响非常明显,修改其中的一些规则,可以令分词错误的句子最后分对了,而同时确又将另一个句子分错了,这注定是一个无底洞,而我不想陷在这里。
这也再一步证明了基于规则的分词方法的缺陷。可是要自己去写一个基于统计的分词方法又不简单,所以还是先用着吧。