跳轉到

8.3 PageRank與網頁搜尋#

概念說明#

TF-IDF 衡量「這份文件與查詢多相關」,但網路上有億萬份相關文件,哪個更重要?1998 年,Larry Page 與 Sergey Brin 提出 PageRank:一個網頁的重要性,由「連向它的網頁的重要性」決定——重要網頁投出的票更值錢。這把整個網頁連結圖變成一個遞迴定義的聲譽系統,也是 Google 起家的核心專利思想。

隨機遊走模型給了它漂亮的機率解釋:想像一個讀者在網頁圖上隨機漫遊——沿出邊等機率跳轉,偶爾(機率 1−d,d 稱為阻尼係數,常取 0.85)傳送到任一隨機頁面。長期而言,讀者停留在各頁面的穩定機率分布就是 PageRank 值。數學上這是馬可夫鏈的平穩分布,求解方法有二:解線性方程組,或冪迭代(power iteration)——反覆用轉移矩陣乘向量直到收斂。後者因可稀疏計算而成為實務標準。

形式化:設圖鄰接矩陣 M(列 = 目標頁、行 = 源頁,各行按出度正規化),PageRank 向量 r 滿足

r = d·M·r + (1−d)/N·𝟙

迭代的直觀意義:每輪,每個頁面把自己的分數均分給出邊(投票);分數也會沿懸空節點(無出邊的頁面)流失,故補上傳送項 (1−d)/N 保證總分守恆為 1,且矩陣為正 → 收斂到唯一平穩分布(Perron-Frobenius 定理保證)。

應用遠超網頁:學術引用(PageRank 式引用權重)、社交網路影響力、推薦系統(item-item 圖)、蛋白質交互網路、交通流量。變體 Personalized PageRank(傳送偏向特定節點)是「找相似使用者/相似頁」的基礎,也是圖神經網路之前的經典圖演算法。

在搜尋引擎中的角色:PageRank 是查詢無關的預計算訊號(快取在索引裡),查詢時與 TF-IDF/BM25 等查詢相關訊號合成最終排名——「相關性」與「重要性」正交互補。現代 Google 已有數百個排名訊號(連結品質、使用者行為、BERT 語義理解),PageRank 仍是其中經典的一項。

演算法步驟(冪迭代)#

  1. 收集連結圖,建出邊表;處理懸空節點(無出邊):視為連向所有頁面(或用隨機傳送項吸收)。
  2. 初始化 r⁰ = 1/N。
  3. 每輪:r^{t+1}[v] = (1−d)/N + d · Σ_{u→v} r^t[u]/outdeg(u);懸空節點的分數均攤到全體。
  4. 檢查收斂:‖r^{t+1} − r^t‖₁ < ε(如 1e−8)即停止;否則繼續(實務 20–50 輪收斂)。
  5. 正規化(總和為 1),依值排序即為重要性排名。

Python 程式碼範例#

import numpy as np

links = {  # 玩具網頁圖:頁 -> 出邊
    "首頁":   ["演算法", "資料結構"],
    "演算法": ["首頁", "搜尋引擎", "排序"],
    "資料結構": ["首頁", "排序"],
    "排序":   ["演算法"],
    "搜尋引擎": ["演算法", "首頁"],
}
pages = list(links)
N = len(pages)
idx = {p: i for i, p in enumerate(pages)}
d = 0.85

def pagerank(links, d=0.85, eps=1e-9, max_iter=100):
    N = len(links)
    out = {p: len(links[p]) for p in links}
    dangling = [p for p in links if out[p] == 0]
    r = np.full(N, 1 / N)
    for it in range(max_iter):
        nxt = np.full(N, (1 - d) / N)
        for p in links:
            if out[p]:
                share = d * r[idx[p]] / out[p]
                for q in links[p]:
                    nxt[idx[q]] += share
        total_dangling = sum(r[idx[p]] for p in dangling)
        nxt += d * total_dangling / N
        if np.abs(nxt - r).sum() < eps:
            r = nxt; break
        r = nxt
    return {p: round(r[idx[p]], 4) for p in pages}

pr = pagerank(links)
for p, v in sorted(pr.items(), key=lambda x: -x[1]):
    print(f"{p:8s} {v:.4f}")
assert abs(sum(pr.values()) - 1) < 1e-3   # 總分守恆
# 「演算法」被最多人指且票源強 => PageRank 最高

# 驗證收斂輪數
def iters(links, d=0.85):
    r = np.full(len(links), 1 / len(links)); out = {p: len(links[p]) for p in links}
    for t in range(1, 101):
        nxt = np.full(len(links), (1 - d) / len(links))
        for p in links:
            for q in links[p]:
                nxt[idx[q]] += d * r[idx[p]] / out[p]
        if np.abs(nxt - r).sum() < 1e-9: return t
        r = nxt
print("收斂輪數:", iters(links))

複雜度分析#

  • 冪迭代:每輪 O(|邊數| + N)(稀疏遍歷),收斂輪數 O(log(N/ε)/log(1/d))——d=0.85 時每輪誤差乘 0.85,約 100 輪可達 1e−7;總計近線性於圖規模,這是它能跑在整個網頁圖上的原因。
  • 矩陣法:稠密矩陣每輪 O(N²) 不可行;實務用稀疏矩陣或 MapReduce 分塊。
  • 空間:O(|邊數|)+分數向量 O(N);真正的網頁圖要分塊流式處理。
  • 與其他訊號合成的複雜度:PageRank 預計算、查詢時 O(1) 查表——重要性訊號的「離線貴、線上免費」正是搜尋引擎的設計美學。

練習#

  1. 把圖改成「鏈狀 A→B→C→D→A」與「星狀中心指向四葉」,分別算 PageRank,驗證「被重要頁面指一次」勝過「被無名頁面指多次」。
  2. 實作 Personalized PageRank:傳送項改為偏向特定節點(如只在「演算法」重啟),輸出與查詢詞語義相近頁面的局部重要性,解釋它如何服務「相似頁推薦」。
  3. 在 1 萬節點的隨機圖(networkx.gnp_random_graph)上,比較你的冪迭代與 networkx.pagerank 的結果與收斂輪數;再調整 d = 0.5、0.9、0.99,觀察收斂速度與分數集中度的變化並解釋原因。