Word2vec加TextRank算法生成文章摘要

       好久不见,CSDN。

      大数据时代的到来在给人们带来海量数据的便利的同时,也带来了大量的数据冗余和垃圾信息。传统的人工书写文本摘要是文章发布和文章阅读极为重要的一环,读者可以快速阅览摘要判断文章的续读必要性。

       然而,人工智能的出现以及现在深度学习的普及,让文章摘要变得十分便利。早在50年前,人们便开始着手于研究自动文本摘要。1958年,Luhn便提出了“词频”的方法,通过计算文章中“keywords”的出现频率找到文章的中心句子以此来生成文章摘要,这被当做文章摘要的鼻祖。在此基础上,Edmundson通过选取标题词、句子位置、线索词和关键词四种特征表示句子计算句子权重,通过句子的分数(权重)排序比较句子的重要性来生成摘要。后来,Mihalcea在google的算法pagerank的基础上提出了计算文章内在结构,主观上评价句子重要性的textrank算法,该算法意义重大极大的推动了自动文摘的发展。后来,更大的方法被发表出来包括比较著名的tf*idf算法。

       当然,以上这些方法皆是基于文章重要句子抽取的算法。我们把它称之为Extractive式方法,这类方法很大程度上保留了文章的key sentence的意思,还原了文章的框架,但对于一些十分复杂,涉及面较广而且中心句不明确的文章效果差强人意。于是便出现了另一种文本摘要的方法,被称之为Abstractive式方法,机器通过阅读文章然后来概括文章的大意。无疑这种方法难度较大,与传统的人工摘要一样,机器必须自己读懂文章才能生成摘要。由于这类方法的种种侷限性,直到去年才google才开源公开了它自己的textsum项目,用于自动生成文摘。

       在google的textsum项目中,用到了seq2seq+attention的深度学习模型,它是google翻译模型。google说这个模型把它的翻译准确率提高到了97%左右,当然国内某度说它的翻译准确率比google更高,然后笔者至今未能有幸体验到如此恐怖的百度翻译。简单的说这种模型是文章的content生成title的模型,RNN神经网络(循环神经网络)将content即文章的内容通过encoder(编码)生成一种sequence,然后另外一个RNN神经网络用来decoder(解码)生成文章的title。这个模型对训练数据的要求极为苛刻,笔者在textsum的基础上用搜歌的语料当做训练模型,在GTX965的GPU上跑了3天左右,最后测试得到的结果令笔者大跌眼镜。生成的句子语义都不通顺更别说组合成为摘要了,在stackoverflow论坛上,很多跑过这个程序都说其效果不佳,当然google自己也表示这个模型还有待优化。当然如果有读者想要试一下这个模型的请移步https://github.com/tensorflow/models/tree/master/textsum。国内目前也有一个deepnlp的团队也做过这个模型的相关测试,有兴趣可以看一下https://github.com/rockingdingo/deepnlp/tree/master/deepnlp/textsum

       在google亲自开源的项目上失败后,笔者着实心灰意冷。再次打开google学术,期待一下能不能有新的发现,果不其然最近两年关于文本摘要的学术发表可谓是琳琅满目。笔者也终于发现了一个目前来说比较成熟的文本摘要方法,通word2vec生成词向量,再利用textrank的方法寻找文本的中心句形成摘要。这种方法被多个学术研究所发表,很多人都提出了各种改进的算法,如果读者想要了解,请搜索顾益军, 夏天. 融合 LDA 与 TextRank 的关键词抽取研究[J]. 现代图书情报技术, 2014(7-8): 41-47. (Gu Yijun, XiaTian. Study on Keyword Extraction with LDA and TextRankCombination [J]. New Technology of Library and InformationService, 2014(7-8): 41-47.)或者融合 Word2vec 与 TextRank 的关键词抽取研究宁建飞 刘降珍(罗定职业技术学院电子信息系 罗定 527200)。这两篇文章都介绍了通过word2vec的方法计算句子的相似性从而获得文本摘要的方法,具体思路笔者会慢慢道来。

       刚刚提到用word2vec+textrank的方法生成文本摘要,首先需要了解word2vec这个深度学习的框架。简单的说word2vec的意思是word to vector,中文翻译过来把它叫做词向量,指的是用空间中的向量用来表示单词,它最初是由Tomas Mikolv提出的,具体请移步Tomas Mikolov, Kai Chen, Greg Corrado, and Jeffrey Dean. Efficient Estimation of Word Representations in Vector Space. In Proceedings of Workshop at ICLR, 2013.

      后来google完成了这个项目,并将其开源。在NLP(Nature Language Processing)中,为了方便处理单词,需要将单词进行编码,也就是讲文字数字化,比较常用也比较令人容易接受的方法就是把一个单词表示成一个向量的形式。比如这里有十个单词:she,he,I,her,his,my…..要将这十个单词表示成向量的形式很简单,比如说she可以用[1,0,0,0,0,0,0,0,0,0]来表示,he可以用[0,1,0,0,0,0,0,0,0,0]来表示。可是如果数量达到了一定程度,以亿计的单词用这种表示方法那肯定是极大的臃肿与恐怖。word2vec在此基础上用低维的向量来表示单词,一般为50维或100维,这种向量被表示成这个样子[0.11212,0.116545,0.878789,0.5644659,……]。

       常用的训练word2vec的模型有CBOW和Skip-Gram。具体模型介绍请看这里模型介绍,关于模型的原理和框架请移步这里模型原理。简单的说word2vec通过训练大量的数据集,这些数据集最好经过查重,去停用词处理,得到关于这些词的向量,并且这些向量存在某种联系,比如说词性相同,同义词,反义词都会被记录在这个模型中。笔者这里使用的是gensim的word2vec库,当然读者可以选择其他语言的,Python的操作会相对简单,具体的代码如下:

