关键词抽取与自动文摘

在自然语言处理中对于关键词抽取与自动文摘这两个主题,有着多种多样的方式去解决它们,这里将介绍一种叫做TextRank的方法,就可以解决这两个问题。我将结合具体的代码,试图将算法解释地更加清楚些。

论文『TextRank: Bringing Order into Texts』中首次提出了TextRank方法,如果想全面了解下这个方法,还是仔细看下这篇论文。

当你一看到TextRank这个名字的时候,你是否会觉得很熟悉,它是否有让你联想到Google的PageRank。其实,该方法就是基于PageRank而来的,只是将page替换成word、sentence以完成关键词抽取与自动文摘任务。当然其中的一些计算准则是以文本为标准的。

如果你还不熟悉PageRank的话,还是去网上找一篇博客来看看,其实是不难的。

关键词抽取中的TextRank

之前我们也提到了,就是将PageRank模型中的page替换成word。因为是用于文本中,所以与用于网页中是不一样的。改动的地方主要有:

PageRank中因为有链接的存在,所以是将存在链接的两个网页相连。而在文本处理中,一个词的出现,与其前后词之间是有联系的。在TextRank中存在一个窗口的概念,将一个词与窗口内的所有词进行连接。窗口值的设置也是比较讲究的。实验表明,如果窗口值设置的过大,不仅带来计算时间上的提高(因为图中的边数增加),而且会造成模型性能的下降。这里模型的性能是使用具体的任务去衡量的,关键词抽取是作为具体任务的前驱。

因为链接是具有指向性的,所以PageRank模型构造的是一个有向图。而在文本中,词与词之间的联系是很难确定方向的,所以既可以构造一个有向图也可以构造一个无向图。实验表明,有向图和无向图在收敛性上是没有差别的,就是它们都会收敛到一个固定值上。至于初始值的设置,在PageRank中就已经证明了,最终的结果与初值的设定无关,只与迭代的次数有关。只要迭代的次数足够,就能收敛到一个固定值。

在PageRank中,会将爬虫所爬到的所有网页都加入到模型中去,至于死链的去除,这都是后话。但在TextRank中,不会将文本中出现的所有词都加入到图模型中。因为不是所有的词都是有其实际意义的,例如说介词。所以在构建图模型时,会利用一定的语法规则过滤掉一些词,以减小图的规模。这里的过滤规则一般来说是基于词性的,所以在之前进行分词是还需要将词的词性给标记出来。实验表明,最佳的选择是保留名词和形容词。

PageRank模型中,连接的关系一般只会发生一次,很少会有一个网页中存在两个指向同一个网页的链接。但在文本中却不同,有一些词是固定的搭配,它们会在一起出现多词。所以TextRank所构造的图应该是一个赋权图。这里的权值应该是两者共同出现的次数。相应的每个结点权值的更新公式应该更改为:

WS(Vi)=(1−d)+d×∑Vj∈In(Vi)wji∑Vk∈Out(Vj)wjkWS(Vj)

WS(V_i) = (1 - d) + d \times \sum_{V_j \in In(V_i)} \frac{w_{ji}}{\sum_{V_k \in Out(V_j)} w_{jk}} WS(V_j)

其中d表示衰减系数,这个在PageRank模型中就已经有了的,WS(Vi)WS(V_i)表示结点ViV_i的权值。在无向图中,In(Vi)、Out(Vi)In(V_i)、Out(V_i)均表示与结点ViV_i相连的结点;在有向图中,则分别表示结点的入边和出边。

在构造完图之后,就与PageRank一样,使用迭代的算法计算每一个结点的权值。根据你想获得的关键词的数目,输出权值最高的那些结点所对应的词。

jieba中的关键词抽取

下面我们以开源中文分词库jieba为例,看看其中关键词抽取的实现。项目的主页是https://github.com/fxsjy/jieba,这里列出的是python的版本,还有其他很多语言的版本可供使用。

