纯Python实现基于统计学模型的新词发现

mlqau0m7

此文章原发表于我原创的B站专栏纯Python实现的基于统计学模型的新词发现
几个月前的一天,我闲着无聊,就随手写了个脚本,载了所有番剧的将近5个G的弹幕下来。

最近又闲着无聊,突然想到这5G的弹幕还在硬盘里躺着,于是想做一下统计。

一开始,我只统计了简单的一些参数:大家什么时候发弹幕,周几发,几月发的多,传统的统计很快就结束了。

但我一直好奇一件事情:不同时间段的B站,大家都在说什么?抱着这样的想法,我使用了jieba库对其进行了简单的分词,然而效果并不理想:由于jieba会预载词典,而弹幕中的文字常常是B站独特文化中的一部分,jieba分词的效果并不是特别理想,其中出现了很多日常生活中常见的词汇,并不够独特。

于是我决定自己写一个新词发现的脚本,经过一番简单的搜索,我发现我有两种选择:基于语言学模型的新词发现和基于统计学模型的新词发现。

两种方法各有利弊,语言学模型可以识别的词汇更准确,同时支持词性标注甚至是情感分析等复杂NLP的功能。但是复杂的功能也意味着复杂的实现,其依赖预先加载的字典,同时编程实现更难,难以在一两天内完成,也对机器性能提出了挑战。

而统计学模型虽然不理解自然语言,但是借助人的自然语言的理解和统计学工具,也可以得到比较好的结果。如果设定比较好的排序方式,得到的结果也能非常令人满意。经过短暂的思考后,我决定使用基于统计学模型的发现方式。

在编程语言的选择上,我选择了Python作为这个工程的主要语言。

选择Python作为主要语言的原因有下面三点

  1. 我最多项目的语言是Python,两年的编程经验也算丰富了,对我来说是Python最简单的。
  2. 有需要可以使用外部库。
  3. 最重要的:Python可以丝滑地处理多语言文本。比如举个例子

    string = "Unicode处理上任何字符的长度都为1,索引时也是如此。😄表情也只是一个字的长度,符合直觉。"
    print(string[0:2] == "Un") # True
    print(string[7:9] == "处理") # True
    print(string[28:31] == '。😄表') # True

    Python在Unicode处理上,任何字符的长度都视为为1,索引时也是如此。表情类似😄也只是一个字的长度,符合直觉。而一些性能更好的语言,如C++、Golang、Rust都没有这种方便处理的特性,需要手动处理UTF-8文本,Java使用UTF-16存储字符串,在处理表情和非本地语言时也容易出现问题。

下面让我们开始动手

我们知道,B站的弹幕下载下来是XML文件,所以想要进行自然语言处理的第一步,自然是提取下载下来的XML中弹幕的内容,其他一概不要。这一步我选择使用基于JVM的kotlin来完成,因为Python处理XML文件太慢了(bs4、lxml、sax都不尽如人意,大概需要十几分钟甚至几十分钟),而kotlin借助Java的sax和多线程,可以在几十秒内处理完所有文件。目标是将每月的弹幕分别整理为以回车分割的文件,抛弃其他信息。
接下来就是Python的出场时间了。标题中写道是纯Python实现,那么有多纯呢?在新词发现这个脚本里,没有使用除了标准库以外的任何其他库,当然也没有使用任何其他外部函数调用。只要有Python,脚本是开箱即用的。

首先使用 re.split 以所有的符号、空格、回车为切割点,形成一个元素是仅由文字组成的列表。

算法上我选择了先进行ngram分词,然后通过计算词频、邻熵和内聚度的方法选出最有可能是新词的片段,下面来简单介绍一下这几种技术,如果想要具体的细节可以大家可以自行搜索。

n-gram:由于中文是没有空格的,所以需要一个办法来切开成句,让我们可以找到其中的词语。而n-gram是最简单粗暴的方法--暴力寻找所有可能的词。方法是构建一个大小为n的窗口,在成句上滑动,将所有窗口中看到的词放入集合中。其中的n可以是2、3、4、5等等。

举个例子,比如 "投币点赞收藏" 这句话中,如果使用2-gram,得到的结果是:【投币 币点 点赞 赞收 收藏】,而如果使用3-gram,得到的结果是:【投币点 币点赞 点赞收 赞收藏】。

词频:没什么多说的,就是一个词出现的频率,实现中就是n-gram出现的频率。

邻熵:可以理解为出现在一个词左右两边不同字的个数,实际实现中会对标点等分隔符做特殊处理。具体的计算公式是:

左邻熵的计算公式,右邻熵类似
聚合度:聚合度是词汇内部的信息,如果聚合度越高,说明内部词之间字和字的联系更紧密,他们更有可能单独成词。
聚合度的计算公式
我的ngram选择是2、3、4、5,即将所有2字词、3字词、4字词、5字词均进行计算,取其中词频超过20的词汇继续计算。

接下来计算邻熵、聚合度、然后用自己一拍脑袋想出的得分计算算法计算最终成词得分,按照得分的从高到低排序。

这个拍脑袋想出来的算法综合考虑了词频、左右熵、最低左右熵、词的长度和内聚度。通过调整一些细微的参数来实现更好的排序。

我采用的算法运行时间复杂度与空间复杂度都是 O(k (n(n-2)) / 2 * size) ,其中k是常数,n为最大的ngram数,size为文件的大小。在实际运行中,处理100M的文件大约需要5分钟,内存空间占用约10G。

在处理后进行回溯,如果一个短词是一个长词的一部分,并且短词的左右邻熵其中一个较低,则从词典中删除这个短词。

花了几个小时时间编写代码,其运行出的结果已经符合我的预期了。

经过排序的n-grams
当然各位也会发现,这样得出的结果,并无法得出长度超过5的词,所以接下来我们进入超长词处理阶段。

在超长词处理中,我们只考虑所有n-gram中的5-gram,我选定一个左邻熵高但右邻熵非常低的词作为起始词,在其他5-gram中寻找前四个字和起始词的后四个字相同的gram,如果找到则拼接,放回集合等待下一轮拼接,或者发现当其右邻熵足够高后,放入处理完成的超长词。当所有的前缀都被处理完后,在原始文件里寻找这些超长词,也为他们计算左右熵以及内聚度,插入到原来的文件中重新排序,最终得到的结果如下图。
加入了超长词后重新排序的n-grams,得分算法对超长词有优化

至此,技术上的问题就已经解决了,代码我会考虑在一段时间之后开源,供大家批评指正。

阅读 1.7k

带学ing

17 声望
0 粉丝
0 条评论
你知道吗?

带学ing

17 声望
0 粉丝
文章目录
宣传栏