树状分词法

最大匹配法分词的缺陷

原文:http://blog.csdn.net/jiljil/article/details/2339136

一、长度限制

(1)词长过短,长词就会被切错

(2)词长过长,效率就比较低。

二、效率低

三、掩盖分词歧义

四、最大匹配的并不一定是想要的分词方式

中文分词算法

一、 高效

二、无长度限制

三、歧义包容

突破口—词库

真正要改善是就是我们的匹配过程,我们要减少匹配过程中的浪费,我们要解决匹配中的词长限制

按字打散,并存放到层次数据库中。以下就是一个示例:红色的字表示树上面的字串是可以单独组成一个词的,例如“感冒”它本身是词库里可以找到的词,所有红色的表示的是终止符。而黄色则表示树上面的字串是无法单独成词的,例如“感冒解”是不存在的词。词库经过这样的改装后,所有的匹配的思维都变掉了。任何一个句子都会打散成单字去与树状结构的单字去匹配,词的长度变成了树的高度,每一次的匹配变成了树的遍历,而这种遍历的效是线性的!

中文分词算法设计

分词的步骤:

(1)首先将要分的全文按标点符号打散成一个一个的句子。这算是预处理的一个步骤,目的是让我们处理的句子短,效率更高。毕竟中间有标点符号的词是不存在的。(注:真正实现时我们是基于lucene的SimpleAnalyzer来做的,因为SimpleAnalyzer本身就是为了将全文打散成句子,因此我们没必要耗费体力去实现这一步)。

(2)我们开始将要处理的句子在树状结构中遍历,如果找到匹配的就继续,如果遇到红色的终止符,我们就发现这个词是一个完整的词了,这样我们就可以把这个词作为一个一个分词了。

(3)从分词后的下一字开始继续做步骤2这样的遍历,如此循环往复就将词分完了。

可以看到,我们字符匹配效率几乎是线性的!我们所要做的只是取出每一个字去树上找到相应的匹配,每次的匹配代价都是O(1)(如果词库用Hash表的话),这样匹配下来的时间复杂度就是字符串本身的长度!对于一个长度为n的字符串来说,它的分词复杂度是O(n)。而最大匹配的平均复杂度是O(n2)。

当然我们这里没有考虑歧义包容与分支处理等情况

问题

词库

首先是词库的保存格式。关系数据库对于我们并不适用,而自定义的二进制文件则实现起来比较困难,而且读写的效率也会有问题。因为我们想到了最简单的方法是利用java的serialization的功能,把整个内存中的树状结构直接序列化成磁盘的文本文件是最方便的。

第二个问题是树的父子节点的导航。我们的树并不是一颗二叉树,父亲的子节点会有好多!尤其是第一层,我们会把词库中所有的首字都取出来作为根节点的子节点,这意味着如果首字有4000个的话,根节点就有4000个儿子。当然随着树层数的增多,节点的儿子数也会减少,毕竟以“感冒”开头的词在整个词库也只有四十多个,而以“感冒清”开头的词则只有两三个了。这意味着如果设计得不合理,我们树的匹配遍历过程并不完全是线性的。最坏的查找算法是O(N)(N代表儿子数)。当然如果我们建词库时将儿子有序排列,再按照二分查找的方法,则我们的复杂度会减到O(lgN),这样的复杂度已经可以接受了。但是还有更简单又更快的存储方式,为什么不使用呢?那就是HashMap,毕竟在HashMap里查找东西时它的效率几乎是线性的,而且实现起来要比二分查询简单得多。当然用HashMap要付出存储空间变大的代价,但这样的代价来换取速度与简单性也是的。

第三个问题是找到有终结符的字后,我们必须要将它建成一个完整的词。这时我们必须能从字个往上回溯,直到找到根结点。因此我们在每个节点里都保存了父节点的指针。

分词查询

1、分支处理。

这是分词算法时歧义包容所必然碰到的问题。为了歧义包容,我们采用了与最大分词法完全不同的理念,我们的理念是将词库中存在词全部收入囊中!而且会发生重叠。例如“感冒解毒胶囊”,由于词库里存在“感冒”、“解毒”和“感冒解毒胶囊”这三个词,因此在分词的时候,我们会分别分出这三个词,这样用户无论输入“感冒”、“解毒”或“感冒解毒胶囊”搜索引擎都会找到相应的结果。

因此当遇到分支时,我们会分解成两条路线!例如当我们匹配到“感冒”的“冒”时,我们会发现一个终止符,代表“感冒”是一个完整的字,将它收录到分词中。接下来我们会分成两支,一支是继续往下走,匹配树的下一层,因为“冒”不是树的叶子,往下走可能会碰到更大的匹配词,例如“感冒解毒胶囊”。而另一支则从根开始,直接用“解”去匹配树的第一层节点,最后发现了“解毒”也是其中的一个词。

2、动态规划法

分支虽然使我们可以消除很多的歧义,但是显然它会带来副作用:导致分词的复杂度变大。如果一个句子很长时,分词的变化也许会呈指数级的增长,从一开始的两个分支变成四个、八个甚至更多。我们会发现很多句子虽然会有很多分支,但是这些分支又经常会汇聚到一个点,变成一个分支。例如:“感冒解毒胶囊可以治感冒”,我们在分词的时候可能会出现“感冒”,“解毒”,“感冒解毒”,“感冒解毒胶囊”等多个分支,但是当我们到达“囊”这个点的时候,所有的分支又会汇集到一起,因为大家接下来要处理的都是“可以治感冒”这个字符串。如果有办法让我们在汇聚以后只处理一个分支,那么算法的时间复杂度就不会象原来想象的那么坏。

而这刚好是动态规划法发挥威力的时候,动态规划要解决的问题是Overlapping sub-problem。它的处理方法就是将所有的子问题记录在公有的变量里(这里指的是类变量,它相对于某个method来说是公有变量,而不是真的全局变量)。当我遇到的子问题已经被处理过一次了,就直接跳过。这样节约的结果可以使算法复杂度得到质的改变,当然由于中文的变化多端,我们无法精确估计使用动态规划法后算法复杂度得到了多大的提高。

实际上的动态规划法的实现起来比说起来反而简单,我们只是简单地放了一个HashSet来存放已经分词过的位置:然后判断的函数也相当的简单:最后在分词的递归函数中加入这一句判断:当这个位置已经被处理过了就直接返回了。

3、词库预load

在使用基于词库的方法时,我们必须要面临的一个问题是:必要将词库读到内存中,而这通常会耗费很长的时间,幸运的是这样的工作我们只需要做一次,当我们将词库load进来以后,所有的工作都会在内存中进行,分词的速度会得到极速提升。我们选择的词库预load时机是我们第一次进行分词时,这相当于lazy load,只有用到的时候我们才去初始化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mrchesian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值