from gensim.models import word2vec import logging # 训练主程序 logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.INFO) # 加载语料 sentences = word2vec.Text8Corpus(u'model/text8') # 训练模型 skip-gram model = word2vec.Word2Vec(sentences, size=200, window=10, min_count=64, sg=1, hs=1, iter=10, workers=25)

训练的语料是笔者在网上下载的维基百科中文语料,大约在一个G左右。在训练的过程中,终端会logging出进度。如果笔者相要训练,可以去网上搜索维基百科中文语料会有很多的资料,如果数据量较大请耐心等待。笔者训练了大约3个小时左右,这个期间你可以倒上一杯茶静静的等待,甚至还有时间看几集军师联盟。

      训练完词向量之后,便是今天的重点。TextRank算法是根据google的pagerank算法改造得来的,google用pagerank算法来计算网页的重要性。textrank在pagerank的原理上用来计算一个句子在整个文章里面的重要性,下面通过一个例子来说明一下(此例子引用了别人的图,笔者着实画不出来):

《Word2vec加TextRank算法生成文章摘要》

      图中每个球都代表了一张网页,每一个箭头代表该网页上有其它网页的超链接,如D球,它有指向A的箭头,代表D网页上有A的连接,而E指向了D表示E网页上有D的连接。被引用的越多代表该网页的重要性越大,根据上图可以绘制出如下表格:

out/inABCDEF
A000000
B001000
C010000
D010000
E010101
F010010

      这张表格被称之为交叉矩阵(也有很多不同的叫法),它反映了网页之间的相互关系,通过这个表格可以计算每张网页被其它网页引用的次数,从而算出这张网页的重要程度,计算公式如下:

《Word2vec加TextRank算法生成文章摘要》

      式中d代表阻尼系数,d∈[0,1],一般取d=0.85。对于B来说,有三个页面推荐了B,S(vi)代表的是页面的初始分数,这里一般设置为1,也可以设置成其他。所以S(B)=(1-0.85)+0.85*((1/2)*1+(1/2)*1+(1/2)*1)便是页面B的分数,依次计算得到所有页面的分数。再将页面分数rank取topN就可以得到N个最重要的页面。

在textrank中,人们用句子的相似性来取代网页之间的相互链接的个数。公式如下:《Word2vec加TextRank算法生成文章摘要》

      前面提到计算两个网页之间的互相引用的次数从而得打网页的重要性,那么句子之间的连续如何建立了?传统的方法是比较句子中相同单词的个数,比如“I am a dog”,”you are a dog”这两个句子有连个相同的单词”a”,”dog”。这两个单词同属于两个句子,因此S(si,sj)=2/log(4)+log(4)。这种传统的句子相似性在某种程度上使句子之间建立起了联系,但是单词的词性,单词的近义词,反义词等诸多因素都未考虑进去,因此这种计算句子之间相似性的方法并不优秀。但是它却比起之前的词频法和tf*idf的方法有了很大的进步。

      接下来,便是今天的重点。一开始就说道基于word2vec的基础再通过textrank的算法来获得文本摘要。既然传统的计算句子相似性的算法不能够满足现在的要求,那么是不是可以通过word2vec来计算句子相似性。在word2vec中,每个单词都被表示成了向量,这些向量通过单词之间的联系建立起关系。如:

# 记载已经训练好的中文模型 model = word2vec.Word2Vec.load("model/word2vec") s = model.similarity("孙悟空", "猪八戒") print(s) #输出为: 0.787711574192 


       可以看到在word2vec的模型中,将猪八戒和孙悟空这连个在传统相似性上毫无瓜葛的词语上建立起了联系。试想一下,现在的文章摘要大多不是使用文章中相同的词语来表达,一般都是近义词或浓缩词,因此这种通过词向量来建立起句子相似性的方法相对而言更加科学。

总结起来使用word2vec+textrank方法的基本流程是:

(1)将article分句,装成一个链表。

(2)再将上述的链表中的每一句(sentence)分词,这里推荐jieba分词,当然最好去掉标点符号,以及一些停用词。得到一个二维的list。

(3)然后将每一个句子中的单词,分别于其它句子进行两两相似性计算。这里以向量的方式计算相似度:

这里提供一种计算句子相似性的方法:很多文献中有很多不同的方法,笔者还没有一一去试验。如计算句子A=[‘word’,’you’,’me’],与句子B=[‘sentence’,’google’,’python’]计算相似性,从word2vec模型中分别得到A中三个单词的词向量v1,v2,v3取其平均值Va(avg)=(v1+v2+v3)/3。对句子B做同样的处理得到Vb(avg),然后计算Va(avg)与Vb(avg)连个向量的夹角余弦值,Cosine Similarity视为句子A与B的相似度值。《Word2vec加TextRank算法生成文章摘要》

具体代码如下:

def cosine_similarity(vec1, vec2): ''' 计算两个向量之间的余弦相似度 :param vec1: :param vec2: :return: ''' tx = np.array(vec1) ty = np.array(vec2) cos1 = np.sum(tx * ty) cos21 = np.sqrt(sum(tx ** 2)) cos22 = np.sqrt(sum(ty ** 2)) cosine_value = cos1 / float(cos21 * cos22) return cosine_value def compute_similarity_by_avg(sents_1, sents_2): ''' 对两个句子求平均词向量 :param sents_1: :param sents_2: :return: ''' if len(sents_1) == 0 or len(sents_2) == 0: return 0.0 vec1 = model[sents_1[0]] for word1 in sents_1[1:]: vec1 = vec1 + model[word1] vec2 = model[sents_2[0]] for word2 in sents_2[1:]: vec2 = vec2 + model[word2] similarity = cosine_similarity(vec1 / len(sents_1), vec2 / len(sents_2)) return similarity 


(4)然后计算每一句的相对于另一句的分数,具体方式在上述的pagerank的算法中。

(5)迭代计算每一句的分数,重复迭代,直到分数的差值在0.0001下。

(6)排序上述得到的句子,取分数最高的topN。便是想要得到的句子。笔者在学习textrank算法时,有幸看到了下列文章,他们介绍这个算法更为详细,文章一:http://blog.csdn.net/oxuzhenyi/article/details/54981372 文章二: http://www.cnblogs.com/chenbjin/p/4600538.html

下面附上本次的源代码和结果示意,有兴趣的朋友可以自己实验。

最后,由于笔者并未实验大量的数据进行结果分析,项目算法和细节还需更大的酝酿和优化。文章写得也很仓促,希望大家多多包容。也希望上述文章能给给大家一点帮助。

