阿龙的自留地

兼济天下则达 独善其身则穷

标签归档: 分词

那些浮躁的年轻人


四个月前,遇到了Sphinx-for-Chinese的分词问题,也就是分词的粒度问题。在使用Sphinx-for-Chinese时,结果中有秦始皇兵马俑博物馆,搜索兵马俑时搜不到它,搜索兵马俑博物馆时才能搜到它。这只因为词典里有兵马俑博物馆这个词,而Sphinx-for-Chinese使用的是mmseg分词算法,所以使用最优分词策略时,将秦始皇兵马俑博物馆分成秦始皇+兵马俑博物馆。当时只是将词典中类似兵马俑博物馆这些词删除,也解决了这一类问题,但显然还是存在问题的。最近产品那边又提出一些问题,如搜索西海,海岸时搜不到西海岸,搜索肯尼亚时,搜索不到肯尼亚山。于是这个分词问题又提上了日程。

对于这些问题,再次使用去除词典中的词显得不太合适。于是只好采用别的办法,考虑使用近义词的办法,可是还是不合适,因为如果将西海和西海岸设置为同义词,那么搜索西海岸时,也能搜索关于西海的东西,这显然不是我想要的。看来只有改源码了,可是看到Sphinx-for-Chinese那些代码,没有信心做到。于是只好求助网络,可是这么专业的问题,在网页上是找不到的。于是求助Sphinx-for-Chinese,也就在这时,我才发现许多时候还是只能靠自己,许多时候程序员是很浮躁的。

在群里问了一些人,有个人说全切分,可是问他如何做,他就不理你了;有人说思路有很多,近义词就是其中一种,可是和他说了近义词的弊端后,他也不说话了;有人说在配置文件里加个选项即可,想来使用Sphinx-for-Chinese也有些时间,怎么就不知道有一个支持细粒度分词的选项呢?了解了之后,才知道CoreSeek才有这个选项,再细问后,他就说自己很忙,问其他人去。想想自己有问题都是看文档,除非遇到解决不了的问题,才会求助于他人。也知道如果有一些例子,会起到很大的帮助,所以才会写了几篇关于Sphinx-for-Chinese的文章。可是却被这帮人当成不看文档的人,真是有些无辜。最后只好求助于Sphinx-for-Chinese的开发人员,从他口中得知,要想解决这个问题,唯一的办法就是修改源码,这和自己预计的是一样的。

或许自己也变得浮躁了,明知道只有改源码,还是去麻烦其他人,企图有更简便的办法。记得当初遇到SCWS时就不是这样,那时遇到分词问题,愣是使用printf输出找到修改的办法。可是现在,遇到问题时想的却是有没哟更简便的办法。幸好组长没有变浮躁,在他的参与下,很快找到了问题的症结,并给出了一个最初的原型。正是这个原型,点燃了星星之火,带领组长和我走向解决问题的道路,虽然目前看来,还不是最优的解决方案,但至少解决了这个问题。

自己也应该沉下心来,不要再那么浮躁了。

追踪scws的分词错误问题


分词在搜索引擎当中是极为基础,分词的准确度,对于搜索结果影响很大。可是要写一个分词程序并没有那么容易,一般都是用开源的分词工具,公司使用的分词工具是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的运作机理,它先将句子中的每个字拿到词典里查询,看看是词的头部还是部分,之后在用一些前缀规则和后缀规则进行处理,之后在用词性语法规则进行分词。而词性语法规则对结果的影响非常明显,修改其中的一些规则,可以令分词错误的句子最后分对了,而同时确又将另一个句子分错了,这注定是一个无底洞,而我不想陷在这里。

这也再一步证明了基于规则的分词方法的缺陷。可是要自己去写一个基于统计的分词方法又不简单,所以还是先用着吧。