首页 > 代码库 > 【算法设计】基于大规模语料的新词发现算法

【算法设计】基于大规模语料的新词发现算法

对中文资料进行自然语言处理时,我们会遇到很多其他语言不会有的困难,例如分词——汉语的词与词之间没有空格,那计算机怎么才知道“已结婚的和尚未结婚的”究竟是“已/结婚/的/和/尚未/结婚/的”,还是“已/结婚/的/和尚/未/结婚/的”呢?

这就是所谓的分词歧义难题。不过,现在很多语言模型都已能比较漂亮地解决这一问题了。但在中文分词领域里,还有一个比分词歧义更令人头疼的东西——未登录词。中文没有首字母大写,专名号也被取消了,这叫计算机如何辨认人名地名之类的东西?更惨的则是机构名、品牌名、专业名词、缩略语、网络新词等,它们的产生机制似乎完全无规律可寻。最近十年来,中文分词领域都在集中攻克这一难关,自动发现新词成为关键的环节。

挖掘新词的传统方法是,先对文本进行分词,然后猜测未能成功匹配的剩余片段就是新词。这似乎陷入了一个怪圈:分词的准确性本身就依赖于词库的完整性,如果词库中根本没有新词,我们又怎能信任分词结果呢?

此时,一种大胆的想法是,首先不依赖于任何已有的词库,仅仅根据词的共同特征,将一段大规模语料中可能成词的文本片段全部提取出来,不管它是新词还是旧词。然后,再把所有抽出来的词和已有词库进行比较,不就能找出新词了吗?有了抽词算法后,我们还能以词为单位做更多有趣的数据挖掘工作。这里,我所选用的语料是人人网2011年12月前半个月部分用户的状态。

成词标准之一:内部凝固程度

要想从一段文本中抽出词来,我们的第一个问题就是,怎样的文本片段才算一个词?大家想到的第一个标准或许是,看这个文本片段出现的次数是否足够多。我们可以把所有出现频数超过某个阈值的片段提取出来,作为该语料中的词汇输出。不过,光是出现频数高还不够,一个经常出现的文本片段有可能不是一个词,而是多个词构成的词组。在人人网用户状态中,“的电影”出现了389次,“电影院”只出现了175次,然而我们却更倾向于把“电影院”当作一个词,因为直觉上看,“电影”和“院”凝固得更紧一些。

为了证明“电影院”一词的内部凝固程度确实很高,我们可以计算一下,如果“电影”和“院”真的是各自独立地在文本中随机出现,它俩正好拼到一起的概率会有多小。在整个2400万字的数据中,“电影”一共出现了2774次,出现的概率约为0.000113。“院”字则出现了4797次,出现的概率约为0.0001969。如果两者之间真的毫无关系,它们恰好拼在了一起的概率就应该是0.000113×0.0001969,约为2.223乘以10的–8次方。但事实上,“电影院”在语料中一共出现了175次,出现概率约为7.183乘以10的–6次方,是预测值的300多倍。类似地,统计可得“的”字的出现概率约为0.0166,因而“的”和“电影”随机组合到了一起的理论概率值为0.0166×0.000113,约为1.875乘以10的–6次方,这与“的电影”出现的真实概率很接近——真实概率约为1.6乘以10的–5次方,是预测值的8.5倍。计算结果表明,“电影院”可能是一个更有意义的搭配,而“的电影”则更像是“的”和“电影”这两个成分偶然拼到一起的。

当然,作为一个无知识库的抽词程序,我们并不知道“电影院”是“电影”加“院”得来的,也并不知道“的电影”是“的”加上“电影”得来的。错误的切分方法会过高地估计该片段的凝合程度。如果我们把“电影院”看作是“电”加“影院”所得,由此得到的凝合程度会更高一些。因此,为了算出一个文本片段的凝合程度,我们需要枚举它的凝合方式——这个文本片段是由哪两部分组合而来的。令p(x)为文本片段x在整个语料中出现的概率,那么我们定义“电影院”的凝合程度就是p(电影院)与p(电)·p(影院)的比值和p(电影院)与p(电影)·p(院)的比值中的较小值,“的电影”的凝合程度则是p(的电影)分别除以p(的)·p(电影)和p(的电)·p(影)所得的商的较小值。

可以想到,凝合程度最高的文本片段就是诸如“蝙蝠”、“蜘蛛”、“彷徨”、“忐忑”之类的词了,这些词里的每一个字几乎总是会和另一个字同时出现,从不在其他场合中使用。

成词标准之二:自由运用程度

