基于新信息熵的新词发现原理《互联网时代的社会语言学:基于SNS的文本数据挖掘》这篇文章已经讲得非常清楚了,在这里主要是通过代码复现这篇文章。
实现的模块主要分为四个部分:从文章中提取所有可能出现的候选词。
计算每一个词的聚合度。
计算每一个词的左临熵和右临熵,即:自由度。
通过聚合度和左右临熵的分值组合来对一个候选词进行打分。
一、提取所有候选词
在我们对词没有概念的情况下我们会对文章的所有可能出现的词进行统计。比如一句话为“天气预报说周五会下雨”,要找出这一句话中的候选词我们可能会说“天”、“天气”、“天气预”等等都可能是一个词,那么这种没有限制的最长词的情况下在短短的10字的句子中可能会出现10!种可能,按照常识,我们需要预先设定一个最长词的长度。比如我们设置最长的词长度为5,那么这一句话中的所有候选词就为:“天”、“天气”、“天气预”、"天气预报"、"天气预报说"、"气"、"气预"、"气预报"、"气预报说"、"气预报说今"......等等,有5+5+5+5+5+5+4+3+2+1=40个候选词。
以下为提取候选词的代码:
def find_words(self, doc):
'''
找出所有可能出现的词, doc为传进去的文本
:param doc:
:param max_len_word:
:return:
'''
len_doc = len(doc)
for i in range(len_doc):
for j in range(i + 1, i + self.max_len_word + 1):
if doc[i:j] in self.words:
self.words[doc[i:j]]['freq'] += 1
else:
self.words[doc[i:j]] = {}
self.words[doc[i:j]]['freq'] = 1
二、计算聚合度
文章用“电影院”成词这个例子来讲聚合度,作者统计了在整个2400万字的数据中“电影”一次出现了2774次,出现的概率为0.000113, “院”字出现了4797次,出现的概率为0.0001969,如果两者间真的毫无关系的话,他们拼接在一起(电影院)的概率为P(电影)×P(院)/2,但其实“电影院"一共出现175次,要远远高于两个词的概率的乘积,是P(电影)×P(院)/2的600多倍,还统计了"的"字出现的概率为0.0166,并且文章中出现的“的电影”的真实概率P(的电影)与P(的)×P(电影)/2很接近,所以表明“电影院”更可能是一个有意义的搭配,而“的电影”则更像是“的”和“电影”两个成分偶然拼接到一起的。
在实现的过程中我们首先需要计算所有候选词出现的概率,然后通过概率计算每一个词的聚合度。那上边的例子来说(令dop表示聚合度):
“天气预报说周五会下雨”
其中令单字的聚合度为0
则dop(天)=0
dop(天气)=P(天)×P(气)
dop(天气预)=P(天)×P(气预)+P(天气)×P(预)
dop(天气预报)=P(天)×P(气预报)+P(天气)×P(预报)+P(天气预)×P(报)
dop(天气预报说)=P(天)×P(气预报说)+P(天气)×P(预报说)+P(天气预)×P(报说)+P(天气预报)×P(说)
......
也就是对词进行二切分,然后切分后的概率的乘积,在这里我去了每一个二切分的概率乘积的和,其实也可以用另一种方法:“电影院”的凝合程度则是 p(电影院) 分别除以 p(电) · p(影院) 和 p(电影) · p(院) 所得的商的较小值,这样处理甚至会有更好的效果,因为用最小值来代表这个词的聚合度,更能有力的证明该词的成词性,及该词的聚合度最小的情况下都成词的话,那么这个词肯定成词。
算出每隔词的聚合度后对每一个聚合度求log,具体为什么要求log后边会提到。
代码实现聚合度如下:
def dop(self):
'''
计算聚合度
:param words:
:return:
'''
len_words = len(self.words)
# 计算每一个词频
for k, v in self.words.items():
self.words[k]['freq_radio'] = self.words[k]['freq']/(5 * len_words)
for k, v in self.words.items():
dop = []
l = len(k)
if l == 1:
self.words[k]['dop'] = 0
else:
for i in range(1, l):
word = self.words[k[0:i]]['freq_radio']*self.words[k[i:l]]['freq_radio']
dop.append(word)
dop = sum(dop)
self.words[k]['dop'] = math.log(self.words[k]['freq_radio']/dop)
三、自由度
光看文本片段内部的凝合程度还不够,我们还需要从整体来看它在外部的表现。考虑“被子”和“辈子”这两个片段。我们可以说“买被子”、“盖被子”、“进被子”、“好被子”、“这被子”等等,在“被子”前面加各种字;但“辈子”的用法却非常固定,除了“一辈子”、“这辈子”、“上辈子”、“下辈子”,基本上“辈子”前面不能加别的字了。“辈子”这个文本片段左边可以出现的字太有限,以至于直觉上我们可能会认为,“辈子”并不单独成词,真正成词的其实是“一辈子”、“这辈子”之类的整体。可见,文本片段的自由运用程度也是判断它是否成词的重要标准。如果一个文本片段能够算作一个词的话,它应该能够灵活地出现在各种不同的环境中,具有非常丰富的左邻字集合和右邻字集合。
我们用信息熵来衡量一个文本片段的左邻字集合和右邻字集合有多随机。考虑这么一句话“吃葡萄不吐葡萄皮不吃葡萄倒吐葡萄皮”,“葡萄”一词出现了四次,其中左邻字分别为 {吃, 吐, 吃, 吐} ,右邻字分别为 {不, 皮, 倒, 皮} 。根据公式,“葡萄”一词的左邻字的信息熵为 – (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 。可见,在这个句子中,“葡萄”一词的右邻字更加丰富一些。
在实现的过程中首先遍历每一个候选词,然后对找出该候选词在文章的所有位置,然后找出该候选词前边一个字符出现的所有情况,然后就根据公式计算该候选词的做临熵,右临熵类似,代码如下:
def left_free(self, doc):
'''
计算左自由度
:param words:
:return:
'''
for k, v in self.words.items():
left_list = [m.start() for m in re.finditer(k, doc) if m.start() != 1]
len_left_list = len(left_list)
left_item = {}
for li in left_list:
if doc[li-1] in left_item:
left_item[doc[li-1]] += 1
else:
left_item[doc[li-1]] = 1
left = 0
for _k, _v in left_item.items():
left += abs((left_item[_k]/len_left_list) * math.log(1/len(left_item)))
self.words[k]['left_free'] = left
def right_free(self, doc):
'''
计算右自由度
:param words:
:return:
'''
for k, v in self.words.items():
right_list = [m.start() for m in re.finditer(k, doc) if m.start() < len(doc)-5]
len_right_list = len(right_list)
right_item = {}
for li in right_list:
if doc[li+len(k)] in right_item:
right_item[doc[li+len(k)]] += 1
else:
right_item[doc[li+len(k)]] = 1
right = 0
for _k, _v in right_item.items():
right += abs((right_item[_k]/len_right_list) * math.log(1/len(right_item)))
self.words[k]['right_free'] = right
四、通过聚合度和左右临熵的分值组合来对一个候选词进行打分
知道了聚合度和自由度,就可以对一个词进行综合打分,我再这里用的简单粗暴的相加(聚合度+自由度),然后使用dataframe做了个排序。
代码如下:
def get_df(self):
df = pd.DataFrame(self.words)
df = df.T
df['score'] = df['dop'] + df['left_free'] + df['right_free']
df = df.sort_values(by='score', ascending=False)
df = df[df['score'] > self.radio]
return df
完整代码如下:
# -*- coding: utf-8 -*-
import re
import math
import pandas as pd
import string
from collections import OrderedDict
class NewWord(object):
def __init__(self, max_len_word, radio):
self.max_len_word = max_len_word
self.radio = radio
self.words = {}
def find_words(self, doc):
'''
找出所有可能出现的词
:param doc:
:param max_len_word:
:return:
'''
len_doc = len(doc)
for i in range(len_doc):
for j in range(i + 1, i + self.max_len_word + 1):
if doc[i:j] in self.words:
self.words[doc[i:j]]['freq'] += 1
else:
self.words[doc[i:j]] = {}
self.words[doc[i:j]]['freq'] = 1
def dop(self):
'''
计算聚合度
:param words:
:return:
'''
len_words = len(self.words)
# 计算每一个词频
for k, v in self.words.items():
self.words[k]['freq_radio'] = self.words[k]['freq']/(5 * len_words)
for k, v in self.words.items():
dop = []
l = len(k)
if l == 1:
self.words[k]['dop'] = 0
else:
for i in range(1, l):
word = self.words[k[0:i]]['freq_radio']*self.words[k[i:l]]['freq_radio']
dop.append(word)
dop = sum(dop)
self.words[k]['dop'] = math.log(self.words[k]['freq_radio']/dop)
def left_free(self, doc):
'''
计算左自由度
:param words:
:return:
'''
for k, v in self.words.items():
left_list = [m.start() for m in re.finditer(k, doc) if m.start() != 1]
len_left_list = len(left_list)
left_item = {}
for li in left_list:
if doc[li-1] in left_item:
left_item[doc[li-1]] += 1
else:
left_item[doc[li-1]] = 1
left = 0
for _k, _v in left_item.items():
left += abs((left_item[_k]/len_left_list) * math.log(1/len(left_item)))
self.words[k]['left_free'] = left
def right_free(self, doc):
'''
计算右自由度
:param words:
:return:
'''
for k, v in self.words.items():
right_list = [m.start() for m in re.finditer(k, doc) if m.start() < len(doc)-5]
len_right_list = len(right_list)
right_item = {}
for li in right_list:
if doc[li+len(k)] in right_item:
right_item[doc[li+len(k)]] += 1
else:
right_item[doc[li+len(k)]] = 1
right = 0
for _k, _v in right_item.items():
right += abs((right_item[_k]/len_right_list) * math.log(1/len(right_item)))
self.words[k]['right_free'] = right
def get_df(self):
df = pd.DataFrame(self.words)
df = df.T
df['score'] = df['dop'] + df['left_free'] + df['right_free']
df = df.sort_values(by='score', ascending=False)
df = df[df['score'] > self.radio]
return df
def run(self, doc):
doc = re.sub('[,,.。"“”‘’\';;::、??!!\n\[\]\(\)()\\/a-zA-Z0-9]', '', doc)
self.find_words(doc)
self.dop()
self.left_free(doc)
self.right_free(doc)
df = self.get_df()
return df
if __name__ == '__main__':
doc = open('./model/data.txt', 'r', encoding='utf-8').read()
nw = NewWord(max_len_word=5, radio=9.6)
df = nw.run(doc)
df.to_csv('./model/text.txt', sep='|', encoding='utf-8')