首先,简单阐述一下文本摘要的任务:该任务的输入是一段文本(例如新闻),输出是能够概括输入的简短文本。
目前文本摘要问题主要分为两类,extractive summarization (抽取式摘要)与 abstractive summarization(生成式摘要)。其中抽取式摘要是从原文本中,挑选出重要的句子,并将这些句子组成摘要。而生成式摘要则是让模型理解输入,使用encoder-decoder模型,生成出摘要。工业界普遍使用抽取式摘要,学术界主要研究生成式摘要。就摘要语句的流畅度而言,抽取式摘要要好于生成式摘要。
1.基于TextRank的抽取式摘要
1.1 PageRank
在了解textrank之前,首先我们需要了解PageRank,推荐大家去阅读《统计学习方法》(第二版)第21章
(1)概述:
PageRank算法是图的链接分析的代表性算法,属于图数据上的无监督学习方法。最初作为互联网网页重要度的计算方法,用于谷歌搜索引擎的网页排序,后来被应用到社会影响力分析,文本摘要等多个问题。
它的基本思想为:在有向图上定义一个随机游走模型,即一阶马尔可夫链,描述随机游走者沿着有向图随机访问各个结点的行为。在一定条件下,极限情况访问的各个结点的概率收敛到平稳分布,这时各个结点的平稳概率值就是其PageRank值,表示结点的重要度。
(2)随机游走模型(Random Walk)