光看文本片段内部的凝合程度还不够,我们还需要从整体来看它在外部的表现。考虑“被子”和“辈子”这两个片段。我们可以说“买被子”、“盖被子”、“进被子”、“好被子”、“这被子”等,在“被子”前面加各种字;但“辈子”的用法却非常固定,除了“一辈子”、“这辈子”、“上辈子”、“下辈子”,基本上“辈子”前面不能加别的字了。“辈子”这个文本片段左边可以出现的字太有限,以至于直觉上我们可能会认为,“辈子”并不单独成词,真正成词的其实是“一辈子”、“这辈子”之类的整体。可见,文本片段的自由运用程度也是判断它是否成词的重要标准。如果一个文本片段能够算作一个词的话,它应该能够灵活地出现在各种不同的环境中,具有非常丰富的左邻字集合和右邻字集合。

“信息熵”是一个非常神奇的概念,它能够反映一个事件的结果平均会给你带来多大的信息量。如果某个结果的发生概率为p,当你知道它确实发生了,你得到的信息量就被定义为-log(p)。p越小,你得到的信息量就越大。如果一颗骰子的六个面分别是1、1、1、2、2、3,那么你知道了投掷的结果是1时可能并不会那么吃惊,它给你带来的信息量是-log(1/2),约为0.693。知道投掷结果是2,给你带来的信息量则是-log(1/3)≈1.0986。知道投掷结果是3,给你带来的信息量则有-log(1/6)≈1.79。但你只有1/2的机会得到0.693的信息量,只有1/3的机会得到1.0986的信息量,只有1/6的机会得到1.79的信息量,因而平均情况下你会得到0.693/2+1.0986/3+1.79/6≈1.0114的信息量。这个1.0114就是那颗骰子的信息熵。现在,假如某颗骰子有100个面,其中99个面都是1,只有一个面上写的2。知道骰子的抛掷结果是2会给你带来一个巨大无比的信息量,它等于-log(1/100),约为4.605;但你只有1%的概率获取到这么大的信息量,其他情况下你只能得到-log(99/100)≈0.01005的信息量。平均情况下,你只能获得0.056的信息量,这就是这颗骰子的信息熵。再考虑一个最极端的情况:如果一颗骰子的六个面都是1,投掷它不会给你带来任何信息,它的信息熵为-log(1)=0。什么时候信息熵会更大呢?换句话说,发生了怎样的事件之后,你最想问一下它的结果如何?直觉上看,当然就是那些结果最不确定的事件。没错,信息熵直观地反映了一个事件的结果有多么的随机。

我们用信息熵来衡量一个文本片段的左邻字集合和右邻字集合有多随机。考虑这么一句话“吃葡萄不吐葡萄皮不吃葡萄倒吐葡萄皮”,“葡萄”一词出现了四次,其中左邻字分别为{吃, 吐, 吃, 吐},右邻字分别为{不, 皮, 倒, 皮}。根据公式,“葡萄”一词的左邻字的信息熵为-(1/2)·log(1/2)-(1/2)·log(1/2)≈0.693,它的右邻字的信息熵则为-(1/2)·log(1/2)-(1/4)·log(1/4)-(1/4)·log(1/4)≈1.04。可见,在这个句子中,“葡萄”一词的右邻字更加丰富一些。

凝固程度和自由程度,两种判断标准缺一不可。若只看前者,程序会找出实际上是“半个词”的片段;若只看后者,程序则会找出诸如“了一”、“的电影”一类的“垃圾词组”。

在人人网用户状态中,“被子”一词一共出现了956次,“辈子”一词一共出现了2330次,两者的右邻字集合的信息熵分别为3.87404和4.11644,数值上非常接近。但“被子”的左邻字用例非常丰富:用得最多的是“晒被子”,它一共出现了162次;其次是“的被子”,出现了85次;接下来分别是“条被子”、“在被子”、“床被子”,分别出现了69次、64次和52次;当然,还有“叠被子”、“盖被子”、“加被子”、“新被子”、“掀被子”、“收被子”、“薄被子”、“踢被子”、“抢被子”等100多种不同的用法构成的长尾。所有左邻字的信息熵为3.67453。但“辈子”的左邻字就很可怜了,2330个“辈子”中有1276个是“一辈子”,有596个“这辈子”,有235个“下辈子”,有149个“上辈子”,有32个“半辈子”,有10个“八辈子”,有7个“几辈子”,有6个“哪辈子”,以及“n辈子”、“两辈子”等13种更罕见的用法。所有左邻字的信息熵仅为1.25963。因而,“辈子”能否成词,明显就有争议了。“下子”则是更典型的例子,310个“下子”的用例中有294个出自“一下子”,5个出自“两下子”,5个出自“这下子”,其余的都是只出现过一次的罕见用法。事实上,“下子”的左邻字信息熵仅为0.294421,我们不应该把它看作一个能灵活运用的词。当然,一些文本片段的左邻字没啥问题,右邻字用例却非常贫乏,例如“交响”、“后遗”、“鹅卵”等,把它们看作单独的词似乎也不太合适。我们不妨就把一个文本片段的自由运用程度定义为它的左邻字信息熵和右邻字信息熵中的较小值。

