PageRank算法的定義與來源、以及PageRank算法原理


一、PageRank算法定義

PageRank,網頁排名,又稱網頁級別、Google左側排名或佩奇排名,是一種由根據網頁之間相互的超鏈接計算的技術,而作為網頁排名的要素之一,以Google公司創辦人拉里·佩奇(Larry Page)之姓來命名。Google用它來體現網頁的相關性和重要性,在搜索引擎優化操作中是經常被用來評估網頁優化的成效因素之一。Google的創始人拉里·佩奇和謝爾蓋·布林於1998年在斯坦福大學發明了這項技術。

PageRank通過網絡浩瀚的超鏈接關系來確定一個頁面的等級。Google把從A頁面到B頁面的鏈接解釋為A頁面給B頁面投票,Google根據投票來源(甚至來源的來源,即鏈接到A頁面的頁面)和投票目標的等級來決定新的等級。簡單的說,一個高等級的頁面可以使其他低等級頁面的等級提升。

 

二、PageRank算法來源

這個要從搜索引擎的發展講起。最早的搜索引擎采用的是 分類目錄[^ref_1] 的方法,即通過人工進行網頁分類並整理出高質量的網站。那時 Yahoo 和國內的 hao123 就是使用的這種方法。

后來網頁越來越多,人工分類已經不現實了。搜索引擎進入了 文本檢索 的時代,即計算用戶查詢關鍵詞與網頁內容的相關程度來返回搜索結果。這種方法突破了數量的限制,但是搜索結果不是很好。因為總有某些網頁來回地倒騰某些關鍵詞使自己的搜索排名靠前。

於是我們的主角要登場了。沒錯,谷歌的兩位創始人,當時還是美國斯坦福大學 (Stanford University) 研究生的佩奇 (Larry Page) 和布林 (Sergey Brin) 開始了對網頁排序問題的研究。他們的借鑒了學術界評判學術論文重要性的通用方法, 那就是看論文的引用次數。由此想到網頁的重要性也可以根據這種方法來評價。於是PageRank的核心思想就誕生了[^ref_2],非常簡單:

1.如果一個網頁被很多其他網頁鏈接到的話說明這個網頁比較重要,也就是PageRank值會相對較高 
2.如果一個PageRank值很高的網頁鏈接到一個其他的網頁,那么被鏈接到的網頁的PageRank值會相應地因此而提高

三、搜索引擎

搜索引擎的功能主要是:根據用戶輸入的關鍵字,返回文檔的鏈接結果。 
搜索引擎主要解決的三大問題:(1)如何獲取文檔資料 (2)如何根據關鍵詞檢索到相關文檔 (3)如何對文檔進行排序,返回給用戶滿意的頁面。

1.獲取文檔資料

利用爬蟲程序,獲取互聯網的頁面資料。爬蟲技術先從一個網頁出發,將該網頁的內容記錄下來,保存到資料庫,接着分析頁面中的超鏈接,分別遞歸得去獲取超鏈接頁面。

2.如何根據關鍵詞檢索到相關文檔

采用倒排索引算法。簡單的說,倒排索引是一對key-value對,key代表關鍵詞,value代表擁有這些關鍵詞的文檔編號或者url。

3.文檔排序

這是搜索引擎最核心的問題,也是google發家致富的法寶 – PageRank算法。

 

四、PageRank算法原理

PageRank算法[^ref_3]總的來說就是預先給每個網頁一個PR值(下面用PR值指代PageRank值),由於PR值物理意義上為一個網頁被訪問概率,所以一般是1N1N,其中N為網頁總數。另外,一般情況下,所有網頁的PR值的總和為1。如果不為1的話也不是不行,最后算出來的不同網頁之間PR值的大小關系仍然是正確的,只是不能直接地反映概率了。

預先給定PR值后,通過下面的算法不斷迭代,直至達到平穩分布為止。

互聯網中的眾多網頁可以看作一個有向圖。下圖是一個簡單的例子[^ref_4]:

