情感分析学习笔记(4)——PageRank原理与python代码实现

PageRank(Page et al., 1998)最开始做出来并非是用于情感分析的,只不过我最近看到一个无监督的情感分析算法名叫PolarityRank(Cruz et al. 2011),这是基于PageRank的思想做的,所以在动手做PolarityRank之前先把PageRank给制作了。
本文不会过多的提起算法原理之类的内容,毕竟基本是搬运的其他大佬的文章,我会把参考链接放在文章中,本文主要讲解代码。

1 简介

PageRank最开始是用于计算网页的重要性的算法,其核心思想是:

  1. 如果一个重要的网页链接到其他网页,那么被链接的网页很重要;
  2. 如果一个网页被多个网页链接,链接的越多,那么该网页越重要;

就好比我们下载论文,除了会下载最新的论文,还会下载引用量很高的论文一样。

在整个网络中,每个网站有个体现其重要性的值,在PageRank中称作 P R PR PR值,整个算法传递的就是该 P R PR PR值。

2 算法原理

2.1 基本公式

根据上面的叙述,我们来用数学公式描述出来。由于有指向关系,所以在网络中是有向图。这里先放一张网络图,方便讲解:
网络图

第一个问题:得出初始 P R PR PR值。

  1. 如果某个网络中的重要程度可以获得,那么可以采用归一化等技术来获得初始 P R PR PR值(比如各位大哥关注我,然后在你关注列表中就有了我的链接嘿嘿嘿);
  2. 但是上面的重要程度我个人认为是很难得到的,所以我们不得不人为给定初始值,每个网站的初始值为 1 N \frac{1}{N} N1 N N N是网站数量。

接着是第二个问题:如何传递 P R PR PR值。传递公式如下:
P R ( i ) = ∑ j ∈ O i P R ( j ) n j PR(i)=\sum_{j \in O_i}\frac{PR(j)}{n_j} PR(i)=jOinjPR(j)

O i O_i Oi是节点 i i i的入度构成的集合, n j n_j nj是节点 j j j的出度。

我们以节点1进行分析,共有3个节点链接到了1(节点2,节点3,节点4),所以 O 1 = { 2 , 3 , 4 } O_1=\{2,3, 4\} O1={2,3,4}。节点2出度为1(因为只有一个出度)(ie. n = 1 n=1 n=1),节点3出度为2(链接到了1, 4)(ie. n = 2 n=2 n=2),节点4出度为1(ie. n = 1 n=1 n=1),所以节点1的 P R ( 1 ) PR(1) PR(1)计算公式为: P R ( 1 ) = P R ( 2 ) 1 + P R ( 3 ) 2 + P R ( 4 ) 1 PR(1)=\frac{PR(2)}{1}+\frac{PR(3)}{2}+\frac{PR(4)}{1} PR(1)=1PR(2)+2PR(3)+1PR(4)

这个公式的意思就是,假设我在网页3,我看到了2个链接,链接1与链接4,那么我就有 1 / 2 1/ 2 1/2的概率点击链接1, 1 / 2 1 / 2 1/2的链接点击链接4。

但是在实际工程应用中不会用这种循环的方法处理(因为会有大量的网站数据),而是会用numpy等高效的矩阵运算,矩阵运算的公式为:
P R ( t + 1 ) = P R ( t ) ∗ a d j PR^{(t+1)}=PR^{(t)} * adj PR(t+1)=PR(t)adj

这里 P R ( i + 1 ) PR^{(i+1)} PR(i+1)指的是第 i + 1 i+1 i+1次迭代的所有节点 P R PR PR值构成的向量, P R ( i ) PR^{(i)} PR(i)是第 i i i次迭代的所有节点 P R PR PR值构成的向量, a d j adj adj是邻接矩阵,不过是除以了出度的邻接矩阵

注:如果不能理解这个矩阵运算为何就可以得到上面公式结果的同学,可以自行手算一下这个矩阵运算,记得把邻接矩阵的每一行每一列是什么就行了。

2.2 排名泄露

排名泄露指的就是有节点没有出度,比如上图中的节点0,这样在运算中会导致最终收敛至0。比如计算上面的图,得到如下结果:

初始PR值:[0.2, 0.2, 0.2, 0.2, 0.2]
邻接矩阵:array([[0. , 0. , 0. , 0. , 0. ],
                [1. , 0. , 0. , 0. , 0. ],
                [0. , 1. , 0. , 0. , 0. ],
                [0. , 0.5, 0. , 0. , 0.5],
                [0. , 1. , 0. , 0. , 0. ]])

得到的结果:
第1次迭代的PR值:[0.2 0.5 0.  0.  0.1]2次迭代的PR值:[0.5 0.1 0.  0.  0. ]3次迭代的PR值:[0.1 0.  0.  0.  0. ]4次迭代的PR值:[0. 0. 0. 0. 0.]5次迭代的PR值:[0. 0. 0. 0. 0.]
迭代完成!
收敛值为: [0. 0. 0. 0. 0.]

为了解决这个问题,大佬们就提出了一个方法:如果一个节点没有出度,那么就设定这个节点对每个节点都有出边。

就是说上面的邻接矩阵变为了:

array([[0.2, 0.2, 0.2, 0.2, 0.2],
       [1. , 0. , 0. , 0. , 0. ],
       [0. , 1. , 0. , 0. , 0. ],
       [0. , 0.5, 0. , 0. , 0.5],
       [0. , 1. , 0. , 0. , 0. ]])

得到的结果为:

1次迭代的PR值:[0.24 0.54 0.04 0.04 0.14]2次迭代的PR值:[0.588 0.248 0.048 0.048 0.068]3次迭代的PR值:[0.3656 0.2576 0.1176 0.1176 0.1416]4次迭代的PR值:[0.33072 0.39112 0.07312 0.07312 0.13192]5次迭代的PR值:[0.457264 0.307744 0.066144 0.066144 0.102704]
...102次迭代的PR值:[0.4  0.32 0.08 0.08 0.12]
迭代完成!
收敛值为: [0.4  0.32 0.08 0.08 0.12]

排名泄露的形象化理解就是,我通过网页3,进入网页4,再进入网页1,最后达到网页0,然后我就找不到链接了,于是我忍痛关闭了Chrome。

2.3 排名下沉

如果一个节点没有入度,那么这个节点的 P R PR PR值最终会收敛至0。

排名下沉与排名泄露的区别就在于,排名下沉是某个节点没有入度,而排名泄露是某个节点没有出度。

比如我们把上面的图改进下:
网络图改进版
于是就会发现没有排名泄露问题,只有排名下沉问题,节点2与3没有入度,整个邻接矩阵变为:

array([[0. , 1. , 0. , 0. , 0. ],
       [1. , 0. , 0. , 0. , 0. ],
       [0. , 1. , 0. , 0. , 0. ],
       [0. , 0.5, 0. , 0. , 0.5],
       [0. , 1. , 0. , 0. , 0. ]])

运行结果为:

1次迭代的PR值:[0.2 0.7 0.  0.  0.1]2次迭代的PR值:[0.7 0.3 0.  0.  0. ]3次迭代的PR值:[0.3 0.7 0.  0.  0. ]4次迭代的PR值:[0.7 0.3 0.  0.  0. ]5次迭代的PR值:[0.3 0.7 0.  0.  0. ]6次迭代的PR值:[0.7 0.3 0.  0.  0. ]

然后我们发现这个人就无聊到了从0到1,再从1回到0……

这里形象化理解就是,我通过网页3进入到网页4,到达了网页1,然后就开始我的01010101之旅。因为没有入度,就是没有链接,我就回不去了。

2.4 排名上升

当有个网页链接到自己时,比如下图:
排名上升
那么这样就会导致所有网络资源都在0了,矩阵如下:

array([[1. , 0. , 0. , 0. , 0. ],
       [1. , 0. , 0. , 0. , 0. ],
       [0. , 1. , 0. , 0. , 0. ],
       [0. , 0.5, 0. , 0. , 0.5],
       [0. , 1. , 0. , 0. , 0. ]])

最后结果:

1次迭代的PR值:[1. 0. 0. 0. 0.]2次迭代的PR值:[1. 0. 0. 0. 0.]

于是大佬们又想了个办法,我看着网页看着看着就想要重新打开个网页,大佬们就给了个阻尼因子 α \alpha α,根据前人经验总结 α = 0.85 \alpha=0.85 α=0.85,就是说有 0.85 0.85 0.85的概率,这个B继续浏览这个网页,但是有 1 − α = 0.15 1-\alpha=0.15 1α=0.15的概率他直接打开一个新的网页,于是公式变为:
P R ( i ) = α ∑ j ∈ O i P R ( j ) n j + 1 − α N PR(i)=\alpha\sum_{j \in O_i}\frac{PR(j)}{n_j}+\frac{1-\alpha}{N} PR(i)=αjOinjPR(j)+N1α
用矩阵表示为:
P R ( t + 1 ) = α ∗ P R ( t ) ∗ a d j + 1 − α N ∗ E PR^{(t+1)}=\alpha * PR^{(t)} * adj + \frac{1-\alpha}{N} * E PR(t+1)=αPR(t)adj+N1αE
这里的 E E E是一个单位向量,用于向量化 1 − α N \frac{1-\alpha}{N} N1α