在实际运用中你会发现,文本片段的凝固程度和自由程度,两种判断标准缺一不可。只看凝固程度的话,程序会找出“巧克”、“俄罗”、“颜六色”、“柴可夫”等实际上是“半个词”的片段;只看自由程度的话,程序则会把“吃了一顿”、“看了一遍”、“睡了一晚”、“去了一趟”中的“了一”提取出来,因为它的左右邻字都太丰富了。

图1 对“四是四十是十十四是十四 四十是四十”的所有后缀进行排序后的结果

抽词算法的实现

我们把文本中出现过的所有长度不超过d的子串都当作潜在的词(即候选词,其中d为自己设定的候选词长度上限,我设定的值为5),再为出现频数、凝固程度和自由程度各设定一个阈值,然后只需要提取出所有满足阈值要求的候选词即可。为了提高效率,我们可以把语料全文视作一整个字符串,并对该字符串的所有后缀按字典序排序。

这样的话,相同的候选词便都集中在了一起,从头到尾扫描一遍便能算出各个候选词的频数和右邻字信息熵。将整个语料逆序后重新排列现在所有的后缀,再扫描一遍后便能统计出每个候选词在原文中所有左邻字的信息熵。另外,有了频数信息后,凝固程度也都很好计算了。这样,我们便得到了一个无须任何知识库的抽词算法,输入一段充分长的文本,这个算法能以大致O(n logn)的效率提取出可能的词来。

实际上我们只需要在内存中储存这些后缀的前d+1个字,或者更好地,只储存它们在语料中的起始位置。

对不同的语料进行抽词,并且按这些词的频数从高到低排序。你会发现,不同文本的用词特征是非常明显的。

《西游记》上册:行者、师父、三藏、八戒、大圣、菩萨、悟空、怎么、和尚、唐僧……

《资本论》全文:商品、形式、货币、我们、过程、自己、机器、社会、部分、表现……

《圣经》全文:以色列、没有、自己、一切、面前、大卫、知道、什么、犹大、祭司……

《时间简史》全文:黑洞、必须、非常、任何、膨胀、科学、预言、太阳、观察、定律……

当然,我也没有忘记对人人网用户状态进行分析——人人网用户状态中最常出现的10个词是:哈哈、什么、今天、怎么、现在、可以、知道、喜欢、终于、这样。事实上,程序从人人网的状态数据中一共抽出了大约1200个词,里面大多数词也确实都是标准的现代汉语词汇。不过别忘了,我们的目标是新词抽取。将所有抽出来的词与已有词库作对比,于是得到了人人网特有的词汇(同样按频数从高到低排序):

尼玛、伤不起、给力、有木有、挂科、坑爹、神马、淡定、老爸、卧槽、牛逼、肿么、苦逼、无语、微博、六级、高数、选课、悲催、基友、蛋疼、很久、人人网、情何以堪、童鞋、哇咔咔、脑残、吐槽、猥琐、奶茶、我勒个去、刷屏、妹纸、胃疼、飘过、考研、弱爆了、太准了、搞基、忽悠、羡慕嫉妒恨、手贱、柯南、狗血、秒杀、装逼、真特么、碎觉、奥特曼、内牛满面、斗地主、腾讯、灰常、偶遇、拉拉、九把刀、高富帅、阿内尔卡、魔兽世界、线代、三国杀、林俊杰、速速、臭美、花痴……

我还想到了更有意思的玩法。为什么不拿每天状态里的词去和前一天作对比,从而提取出这一天里特有的词呢?这样一来,我们就能从用户状态中提取出每日热点了!事实上,有了抽词算法之后,我们就能够以词语为单位,在各个维度上挖掘人们在社交网络中的用词动向:今年网络上最流行的新词是什么?男生和女生都喜欢说些什么?不同地方、不同年龄段的人都爱用什么样的词?这些问题曾经需要靠大规模语言统计调查才能得到一个满意的回答,而现在,借助互联网海量的用户生成内容,我们可以轻而易举地得到答案,为社会语言学的研究提供真实可靠材料。

 

【算法设计】基于大规模语料的新词发现算法