新词发现(1)——核心代码解读

主要思想是利用三个指标,即频数凝固度(取对数之后就是点互信息)与自由度(边界熵)来判断一个片段是否成词,并参考了苏神的代码,详见参考部分。

前奏

读取外部文章,作为一个字符串str格式

1
2
f = open('天龙八部.txt', 'r', encoding='gbk', errors='ignore')
s = f.read()

使用正则表达式,去除不需要的标点符号

1
2
3
4
# 去掉标点字
drop_dict = [u',', u'\n', u'。', u'、', u':', u'(', u')', u'[', u']', u'.', u',', u' ', u'\u3000', u'”', u'“', u'?', u'?', u'!', u'‘', u'’', u'…']
for i in drop_dict:
s = s.replace(i, '')

一些阈值初始设定

1
2
3
4
5
myre = {2:'(..)', 3:'(...)', 4:'(....)', 5:'(.....)', 6:'(......)', 7:'(.......)'}
min_count = 10 #最小频数
min_support = 30 #内部凝固程度,越大表示越相关
max_sep = 3 #最大候选词的字数
min_s = 3 #衡量词语的自由运用程度,越大说明越有可能独立成词

内部凝固度

通过list()方法把str按单字符切分,并逐字统计

1
2
3
4
5
# t为切分的词组并词频统计
t = []
t.append(pd.Series(list(s)).value_counts())
tsum = t[0].sum() #总字数
rt = [] #只存放词组

假设为2-grams模型,一段文本诸如"慕容公子真厉害",会被依次切分为"慕容/公子/真厉""容公/子真/厉害",合并后进行词频统计,并剔除出现次数过小的词语(限制出现频数)。

接着,再计算一个词语的内部凝固程度(取对数后即为点互信息PMI),剔除掉低于阈值的词语。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for m in range(2, max_sep+1):
print(u'正在生成%s字词...' %m)
t.append([])
# 切分
for i in range(m):
t[m-1] = t[m-1] +re.findall(myre[m], s[i:])
t[m-1] = pd.Series(t[m-1]).value_counts()
t[m-1] = t[m-1][t[m-1] > min_count] #最小次数筛选
tt = t[m-1][:]
# PMI互信息
for k in range(m-1):
qq = np.array(list(map(lambda ms: tsum*t[m-1][ms] / t[m-2-k][ms[:m-1-k]] / t[k][ms[m-1-k:]], tt.index))) > min_support
tt = tt[qq]
rt.append(list(tt.index))

rt形式如下:

这里补充一点,点互信息(即PMI)衡量的是两个事物之间的相关性,具体公式如下:

我们知道,若两个变量独立同分布,则有$p(x,y) = p(x)p(y)$, 但若$x$与$y$高度相关,则有$p(x,y) > p(x)p(y)$,所以PMI越大,相关性高。放入体本文的任务里,具体为"段誉"俩字经常同时出现,内部凝固程度高,所以不应该切分成'段'/'誉'

信息熵

第一步通过(.)%s(.)的正则方式切分出左邻字与右邻字,如"慕容公子真厉害啊"可切分成[('慕', '容公', '子'), ('真', '厉害', '啊')]的形式(n-grams=2)

然后把我们需要的中间那列作为索引并排序!intersect1d()方法可以找出两个列表之间的共同元素,之后计算左(右)邻信息熵。

其中pd.Series(pp[0][s]).value_counts()为取出交集中某个词语的全部左邻字并计数,再通过cal_s()函数计算信息熵,只取信息熵大于一定阈值的词语(如"慕容"),右邻信息熵同样如此,最后更新结果集rt

1
2
3
4
5
6
7
8
9
10
11
12
for i in range(2, max_sep+1):
print(u'在进行%s字词的最大熵筛选(%s)...' %(i, len(rt[i-2])))
pp = []
for j in range(i+2):
pp = pp + re.findall('(.)%s(.)' %myre[i], s[j:])
# 取中间一列作为索引
pp = pd.DataFrame(pp).set_index(1).sort_index()
# 取交集。排序很重要,可以加快检索速度
index = np.sort(np.intersect1d(rt[i-2], pp.index))
# 左邻与右邻信息熵
index = index[np.array(list(map(lambda s: cal_S(pd.Series(pp[0][s]).value_counts()), index))) > min_s]
rt[i-2] = index[np.array(list(map(lambda s: cal_S(pd.Series(pp[2][s]).value_counts()), index))) > min_s]

关于信息熵:熟悉决策树的同学一定不陌生,在这里可理解为词语的自由运用程度。譬如"辈子",大部分的情况下只会出现"上辈子""一辈子""几辈子"这种情况,语境自然不丰富,其左邻信息熵相对较小,故"辈子"无法单独成词。原文中的代码如下:

1
2
def cal_S(sl):
return -( (sl/sl.sum()).apply(log)*sl/sl.sum() ).sum()

最后一步

后面只需要我们在原有的词频统计表中,取出满足我们要求的词语出来,并按频数降序排序出即可。

1
2
3
for i in range(len(rt)):
t[i+1] = t[i+1][rt[i]] #只取在rt中出现过的单词
t[i+1] = t[i+1].sort_values(ascending=False)

我们来观测下输出结果

参考资料

原文:新词发现的信息熵方法与实现