3 代码讲解

以上是理论,接下来是代码讲解。代码我放在的github上,github链接:https://github.com/Balding-Lee/pagerank,这里只讲解一些关键部分。

3.1 网络图生成

网络图我是使用的networkx生成的,代码如下:

def get_init_pr(dg):
    """
    获得每个节点的初始PR值

    :param dg: 有向图
    """
    nodes_num = dg.number_of_nodes()
    for node in dg.nodes:
        pr = 1 / nodes_num
        dg.add_nodes_from([node], pr=pr)


def create_network():
    """
    创建有向图

    :return dg: 有向图
    """
    dg = nx.DiGraph()  # 创建有向图
    dg.add_nodes_from(['0', '1', '2', '3', '4'])  # 添加节点
    dg.add_edges_from([('1', '0'), ('2', '1'), ('3', '4'), ('4', '1'), ('3', '1')])  # 添加边

    get_init_pr(dg)

    return dg

networkx的官方文档写的挺好的,链接如下:https://networkx.org/documentation/latest/tutorial.html

这里只需要说一句我在有向图中,每个节点存入了一个’pr’值,方便后面初始向量的生成。

3.2 邻接矩阵和初始向量生成

3.2.1 生成邻接矩阵

我的邻接矩阵是从有向图中生成的,代码如下:

def create_matrix(dg):
    """
    通过有向图生成邻接矩阵
    :param dg: 有向图
    :return adj_matrix: 邻接矩阵
    """
    node_num = dg.number_of_nodes()

    adj_matrix = np.zeros((node_num, node_num))

    for edge in dg.edges:
        adj_matrix[int(edge[0])][int(edge[1])] = 1

    solve_ranking_leaked(adj_matrix)
    calc_out_degree_ratio(adj_matrix)
    # print(adj_matrix)
    return adj_matrix

首先生成一个同款大小的0矩阵,然后将出度给改为1,接着有两个方法,第一个是解决排名泄露问题,第二个是将邻接矩阵给改为跳转的概率表达的邻接矩阵(我真不知道怎么表达这句话,就是把邻接矩阵的每个边给改为 1 n \frac{1}{n} n1)。

解决排名泄露:

def solve_ranking_leaked(adj_matrix):
    """
    解决排名泄露问题
    :param adj_matrix: 邻接矩阵
    """
    col_num = np.size(adj_matrix, 1)  # 获得邻接矩阵列数

    row_num = 0
    for row in adj_matrix:
        # 如果排名泄露,那么这个节点对每个节点都有出链
        if sum(row) == 0:
            for col in range(col_num):
                adj_matrix[row_num][col] = 1
        row_num += 1

由于排名泄露的话,那么这个节点的出度为0,所以只需要判断sum是否为0就行。因为是邻接矩阵,所以只需要求行数或者列数,就可以得出有多少个节点。

将邻接矩阵变为概率表达:

def calc_out_degree_ratio(adj_matrix):
    """
    计算每个节点影响力传播的比率,即该节点有多大的概率把影响力传递给下一个节点
    公式:
        out_edge / n
        out_edge: 出边,即这个节点可以把影响力传递给下一个节点
        n: 这个节点共有多少个出度

    :param adj_matrix: 邻接矩阵
    """
    row_num = 0
    for row in adj_matrix:
        n = sum(row)  # 求该节点的所有出度
        col_num = 0
        for col in row:
            adj_matrix[row_num][col_num] = col / n  # out_edge / n
            col_num += 1

        row_num += 1

因为上一步已经解决了排名泄露的问题,所以到这里的时候每个节点都有了出度,所以不用担心除以0的问题。

3.2.2 生成初始向量

def create_pr_vector(dg):
    """
    创建初始PR值的向量

    :param dg: 有向图
    :return pr_vec: 初始PR值向量
    """
    pr_list = []
    nodes = dg.nodes

    # 将初始PR值存入列表
    for node in nodes:
        pr_list.append(nodes[node]['pr'])

    pr_vec = np.array(pr_list)  # 将列表转换为ndarray对象

    return pr_vec

