杰瑞科技汇

Python如何实现PageRank算法?

PageRank 算法核心思想

PageRank 的核心思想是:一个网页的重要性(排名)取决于指向它的其他网页的数量和质量。

Python如何实现PageRank算法?-图1
(图片来源网络,侵删)
  • 数量:指向一个网页的链接越多,这个网页就越重要。
  • 质量:一个由高排名网页指向的链接,比一个由低排名网页指向的链接更有价值。

这形成了一个递归的定义:一个网页的排名取决于其他网页的排名,为了解决这个问题,PageRank 使用了迭代算法,通过多次迭代,让排名值逐渐收敛到一个稳定状态。

PageRank 公式

PageRank 的经典公式如下:

$$PR(A) = (1-d) + d \left( \frac{PR(T_1)}{C(T_1)} + \frac{PR(T_2)}{C(T_2)} + \dots + \frac{PR(T_n)}{C(T_n)} \right)$$

  • $PR(A)$ 是页面 A 的 PageRank 值。
  • $PR(T_1)$ 到 $PR(T_n)$ 是所有指向页面 A 的页面($T_1, T_2, \dots, T_n$)的 PageRank 值。
  • $C(T_i)$ 是页面 $T_i$ 的出链数量(即它链接出去的页面总数)。
  • $d$ 是阻尼系数,通常取值为 0.85,它表示用户有 $d$ 的概率会沿着链接点击,而有 $(1-d)$ 的概率会随机跳转到任意一个页面(避免了“陷阱”问题)。

Python 从零实现 PageRank

我们将使用最基础的数据结构——字典和列表——来实现一个简单的 PageRank 算法。

Python如何实现PageRank算法?-图2
(图片来源网络,侵删)

步骤 1: 定义网络结构

我们需要一个数据结构来表示网页之间的链接关系,一个字典非常适合这个任务,其中键是源页面,值是它指向的目标页面列表。

# 定义一个简单的网页链接图
# 键是源页面,值是它链接到的目标页面列表
graph = {
    'A': ['B', 'C'],
    'B': ['C'],
    'C': ['A'],
    'D': ['C'],
    'E': ['A', 'B', 'C', 'D']
}

步骤 2: 准备数据

我们需要计算每个页面的出链数量,这在公式中是分母。

# 计算每个页面的出链数量
out_links = {page: len(links) for page, links in graph.items()}
# 获取所有页面的集合
all_pages = list(graph.keys())
num_pages = len(all_pages)
print("所有页面:", all_pages)
print("出链数量:", out_links)

步骤 3: 实现迭代算法

我们将使用一个循环来不断更新每个页面的 PageRank 值,直到所有页面的 PageRank 值变化非常小(收敛)为止。

def simple_pagerank(graph, damping_factor=0.85, epsilon=1e-6, max_iterations=100):
    """
    一个简单的 PageRank 实现。
    :param graph: 字典,表示网页链接图。
    :param damping_factor: 阻尼系数 d。
    :param epsilon: 收敛阈值,当所有节点的 PR 值变化小于此值时停止迭代。
    :param max_iterations: 最大迭代次数,防止无限循环。
    :return: 字典,包含每个页面的最终 PageRank 值。
    """
    # 1. 初始化
    pages = list(graph.keys())
    num_pages = len(pages)
    # 初始时,所有页面的 PR 值相等
    pagerank = {page: 1.0 / num_pages for page in pages}
    # 计算每个页面的出链数量
    out_links = {page: len(links) for page, links in graph.items()}
    # 2. 迭代计算
    for i in range(max_iterations):
        new_pagerank = {}
        converged = True
        # 计算每个页面的新 PR 值
        for page in pages:
            # 第一部分:随机跳转的概率 (1-d)
            rank_sum = (1 - damping_factor) / num_pages
            # 第二部分:来自所有入链页面的贡献
            # 遍历所有页面,看它们是否链接到当前页面
            for source_page, links in graph.items():
                if page in links:
                    # source_page 有出链,则贡献其 PR 值的一部分
                    if out_links[source_page] > 0:
                        rank_sum += damping_factor * pagerank[source_page] / out_links[source_page]
            new_pagerank[page] = rank_sum
            # 检查是否收敛
            if abs(new_pagerank[page] - pagerank[page]) > epsilon:
                converged = False
        # 更新 PR 值
        pagerank = new_pagerank
        # 如果已经收敛,则提前结束
        if converged:
            print(f"在第 {i+1} 次迭代后收敛。")
            break
    return pagerank
# --- 运行 ---
final_pagerank = simple_pagerank(graph)
print("\n最终的 PageRank 值:")
for page, rank in sorted(final_pagerank.items(), key=lambda item: item[1], reverse=True):
    print(f"页面 {page}: {rank:.4f}")

代码解释

  1. 初始化:所有页面的初始 PageRank 值设为 1/N,N 是页面总数。
  2. 迭代循环:我们进行最多 max_iterations 次迭代。
  3. 计算新 PR 值:对于每个页面 page
    • 随机跳转部分(1-d)/N 是一个基础值,确保即使没有页面指向它,它也能获得一定的排名。
    • 链接贡献部分:遍历整个图,找到所有指向 page 的页面 source_page,对于每一个 source_page,将其当前的 PR 值 pagerank[source_page] 除以其出链数 out_links[source_page],再乘以阻尼系数 d,累加到 rank_sum 中。
  4. 收敛检查:计算完所有页面的新 PR 值后,检查每个页面的新旧值之差是否都小于 epsilon,如果是,说明算法已经收敛,可以提前结束循环。
  5. 更新:将新计算出的 new_pagerank 赋值给 pagerank,为下一次迭代做准备。

优化实现:使用矩阵运算 (NumPy)

上面的实现虽然直观,但对于大规模网络效率低下,我们可以利用线性代数和 NumPy 来优化,因为 PageRank 本质上是一个矩阵-向量乘法问题。

Python如何实现PageRank算法?-图3
(图片来源网络,侵删)

网络的矩阵表示

我们可以用邻接矩阵 $M$ 来表示图。$M_{ij} = 1$ 表示页面 $j$ 链接到页面 $i$,否则为 0。

为了处理出链页面,我们将 $M$ 的每一列除以其列和(即对应页面的出链数),得到转移矩阵 $G$,如果某页面没有出链(称为“悬挂节点”),我们将其对应列的所有元素设为 1/N,模拟随机跳转。

优化后的代码

import numpy as np
def matrix_pagerank(graph, damping_factor=0.85, epsilon=1e-6, max_iterations=100):
    """
    使用 NumPy 矩阵运算实现的 PageRank 算法。
    :param graph: 字典,表示网页链接图。
    :param damping_factor: 阻尼系数 d。
    :param epsilon: 收敛阈值。
    :param max_iterations: 最大迭代次数。
    :return: NumPy 数组,包含每个页面的最终 PageRank 值。
    """
    pages = list(graph.keys())
    num_pages = len(pages)
    page_index = {page: i for i, page in enumerate(pages)}
    # 1. 构建邻接矩阵 M
    M = np.zeros((num_pages, num_pages))
    for source, targets in graph.items():
        for target in targets:
            M[page_index[target], page_index[source]] = 1
    # 2. 构建转移矩阵 G
    # 计算每个页面的出链数
    out_degree = np.sum(M, axis=0)
    # 处理悬挂节点 (出链数为0的页面)
    # 将这些页面的列替换为 1/N
    dangling_nodes = out_degree == 0
    G = np.zeros_like(M, dtype=float)
    for j in range(num_pages):
        if dangling_nodes[j]:
            G[:, j] = 1.0 / num_pages
        else:
            G[:, j] = M[:, j] / out_degree[j]
    # 3. 构建 PageRank 矩阵 A
    # A = d * G + (1-d) * E
    # E 是一个所有元素都为 1/N 的矩阵
    E = np.ones((num_pages, num_pages)) / num_pages
    A = damping_factor * G + (1 - damping_factor) * E
    # 4. 迭代计算
    # 初始化 PR 向量 v
    v = np.ones(num_pages) / num_pages
    for i in range(max_iterations):
        v_new = A @ v  # 矩阵-向量乘法
        if np.linalg.norm(v_new - v, 1) < epsilon: # L1 范数检查收敛
            print(f"在第 {i+1} 次迭代后收敛。")
            break
        v = v_new
    return v
# --- 运行 ---
final_pagerank_matrix = matrix_pagerank(graph)
print("\n使用 NumPy 矩阵运算的最终 PageRank 值:")
for i, rank in enumerate(final_pagerank_matrix):
    print(f"页面 {list(graph.keys())[i]}: {rank:.4f}")

优势

  • 速度:NumPy 的底层是 C 语言实现的,矩阵运算比 Python 循环快几个数量级,尤其适合大规模数据。
  • 简洁A @ v 一行代码就完成了复杂的求和计算,代码更简洁,更符合数学思想。

使用专业库 networkx

在实际项目中,我们通常会使用成熟的图计算库,如 networkx,它不仅内置了 PageRank 算法,还提供了丰富的图分析功能。

安装 networkx

pip install networkx

networkx 实现

import networkx as nx
# 1. 创建一个有向图
G = nx.DiGraph()
# 2. 添加边 (链接关系)
# 从我们之前的 graph 字典添加
for source, targets in graph.items():
    for target in targets:
        G.add_edge(source, target)
# 3. 使用 networkx 的 pagerank 函数
# 它已经处理了所有细节,包括悬挂节点和收敛判断
nx_pagerank = nx.pagerank(G, alpha=0.85, max_iter=100, tol=1e-06)
print("\n使用 networkx 的最终 PageRank 值:")
for page, rank in sorted(nx_pagerank.items(), key=lambda item: item[1], reverse=True):
    print(f"页面 {page}: {rank:.4f}")
# 你还可以轻松地可视化这个图
# import matplotlib.pyplot as plt
# pos = nx.spring_layout(G) # 布局算法
# nx.draw(G, pos, with_labels=True, node_size=1000, node_color='skyblue', font_size=15, font_weight='bold')
# plt.show()

优势

  • 便捷:代码量最少,一行 nx.pagerank() 即可完成。
  • 健壮:库已经处理了各种边界情况(如悬挂节点、非连通图等)。
  • 功能强大:除了 PageRank,networkx 还提供了数百种其他图算法和分析工具。
方法 优点 缺点 适用场景
从零实现 (字典) 易于理解 PageRank 的基本原理 效率低,代码冗长 学习和教学目的
矩阵实现 (NumPy) 高效,代码简洁,符合数学思想 需要理解线性代数 中小规模网络,或需要自己实现核心逻辑时
专业库 (networkx) 极其便捷、健壮、功能强大 依赖外部库,对初学者来说像个“黑盒” 绝大多数实际项目和工程应用

对于学习和理解,建议从从零实现开始;对于性能有要求的自定义项目,NumPy 矩阵实现是很好的选择;而在实际工作中,直接使用 networkx 是最高效、最可靠的做法。

分享:
扫描分享到社交APP
上一篇
下一篇