1.前言
在介紹TextRank前,我想先給大家介紹下PageRank,實質上個人認爲可以把TextRank當做PageRank2.0。
谷歌的兩位創始人的佩奇和布林,借鑑了學術界評判學術論文重要性的通用方法,“那就是看論文的引用次數”。由此想到網頁的重要性也可以根據這種方法來評價。於是PageRank的核心思想就誕生了:
- 如果一個網頁被很多其他網頁鏈接到的話說明這個網頁比較重要,也就是PageRank值會相對較高
- 如果一個PageRank值很高的網頁鏈接到一個其他的網頁,那麼被鏈接到的網頁的PageRank值會相應地因此而提高
2.PageRank算法原理
從PageRank的核心思想出發,我們
- 用定義這個頁面的PageRank值
- 用來表示所有其他可以鏈接到這個頁面的集合
- 那麼便可以用來定義集合中的其中一個(由頁面 鏈接到頁面)
我們可以給一個頁面的PageRank值做這樣的定義:
以上圖爲例,PangRank的值就可以表示爲:
這樣的計算有個很明顯的漏洞,可以看到除了C點,其他三個點都外鏈了2個節點,那麼對於B,D來說,是可以選擇不去A的而去另外一個鏈接。所以外鏈進入A是有一定概率的。針對這點修整PangRank的表示方法:
用矩陣的思想處理這個公式,設定:
- 爲一個N維的PangRank值向量,
- 爲所有頁面的集合,且A有這樣定義:
那麼對於大量PageRank,我們便可以轉化爲這樣的形式:
這樣看似優化了很多,但是還存在一定的問題,我們得事先知道其他相關網站的PageRank值,才能得到指定網站的PageRank值。而其他網站的PageRank值還得從一些具有其鏈接的網站的PageRank值求得。這就變成了一個先有雞還是先有蛋的問題。
PageRank採用power iteration來解決這個問題:
我們給定一個初始值假定爲,然後通過下式子不斷的迭代求解:
直到最後收斂於,即差別小於某個閾值。
於是計算PageRank值的過程就變成了一個 Markov 過程,那麼PageRank算法的證明也就轉爲證明 Markov 過程的收斂性證明,
若一個 Markov 過程收斂,那麼它的狀態轉移矩陣需要滿足:
- stochastic matrix:則行至少存在一個非零值,即必須存在一個外鏈接(沒有外鏈接的網頁被稱爲dangling pages);
- 不可約(irreducible):即矩陣所對應的有向圖必須是強連通的,對於任意兩個節點,存在一個從到的路徑;
- 非週期性(aperiodic):即每個節點存在自迴路。
顯然,在一般情況下,這樣的情況都是不滿足的,爲了滿足性質stochastic matrix,可以把全爲0的行替換爲,其中爲單位向量;同時爲了滿足性質不可約、非週期,需要做平滑處理:
(個人覺得算法領域很多公式的可解釋性不強,大多是爲了解決某些限定條件,而人爲選擇的比較好的優化方式,比如下面公式,把的矩陣乘以的一個阻尼係數,(這個通常取0.85實踐出來的,好像沒什麼數學解釋),實質上是爲了給數據做一個平滑處理,都加上一個實質是爲了替代爲零項)
於是
就被改寫爲:
3.TextRank算法原理
用TextRank提取來提取關鍵詞,用PageRank的思想來解釋它:
- 如果一個單詞出現在很多單詞後面的話,那麼說明這個單詞比較重要
- 一個TextRank值很高的單詞後面跟着的一個單詞,那麼這個單詞的TextRank值會相應地因此而提高
這樣TextRank的公式就可以由PageRank公式改寫爲:
公式的意思很明顯:
TextRank中一個單詞的權重取決於與在前面的各個點組成的這條邊的權重,以及這個點到其他其他邊的權重之和。
4.python編程實現
因爲在瞭解textrank的時候,參考了jieba分詞和TextRank4zh這2個開源庫的寫法。但是兩者無論寫法和運算規則都有很大出入,結合公式來說本人覺得jieba做的更符合公式,TextRank4zh更具有準確性,因爲TextRank4zh在公式上面做了一定的優化。
Jieba分詞TextRank:
- 對每個句子進行分詞和詞性標註處理
- 過濾掉除指定詞性外的其他單詞,過濾掉出現在停用詞表的單詞,過濾掉長度小於2的單詞
- 將剩下的單詞中循環選擇一個單詞,將其與其後面4個單詞分別組合成4條邊。
例如: ['有','媒體', '曝光','高圓圓', '和', '趙又廷','現身', '臺北', '桃園','機場','的', '照片']
對於‘媒體‘這個單詞,就有('媒體', '曝光')、('媒體', '圓')、('媒體', '和')、('媒體', '趙又廷')4條邊,且每條邊權值爲1,當這條邊在之後再次出現時,權值再在基礎上加1.
- 有了這些數據後,我們就可以構建出候選關鍵詞圖,圖的概念有基礎的人可能會很好理解,不理解其實也沒關係,按上面例子,你只用知道這一步我們把2個單詞組成的邊,和其權值記錄了下來。
- 這樣我們就可以套用TextRank的公式,迭代傳播各節點的權值,直至收斂。
- 對結果中的Rank值進行倒序排序,篩選出前面的幾個單詞,就是我們需要的關鍵詞了。
def textrank(self, sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False):
self.pos_filt = frozenset(allowPOS)
# 定義無向有權圖
g = UndirectWeightedGraph()
# 定義共現詞典
cm = defaultdict(int)
# 分詞
words = tuple(self.tokenizer.cut(sentence))
# 依次遍歷每個詞
for i, wp in enumerate(words):
# 詞i 滿足過濾條件
if self.pairfilter(wp):
# 依次遍歷詞i 之後窗口範圍內的詞
for j in xrange(i + 1, i + self.span):
# 詞j 不能超出整個句子
if j >= len(words):
break
# 詞j不滿足過濾條件,則跳過
if not self.pairfilter(words[j]):
continue
# 將詞i和詞j作爲key,出現的次數作爲value,添加到共現詞典中
if allowPOS and withFlag:
cm[(wp, words[j])] += 1
else:
cm[(wp.word, words[j].word)] += 1
# 依次遍歷共現詞典的每個元素,將詞i,詞j作爲一條邊起始點和終止點,共現的次數作爲邊的權重
for terms, w in cm.items():
g.addEdge(terms[0], terms[1], w)
# 運行textrank算法
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)
# 輸出topK個詞作爲關鍵詞
if topK:
return tags[:topK]
else:
return tags
關於有向圖的數據結構:
def addEdge(self, start, end, weight):
# use a tuple (start, end, weight) instead of a Edge object
self.graph[start].append((start, end, weight))
self.graph[end].append((end, start, weight))
關於TextRank的手寫:
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] = wsdef
outSum[n] = sum((e[2] for e in out), 0.0)
# this line for build stable iteration
sorted_keys = sorted(self.graph.keys())
# 遍歷若干次
for x in xrange(10): # 10 iters
# 遍歷各個結點
for n in sorted_keys:
s = 0
# 遍歷結點的入度結點
for e in self.graph[n]:
# 將這些入度結點貢獻後的權值相加
# 貢獻率 = 入度結點與結點n的共現次數 / 入度結點的所有出度的次數
s += e[2] / outSum[e[1]] * ws[e[1]]
# 更新結點n的權值
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 = w
if 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 ws
jieba.analyse.textrank完整源碼如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import sys
from operator import itemgetter
from collections import defaultdict
import jieba.posseg
from .tfidf import KeywordExtractor
from .._compat import *
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 object
self.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] = wsdef
outSum[n] = sum((e[2] for e in out), 0.0)
# this line for build stable iteration
sorted_keys = sorted(self.graph.keys())
for x in xrange(10): # 10 iters
for n in sorted_keys:
s = 0
for 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 = w
if 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 ws
class 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
def pairfilter(self, wp):
return (wp.flag in self.pos_filt and len(wp.word.strip()) >= 2
and 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.cut
if 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):
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
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
extract_tags = textrank
打賞一下作者: