1.新词提取
概述
新词是一个相对的概念,每个人的标准都不一样,所以我们这里定义: 词典之外的词语(OOV)称作新词。新词的提取对中文分词而言具有重要的意义,因为语料库的标注成本很高。那么如何修订领域词典呢,此时,无监督的新词提取算法就体现了现实意义。
1. 基本原理
1) 提取出大量文本(生语料)中的词语,无论新旧。
2) 用词典过滤掉已有的词语,于是得到新词。
笼统的讲一下如何无监督的提取出文本中的单词:
给定一段文本,随机取一个片段,如果这个片段左右的搭配很丰富,并且片段内部成分搭配很固定,则可以认为这是一个词。将这样的片段筛选出来,按照频次由高到低排序,排在前面的有很高概率是词。
如果文本足够大,再用通用的词典过滤掉“旧词”,就可以得到“新词”。
片段外部左右搭配的丰富程度,可以用信息熵来衡量,而片段内部搭配的固定程度可以用子序列的互信息来衡量。
(1) 信息熵
在信息论中,信息熵( entropy )指的是某条消息所含的信息量。它反映的是听说某个消息之后,关于该事件的不确定性的减少量。比如抛硬币之前,我们不知道“硬币正反”这个事件的结果。但是一旦有人告诉我们“硬币是正面”这条消息,我们对该次抛硬币事件的不确定性立即降为零,这种不确定性的减小量就是信息熵。公式如下:
给定字符串 S 作为词语备选,X 定义为该字符串左边可能出现的字符(左邻字),则称 H(X) 为 S 的左信息熵,类似的,定义右信息熵 H(Y),例如下列句子:
两只蝴蝶飞啊飞
这些蝴蝶飞走了
那么对于字符串蝴蝶,它的左邻近字及频次为count(只)=1、count(些)=1,所以它的左信息熵为1,而右信息熵为0。因为生语料库中蝴蝶的右邻字一定是飞。假如我们再收集一些句子,比如“蝴蝶效应”“蝴蝶蜕变”之类,就会观察到右信息熵会增大不少。
左右信息熵越大,说明字符串可能的搭配就越丰富,该字符串就是一个词的可能性就越大。
光考虑左右信息熵是不够的,比如“吃了一顿”“看了一遍”“睡了一晚”“去了一趟”中的了一的左右搭配也很丰富。为了更好的效果,我们还必须考虑词语内部片段的凝聚程度,这种凝聚程度由互信息衡量。
(2) 互信息
两个离散随机变量相关程度的度量
关于互信息和信息熵更详细的计算可参考网站:https://cn.bing.com/searchq=%E4%BA%92%E4%BF%A1%E6%81%AF%E8%AE%A1%E7%AE%97&form=ANNTH1&refig=bcf490b88b66441c8b9710b8603b993b
互信息的定义可以用韦恩图直观表达:
其中,左圆圈表示H(X),右圆圈表示H(Y)。它们的并集是联合分布的信息熵H(X,Y),差集有多件嫡,交集就是互信息。可见互信息越大,两个随机变量的关联就越密切,或者说同时发生的可能性越大。
对于三个字以上的片段,可能有多种组合方式,计算上可以选取所有组合方式中互信息最小的那一种为代表。有了左右信息熵和互信息之后,将两个指标低于一定阈值的片段过滤掉,剩下的片段按频次降序排序,截取最高频次的 N 个片段即完成了词语提取流程。
实现:
from pyhanlp import *
import os
from pyhanlp.static import download, remove_file, HANLP_DATA_PATH
import zipfile
def test_data_path():
"""
获取测试数据路径,位于$root/data/test,根目录由配置文件指定。
:return:
"""
data_path = os.path.join(HANLP_DATA_PATH, 'test')
if not os.path.isdir(data_path):
os.mkdir(data_path)
return data_path
def ensure_data(data_name, data_url):
root_path = test_data_path()
dest_path = os.path.join(root_path, data_name)
if os.path.exists(dest_path):
return dest_path
if data_url.endswith('.zip'):
dest_path += '.zip'
download(data_url, dest_path)
if data_url.endswith('.zip'):
with zipfile.ZipFile(dest_path, "r") as archive:
archive.extractall(root_path)
remove_file(dest_path)
dest_path = dest_path[:-len('.zip')]
return dest_path
HLM_PATH = ensure_data("红楼梦.txt", "http://file.hankcs.com/corpus/红楼梦.zip")
XYJ_PATH = ensure_data("西游记.txt", "http://file.hankcs.com/corpus/西游记.zip")
SHZ_PATH = ensure_data("水浒传.txt", "http://file.hankcs.com/corpus/水浒传.zip")
SAN_PATH = ensure_data("三国演义.txt", "http://file.hankcs.com/corpus/三国演义.zip")
WEIBO_PATH = ensure_data("weibo-classification", "http://file.hankcs.com/corpus/weibo-classification.zip")
def test_weibo():
for folder in os.listdir(WEIBO_PATH):
print(folder)
big_text = ""
for file in os.listdir(os.path.join(WEIBO_PATH, folder)):
with open(os.path.join(WEIBO_PATH, folder, file),encoding='utf-8') as src:
big_text += "".join(src.readlines())
word_info_list = HanLP.extractWords(big_text, 100)
print(word_info_list)
def extract(corpus):
print("%s 热词" % corpus)
word_info_list = HanLP.extractWords(IOUtil.newBufferedReader(corpus), 100)
print(word_info_list)
print("%s 新词" % corpus)
word_info_list = HanLP.extractWords(IOUtil.newBufferedReader(corpus), 100, True)
print(word_info_list)
if __name__ == '__main__':
extract(HLM_PATH)
extract(XYJ_PATH)
extract(SHZ_PATH)
extract(SAN_PATH)
test_weibo()
2.关键词提取
词语颗粒度的信息抽取还存在另一个需求,即提取文章中重要的单词,称为关键词提取。关键词也是一个没有定量的标准,无法统一语料库,所以就可以利用无监督学习来完成。
分别介绍词频、TF-IDF和TextRank算法,单文档提起可以用词频和TextRank,多文档可以使用TF-IDF来提取关键词。
1.词频统计
关键词通常在文章中反复出现,为了解释关键词,作者通常会反复提及它们。通过统计文章中每种词语的词频并排序,可以初步获取部分关键词。
不过文章中反复出现的词语却不一定是关键词,例如“的”。所以在统计词频之前需要去掉停用词。
词频统计的流程一般是分词、停用词过滤、按词频取前 n 个。其中,求 m 个元素中前 n (n<=m) 大元素的问题通常通过最大堆解决,复杂度为 O(mlogn)。
使用pyhanlp包实现:
from pyhanlp import *
TermFrequency = JClass('com.hankcs.hanlp.corpus.occurrence.TermFrequency')
TermFrequencyCounter = JClass('com.hankcs.hanlp.mining.word.TermFrequencyCounter')
if __name__ == '__main__':
counter = TermFrequencyCounter()
counter.add("加油加油中国队!") # 第一个文档
counter.add("中国观众高呼加油中国") # 第二个文档
for termFrequency in counter: # 遍历每个词与词频
print("%s=%d" % (termFrequency.getTerm(), termFrequency.getFrequency()))
print(counter.top(2)) # 取 top N
# 根据词频提取关键词
print('')
print(TermFrequencyCounter.getKeywordList("女排夺冠,观众欢呼女排女排女排!", 3))
结果如下:
中国=2
中国队=1
加油=3
观众=1
高呼=1
[加油=3, 中国=2]
用词频来提取关键词有一个缺陷,那就是高频词并不等价于关键词。比如在一个体育网站中,所有文章都是奥运会报道,导致“奥运会”词频最高,用户希望通过关键词看到每篇文章的特色。此时,TF-IDF 就派上用场了。
2.TF-IDF
TF-IDF (Term Frequency-lnverse Document Frequency,词频-倒排文档频次)是信息检索中衡量一个词语重要程度的统计指标,被广泛用于Lucene、Solr、Elasticsearch 等搜索引擎。 相较于词频,TF-IDF 还综合考虑词语的稀有程度。在TF-IDF计算方法中,一个词语的重要程度不光正比于它在文档中的频次,还反比于有多少文档包含它。包含该词语的文档趣多,就说明它越宽泛, 越不能体现文档的特色。 正是因为需要考虑整个语料库或文档集合,所以TF-IDF在关键词提取时属于多文档方法。
计算公式如下(我在其他网站看到了其他版本的计算公式,但是思想都是一致的):
其中,t 代表单词,d 代表文档,TF(t,d) 代表 t 在 d 中出现频次,DF(t) 代表有多少篇文档包含 t。DF 的倒数称为IDF,这也是 TF-IDF 得名的由来。
观察输出结果,可以看出 TF-IDF 有效的避免了给予“奥运会”这个宽泛的词语过高的权重。 TF-IDF是基于多文档的统计量,所以需要输入多篇文档才能开始计算。在大型语料库上的统计类似于一种学习过程,假如我们没有这么大型的语料库或者存储IDF的内存,同时又想改善词频统计的效果该怎么办呢?此时可以使用TextRank算法。
使用Hanlp实例如下:
from pyhanlp import *
TfIdfCounter = JClass('com.hankcs.hanlp.mining.word.TfIdfCounter')
if __name__ == '__main__':
counter = TfIdfCounter()
counter.add("《女排夺冠》", "女排北京奥运会夺冠") # 输入多篇文档
counter.add("《羽毛球男单》", "北京奥运会的羽毛球男单决赛")
counter.add("《女排》", "中国队女排夺北京奥运会金牌重返巅峰,观众欢呼女排女排女排!")
counter.compute() # 输入完毕
for id in counter.documents():
print(id + " : " + counter.getKeywordsOf(id, 3).toString()) # 根据每篇文档的TF-IDF提取关键词
# 根据语料库已有的IDF信息为语料库之外的新文档提取关键词
print('')
print(counter.getKeywords("奥运会反兴奋剂", 2))
结果如下:
《女排》 : [女排=5.150728289807123, 重返=1.6931471805599454, 巅峰=1.6931471805599454]
《女排夺冠》 : [夺冠=1.6931471805599454, 女排=1.2876820724517808, 奥运会=1.0]
《羽毛球男单》 : [决赛=1.6931471805599454, 羽毛球=1.6931471805599454, 男单=1.6931471805599454]
[反, 兴奋剂]
3.TextRank
(部分内容摘取自知乎内容:https://zhuanlan.zhihu.com/p/240676850)
TextRank算法是由 Google 搜索的核心网页排序算法 PageRank 改编而来,利用图模型来提取文章中的关键词,首先介绍一下 PageRank 排序算法。
PageRank算法用于解决互联网网页的价值排序问题,对于某个关键词的搜索,往往会有很多网页与之相关,如何对这些网站进行排序然后返回给用户最有”价值“的网站?最直观的,对每个网页进行“打分”,而打分标准至关重要。PageRank考虑到不同网页之间,一般会通过超链接相连,即用户可以通过A网页中的链接,跳转到B网页,这种互相跳转关系,可以理解为一种“投票”行为,A网页连接到B网页,表示A网页对B网页的认可,即A网页给B网页投了一票。给B网页投票(链接)的越多,B网页的价值也就越大,所以:
其中
公式中,某个网页的价值,是由连接到(进入)这个网页的每个网页的价值和对应的权重决定的。一个网站,如果越多的网站链接到它,说明这个网站越有价值,为什么要加入一个权重呢?公式可以看到,权重是从某个网页链接出去的数量的倒数,数量越多,权重越小,好比是投票,某个人投出的票越多,说明这个人的票越没有含金量。
从公式中可以看到这是一个迭代公式,所以存在“先有鸡还是先有蛋”的问题,对于这个问题,解决办法是给每一个节点一个初始值,一般是1/N,N即N个网页。
下面来计算一下:
第一轮:
…以下的计算省略,并不符合数学的优雅性。
为了体现数学的优雅性,引入新的概念——邻接矩阵(Adjacency Matrix)。
首先介绍一个词:图(Graph)。做知识图谱的肯定很了解它,当然,随着相关理论的发展,图论越来越多的出现在了机器学习和深度学习的各个领域,并且取得了很好的效果。
这里就进行简单的介绍,所谓“图”,由节点(node)和边(edge)构成,在这里,节点就是网页,两网页间是否存在边则由两网页是否存在超链接决定。
上图中,可以认为是A-E,5个网页构成的图,节点与节点之间存在着边,图中存在箭头,此时的图称为“有向图”。
B到C的箭头表示B网页有到C网页的链接,而A、B之间的箭头表示A、B网页之间相互链接。
这是图的直观展示,如何转化成数学表示呢?就要靠邻接矩阵。
G就是表示上面图的邻接矩阵,第i行第j列为1,表示第i个节点到第j个节点有边,比如第1行第2列,表示节点A到节点B的边。G中的1表示无权重的图,如果是有权图,则这里的1可以替换为相应权重。
有了邻接矩阵,通过标准化,我们可以计算出概率转移矩阵:
第i行表示进入到第i个节点的概率分布,而第j列,表示第j个节点的出节点概率分布。举个例子:比如取(i=2,j=1)(下标从1开始) 对应w(i,j)=1/2,表示由A转移到B的概率。
这里突然扯到了概率转移矩阵,实际这是对前面的“投票”打分机制的一种概率抽象,可以这么理解,给到一只猴子和一台电脑,这个猴子随机选择一个网页,然后随机点击网页上的超链接在网页中跳转,一段时间后,猴子在每个网页上停留的概率都会有一个稳定值,这个值就是我们要求的每个网页的“价值”。
我们可以用一个5维列向量S表示5个节点的概率初始值,也就是一个随机向量。
则
(解释:此处是将使用公式迭代的方式转化成了邻接矩阵与上一个向量的相乘,仍然符合矩阵迭代的过程)
相当于我们对随机向量S反复进行W概率转移过程,补充一点,公式(3)中,概率转移矩阵W左乘随机列向量S,所以W是一个左随机矩阵,也有相反的情况,即概率矩阵右乘随机行向量,那么这个时候就是一个右随机矩阵。
我们利用矩阵运算来进行前面的迭代公式计算:
第一轮:
我们希望得到一个稳定值,于是迭代100轮,
收敛到几乎为0了,这显然是不合理的,为什么呢?实际上,这也是PageRank最初遇到的问题之一,即Dead Ends问题,回到最上面的A-E节点的连接图,可以看到,D节点不存在外链,这种节点,就称为Dead Ends,解决办法呢,就是加入一个阻尼因子:
其实这个d有些类似机器学习中目标函数里的正则项,加入的作用也是让整个计算更平滑一些。此外,虽然前面说W矩阵是概率转移矩阵,但它并不真正满足概率转移矩阵的定义:
矩阵各元素都是非负的,并且各行(列)元素之和等于1,各元素用概率表示,在一定条件下是互相转移的。
此外,求S的过程,实际是一个马尔科夫收敛过程,而马尔可夫收敛,也需要满足一定的条件,首先必须满足转移矩阵的定义,其次转移矩阵不可约,且非周期。转移矩阵不可约指的是每一个状态都可来自任意的其它状态,也就是任意两个网页都可以通过若干中间网页链接。周期指的是存在一个最小的正整数 k,使得从某状态 i 出发又回到状态 i 的所有路径的长度都是 k 的整数倍,也就是Dead Ends问题,这里由于d的存在,也使得非周期性得到满足。
当加入了阻尼因子后,可以认为用户浏览到任何一个页面,都有可能以一个极小的概率转移到另外一个页面。
同样基于公式进行计算,第一轮:
写成矩阵运算,不过这次加入了d,则:
至此,PageRank的原理和计算过程基本介绍完毕,不难发现,构建“图”,或者说邻接矩阵,是最基础和重要的一步,最终结果也只受邻接矩阵的影响。对于文本来说,TextRank又是如何构建图的呢?这需要结合具体任务去看。
关键词提取任务
在这个任务中,词就是Graph中的节点,而词与词之间的边,则利用“共现”关系来确定。所谓“共现”,就是共同出现,即在一个给定大小的滑动窗口内的词,认为是共同出现的,而这些单词间也就存在着边,举例:
“淡黄的长裙,蓬松的头发。牵着我的手看最新展出的油画”
分词后:
淡黄 长裙 蓬松 头发
牵 我 手 看 最新 展出 油画
给定窗口为2,依次滑动
淡黄 长裙
长裙 蓬松
蓬松 头发
牵 我
我 手
。。。
则“淡黄”和“长裙”两个节点间存在边,
也可以取窗口为3,则此时,“淡黄”不仅和“长裙”存在边,也和“蓬松”存在边。
不难发现,相对于PageRank里的无权有向图,这里建立的是无权无向图,原论文中对于关键词提取任务主要也是构建的无向无权图,对于有向图,论文提到是基于词的前后顺序角度去考虑,即给定窗口,比如对于“长裙”来说,“淡黄”与它之间是入边,而“蓬松”与它之间是出边,但是效果都要比无向图差。最后用我们的公式进行计算即可。
文本摘要任务(关键句提取)
文本摘要任务,也可以理解为“关键句“提取任务,在这个任务中,节点不再是词,而是句子。而句与句之间的联系,也不再使用”共现“来确定,还是利用相似度确定。因此,此时构造的是有权无向图。对于相似度的计算方法,论文中给出了一种:
其中,分母即两个句子的词数取对数后求和,分子是同属于两个句子的词的数量。
当然,也可以使用其他相似度计算方法,比如在有的改进的TextRank方法中,会使用余弦相似度,即先把两个句子分词,词向量化后,利用词向量加和求平均的方式计算句向量,然后再计算两个句子的余弦相似度。
假设我们有A-E五个句子,则构造的邻接矩阵则是:
可以看到,是一个对称矩阵,这是因为两个句子之间不存在方向的关系,这也是无向图的邻接矩阵的特点之一。
同样,也可以进行标准化处理,实际上,标准化处理后的权重,就是式子(4)中对应的权重。仍然可以利用矩阵计算公式(4)进行迭代计算。
TextRank的论文中测试了很多种方法,结合实际来看,TextRank的优缺点总结如下:
优点:
1) 无监督方式,无需构造数据集训练。
2) 算法原理简单且部署简单。
3) 继承了PageRank的思想,效果相对较好,相对于TF-IDF方法,可以更充分的利用文本元素之间的关系。
缺点:
1) 结果受分词、文本清洗影响较大,即对于某些停用词的保留与否,直接影响最终结果。
2) 虽然与TF-IDF比,停止利用了词频,但是仍然受高频词的影响。
实战
至此,TextRank介绍完毕,在实操过程中,小老弟发现网上的代码很多是基于networkx包里的pagerank方法进行的计算,与论文公式计算的结果有出入,本着“纸上得来终觉浅“的原则,小老弟动手写了一下TextRank。项目主要结构如下:
-TextRank
--textPro.py : 文本处理,分句分词去停用词,根据词性过滤词。
--textRank.py:实现抽取N个关键词和N个关键句。
--utils.py:共现矩阵的构造,值的计算等。
--const.py:某些常量
最后贴上大佬手撕TextRank的Github项目:
https://link.zhihu.com/?target=https%3A//github.com/abner-wong/textrank