用SQL来简单实现PageRank算法

1. 算法简介

PageRank里的Page其实来自于创始人Larry Page之姓,碰巧算法的目标就是给各种网页(page)来做一个排名,从而得到哪些网页该排在前面展示,不得不说科技圈的人真的都喜欢玩双关Punchline,试想一下,如果谷歌的创始人姓金的话,这个算法现在可能得叫”国王排名(King Rank)“算法了hhh。

1.1 术语

1. 节点(node), 指的是图(graph)上的单个点,在谷歌的网页排名里节点可以看做是一个网页
2. PR值(Page Rank value),指某个节点的得分权重,值越高,其重要性越大,在网页中排名越靠前,相应地就会显示在搜索结果的前几位
3. 阻尼系数d(damping factor),指从当前节点到其指向的下一个节点的衰减系数,后面算法流程中会讲到
4. 槽节点(sink node),指没有出度只有入度的节点,其不指向任何其他节点

1.2 算法过程

还是以谷歌的网页排名为例,每个网页的PR值可以看做是上网者停留在该网页的概率值,那么在上网冲浪的过程中,他访问的下一个网站可能是通过以下两种途径:

  1. 从当前网站的链接(link)指向的下一个网页
  2. 任何其他途径调到任意某个网页

那么这两种途径分别是怎么计算呢,这时候阻尼系数d就派上用场了,d可以认为是从当前网页跳转到它指向的下一个网站的概率,那么假设要计算的节点为j,第一种途径得到的PR值贡献为 P R j 1 = d × ∑ k ∈ n o d e s   p o i n t i n g   n o d e j P R o l d ( n o d e k ) N o .   o f   o u t g o i n g   l i n k s   o f   n o d e k PR_{j1} = d \times \sum_{k\in{nodes\space pointing\space node_j}}{\frac {PR_{old}(node_k)} {No.\space of\space outgoing\space links\space of\space node_k}} PRj1=d×knodes pointing nodejNo. of outgoing links of nodekPRold(nodek)

即所有指向j节点的节点k们的PR值的贡献和乘以一个阻尼系数,而第二种途径的PR值就比较容易计算了 P R j 2 = ( 1 − d ) × 1 n PR_{j2} = (1 - d) \times \frac 1 n PRj2=(1d)×n1

其中n为总的节点数量。而要求的节点j的值为两部分之和 P R j = P R j 1 + P R j 2 PR_j = PR_{j1} + PR_{j2} PRj=PRj1+PRj2

对每一个节点使用这样的方法计算,来更新其PR值直到收敛。

下图1是一个比较简单的例子直观的来看如何计算PR值
图1

2. 常见问题

2.1 算法何时收敛

本文的实现采用迭代法,把每次更新前后的PR值求和,小于某个设定值( ϵ = 0.01 \epsilon = 0.01 ϵ=0.01)即停止迭代,公示表示如下 ∑ j ∣ P R n e w ( n o d e j ) − P R o l d ( n o d e j ) ∣ ≤ ϵ \sum _j {|{PR_{new}{(node_j)} - PR_{old}(node_j)}| \le \epsilon} jPRnew(nodej)PRold(nodej)ϵ

2.2 槽(sink)节点

槽节点指没有出度的节点,它不指向任何节点,这样会导致一个问题就是所有的PR值加起来小于1,并且最终所有的PR值的和趋向于0,如下图一个简单的例子

图2

解决方案是给每一个槽节点增加通向所有其他节点(包括自身),如上图1中的情况,E节点就是一个槽节点,给其他节点增加一条边的结果如下图所示
在这里插入图片描述

2.3 初始PR值怎么设置

每个节点的初始值一样,为 P R i n i t = 1 n u m b e r   o f   n o d e s PR_{init} = \frac 1 {number\space of\space nodes} PRinit=number of nodes1

3. 算法实现

网上有不少用其他语言实现PageRank算法的案例,这里就不重复了,放一个本人用SQL的实现

首先数据来源的DDL如下

/* 节点表,主要是一个id, 和一个title */
CREATE TABLE node
(
	paperid INTEGER NOT NULL,
  papertitle VARCHAR(100) NOT NULL,
  PRIMARY KEY (paperid)
);
/* 边表,起始边为paperid, 终边为citedpaperid */
CREATE TABLE edge
(
	paperid INTEGER NOT NULL,
  citedpaperid INTEGER NOT NULL,
 	PRIMARY KEY (paperid)
);

接下来开始实现

创建一个page_rank表以及一个临时t_page_rank表

/* create a page_rank table, and a temprary page_rank table for updating the old one */
DROP TABLE IF EXISTS page_rank;
CREATE TABLE page_rank
(
    paperid INTEGER NOT NULL,
    pr_val FLOAT NOT NULL,
    out_links INTEGER NOT NULL,
    PRIMARY KEY (paperid)
);
DROP TABLE IF EXISTS t_page_rank;
CREATE TABLE t_page_rank
(
    paperid INTEGER NOT NULL,
    n_pr_val FLOAT NOT NULL
);

初始化page_rank表,设置每个节点的初始值为1/n

/* init page_rank table, set each node the page_rank_value as 1 / (number of nodes) */
CREATE OR REPLACE FUNCTION
init_page_rank()
RETURNS VOID AS
$$
DECLARE
    row_data RECORD;
    cnt INTEGER;
    init_pr FLOAT;
BEGIN
    init_pr = 1.0 / (select count(*) from node);
    TRUNCATE TABLE page_rank;
    FOR row_data in SELECT * FROM node LOOP
        cnt = (select count(*) from edge where edge.paperid = row_data.paperid);
        INSERT INTO page_rank(paperid, pr_val, out_links)
        VALUES (row_data.paperid, init_pr, cnt);
    END LOOP;
    raise notice 'init page rank table success';
