基于TextRank算法的文本摘要
文本摘要是自然语言处理(NLP)的应用之一,一定会对我们的生活产生巨大影响。随着数字媒体的发展和出版业的不断增长,谁还会有时间完整地浏览整篇文章、文档、书籍来决定它们是否有用呢?
利用计算机将大量的文本进行处理,产生简洁、精炼内容的过程就是文本摘要,人们可通过阅读摘要来把握文本主要内容,这不仅大大节省时间,更提高阅读效率。但人工摘要耗时又耗力,已不能满足日益增长的信息需求,因此借助计算机进行文本处理的自动文摘应运而生。
这类任务到目前为止主要分为两类:
- 抽取式摘要:这种方法依赖于从文本中提取几个部分,例如短语、句子,把它们堆叠起来创建摘要。因此,这种抽取型的方法最重要的是识别出适合总结文本的句子。
- 生成式摘要:通过建立抽象的语意表示,使用自然语言生成技术,形成摘要。可能总结中的文本甚至没有在原文中出现。
目前主要方法有:
- 基于统计: 统计词频,位置等信息,计算句子权值,再选取权值高的句子作为文摘。简单易用,但对词句的使用大多仅停留在表面信息。
- 基于图模型: 构建拓扑结构图,对词句进行排序。例如,TextRank、LexRank 等。
- 基于潜在语义: 使用主题模型,挖掘词句隐藏信息。例如,LDA、HMM 等。
- 基于整数规划: 将文摘问题转为整数线性规划,求全局最优解。
TextRank 是一种从 PageRank 发展而来的抽取型摘要算法。关于 PageRank 的介绍可以查看 我的这篇博客。
基于 TextRank 的摘要算法在 PageRank 的基础上,用句子代替网页,把每个句子分别看做一个节点,如果两个句子有相似性,那么认为这两个句子对应的节点之间存在一条无向有权边,而句子的相似性方法是根据如下公式:
S i m i l a r i t y ( S i , S j ) = ∣ w k ∣ w k ∈ S i ∩ w k ∈ S j ∣ l o g ( ∣ S i ∣ ) + l o g ( ∣ S j ∣ ) Similarity(S_i,S_j)=\frac{|w_k|w_k\in S_i\cap w_k\in S_j|}{log(|S_i|)+log(|S_j|)} Similarity(Si,Sj)=log(∣Si∣)+log(∣Sj∣)∣wk∣wk∈Si∩wk∈Sj∣
其中 S i S_i Si、 S j S_j Sj 分别表示两个句子, w k w_k wk 表示句子中的词,那么分子部分的意思是同时出现在两个句子中的同一个词的个数,分母是对句子中词的个数求对数和。分母这样的设计可以抑制较长的句子在相似度计算上的优势。不过通常在构造完句子词向量之后用余弦相似度就可以计算。
其主要的流程如下图。
- 首先把文章合成文本数据。
- 把文本分割成单个句子。
- 为每个句子找到词向量表示。
- 计算句子向量间的相似性。
- 将相似性矩阵转换为以句子为节点,相似性得分为边的图结构,用于句子 TextRank 计算。
- 最后一定数量的排名最高的句子构成最后的摘要。
SnowNLP 中的 文本摘要 使用的即是 TextRank 算法,源码如下所示。
from __future__ import unicode_literals
from ..sim.bm25 import BM25
class TextRank(object):
def __init__(self, docs):
self.docs = docs
self.bm25 = BM25(docs)
self.D = len(docs)
self.d = 0.85
self.weight = []
self.weight_sum = []
self.vertex = []
self.max_iter = 200
self.min_diff = 0.001
self.top = []
def solve(self):
for cnt, doc in enumerate(self.docs):
scores = self.bm25.simall(doc)
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 = 0
for 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:
continue
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 = m
if max_diff <= self.min_diff:
break
self.top = list(enumerate(self.vertex))
self.top = sorted(self.top, key=lambda x: x[1], reverse=True)
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))
from . import normal
from . import seg
from .summary import textrank
class SnowNLP(object):
def __init__(self, doc):
self.doc = doc
......
def summary(self, limit=5):
doc = []
sents = self.sentences
for sent in sents:
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