一个古老中文无词典抽词算法的MMA实现
思路
无词典抽词的主要问题就是如何判定某个序列是一个词。这里根据 matrix67 大佬的一篇老旧博文(最近访问不了,以后再补链接)选取了两个参数,一个我把它叫做结合度(combinity),定义如下:
对于字符序列 a 和 b,
cbt(a,b) = P("ab") / (P("a")*P("b"))
即 ab 的频率除以 a 和 b 分别出现的频率之积。
这样做的原理是:姑且认为 a 和 b 一起出现的频率越高就越可能是一个词。但是如果 a 或 b 本身就频率就很高,那么 a b 碰巧在一起的概率也会很高,因此要除以他们本身频率来修正。(比如一篇文章中“国的”出现的频率可能比“中国”高)。
另一个参数为边界熵。定义为语料库中某一个字符串左边/右边第一个字符的熵。这是因为:如果一个字符串 a
是一个词,那么 a 左右的字符都是自由的,边界熵应该较高。反之如果 a 不是一个词,那么 a 的左右应该经常出现能够和 a 组成词的字符, 边界熵较低。
最后给结合度和边界熵设定一个阈值就完成了整个算法的思路。
程序设计
结合度的算法没有任何难度,直接计算频率即可,不过由于词可能不止由一个字组词,因此对于 abcdef 这样一个词,应该计算任意切分的结合度并取最小值,也就是计算 cbt(a,bcdef), cbt(ab,cdef) ……的最小值。
这种计算 MMA 可以很好地完成。以“斗宗强者”为例
先取得所有切分:
s = "斗宗强者"; {StringTake[s, #], StringTake[s, {# + 1, -1}]} & /@ Range[StringLength@s - 1]
输出为:{{"斗", "宗强者"}, {"斗宗", "强者"}, {"斗宗强", "者"}}
, 然后依次计算结合度再取最小值即可。
而边界熵的计算,首先要取得所有边界字符,用 StringCases
可以实现, 燃油 MMA 自带了求熵函数,直接求就行了……
leftEntropy[corpus_, w_] := N@Entropy@StringCases[corpus, x_ ~~ w :> x]
rightEntropy[corpus_, w_] := N@Entropy@StringCases[corpus, w ~~ x_ :> x]
最后对一篇文章递归抽出所有词即可。全部代码:https://github.com/Lo-Aidas/MathematicaToys/blob/master/FindWords.nb
充数的实例
以小说《斗破苍穹》第300w~310w 字为语料库,绘制第 300w ~ 302w 的单词云:
wordcloud.png不得不说确实中二度爆表……