3.3 PageRank算法

其实解决了上面的图的问题,这个算法就不是难点了,因为公式就一行嘛,不过根据前人总结的经验,收敛条件有两个:

  1. 就单纯的收敛了;
  2. 迭代200次。
def pagerank(adj_matrix, pr_vec):
    """
    pagerank算法:
        V_{i+1} = alpha * V_i * adj + ((1 - alpha) / N) * E

    收敛条件:
        1. V_{i+1} == V_i
        2. 迭代次数200次

    就是说如果PR向量并没有发生改变,那么收敛结束,得到的PR值就是最终的PR值
    但是假设迭代次数过高后且未发生收敛,那么就会陷入死循环等,根据前人总结的经验,设置为迭代20次

    :param adj_matrix: 邻接矩阵
    :param pr_vec: 每个节点PR值的初始向量
    :return:
    """
    num_nodes = np.size(adj_matrix, 1)  # 获得矩阵列数(ie. 节点数)
    jump_value = (1 - alpha) / num_nodes  # 从其他页面跳转入所在页面的概率(标量)
    jump_vec = jump_value * np.ones(num_nodes)  # 向量化

    # iter_list = []
    # pr_list = []

    for n_iter in range(1, 201):
        pr_new = alpha * np.dot(pr_vec, adj_matrix) + jump_vec

        print("第{0}次迭代的PR值:{1}".format(n_iter, pr_new))

        # iter_list.append(n_iter)
        # pr_list.append(tuple(pr_new))

        if (pr_new == pr_vec).all():
            break
        # else:
        #     # 调试用
        #     test = pr_new - pr_vec

        pr_vec = pr_new

    # draw(iter_list, pr_list)

    print("迭代完成!")
    print("收敛值为:", pr_vec)

运行结果如下:

1次迭代的PR值:[0.234 0.489 0.064 0.064 0.149]2次迭代的PR值:[0.48543 0.27803 0.06978 0.06978 0.09698]3次迭代的PR值:[0.3488486 0.2839256 0.1125231 0.1125231 0.1421796]4次迭代的PR值:[0.33064102 0.35362387 0.08930426 0.08930426 0.13712658]5次迭代的PR值:[0.38678927 0.3166295  0.08620897 0.08620897 0.12416329]
...33次迭代的PR值:[0.3644572  0.32058761 0.09195772 0.09195772 0.13103975]34次迭代的PR值:[0.36445719 0.32058761 0.09195772 0.09195772 0.13103975]35次迭代的PR值:[0.36445719 0.32058761 0.09195772 0.09195772 0.13103976]36次迭代的PR值:[0.36445719 0.32058761 0.09195772 0.09195772 0.13103975]
...73次迭代的PR值:[0.36445719 0.32058761 0.09195772 0.09195772 0.13103975]
迭代完成!
收敛值为: [0.36445719 0.32058761 0.09195772 0.09195772 0.13103975]

我对这个收敛过程做了一个图,大家可以看一下:
收敛过程
少的那个线是和红线重合了,因为两个的值是相同的。

pr_new是用于判断是否收敛,我对收敛的判断就是如果前一个值与后一个值相同,那么就收敛。(因为矩阵运算,邻接矩阵不会发生改变)

同学们可能会有疑惑,为什么在30+次的时候输出的值都一样了,还没有收敛,于是我进行了调试,调试的内容是:

pr_new - pr_vec

得到的结果如下:
第33次迭代
第34次迭代
第35次迭代
大家可以看到,还是有细微差别,至于为什么有这些差别,一方面是浮点数,第二方面就是我对向量判断用了.all()方法,估计与.all()中判断相同的精度有关,具体的我也没有查询资料,有兴趣的同学可以深入了解。

这里改进的方法也很简单,将判断的代码改一改,改为如下公式:
∣ P R ( i + 1 ) − P R ( i + 1 ) ∣ < ϵ |PR^{(i+1)}-PR^{(i+1)}|<\epsilon PR(i+1)PR(i+1)<ϵ
ϵ \epsilon ϵ大家可以取一个极小值,用于收敛判断。

4 参考

[1] https://networkx.org/documentation/latest/index.html
[2]SuPhoebe.PageRank算法 – 从原理到实现[EB/OL].https://blog.csdn.net/u013007900/article/details/88961913,2019-4-2.
[3]李鹏宇.对PageRank算法的简单理解[EB/OL].https://zhuanlan.zhihu.com/p/81691075,2020-4-27.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值