互联网中经常会出现很多网红词,比如最近的"佛系青年",“食草男”.这些词并不在词典中,那么有什么方法能够发现这些新词呢?机缘之下有幸阅读互联网时代的社会语言学:基于SNS的文本数据挖掘,受益匪浅.

本文的思想完全来自matrix67.com,这是一种不依赖词库的新词发现方法.使用词频,内部凝固程度,自由程度三个考察纬度进行新词筛选.

词频

这点很好理解,因为不是词的话出现的频率一般比较低,词的出现频率会比较高.所以可以设置一个词频阀值,高于这个阀值的判断为词,否则判定为不是词.

内部凝固程度

仅用词频是不够的,例如"的电影"的词频会比"电影院"的词频高,显然"电影院"是一个词,而"的电影"不是一个词.

假设"电影院"是随机出现的,那么应该有:

P(电影院) = P(电影) \times P(院)

但事实上"电影院"出现的概率比"电影"的概率与院的概率乘积大很多:

P(电影院) >> P(电影) \times P(院)

所以我们有理由相信"电影院"是一个词.

基于这种思想,我们可以考察一个词的内部凝固程度:

n_{电影院} = \min(\frac{p(电影院)}{p(电)\times p(影院)},\frac{p(电影院)}{p(电影)\times p(院)})

n_{的电影} = \min(\frac{p(的电影)}{p(的电)\times p(影)},\frac{p(的电影)}{p(的)\times p(电影)})

内部凝固程度在也被称为提升度.

自由程度

考虑“被子”和“辈子”这两个片段。我们一般认为"被子"是一个词,而认为"辈子"不是一个词.

"被子"前面可以加各种字,例如:“买被子”、“盖被子”、“进被子”、“好被子”、“这被子”等等,一个词的左临词会非常丰富.但“辈子”的用法却非常固定,除了“一辈子”、“这辈子”、“上辈子”、“下辈子”,基本上“辈子”前面不能加别的字了。所以我们会认为"一辈子"是词而"辈子"不是词.

从信息论的角度,词的左右临词带来的信息量比较大,否则左右临词带来的信息量较少.例如"被子"的右临词带来较大的信息量,"辈子"的左右临词带来较少的信息量.

一个文本片段的自由运用程度定义为它的左邻字信息熵和右邻字信息熵中的较小值.

S = \min(-\log p(左临词),-\log p(右临词))

使用此种方法对金庸小说<射雕英雄传>进行新词发现部分结果如下:

杨过,5994
郭靖,1431
黄蓉,1428
甚么,1313
自己,1211
法王,1171
武功,954
郭芙,861
笑道,832
郭襄,779
心想,774
二人,769
咱们,705
蒙古,661
如此,620
师父,609
听得,593
此时,571
当下,558
突然,553
弟子,539
如何,514
姑娘,478
眼见,438
不敢,434
今日,423
原来,421
功夫,421
之间,412
虽然,395
姑姑,373
两个,371
长剑,359
绿萼,357
脸上,345
众人,343
当真,340
伸手,339
喝道,336
左手,336
女儿,335
怎么,332
霍都,328
这般,327
性命,327
神雕,318
程英,308
跟着,304
登时,304
一灯,304

另外有大神提供了此算法的编程实现,以下代码来自https://kexue.fm/archives/3491

import numpy as np
import pandas as pd
import re
from numpy import log, min

f = open('shendiaoxialu.txt', 'r', encoding='UTF-8')  # 读取文章
s = f.read()  # 读取为一个字符串

# 去除标点
drop_dict = [',', '\n', '。', '、', ':', '(', ')', '[', ']', '.', ',', ' ', '\u3000', '”',
             '“', '?', '?', '!', '‘', '’', '…']
for i in drop_dict:
    s = s.replace(i, '')

# 为了方便调用,自定义了一个正则表达式的词典
myre = {2: '(..)', 3: '(...)', 4: '(....)', 5: '(.....)', 6: '(......)', 7: '(.......)'}

min_count = 10  # 录取词语最小出现次数
min_support = 30  # 录取词语最低支持度,1代表着随机组合
min_s = 3  # 录取词语最低信息熵,越大说明越有可能独立成词
max_sep = 4  # 候选词语的最大字数
t = []  # 保存结果用

t.append(pd.Series(list(s)).value_counts())  # 逐字统计

tsum = t[0].sum()  # 统计总字数
rt = []  # 保存结果用

for m in range(2, max_sep + 1):
    print('正在生成%s字词...' % m)
    t.append([])

    # 生成所有可能的m字词
    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]
    for k in range(m - 1):
        # 例如  ms = "杨过"
        # tsum * t[m - 1][ms]               num(总字数) * num(杨过)
        # t[m - 2 - k][ms[:m - 1 - k]]      num(杨)
        # t[k][ms[m - 1 - k:]]              num(过)
        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(tt.index)


def cal_S(sl):  # 信息熵计算函数
    return -((sl / sl.sum()).apply(log) * sl / sl.sum()).sum()


# import pandas as pd
# import re
#
# s = 'faaffaafbaabbaabb'
# pp = re.findall('(.)(..)(.)',s)
# pp = pd.DataFrame(pp).set_index(1).sort_index()
# pd.Series(pp[0]["aa"]).value_counts()

for i in range(2, max_sep + 1):
    print('正在进行%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]

# 下面都是输出前处理
for i in range(len(rt)):
    t[i + 1] = t[i + 1][rt[i]]
    t[i + 1] = t[i + 1].sort_values(ascending=False)

# 保存结果并输出
pd.DataFrame(pd.concat(t[1:])).to_csv('result.txt', header=False)

posted @ 2018-12-14 18:46:14
评论加载中...

发表评论