# 用于表示文本的无向带权图
class UndirectWeightedGraph:d = 0.85 # 衰减系数def __init__(self):# 用一条边的起始结点作为关键字的字典来表示整个图的信息self.graph = defaultdict(list)def addEdge(self, start, end, weight):# use a tuple (start, end, weight) instead of a Edge objectself.graph[start].append((start, end, weight))self.graph[end].append((end, start, weight))def rank(self):ws = defaultdict(float) # 表示每一个结点的权重outSum = defaultdict(float) # 结点所关联边的权值之和wsdef = 1.0 / (len(self.graph) or 1.0) # 为每个结点初始化一个权值for n, out in self.graph.items():ws[n] = wsdefoutSum[n] = sum((e[2] for e in out), 0.0)# this line for build stable iterationsorted_keys = sorted(self.graph.keys())for x in xrange(10):  # 10 iters,textrank值的计算只进行10次循环# 进行textrank值的更新for n in sorted_keys:s = 0for e in self.graph[n]:s += e[2] / outSum[e[1]] * ws[e[1]]ws[n] = (1 - self.d) + self.d * s(min_rank, max_rank) = (sys.float_info[0], sys.float_info[3]) # 系统设定的最大和最小值for w in itervalues(ws): # 返回值的迭代器if w < min_rank:min_rank = wif w > max_rank:max_rank = w# 将权值进行归一化for n, w in ws.items():# to unify the weights, don't *100.ws[n] = (w - min_rank / 10.0) / (max_rank - min_rank / 10.0)return wsclass TextRank(KeywordExtractor):def __init__(self):self.tokenizer = self.postokenizer = jieba.posseg.dt # 对输入文本进行分词的方法self.stop_words = self.STOP_WORDS.copy() # 停用词列表self.pos_filt = frozenset(('ns', 'n', 'vn', 'v')) # 词性的筛选集self.span = 5 # 表示词与词之间有联系的窗口的大小为5def pairfilter(self, wp):# 确保该词的词性是我们所需要的,且这个词包含两个字以上,且都是小写,并且没有出现在停用词列表中return (wp.flag in self.pos_filt and len(wp.word.strip()) >= 2and wp.word.lower() not in self.stop_words)def textrank(self, sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False):"""Extract keywords from sentence using TextRank algorithm.Parameter:- topK: return how many top keywords. `None` for all possible words.- withWeight: if True, return a list of (word, weight);if False, return a list of words.- allowPOS: the allowed POS list eg. ['ns', 'n', 'vn', 'v'].if the POS of w is not in this list, it will be filtered.- withFlag: if True, return a list of pair(word, weight) like posseg.cutif False, return a list of words"""self.pos_filt = frozenset(allowPOS)g = UndirectWeightedGraph()cm = defaultdict(int)words = tuple(self.tokenizer.cut(sentence)) # 首先必须要对文本进行分词for i, wp in enumerate(words): # i和wp分别代表words的下标与其对应的元素,元素内容包括词以及其对应的词性if self.pairfilter(wp):for j in xrange(i + 1, i + self.span):if j >= len(words):breakif not self.pairfilter(words[j]):continueif allowPOS and withFlag:cm[(wp, words[j])] += 1else:cm[(wp.word, words[j].word)] += 1 # 如果两个词出现在同一个窗口内,则在两者间加上一条边for terms, w in cm.items():g.addEdge(terms[0], terms[1], w) # 将共同出现的次数作为边的权值,表示边的关键字用词而非词的编号nodes_rank = g.rank()if withWeight:tags = sorted(nodes_rank.items(), key=itemgetter(1), reverse=True)else:tags = sorted(nodes_rank, key=nodes_rank.__getitem__, reverse=True)if topK:return tags[:topK]else:return tags

自动文摘中的TextRank

在关键词抽取任务中,图模型中的结点是词,在自动文摘中结点表示的就是句子。模型的构建方式自然也要发生一些改变,主要有以下两点:

首先,窗口模型在这个任务中并不适用,因为前后句子间的关系不像是词间的关系那么紧密。

其次,图中的权值使用的是句子间的相似度,具体是使用哪一种衡量标准,还是要看实现吧。

模型构建好之后,还是利用迭代的方式求出每一个句子的权值。根据你所希望文摘的长度,输出权值最高的句子。我觉得这个方法所存在的最为重要的一个问题是:因为只是从文章中抽取出几个句子作为代表,这些句子之间逻辑上可能并没有联系,所以得到的文摘看上去会有一些别扭。当然也有方法是根据文摘的内容自动地生成摘要,那样的话,逻辑上会更为通顺一些。当然方法的难度上肯定会更大,这个就不在讨论范围之内了。

snownlp中的自动文摘

接下来,我们以开源库snownlp为例,看一下其中的自动文摘算法实现。算法的主页是https://github.com/isnowfy/snownlp,其主要的任务与jieba还是非常类似的。