如上图可知,每个结点访问其链接的结点的概率是相同的。
(3)PageRank的基本定义
给定一个包含n个结点的强连通且非周期性的有向图,在其基础上定义随机游走模型,假设转移矩阵为M,在时刻0,1,2,3,...,t,...访问各个结点的概率分布为R0,MR0,M平方R0,M立方R0,...,以此类推。
则极限 = R存在(M的t次方乘以R0),极限向量R表示马尔科夫链的平稳分布,满足MR=R。平稳分布R称为这个有向图的PageRank,R的各个分量称为各个结点的PageRank值
极限存在定理:不可约且非周期的有限状态马尔可夫链,有唯一平稳分布存在,而且当时间趋于无穷时状态分布收敛于唯一的平稳分布。
(4)PageRank的一般定义
我们设想一下,在成千上万个网页中,大部分网页并没有超链接,所以基本定义不适用于这种情况,于是提出了PageRank的一般定义,一般定义的思想就是在基本定义上引入平滑项,这里就不细述了。
(5)PageRank的计算
①迭代计算法:更新足够小时,停止迭代。
②幂法:近似计算矩阵的主特征值和主特征向量。
③代数算法:R = ( I - dM)^-1 *(1-d)/n *
1.2 TextRank
将文本中的每个句子分别看作一个节点,如果两个句子有相似性,那么认为这两个句子对应的节点之间存在一条无向边。考察句子相似度的方法:(分母这么设置的原因是可以遏制较长的句子在相似度计算上的优势)
根据以上相似度公式循环计算任意两个节点之间的相似度,根据阈值去掉两个节点之间相似度较低的边连接。构建出节点连接图,然后计算TextRank值,最后对所有的TextRank值排序,选出TextRank值最高的几个节点对应的句子作为摘要。
1.3 实战
参考github:https://github.com/DengYangyong/textrank_summarization
import numpy as np
import pandas as pd
import re,os,jieba
from itertools import chain
from sklearn.metrics.pairwise import cosine_similarity
import networkx as nx
def seg_depart(sentence):
# 去掉非汉字字符
sentence = re.sub(r'[^\u4e00-\u9fa5]+','',sentence)
sentence_depart = jieba.cut(sentence.strip())
word_list = []
for word in sentence_depart:
if word not in stopwords:
word_list.append(word)
# 如果句子整个被过滤掉了,如:'02-2717:56'被过滤,那就返回[],保持句子的数量不变
return word_list
word_embeddings = {}
f = open('D://文本摘要/textrank/搜狗词向量/sgns.sogounews.bigram-char', encoding='utf-8')
for line in f:
# 把第一行的内容去掉
if '365113 300\n' not in line:
values = line.split()
# print(values)
# 第一个元素是词语
word = values[0]
try:
embedding = np.asarray(values[1:], dtype='float32')
word_embeddings[word] = embedding
except:
word = values[0]+values[1]
try:
embedding = np.asarray(values[2:], dtype='float32')
word_embeddings[word] = embedding
except:
print('这个问题我还暂时无法处理:')
print(values[:10])
f.close()
print("一共有"+str(len(word_embeddings))+"个词语/字。")
# 文档所在的文件夹
c_root = 'D://文本摘要/textrank/textrank_易会满/cnews/'
f = open('D://newsdata.txt','r',encoding='utf-8')
data_list = f.readlines()
f.close()
stopwords = [line.strip() for line in open('D://文本摘要/textrank/textrank_易会满/stopwords.txt',encoding='UTF-8').readlines()]
stopwords = list(set(stopwords))
for i in range(len(stopwords)):
stopwords[i] = stopwords[i].replace('\n','')
f1 = open("D://sum.txt",'w',encoding='utf-8')
for wja in range(len(data_list)):
article = data_list[wja]
sentences_list = []
if article.strip():
line_split = re.split(r'[。!;?]',article.strip())
line_split = [line.strip() for line in line_split if line.strip() not in ['。','!','?',';'] and len(line.strip())>1]
sentences_list.append(line_split)
sentences_list = list(chain.from_iterable(sentences_list))
sentence_word_list = []
for sentence in sentences_list:
line_seg = seg_depart(sentence)
sentence_word_list.append(line_seg)
# print(sentence_word_list)
# for i in range(len(sentence_word_list)):
# print(sentences_list[i])
# print(sentence_word_list[i])
sentence_vectors = []
for i in sentence_word_list:
if len(i)!=0:
# 如果句子中的词语不在字典中,那就把embedding设为300维元素为0的向量。
# 得到句子中全部词的词向量后,求平均值,得到句子的向量表示
v = sum([word_embeddings.get(w, np.zeros((300,))) for w in i])/(len(i))
else:
# 如果句子为[],那么就向量表示为300维元素为0个向量。
v = np.zeros((300,))
sentence_vectors.append(v)
sim_mat = np.zeros([len(sentences_list), len(sentences_list)])
for i in range(len(sentences_list)):
for j in range(len(sentences_list)):
if i != j:
sim_mat[i][j] = cosine_similarity(sentence_vectors[i].reshape(1,300), sentence_vectors[j].reshape(1,300))[0,0]
nx_graph = nx.from_numpy_array(sim_mat)
# 得到所有句子的textrank值
scores = nx.pagerank(nx_graph)
# 根据textrank值对未处理的句子进行排序
ranked_sentences = sorted(((scores[i],s) for i,s in enumerate(sentences_list)), reverse=True)
# 取出得分最高的前10个句子作为摘要
sn = 5
if sn > len(sentences_list):
sn = len(sentences_list)
# print(sentences_list)
summary = list()
for i in range(sn):
summary.append(ranked_sentences[i][1])
sum2 = list()#存储最终摘要
for kkk in sentences_list:
if kkk in summary:
sum2.append(kkk)
summarization = '。'.join(sum2)
f1.write(summarization+'\n')
if wja%100 == 0:
print('已处理了'+str(wja)+'条')
if wja%1000 == 0:
print(summarization)
f1.close()
原github的代码里有非常详细的备注,请大家移步至https://github.com/DengYangyong/textrank_summarization
以上代码,也是借鉴了该作者的代码,进行了少量修改,用于我自己的一个新闻摘到的任务。
2. 基于BERT的抽取式摘要
paper地址:https://arxiv.org/abs/1903.10318
code地址:https://github.com/nlpyang/BertSum
2.1 Paper解读

此前的DL在抽取式摘要任务方面已经达到了瓶颈,直到BERT横空出世。作者尝试了多种BERT变体,最终定义了如下模型:

首先,BERT原型并不适合文本摘要,BERT有表示不同句子的分段嵌入,但它只有两个标签,而文本摘要则是多个句子,因此需要做一些改进。作者在每个句子前加【CLS】标签,后加【SEP】标签。利用【CLS】获取到每个句子的sentence-level的特征。之后的Summarization layer作者做了不同的尝试:①设置一个简单的线性分类器+sigmoid ②使用transformer+sigmoid ③使用LSTM+sigmoid

我们可以看出,最好的方法是使用②transformer(作者又做了对比实验,分别测试了1,2,3层的transformer的效果,实验结果显示2层的transformer效果最好),值得注意的是使用LSTM的方法甚至不如一个简单的线性分类器。