import jieba import math from string import punctuation from heapq import nlargest from itertools import product, count from gensim.models import word2vec import numpy as np model = word2vec.Word2Vec.load("chinese_model/word2vec_wx") np.seterr(all='warn') def cut_sentences(sentence): puns = frozenset(u'。!?') tmp = [] for ch in sentence: tmp.append(ch) if puns.__contains__(ch): yield ''.join(tmp) tmp = [] yield ''.join(tmp) # 句子中的stopwords def create_stopwords(): stop_list = [line.strip() for line in open("stopwords.txt", 'r', encoding='utf-8').readlines()] return stop_list def two_sentences_similarity(sents_1, sents_2): ''' 计算两个句子的相似性 :param sents_1: :param sents_2: :return: ''' counter = 0 for sent in sents_1: if sent in sents_2: counter += 1 return counter / (math.log(len(sents_1) + len(sents_2))) def create_graph(word_sent): """ 传入句子链表 返回句子之间相似度的图 :param word_sent: :return: """ num = len(word_sent) board = [[0.0 for _ in range(num)] for _ in range(num)] for i, j in product(range(num), repeat=2): if i != j: board[i][j] = compute_similarity_by_avg(word_sent[i], word_sent[j]) return board def cosine_similarity(vec1, vec2): ''' 计算两个向量之间的余弦相似度 :param vec1: :param vec2: :return: ''' tx = np.array(vec1) ty = np.array(vec2) cos1 = np.sum(tx * ty) cos21 = np.sqrt(sum(tx ** 2)) cos22 = np.sqrt(sum(ty ** 2)) cosine_value = cos1 / float(cos21 * cos22) return cosine_value def compute_similarity_by_avg(sents_1, sents_2): ''' 对两个句子求平均词向量 :param sents_1: :param sents_2: :return: ''' if len(sents_1) == 0 or len(sents_2) == 0: return 0.0 vec1 = model[sents_1[0]] for word1 in sents_1[1:]: vec1 = vec1 + model[word1] vec2 = model[sents_2[0]] for word2 in sents_2[1:]: vec2 = vec2 + model[word2] similarity = cosine_similarity(vec1 / len(sents_1), vec2 / len(sents_2)) return similarity def calculate_score(weight_graph, scores, i): """ 计算句子在图中的分数 :param weight_graph: :param scores: :param i: :return: """ length = len(weight_graph) d = 0.85 added_score = 0.0 for j in range(length): fraction = 0.0 denominator = 0.0 # 计算分子 fraction = weight_graph[j][i] * scores[j] # 计算分母 for k in range(length): denominator += weight_graph[j][k] if denominator == 0: denominator = 1 added_score += fraction / denominator # 算出最终的分数 weighted_score = (1 - d) + d * added_score return weighted_score def weight_sentences_rank(weight_graph): ''' 输入相似度的图(矩阵) 返回各个句子的分数 :param weight_graph: :return: ''' # 初始分数设置为0.5 scores = [0.5 for _ in range(len(weight_graph))] old_scores = [0.0 for _ in range(len(weight_graph))] # 开始迭代 while different(scores, old_scores): for i in range(len(weight_graph)): old_scores[i] = scores[i] for i in range(len(weight_graph)): scores[i] = calculate_score(weight_graph, scores, i) return scores def different(scores, old_scores): ''' 判断前后分数有无变化 :param scores: :param old_scores: :return: ''' flag = False for i in range(len(scores)): if math.fabs(scores[i] - old_scores[i]) >= 0.0001: flag = True break return flag def filter_symbols(sents): stopwords = create_stopwords() + ['。', ' ', '.'] _sents = [] for sentence in sents: for word in sentence: if word in stopwords: sentence.remove(word) if sentence: _sents.append(sentence) return _sents def filter_model(sents): _sents = [] for sentence in sents: for word in sentence: if word not in model: sentence.remove(word) if sentence: _sents.append(sentence) return _sents def summarize(text, n): tokens = cut_sentences(text) sentences = [] sents = [] for sent in tokens: sentences.append(sent) sents.append([word for word in jieba.cut(sent) if word]) # sents = filter_symbols(sents) sents = filter_model(sents) graph = create_graph(sents) scores = weight_sentences_rank(graph) sent_selected = nlargest(n, zip(scores, count())) sent_index = [] for i in range(n): sent_index.append(sent_selected[i][1]) return [sentences[i] for i in sent_index] if __name__ == '__main__': with open("news.txt", "r", encoding='utf-8') as myfile: text = myfile.read().replace('\n', '') print(summarize(text, 2)) 
#结果:
['我觉得这种以亲身经历为证的运动研究是很有说服力的,因此它也改变了我以前给病人进行开导时惯常的话语方式。', '但对于像我本人这样几乎不参加体育活动的“沉默的大多数”而言,进行高强度间歇性锻炼很显然比什么都不做要好很多,而且从全球范围来看,它可以挽救数以百万计的生命'] 
#原文:
我觉得这种以亲身经历为证的运动研究是很有说服力的,因此它也改变了我以前给病人进行开导时惯常的话语方式。
以前,我总是向病人提及惯常推荐的运动时长(即每周低强度运动150分钟或高强度运动90分钟),但现在我可以将运动形容得更有吸引力——15分钟一次,每周3次。
当然,这对于已经在坚持健身或参加体育运动的人来说,并无吸引力。而且,关于高强度间歇性锻炼,目前还缺少长期效果和风险减少方面的数据。

但对于像我本人这样几乎不参加体育活动的沉默的大多数而言,进行高强度间歇性锻炼很显然比什么都不做要好很多,而且从全球范围来看,它可以挽救数以百万计的生命
点赞