class SnowNLP(object):@propertydef sentences(self): # 将一篇文章按句划分,实际上是细分到了逗号的return normal.get_sentences(self.doc)def summary(self, limit=5):doc = []sents = self.sentencesfor sent in sents: # 这里对整个句子进行了分词,是为了方便计算句子间的相似度。一个句子的分词结果是以一个list方式存储的,并没有破坏句子的完整性words = seg.seg(sent)words = normal.filter_stop(words)doc.append(words)rank = textrank.TextRank(doc)rank.solve()ret = []for index in rank.top_index(limit):ret.append(sents[index]) # 这里应该返回整个句子,而不是句子的分词结果return ret
class TextRank(object):def __init__(self, docs):self.docs = docsself.bm25 = BM25(docs)self.D = len(docs)self.d = 0.85self.weight = [] # 存储一篇文档中每个句子间的相似度,作为连接边的权值self.weight_sum = [] # 某一个句子与其他句子之间的相似度之和, 作为该点的入度(出度)self.vertex = [] # 每一个句子的textrank值self.max_iter = 200self.min_diff = 0.001self.top = []def solve(self):for cnt, doc in enumerate(self.docs):scores = self.bm25.simall(doc) # 使用BM25作为衡量句子间相似度的标准self.weight.append(scores)self.weight_sum.append(sum(scores)-scores[cnt])self.vertex.append(1.0)for _ in range(self.max_iter):m = []max_diff = 0for i in range(self.D):m.append(1-self.d)for j in range(self.D):if j == i or self.weight_sum[j] == 0: # 判断是否等于0是为了避免除0错误continue# 进行textrank值的更新m[-1] += (self.d*self.weight[j][i]/ self.weight_sum[j]*self.vertex[j])if abs(m[-1] - self.vertex[i]) > max_diff: # 记录最大的改变量max_diff = abs(m[-1] - self.vertex[i])self.vertex = mif max_diff <= self.min_diff: # 如果textrank值没有明显的变化,则跳出循环breakself.top = list(enumerate(self.vertex))self.top = sorted(self.top, key=lambda x: x[1], reverse=True) # 按照textrank的值进行排序def top_index(self, limit):return list(map(lambda x: x[0], self.top))[:limit]def top(self, limit):return list(map(lambda x: self.docs[x[0]], self.top))

这里使用了使用了BM25作为衡量两个句子间相似度的标准,关于BM25的含义,可参考wiki:https://en.wikipedia.org/wiki/Okapi_BM25

class BM25(object):def __init__(self, docs):self.D = len(docs)self.avgdl = sum([len(doc)+0.0 for doc in docs]) / self.D # 计算文档的平均长度self.docs = docsself.f = [] # 统计每一个词出现的次数self.df = {} # 统计一个词在多少篇文档中出现过self.idf = {}self.k1 = 1.5self.b = 0.75self.init()def init(self):for doc in self.docs: # 在传入文档时,每一个文档都已经是分好词了的,每一篇文档就是一个listtmp = {}for word in doc:if not word in tmp:tmp[word] = 0tmp[word] += 1self.f.append(tmp)for k, v in tmp.items():if k not in self.df:self.df[k] = 0self.df[k] += 1for k, v in self.df.items():self.idf[k] = math.log(self.D-v+0.5)-math.log(v+0.5) # 计算逆文档频率,这个公式在wiki中有介绍,并不是一般我们计算逆文档频率的公式def sim(self, doc, index):score = 0for word in doc:if word not in self.f[index]:continued = len(self.docs[index])# 计算输入与该篇文档的相似度score += (self.idf[word]*self.f[index][word]*(self.k1+1)/ (self.f[index][word]+self.k1*(1-self.b+self.b*d/ self.avgdl)))return scoredef simall(self, doc): # 将输入看做是查询语句,计算相似度scores = [] # 计算文档之间两两相似度for index in range(self.D):score = self.sim(doc, index)scores.append(score)return scores