END;
$$
LANGUAGE plpgsql;

计算某个节点的新PR值,有两种思路,

  1. 给所有的sink节点的新边都加入到edge表中,这种方法适合node节点不多且sink节点也不多的情况
  2. 把所有的sink节点单独计算其PR值的贡献,为 ∑ s i n k _ n o d e P R o l d n \sum_{sink\_node} \frac {PR_{old}} {n} sink_nodenPRold

这里用的是第二种思路

/* a function to calculate the new pr_val of one particular node */
/* pid denotes the paperid of the node, sink_donation denotes the page_rank_value donation from sink nodes */
/* ret_val_1 denotes the first part of new page_rank_value,
   ret_val_2 denotes the second part which is from the incoming nodes
*/
CREATE OR REPLACE FUNCTION
calculate_one(pid INTEGER, sink_donation FLOAT)
RETURNS FLOAT AS 
$$
DECLARE
    dprob FLOAT;
    node_cnt INTEGER;
    ret_val_1 FLOAT;
    ret_val_2 FLOAT;
BEGIN
    dprob = 0.85;
    node_cnt = (select count(*) from node);
    ret_val_1 = (1 - dprob) / node_cnt;
    ret_val_2 = (SELECT SUM(out2.donation) 
                    FROM (
                        SELECT (pr.pr_val / pr.out_links) as donation
                        FROM page_rank as pr
                        WHERE pr.paperid 
                        IN (select edge.paperid FROM edge WHERE edge.citedpaperid = pid)
                    ) as out2);
    IF (select ret_val_2) is NULL THEN
        ret_val_2 = 0;
    END IF;
    RETURN ret_val_1 + (ret_val_2 + sink_donation) * dprob;
END;
$$
LANGUAGE plpgsql;

更新所有节点的PR值

/* the function for unpdating the page_rank_value for all nodes */
/* delta denotes the sum of the new-old pr_value of each nodes 
   epislon denotes the threshold whether to update page_rank table
*/
CREATE OR REPLACE FUNCTION
update_page_rank()
RETURNS FLOAT AS
$$
DECLARE
    delta FLOAT;
    row_data RECORD;
    new_pr_val FLOAT;
    epislon FLOAT;
    sink_donation FLOAT;
BEGIN
    delta = 0;
    epislon = 0.01;
    sink_donation = 0;
    /* sink_donation is the pr_value of sink nodes all add up and divided by the counts */
    sink_donation = (SELECT sum(page_rank.pr_val) from page_rank where page_rank.out_links = 0);
    sink_donation = sink_donation / (select count(*) from node);
    FOR row_data in select * from page_rank LOOP
        new_pr_val = calculate_one(row_data.paperid, sink_donation);
        INSERT INTO t_page_rank(paperid, n_pr_val)
            VALUES (row_data.paperid, new_pr_val);
        delta = delta + ABS(new_pr_val - row_data.pr_val);
        
    END LOOP;
    raise notice 'delta %', delta;
    IF delta > epislon THEN
        raise notice 'before update';
        FOR row_data in select * from page_rank LOOP
            UPDATE page_rank
            SET pr_val = 
                        (select n_pr_val from t_page_rank where t_page_rank.paperid = row_data.paperid)
            WHERE paperid = row_data.paperid;
        END LOOP;
        raise notice 'after update';
    END IF;
    TRUNCATE TABLE t_page_rank;
    RETURN delta;
END;
$$
LANGUAGE plpgsql;

开始迭代

/* the main iteration function, no input and output */
CREATE OR REPLACE FUNCTION
begin_iteration()
RETURNS VOID AS
$$
DECLARE
    epislon FLOAT;
    delta FLOAT;
    iterator INTEGER;
BEGIN
    epislon = 0.01;
    iterator = 0;
    LOOP 
        delta = update_page_rank();
        IF delta < epislon THEN
            EXIT;
        ELSE
            raise notice 'iterator %, delta %', iterator, delta;
        END IF;
        iterator = iterator + 1;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

上面所有的过程都用function的形式写出来,所以最终的主流程可以很简洁,如下

/* main process */
SELECT * FROM init_page_rank();
SELECT * FROM begin_iteration(); 

/* show result */
SELECT a.paperid, a.papertitle, b.pr_val as pv
FROM node as a
INNER JOIN page_rank as b
ON a.paperid = b.paperid
ORDER BY pv DESC
LIMIT 10;

写个简单的测试用例试一下

TRUNCATE TABLE node;
TRUNCATE TABLE edge;

/* test checked and answer is same as expected */
INSERT INTO node(paperid, papertitle) VALUES
(1, 'aaa'),
(2, 'bbb'),
(3, 'ccc'),
(4, 'ddd'),
(5, 'eee');
INSERT INTO edge(paperid, citedpaperid) VALUES
(1, 2),
(1, 3),
(1, 4),
(2, 3),
(3, 5),
(4, 3),
(4, 5);

SELECT * FROM init_page_rank();
SELECT * FROM begin_iteration();
/* show the result */
SELECT a.paperid, a.papertitle, b.pr_val as pv
FROM node as a
INNER JOIN page_rank as b
ON a.paperid = b.paperid
ORDER BY pv DESC
LIMIT 10;

结果如下
图4

4. 总结

迭代算法的速度还是略慢,单机测试10k数量级的node更新一次page_rank需要10秒左右,后续考虑用矩阵运算的方式提提速

猜你喜欢

转载自blog.csdn.net/Mint2yx4/article/details/125032918