本文將介紹PageRank算法的相關內容,具體以下:php
1.算法來源
2.算法原理
3.算法證實
4.PR值計算方法
4.1 冪迭代法
4.2 特徵值法
4.3 代數法
5.算法實現
5.1 基於迭代法的簡單實現
5.2 MapReduce實現
6.PageRank算法的缺點
7.寫在最後
參考資料html
這個要從搜索引擎的發展講起。最先的搜索引擎採用的是 分類目錄[^ref_1] 的方法,即經過人工進行網頁分類並整理出高質量的網站。那時 Yahoo 和國內的 hao123 就是使用的這種方法。node
後來網頁愈來愈多,人工分類已經不現實了。搜索引擎進入了 文本檢索 的時代,即計算用戶查詢關鍵詞與網頁內容的相關程度來返回搜索結果。這種方法突破了數量的限制,可是搜索結果不是很好。由於總有某些網頁來回地倒騰某些關鍵詞使本身的搜索排名靠前。python
因而咱們的主角要登場了。沒錯,谷歌的兩位創始人,當時仍是美國斯坦福大學 (Stanford University) 研究生的佩奇 (Larry Page) 和布林 (Sergey Brin) 開始了對網頁排序問題的研究。他們的借鑑了學術界評判學術論文重要性的通用方法, 那就是看論文的引用次數。由此想到網頁的重要性也能夠根據這種方法來評價。因而PageRank的核心思想就誕生了[^ref_2],很是簡單:算法
- 若是一個網頁被不少其餘網頁連接到的話說明這個網頁比較重要,也就是PageRank值會相對較高
- 若是一個PageRank值很高的網頁連接到一個其餘的網頁,那麼被連接到的網頁的PageRank值會相應地所以而提升
就以下圖所示(一個概念圖):segmentfault
PageRank算法[^ref_3]總的來講就是預先給每一個網頁一個PR值(下面用PR值指代PageRank值),因爲PR值物理意義上爲一個網頁被訪問機率,因此通常是$ \frac{1}{N} $,其中N爲網頁總數。另外,通常狀況下,全部網頁的PR值的總和爲1。若是不爲1的話也不是不行,最後算出來的不一樣網頁之間PR值的大小關係仍然是正確的,只是不能直接地反映機率了。markdown
預先給定PR值後,經過下面的算法不斷迭代,直至達到平穩分佈爲止。網絡
互聯網中的衆多網頁能夠看做一個有向圖。下圖是一個簡單的例子[^ref_4]:app
這時A的PR值就能夠表示爲:框架
\[ PR(A) = PR(B) + PR(C) \]
然而圖中除了C以外,B和D都不止有一條出鏈,因此上面的計算式並不許確。想象一個用戶如今在瀏覽B網頁,那麼下一步他打開A網頁仍是D網頁在統計上應該是相同機率的。因此A的PR值應該表述爲:
\[ PR(A) = \frac{PR(B)}{2} + \frac{PR(C)}{1} \]
互聯網中不乏一些沒有出鏈的網頁,以下圖:
圖中的C網頁沒有出鏈,對其餘網頁沒有PR值的貢獻,咱們不喜歡這種自私的網頁(實際上是爲了知足 Markov 鏈的收斂性),因而設定其對全部的網頁(包括它本身)都有出鏈,則此圖中A的PR值可表示爲:
\[ PR(A) = \frac{PR(B)}{2} + \frac{PR(C)}{4} \]
然而咱們再考慮一種狀況:互聯網中一個網頁只有對本身的出鏈,或者幾個網頁的出鏈造成一個循環圈。那麼在不斷地迭代過程當中,這一個或幾個網頁的PR值將只增不減,顯然不合理。以下圖中的C網頁就是剛剛說的只有對本身的出鏈的網頁:
爲了解決這個問題。咱們想象一個隨機瀏覽網頁的人,當他到達C網頁後,顯然不會傻傻地一直被C網頁的小把戲困住。咱們假定他有一個肯定的機率會輸入網址直接跳轉到一個隨機的網頁,而且跳轉到每一個網頁的機率是同樣的。因而則此圖中A的PR值可表示爲:
\[ PR(A) = \alpha(\frac{PR(B)}{2}) + \frac{(1 - \alpha)}{4}\]
在通常狀況下,一個網頁的PR值計算以下:
\[ PR(p_{i}) = \alpha \sum_{p_{j} \in M_{p_{i}}} \frac{PR(p_{j})}{L(p_{j})} + \frac{(1 - \alpha)}{N} \]
其中\(M_{p_{i}}\)是全部對\(p_{i}\)網頁有出鏈的網頁集合,\(L(p_{j})\)是網頁\(p_{j}\)的出鏈數目,\(N\)是網頁總數,\(\alpha\)通常取0.85。
根據上面的公式,咱們能夠計算每一個網頁的PR值,在不斷迭代趨於平穩的時候,即爲最終結果。具體怎樣算是趨於平穩,咱們在下面的PR值計算方法部分再作解釋。
- $ \lim_{n \rightarrow \infty}P_{n} $是否存在?
- 若是極限存在,那麼它是否與\(P_0\)的選取無關?
PageRank算法的正確性證實包括上面兩點[^ref_5]。爲了方便證實,咱們先將PR值的計算方法轉換一下。
仍然拿剛剛的例子來講
咱們能夠用一個矩陣來表示這張圖的出鏈入鏈關係,\(S_{ij} = 0\)表示\(j\)網頁沒有對\(i\)網頁的出鏈:
\[ S = \left( \begin{array}{cccc} 0 & 1/2 & 0 & 0 \\ 1/3 & 0 & 0 & 1/2 \\ 1/3 & 0 & 1 & 1/2 \\ 1/3 & 1/2 & 0 & 0 \\ \end{array} \right) \]
取\(e\)爲全部份量都爲 1 的列向量,接着定義矩陣:
\[ A = \alpha S + \frac{(1 - \alpha)}{N}ee^T \]
則PR值的計算以下,其中\(P_{n}\)爲第n次迭代時各網頁PR值組成的列向量:
\[ P_{n+1} = A P_{n} \]
因而計算PR值的過程就變成了一個 Markov 過程,那麼PageRank算法的證實也就轉爲證實 Markov 過程的收斂性證實:若是這個 Markov 過程收斂,那麼$ \lim_{n \rightarrow \infty}P_{n} \(存在,且與\)P_0$的選取無關。
若一個 Markov 過程收斂,那麼它的狀態轉移矩陣\(A\)須要知足[^ref_6]:
- A爲隨機矩陣。
- A是不可約的。
- A是非週期的。
先看第一點,隨機矩陣又叫機率矩陣或 Markov 矩陣,知足如下條件:
\[ 令a_{ij}爲矩陣A中第i行第j列的元素,則 \forall i = 1 \dots n, j = 1 \dots n, a_{ij} \geq 0, 且 \forall i = 1 \dots n, \sum_{j = 1}^n a_{ij} = 1 \]
顯然咱們的A矩陣全部元素都大於等於0,而且每一列的元素和都爲1。
第二點,不可約矩陣:方針A是不可約的當且僅當與A對應的有向圖是強聯通的。有向圖\(G = (V,E)\)是強聯通的當且僅當對每一對節點對\(u,v \in V\),存在從\(u\)到\(v\)的路徑。由於咱們在以前設定用戶在瀏覽頁面的時候有肯定機率經過輸入網址的方式訪問一個隨機網頁,因此A矩陣一樣知足不可約的要求。
第三點,要求A是非週期的。所謂週期性,體如今Markov鏈的週期性上。即若A是週期性的,那麼這個Markov鏈的狀態就是週期性變化的。由於A是素矩陣(素矩陣指自身的某個次冪爲正矩陣的矩陣),因此A是非週期的。
至此,咱們證實了PageRank算法的正確性。
首先給每一個頁面賦予隨機的PR值,而後經過$ P_{n+1} = A P_{n} $不斷地迭代PR值。當知足下面的不等式後迭代結束,得到全部頁面的PR值:
\[ |P_{n+1} - P_{n}| < \epsilon \]
當上面提到的Markov鏈收斂時,必有:
\[ P = A P \Rightarrow P爲矩陣A特徵值1對應的特徵向量 \\ (隨機矩陣必有特徵值1,且其特徵向量全部份量全爲正或全爲負) \]
類似的,當上面提到的Markov鏈收斂時,必有:
\[ P = A P \\ \Rightarrow P = \lgroup \alpha S + \frac{(1 - \alpha)}{N}ee^T \rgroup P \\ 又\because e爲全部份量都爲 1 的列向量,P的全部份量之和爲1 \\ \Rightarrow P = \alpha SP + \frac{(1 - \alpha)}{N}e \\ \Rightarrow (ee^T - \alpha S)P = \frac{(1 - \alpha)}{N}e \\ \Rightarrow P = (ee^T - \alpha S)^{-1} \frac{(1 - \alpha)}{N}e \\ \]
用python實現[^ref_7],須要先安裝python-graph-core。
# -*- coding: utf-8 -*- from pygraph.classes.digraph import digraph class PRIterator: __doc__ = '''計算一張圖中的PR值''' def __init__(self, dg): self.damping_factor = 0.85 # 阻尼係數,即α self.max_iterations = 100 # 最大迭代次數 self.min_delta = 0.00001 # 肯定迭代是否結束的參數,即ϵ self.graph = dg def page_rank(self): # 先將圖中沒有出鏈的節點改成對全部節點都有出鏈 for node in self.graph.nodes(): if len(self.graph.neighbors(node)) == 0: for node2 in self.graph.nodes(): digraph.add_edge(self.graph, (node, node2)) nodes = self.graph.nodes() graph_size = len(nodes) if graph_size == 0: return {} page_rank = dict.fromkeys(nodes, 1.0 / graph_size) # 給每一個節點賦予初始的PR值 damping_value = (1.0 - self.damping_factor) / graph_size # 公式中的(1−α)/N部分 flag = False for i in range(self.max_iterations): change = 0 for node in nodes: rank = 0 for incident_page in self.graph.incidents(node): # 遍歷全部「入射」的頁面 rank += self.damping_factor * (page_rank[incident_page] / len(self.graph.neighbors(incident_page))) rank += damping_value change += abs(page_rank[node] - rank) # 絕對值 page_rank[node] = rank print("This is NO.%s iteration" % (i + 1)) print(page_rank) if change < self.min_delta: flag = True break if flag: print("finished in %s iterations!" % node) else: print("finished out of 100 iterations!") return page_rank if __name__ == '__main__': dg = digraph() dg.add_nodes(["A", "B", "C", "D", "E"]) dg.add_edge(("A", "B")) dg.add_edge(("A", "C")) dg.add_edge(("A", "D")) dg.add_edge(("B", "D")) dg.add_edge(("C", "E")) dg.add_edge(("D", "E")) dg.add_edge(("B", "E")) dg.add_edge(("E", "A")) pr = PRIterator(dg) page_ranks = pr.page_rank() print("The final page rank is\n", page_ranks)
運行結果:
finished in 36 iterations! The final page rank is {'A': 0.2963453309000821, 'C': 0.11396451042168992, 'B': 0.11396451042168992, 'E': 0.31334518664434013, 'D': 0.16239975107315852}
程序中給出的網頁之間的關係一開始以下:
迭代結束後以下:
做爲Hadoop(分佈式系統平臺)的核心模塊之一,MapReduce是一個高效的分佈式計算框架。下面首先簡要介紹一下MapReduce原理。
所謂MapReduce,就是兩種操做:Mapping和Reducing[^ref_8]。
- 映射(Mapping):對集合裏的每一個目標應用同一個操做。
- 化簡(Reducing ):遍歷Mapping返回的集合中的元素來返回一個綜合的結果。
就拿一個最經典的例子來講:如今有3個文本文件,須要統計出全部出現過的詞的詞頻。傳統的想法是讓一我的順序閱讀這3個文件,每遇到一個單詞,就看以前有沒有遇到過。遇到過的話詞頻加一:(單詞,N + 1),不然就記錄新詞,詞頻爲一:(單詞,1)。
MapReduce方式爲:把這3個文件分給3我的,每一個人閱讀一份文件。每當遇到一個單詞,就記錄這個單詞:(單詞,1)(無論以前有沒有遇到過這個單詞,也就是說可能出現多個相同單詞的記錄)。以後將再派一我的把相同單詞的記錄相加,便可獲得最終結果。
詞頻統計的具體實現可見點我。
下面是使用MapReduce實現PageRank的具體代碼[^ref_9]。首先是通用的map與reduce模塊。如果感受理解有困難,能夠先看看詞頻統計的實現代碼,其中一樣使用了下面的模塊:
class MapReduce: __doc__ = '''提供map_reduce功能''' @staticmethod def map_reduce(i, mapper, reducer): """ map_reduce方法 :param i: 須要MapReduce的集合 :param mapper: 自定義mapper方法 :param reducer: 自定義reducer方法 :return: 以自定義reducer方法的返回值爲元素的一個列表 """ intermediate = [] # 存放全部的(intermediate_key, intermediate_value) for (key, value) in i.items(): intermediate.extend(mapper(key, value)) # sorted返回一個排序好的list,由於list中的元素是一個個的tuple,key設定按照tuple中第幾個元素排序 # groupby把迭代器中相鄰的重複元素挑出來放在一塊兒,key設定按照tuple中第幾個元素爲關鍵字來挑選重複元素 # 下面的循環中groupby返回的key是intermediate_key,而group是個list,是1個或多個 # 有着相同intermediate_key的(intermediate_key, intermediate_value) groups = {} for key, group in itertools.groupby(sorted(intermediate, key=lambda im: im[0]), key=lambda x: x[0]): groups[key] = [y for x, y in group] # groups是一個字典,其key爲上面說到的intermediate_key,value爲全部對應intermediate_key的intermediate_value # 組成的一個列表 return [reducer(intermediate_key, groups[intermediate_key]) for intermediate_key in groups]
接着是計算PR值的類,其中實現了用於計算PR值的mapper和reducer:
class PRMapReduce: __doc__ = '''計算PR值''' def __init__(self, dg): self.damping_factor = 0.85 # 阻尼係數,即α self.max_iterations = 100 # 最大迭代次數 self.min_delta = 0.00001 # 肯定迭代是否結束的參數,即ϵ self.num_of_pages = len(dg.nodes()) # 總網頁數 # graph表示整個網絡圖。是字典類型。 # graph[i][0] 存放第i網頁的PR值 # graph[i][1] 存放第i網頁的出鏈數量 # graph[i][2] 存放第i網頁的出鏈網頁,是一個列表 self.graph = {} for node in dg.nodes(): self.graph[node] = [1.0 / self.num_of_pages, len(dg.neighbors(node)), dg.neighbors(node)] def ip_mapper(self, input_key, input_value): """ 看一個網頁是否有出鏈,返回值中的 1 沒有什麼物理含義,只是爲了在 map_reduce中的groups字典的key只有1,對應的value爲全部的懸掛網頁 的PR值 :param input_key: 網頁名,如 A :param input_value: self.graph[input_key] :return: 若是沒有出鏈,即懸掛網頁,那麼就返回[(1,這個網頁的PR值)];不然就返回[] """ if input_value[1] == 0: return [(1, input_value[0])] else: return [] def ip_reducer(self, input_key, input_value_list): """ 計算全部懸掛網頁的PR值之和 :param input_key: 根據ip_mapper的返回值來看,這個input_key就是:1 :param input_value_list: 全部懸掛網頁的PR值 :return: 全部懸掛網頁的PR值之和 """ return sum(input_value_list) def pr_mapper(self, input_key, input_value): """ mapper方法 :param input_key: 網頁名,如 A :param input_value: self.graph[input_key],即這個網頁的相關信息 :return: [(網頁名, 0.0), (出鏈網頁1, 出鏈網頁1分得的PR值), (出鏈網頁2, 出鏈網頁2分得的PR值)...] """ return [(input_key, 0.0)] + [(out_link, input_value[0] / input_value[1]) for out_link in input_value[2]] def pr_reducer_inter(self, intermediate_key, intermediate_value_list, dp): """ reducer方法 :param intermediate_key: 網頁名,如 A :param intermediate_value_list: A全部分得的PR值的列表:[0.0,分得的PR值,分得的PR值...] :param dp: 全部懸掛網頁的PR值之和 :return: (網頁名,計算所得的PR值) """ return (intermediate_key, self.damping_factor * sum(intermediate_value_list) + self.damping_factor * dp / self.num_of_pages + (1.0 - self.damping_factor) / self.num_of_pages) def page_rank(self): """ 計算PR值,每次迭代都須要兩次調用MapReduce。一次是計算懸掛網頁PR值之和,一次 是計算全部網頁的PR值 :return: self.graph,其中的PR值已經計算好 """ iteration = 1 # 迭代次數 change = 1 # 記錄每輪迭代後的PR值變化狀況,初始值爲1保證至少有一次迭代 while change > self.min_delta: print("Iteration: " + str(iteration)) # 由於可能存在懸掛網頁,因此纔有下面這個dangling_list # dangling_list存放的是[全部懸掛網頁的PR值之和] # dp表示全部懸掛網頁的PR值之和 dangling_list = MapReduce.map_reduce(self.graph, self.ip_mapper, self.ip_reducer) if dangling_list: dp = dangling_list[0] else: dp = 0 # 由於MapReduce.map_reduce中要求的reducer只能有兩個參數,而咱們 # 須要傳3個參數(多了一個全部懸掛網頁的PR值之和,即dp),因此採用 # 下面的lambda表達式來達到目的 # new_pr爲一個列表,元素爲:(網頁名,計算所得的PR值) new_pr = MapReduce.map_reduce(self.graph, self.pr_mapper, lambda x, y: self.pr_reducer_inter(x, y, dp)) # 計算此輪PR值的變化狀況 change = sum([abs(new_pr[i][1] - self.graph[new_pr[i][0]][0]) for i in range(self.num_of_pages)]) print("Change: " + str(change)) # 更新PR值 for i in range(self.num_of_pages): self.graph[new_pr[i][0]][0] = new_pr[i][1] iteration += 1 return self.graph
最後是測試部分,我使用了python的digraph建立了一個有向圖,並調用上面的方法來計算PR值:
if __name__ == '__main__': dg = digraph() dg.add_nodes(["A", "B", "C", "D", "E"]) dg.add_edge(("A", "B")) dg.add_edge(("A", "C")) dg.add_edge(("A", "D")) dg.add_edge(("B", "D")) dg.add_edge(("C", "E")) dg.add_edge(("D", "E")) dg.add_edge(("B", "E")) dg.add_edge(("E", "A")) pr = PRMapReduce(dg) page_ranks = pr.page_rank() print("The final page rank is") for key, value in page_ranks.items(): print(key + " : ", value[0])
附上運行結果:
Iteration: 44 Change: 1.275194338951069e-05 Iteration: 45 Change: 1.0046004543212694e-05 Iteration: 46 Change: 7.15337406470562e-06 The final page rank is E : 0.3133376132128915 C : 0.11396289866948645 B : 0.11396289866948645 A : 0.2963400114149353 D : 0.1623965780332006
以上即是PageRank的MapReduce實現。代碼中的註釋較爲詳細,理解應該不難。
這是一個天才的算法,原理簡單但效果驚人。然而,PageRank算法仍是有一些弊端。
第一,沒有區分站內導航連接。不少網站的首頁都有不少對站內其餘頁面的連接,稱爲站內導航連接。這些連接與不一樣網站之間的連接相比,確定是後者更能體現PageRank值的傳遞關係。
第二,沒有過濾廣告連接和功能連接(例如常見的「分享到微博」)。這些連接一般沒有什麼實際價值,前者連接到廣告頁面,後者經常連接到某個社交網站首頁。
第三,對新網頁不友好。一個新網頁的通常入鏈相對較少,即便它的內容的質量很高,要成爲一個高PR值的頁面仍須要很長時間的推廣。
針對PageRank算法的缺點,有人提出了TrustRank算法。其最初來自於2004年斯坦福大學和雅虎的一項聯合研究,用來檢測垃圾網站。TrustRank算法的工做原理:先人工去識別高質量的頁面(即「種子」頁面),那麼由「種子」頁面指向的頁面也多是高質量頁面,即其TR值也高,與「種子」頁面的連接越遠,頁面的TR值越低。「種子」頁面可選出鏈數較多的網頁,也可選PR值較高的網站。
TrustRank算法給出每一個網頁的TR值。將PR值與TR值結合起來,能夠更準確地判斷網頁的重要性。
谷歌用PR值來劃分網頁的等級,有0~10級,通常4級以上的都是比較好的網頁了。谷歌本身PR值爲9,百度也是9,博客園的PR值則爲6。
現在PR值雖不如之前重要了(沒有區分頁面內的導航連接、廣告連接和功能連接致使PR值自己可以反映出的網頁價值不精確,而且對新網頁不友好),可是流量交易裏PR值仍是個很重要的參考因素。
最後,有一個圖形化網站提供PageRank過程的動態圖:點我。
1:《這就是搜索引擎:核心技術詳解》,張俊林
2:當年PageRank誕生的論文:The PageRank Citation Ranking: Bringing Order to the Web
3:維基百科PageRank
5:博客《谷歌背後的數學》,盧昌海
6:博客PageRank背後的數學