這時A的PR值就可以表示為:PR(A)=PR(B)+PR(C)

 

 

 

然而圖中除了C之外,B和D都不止有一條出鏈,所以上面的計算式並不准確。想象一個用戶現在在瀏覽B網頁,那么下一步他打開A網頁還是D網頁在統計上應該是相同概率的。所以A的PR值應該表述為:PR(A)=PR(B)/2+PR(C)/1

 

 

 

互聯網中不乏一些沒有出鏈的網頁,如下圖:

圖中的C網頁沒有出鏈,對其他網頁沒有PR值的貢獻,我們不喜歡這種自私的網頁(其實是為了滿足 Markov 鏈的收斂性),於是設定其對所有的網頁(包括它自己)都有出鏈,則此圖中A的PR值可表示為:PR(A)=PR(B)/2+PR(C)/4

 

 

 

然而我們再考慮一種情況:互聯網中一個網頁只有對自己的出鏈,或者幾個網頁的出鏈形成一個循環圈。那么在不斷地迭代過程中,這一個或幾個網頁的PR值將只增不減,顯然不合理。如下圖中的C網頁就是剛剛說的只有對自己的出鏈的網頁:

為了解決這個問題。我們想象一個隨機瀏覽網頁的人,當他到達C網頁后,顯然不會傻傻地一直被C網頁的小把戲困住。我們假定他有一個確定的概率會輸入網址直接跳轉到一個隨機的網頁,並且跳轉到每個網頁的概率是一樣的。於是則此圖中A的PR值可表示為:PR(A)=α*(PR(B)/2+(1−α)/4

 

 

 

在一般情況下,一個網頁的PR值計算如下:

 

 

其中MpiMpi是所有對pipi網頁有出鏈的網頁集合,L(pj)L(pj)是網頁pjpj的出鏈數目,NN是網頁總數,αα一般取0.85。
根據上面的公式,我們可以計算每個網頁的PR值,在不斷迭代趨於平穩的時候,即為最終結果。具體怎樣算是趨於平穩,我們在下面的PR值計算方法部分再做解釋。

 

五、算法實現

1 基於迭代法的簡單實現

用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)

虎課網https://www.wode007.com/sites/73267.html 設計塢https://www.wode007.com/sites/73738.html

運行結果:

finished in 36 iterations!
The final page rank is
{'A': 0.2963453309000821, 'C': 0.11396451042168992, 'B': 0.11396451042168992, 'E': 0.31334518664434013, 'D': 0.16239975107315852}

  

程序中給出的網頁之間的關系一開始如下:

迭代結束后如下:

2 MapReduce實現

作為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算法還是有一些弊端。

第一,沒有區分站內導航鏈接。很多網站的首頁都有很多對站內其他頁面的鏈接,稱為站內導航鏈接。這些鏈接與不同網站之間的鏈接相比,肯定是后者更能體現PageRank值的傳遞關系。

第二,沒有過濾廣告鏈接和功能鏈接(例如常見的“分享到微博”)。這些鏈接通常沒有什么實際價值,前者鏈接到廣告頁面,后者常常鏈接到某個社交網站首頁。

第三,對新網頁不友好。一個新網頁的一般入鏈相對較少,即使它的內容的質量很高,要成為一個高PR值的頁面仍需要很長時間的推廣。

針對PageRank算法的缺點,有人提出了TrustRank算法。其最初來自於2004年斯坦福大學和雅虎的一項聯合研究,用來檢測垃圾網站。TrustRank算法的工作原理:先人工去識別高質量的頁面(即“種子”頁面),那么由“種子”頁面指向的頁面也可能是高質量頁面,即其TR值也高,與“種子”頁面的鏈接越遠,頁面的TR值越低。“種子”頁面可選出鏈數較多的網頁,也可選PR值較高的網站。

TrustRank算法給出每個網頁的TR值。將PR值與TR值結合起來,可以更准確地判斷網頁的重要性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM