一.NLG文本生成任務
文本生成NLG,不同於文本理解NLU(例如分詞、詞向量、分類、實體提取),是重在文本生成的另一種關鍵技術(常用的有翻譯、摘要、同義句生成等)。
傳統的文本生成NLG任務主要是抽取式的,生成式的方法看起來到現在使用也沒有那麼普遍。現在,我記錄的是textrank,一種使用比較廣泛的抽取式關鍵句提取算法。
二、TextRank前世今生
要說TextRank(TextRank: Bringing Order into Texts),還得從它的爸爸PageRank說起。
PageRank(The PageRank Citation Ranking: Bringing Order to the Web),是上世紀90年代末提出的一種計算網頁權重的算法。當時,互聯網技術突飛猛進,各種網頁網站爆炸式增長,業界急需一種相對比較準確的網頁重要性計算方法,是人們能夠從海量互聯網世界中找出自己需要的信息。
百度百科如是介紹他的思想:PageRank通過網絡浩瀚的超鏈接關係來確定一個頁面的等級。Google把從A頁面到B頁面的鏈接解釋爲A頁面給B頁面投票,Google根據投票來源(甚至來源的來源,即鏈接到A頁面的頁面)和投票目標的等級來決定新的等級。簡單的說,一個高等級的頁面可以使其他低等級頁面的等級提升。
具體說來就是,PageRank有兩個基本思想,也可以說是假設,即數量假設:一個網頁被越多的其他頁面鏈接,就越重);質量假設:一個網頁越是被高質量的網頁鏈接,就越重要。
總的來說就是一句話,從全局角度考慮,獲取重要的信息。
三、TextRank原理
首先不得不說的是原始論文的PageRank的公式,d是阻力系數,取0.85;In(Vi)是鏈入Vi頁面的網頁集合, Out(Vj)是鏈出Vi頁面的網頁集合:
TextRank的公式是這樣的,Wji表示兩節點邊的重要程度,入度和初度的意思同PageRank:
當然,雖然TextRank和PageRank的思想相近,但是它們還是有不同點的。一是邊權重的構建不同,PageRank利用的是網頁的連接關係,而TextRank利用的是詞的共現信息(或者是兩兩句子的文本相似度);二是邊關係的不同,PageRank是有向無權邊,而TextRank的是無向有權邊。
四、jieba,TextRank4ZH,gensim的TextRank實現比較(推薦TextRank4ZH)
1.jieba
defaultdict字典存儲出入度,10 iters循環,只有關鍵詞的textrank
2.TextRank4ZH
數據預處理爲(不變-去停用詞-或保留重要tag)句子的詞共現構建numpy矩陣, networkx的pagerank作爲textrank優化,使用了networkx和jieba外部包。支持關鍵詞和關鍵句。
3.gensim
數據預處理比較多且針對英文(比如說關鍵詞取3,這樣中文較多的二字的詞就取不到,不如TextRank4ZH的保留重要tag;再比如切句全是英文的分隔符.?!,不如TextRank4ZH的。?!......);相似度計算爲BM25比詞共現矩陣先進;gensim自己實現構建Graph、PageRank等不需要額外包;輸入最小句子長度設置爲10。
五、碼源分析
主要參考jieba,TextRank4ZH,gensim,
Jieba的TextRank使用defaultdict字典存儲句子的相鄰詞語之間的共現關係,代碼如下:
g = UndirectWeightedGraph()
cm = defaultdict(int) # 這個字典, 不存在時候會返回空,而不像dict那樣報錯
words = tuple(self.tokenizer.cut(sentence))
for i, wp in enumerate(words): # textrank和pagerank一樣,會有兩個for循環
if self.pairfilter(wp):
for j in xrange(i + 1, i + self.span):
if j >= len(words):
break
if not self.pairfilter(words[j]):
continue
if allowPOS and withFlag:
cm[(wp, words[j])] += 1
else:
cm[(wp.word, words[j].word)] += 1 # 例如: dict[('我','喜歡')]=1
for terms, w in cm.items():
g.addEdge(terms[0], terms[1], w) # 保存(詞1, 詞1的下一個詞[詞2], 頻次[詞1詞2的])
nodes_rank = g.rank()
TextRank4ZH使用的是numpy保存共現句子間相鄰詞語的共現信息,然後再用了networkx.pagerank計算句子得分,據說這是優化,加快了計算速度等。代碼如下:
,
sorted_sentences = []
_source = words
sentences_num = len(_source)
graph = np.zeros((sentences_num, sentences_num))
# 構建兩兩相似度矩陣
for x in xrange(sentences_num):
for y in xrange(x, sentences_num):
similarity = sim_func( _source[x], _source[y] )
graph[x, y] = similarity
graph[y, x] = similarity
nx_graph = nx.from_numpy_matrix(graph) # 調用pagerank實現
scores = nx.pagerank(nx_graph, **pagerank_config) # this is a dict
sorted_scores = sorted(scores.items(), key = lambda item: item[1], reverse=True)
# 切句
sentence_delimiters = ['?', '!', ';', '?', '!', '。', ';', '……', '…', '\n']
# 提取重要tag的中文詞語
allow_speech_tags = ['an', 'i', 'j', 'l', 'n', 'nr', 'nrfg', 'ns', 'nt', 'nz', 't', 'v', 'vd', 'vn', 'eng']
gensim的textrank是gensim.summarization.summarizer.summarize。
##########先是分句和數據預處理(依次是: 小寫, html, 標點符號, 多個空格, 數字時間等, 英文停用詞,大於3的句子,提取英文單詞詞幹[如複數])
###############################
DEFAULT_FILTERS = [
lambda x: x.lower(), strip_tags, strip_punctuation,
strip_multiple_whitespaces, strip_numeric,
remove_stopwords, strip_short, stem_text
]
# 1.切分句子, 非貪婪搜索, 以英文句號-感嘆號-問號.!?加空格, 或者是換行符\n爲標誌,不適合中文
RE_SENTENCE = re.compile(r'(\S.+?[.!?])(?=\s+|$)|(\S.+?)(?=[\n]|$)', re.UNICODE)
# \w 的釋義都是指包含大 小寫字母數字和下劃線
# \s 空格
# ^ 匹配字符串的開始
# $ 匹配字符串的結束
# 2.去除標點符號(punctuation),可以發現,有一些特殊字符和中文的
punctuation = """!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
# 3.去除html標籤
RE_TAGS = re.compile(r"<([^>]+)>", re.UNICODE)
# 4.空格,時間數字等, 時間等,例如"24.0hours"改爲"24.0 hours"
# 5.大於len=3的單詞, 感覺這點中文不適用,strip_short(s, minsize=2)比較合適
# 構建子典等
corpus = _build_corpus(sentences)
# 關鍵
most_important_docs = summarize_corpus(corpus, ratio=ratio if word_count is None else 1)
# page rank
def pagerank_weighted(graph, damping=0.85):
"""Get dictionary of `graph` nodes and its ranks.
Parameters
----------
graph : :class:`~gensim.summarization.graph.Graph`
Given graph.
damping : float
Damping parameter, optional
Returns
-------
dict
Nodes of `graph` as keys, its ranks as values.
"""
coeff_adjacency_matrix = build_adjacency_matrix(graph, coeff=damping)
probabilities = (1 - damping) / float(len(graph))
pagerank_matrix = coeff_adjacency_matrix.toarray()
# trying to minimize memory allocations
pagerank_matrix += probabilities
vec = principal_eigenvector(pagerank_matrix.T)
# Because pagerank_matrix is positive, vec is always real (i.e. not complex)
return process_results(graph, vec.real)
希望對你有所幫助!