关键词抽取与自动文摘相关推荐

  1. 自然语言处理NLP——中文抽取式自动文摘(包括中文语料库处理、三种方法实现自动文摘、Rouge评价方法对自动文摘进行打分)

    利用三种方法实现抽取式自动摘要,并给摘要结果打分(一.textrank 二.word2vec+textrank 三.MMR 四.Rouge评测) 具体代码我上传到了Github上,其中有45篇小论文( ...

  2. 简易中文自动文摘系统(合集)

    目录 简易中文自动文摘系统(一):绪论 自动文摘的介绍 自动文摘分类 简易中文自动文摘系统(二):中文语料库的准备 中文语料库 jieba分词 简易中文自动文摘系统(三):模型训练 词向量 word2 ...

  3. AAAI 2020 | 多模态基准指导的生成式多模态自动文摘

    2020-01-06 10:17 导语:基本想法是优化多模态摘要训练的目标函数~ 作者 | 朱军楠.张家俊 多模态自动文摘是指利用计算机实现从含有两种或两种以上模态(如图片.文本等)的信息中生成图文式 ...

  4. 论文浅尝 - AAAI2020 | 多模态基准指导的多模态自动文摘

    论文笔记整理:刘雅,天津大学硕士. 链接: https://aaai.org/ojs/index.php/AAAI/article/view/6525 动机 近年来,随着计算机视觉以及自然语言处理技术 ...

  5. 【CIPS 2016】(8-10章)信息抽取、情感分析自动文摘 (研究进展、现状趋势)

    CIPS 2016 笔记整理 <中文信息处理发展报告(2016)>是中国中文信息学会召集专家对本领域学科方 向和前沿技术的一次梳理,官方定位是深度科普,旨在向政府.企业.媒体等对中文 信息 ...

  6. 自动文摘系统实现总结

    自动文摘系统实现总结 应用场景 利用Ai系统帮助人把不同分类的大量文档自动做总结,重要点总结,比较典型公司美国alphasense公司主要做这块终端抽取重要信息,大量节省人力成本和时间成本 自动文摘有 ...

  7. 中文自动文摘关键技术总结

    中文自动文摘关键技术总结 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多AI干货 csdn:https://blog.csdn.net/abcgkj github:ht ...

  8. python关键词提取源码_Python 结巴分词 关键词抽取分析

    关键词抽取就是从文本里面把跟这篇文档意义最相关的一些词抽取出来.这个可以追溯到文献检索初期,当时还不支持全文搜索的时候,关键词就可以作为搜索这篇论文的词语.因此,目前依然可以在论文中看到关键词这一项. ...

  9. python分词_Python 结巴分词实现关键词抽取分析

    1 简介 关键词抽取就是从文本里面把跟这篇文档意义最相关的一些词抽取出来.这个可以追溯到文献检索初期,当时还不支持全文搜索的时候,关键词就可以作为搜索这篇论文的词语.因此,目前依然可以在论文中看到关键 ...

最新文章

  1. Hibernate(1) 阻抗不匹配
  2. HDU 5606 tree 并查集
  3. 我从哆啦A梦的口袋里,掏出一辆充气电动车
  4. JAVA-初步认识-第十一章-异常-概述
  5. 装机人员工具_吕梁采购气伏式包装机-哪家好-强盛包装机械
  6. self.view = nil 和[self.view release]的区别
  7. 【C语言进阶深度学习记录】七 C语言中的循环语句
  8. struct interface_GCTT | 接受 interface 参数,返回 struct 在 go 中意味着什么
  9. Akka增加消息的灵活性《eleven》译
  10. 四二拍用音符怎么表示_2020圣诞平安夜怎么发朋友圈?朋友圈关于平安夜经典语录精选...
  11. python如何发布项目_python如何发布自已pip项目的方法步骤
  12. 2021-06-22 超链接伪类
  13. import/export win7中电源计划
  14. AssetBundle接口详解与优化
  15. QT中使用ActiveX
  16. 流媒体相关资源下载地址(整理)
  17. 【毕业设计】基于大数据的招聘与租房分析可视化系统
  18. AM5728 eHRPWM 驱动和中断设计随笔
  19. 《Node.js区块链开发》PDF版电子书下载
  20. [JS] js-xlsx生成Excel(模拟下载)

热门文章

  1. 企业路由器配置PPTP PC到站点模式Virtual Private Network指南_1(外网访问内网资源)
  2. iOS常用数学函数(公式)
  3. python项目成功打包成exe,运行exe时报错:Unhandled exception in script:Failed to excute
  4. 通达信交易接口可以设定自动止盈止损吗?
  5. 圆通快递 速度奇慢 服务恶劣
  6. java canwrite_Java File canWrite()用法及代码示例
  7. 项目中文乱码(jdk18乱码)
  8. NOIP2008全省提高组获奖名单
  9. 常用编程软件下载地址
  10. 【白话机器学习系列】白话Broadcasting