http://www.matrix67.com/blog/archives/5044
互联网时代的社会语言学:基于SNS的文本数据挖掘
今年上半年,我在人人网实习了一段时间,期间得到了很多宝贵的数据,并做了一些还算有意义的事情,在这里和大家一块儿分享。感谢人人网提供的数据与工作环境,感谢赵继承博士、詹卫东老师的支持和建议。在这项工作中,我得到了很多与众人交流的机会,特别感谢 OpenParty 、 TEDxBeijing 提供的平台。本文已发表在了《程序员》杂志,分上下两部分刊于 2012 年 7 月刊和 8 月刊,在此感谢卢鸫翔编辑的辛勤工作。由于众所周知的原因,《程序员》刊出的文章被和谐过(看到后面大家就自动地知道被和谐的内容是什么了),因而我决定把完整版发在 Blog 上,同时与更多的人一同分享。对此感兴趣的朋友可以给我发邮件继续交流。好了,开始说正文吧。
作为中文系应用语言学专业的学生以及一名数学 Geek ,我非常热衷于用计算的方法去分析汉语资料。汉语是一种独特而神奇的语言。对汉语资料进行自然语言处理时,我们会遇到很多其他语言不会有的困难,比如分词——汉语的词与词之间没有空格,那计算机怎么才知道,“已结婚的和尚未结婚的青年都要实行计划生育”究竟说的是“已/结婚/的/和/尚未/结婚/的/青年”,还是“已/结婚/的/和尚/未/结婚/的/青年”呢?这就是所谓的分词歧义难题。不过,现在很多语言模型已经能比较漂亮地解决这一问题了。但在中文分词领域里,还有一个比分词歧义更令人头疼的东西——未登录词。中文没有首字母大写,专名号也被取消了,这叫计算机如何辨认人名地名之类的东西?更惨的则是机构名、品牌名、专业名词、缩略语、网络新词等等,它们的产生机制似乎完全无规律可寻。最近十年来,中文分词领域都在集中攻克这一难关。自动发现新词成为了关键的环节。
挖掘新词的传统方法是,先对文本进行分词,然后猜测未能成功匹配的剩余片段就是新词。这似乎陷入了一个怪圈:分词的准确性本身就依赖于词库的完整性,如果词库中根本没有新词,我们又怎么能信任分词结果呢?此时,一种大胆的想法是,首先不依赖于任何已有的词库,仅仅根据词的共同特征,将一段大规模语料中可能成词的文本片段全部提取出来,不管它是新词还是旧词。然后,再把所有抽出来的词和已有词库进行比较,不就能找出新词了吗?有了抽词算法后,我们还能以词为单位做更多有趣的数据挖掘工作。这里,我所选用的语料是人人网 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 ;但你只有百分之一的概率获取到这么大的信息量,其他情况下你只能得到 – 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 ,我们不应该把它看作一个能灵活运用的词。当然,一些文本片段的左邻字没啥问题,右邻字用例却非常贫乏,例如“交响”、“后遗”、“鹅卵”等,把它们看作单独的词似乎也不太合适。我们不妨就把一个文本片段的自由运用程度定义为它的左邻字信息熵和右邻字信息熵中的较小值。
在实际运用中你会发现,文本片段的凝固程度和自由程度,两种判断标准缺一不可。只看凝固程度的话,程序会找出“巧克”、“俄罗”、“颜六色”、“柴可夫”等实际上是“半个词”的片段;只看自由程度的话,程序则会把“吃了一顿”、“看了一遍”、“睡了一晚”、“去了一趟”中的“了一”提取出来,因为它的左右邻字都太丰富了。
我们把文本中出现过的所有长度不超过 d 的子串都当作潜在的词(即候选词,其中 d 为自己设定的候选词长度上限,我设定的值为 5 ),再为出现频数、凝固程度和自由程度各设定一个阈值,然后只需要提取出所有满足阈值要求的候选词即可。为了提高效率,我们可以把语料全文视作一整个字符串,并对该字符串的所有后缀按字典序排序。下表就是对“四是四十是十十四是十四四十是四十”的所有后缀进行排序后的结果。实际上我们只需要在内存中存储这些后缀的前 d + 1 个字,或者更好地,只储存它们在语料中的起始位置。
这样的话,相同的候选词便都集中在了一起,从头到尾扫描一遍便能算出各个候选词的频数和右邻字信息熵。将整个语料逆序后重新排列所有的后缀,再扫描一遍后便能统计出每个候选词的左邻字信息熵。另外,有了频数信息后,凝固程度也都很好计算了。这样,我们便得到了一个无需任何知识库的抽词算法,输入一段充分长的文本,这个算法能以大致 O(n · logn) 的效率提取出可能的词来。
对不同的语料进行抽词,并且按这些词的频数从高到低排序。你会发现,不同文本的用词特征是非常明显的。下面是对《西游记》上册的抽词结果:
《资本论》全文:
《圣经》全文:
《时间简史》全文:
哦,对了,还有我最喜欢的,《人民日报》 2000 年 4 月新闻版的抽词结果:
当然,我也没有忘记对人人网用户状态进行分析——人人网用户状态中最常出现的词是:
事实上,程序从人人网的状态数据中一共抽出了大约 1200 个词,里面大多数词也确实都是标准的现代汉语词汇。不过别忘了,我们的目标是新词抽取。将所有抽出来的词与已有词库作对比,于是得到了人人网特有的词汇(同样按频数从高到低排序):
我还想到了更有意思的玩法。为什么不拿每一天状态里的词去和前一天的状态作对比,从而提取出这一天里特有的词呢?这样一来,我们就能从人人网的用户状态中提取出每日热点了!从手里的数据规模看,这是完全有可能的。我选了 12 个比较具有代表性的词,并列出了它们在 2011 年 12 月 13 日的用户状态中出现的频数(左列的数),以及 2011 年 12 月 14 日的用户状态中出现的频数(右列的数):
大家可以从直觉上迅速判断出,哪些词可以算作是 12 月 14 日的热词。比方说,“下雪”一词在 12 月 13 日只出现了 33 次,在 12 月 14 日却出现了 92 次,后者是前者的 2.8 倍,这不大可能是巧合,初步判断一定是 12 月 14 日真的有什么地方下雪了。“那些年”在 12 月 14 日的频数确实比 12 月 13 日更多,但相差并不大,我们没有理由认为它是当日的一个热词。
一个问题摆在了我们面前:我们如何去量化一个词的“当日热度”?第一想法当然是简单地看一看每个词的当日频数和昨日频数之间的倍数关系,不过细想一下你就发现问题了:它不能解决样本过少带来的偶然性。 12 月 14 日“李宇春”一词的出现频数是 12 月 13 日的 4 倍,这超过了“下雪”一词的 2.8 倍,但我们却更愿意相信“李宇春”的现象只是一个偶然。更麻烦的则是“电磁炉”一行, 12 月 14 日的频数是 12 月 13 日的无穷多倍,但显然我们也不能因此就认为“电磁炉”是 12 月 14 日最热的词。
忽略所有样本过少的词?这似乎也不太好,样本少的词也有可能真的是热词。比如“北半球”一词,虽然它在两天里的频数都很少,但这个 9 倍的关系确实不容忽视。事实上,人眼很容易看出哪些词真的是 12 月 14 日的热词:除了“下雪”以外,“看见”、“北半球”和“脖子”也应该是热词。你或许坚信后三个词异峰突起的背后一定有什么原因(并且迫切地想知道这个原因究竟是什么),但却会果断地把“李宇春”和“电磁炉”这两个“异常”归结为偶然原因。你的直觉是对的—— 2011 年 12 月 14 日发生了极其壮观的双子座流星雨,此乃北半球三大流星雨之一。白天网友们不断转发新闻,因而“北半球”一词热了起来;晚上网友们不断发消息说“看见了”、“又看见了”,“看见”一词的出现频数猛增;最后呢,仰望天空一晚上,脖子终于出毛病了,于是回家路上一个劲儿地发“脖子难受”。
让计算机也能聪明地排除偶然因素,这是我们在数据挖掘过程中经常遇到的问题。我们经常需要对样本过少的项目进行“平滑”操作,以避免分母过小带来的奇点。这里,我采用的是一个非常容易理解的方法:一个词的样本太少,就给这个词的热度打折扣。为了便于说明,我们选出四个词为例来分析。
下表截取了前四个词,右边四列分别表示各词在 12 月 13 日出现的频数,在 12 月 14 日出现的频数,在两天里一共出现的总频数,以及后一天的频数所占的比重。第三列数字是前两列数字之和,第四列数字则是第二列数字除以第三列数字的结果。最后一列应该是一个 0 到 1 之间的数,它表明对应的词有多大概率出现在了 12 月 14 日这一天。最后一列可以看作是各词的得分。可以看到,此时“下雪”的得分低于“李宇春”,这是我们不希望看到的结果。“李宇春”的样本太少,我们想以此为缘由把它的得分拖下去。
怎么做呢?我们把每个词的得分都和全局平均分取一个加权平均!首先计算出这四个词的平均总频数,为 313.75 ;再计算出这四个词的平均得分,为 0.719 。接下来,我们假设已经有 313.75 个人预先给每个词都打了 0.719 分,换句话说每个词都已经收到了 313.75 次评分,并且所有这 313.75 个评分都是 0.719 分。“下雪”这个词则还有额外的 125 个人评分,其中每个人都给了 0.736 分。因此,“下雪”一词的最终得分就是:
类似地,其他几个词的得分依次为:
容易看出,此时样本越大的词,就越有能力把最终得分拉向自己本来的得分,样本太小的词,最终得分将会与全局平均分非常接近。经过这么一番调整,“下雪”一词的得分便高于了“李宇春”。实际运用中, 313.75 这个数也可以由你自己来定,定得越高就表明你越在意样本过少带来的负面影响。这种与全局平均取加权平均的思想叫做 Bayesian average ,从上面的若干式子里很容易看出,它实际上是最常见的平滑处理方法之一——分子分母都加上一个常数——的一种特殊形式。
利用之前的抽词程序抽取出人人网每一天内用户状态所含的词,把它们的频数都与前一天的作对比,再利用刚才的方法加以平滑,便能得出每一天的热词了。我手上的数据是人人网 2011 年 12 月上半月的数据,因此我可以得出从 12 月 2 日到 12 月 15 日的热词(选取每日前 5 名,按得分从高到低)。
看来, 12 月 14 日果然有流星雨发生。
注意,由于我们仅仅对比了相邻两天的状态,因而产生了个别实际上是由工作日/休息日的区别造成的“热词”,比如“教室”、“老师”、“星期二”等。把这样的词当作热词可能并不太妥。结合上周同日的数据,或者干脆直接与之前整个一周的数据来对比,或许可以部分地解决这一问题。
事实上,有了上述工具,我们可以任意比较两段不同文本中的用词特点。更有趣的是,人人网状态的大多数发布者都填写了性别和年龄的个人信息,我们为何不把状态重新分成男性和女性两组,或者 80 后和 90 后两组,挖掘出不同属性的人都爱说什么?要知道,在过去,这样的问题需要进行大规模语言统计调查才能回答!然而,在互联网海量用户生成内容的支持下,我们可以轻而易举地挖掘出答案来。
我真的做了这个工作(基于另一段日期内的数据)。男性爱说的词有:
下面则是女性爱说的词:
下面是 90 后用户爱用的词:
下面则是 80 后用户爱用的词:
不仅如此,不少状态还带有地理位置信息,因而我们可以站在空间的维度对信息进行观察。这个地方的人都爱说些什么?爱说这个词的人都分布在哪里?借助这些包含地理位置的签到信息,我们也能挖掘出很多有意思的结果来。例如,对北京用户的签到信息进行抽词,然后对于每一个抽出来的词,筛选出所有包含该词的签到信息并按地理坐标的位置聚类,这样我们便能找出那些地理分布最集中的词。结果非常有趣:“考试”一词集中分布在海淀众高校区,“天津”一词集中出现在北京南站,“逛街”一词则全都在西单附近扎堆。北京首都国际机场也是一个非常特别的地点,“北京”、“登机”、“终于”、“再见”等词在这里出现的密度极高。
从全国范围来看,不同区域的人也有明显的用词区别。我们可以将全国地图划分成网格,统计出所有签到信息在各个小格内出现的频数,作为标准分布;然后对于每一个抽出来的词,统计出包含该词的签到信息在各个小格内出现的频数,并与标准分布进行对比(可以采用余弦距离等公式),从而找出那些分布最反常的词。程序运行后发现,这样的词还真不少。一些明显具有南北差异的词,分布就会与整个背景相差甚远。例如,在节假日的时候,“滑雪”一词主要在北方出现,“登山”一词则主要在南方出现。地方特色也是造成词语分布差异的一大原因,例如“三里屯”一词几乎只在北京出现,“热干面”一词集中出现在武汉地区,“地铁”一词明显只有个别城市有所涉及。这种由当地人的用词特征反映出来的真实的地方特色,很可能是许多旅游爱好者梦寐以求的信息。另外,方言也会导致用词分布差异,例如“咋这么”主要分布在北方地区,“搞不懂”主要分布在南方城市,“伐”则非常集中地出现在上海地区。当数据规模足够大时,或许我们能通过计算的方法,自动对中国的方言区进行划分。
其实,不仅仅是发布时间、用户年龄、用户性别、地理位置这四个维度,我们还可以对浏览器、用户职业、用户活跃度、用户行为偏好等各种各样的维度进行分析,甚至可以综合考虑以上维度,在某个特定范围内挖掘热点事件,或者根据语言习惯去寻找出某个特定的人群。或许这听上去太过理想化,不过我坚信,有了合适的算法,这些想法终究会被一一实现。