![f23d7af6e9f2b6aee8e0feccbdf92211.png](https://i-blog.csdnimg.cn/blog_migrate/259d92d2b1cd6fc5dff252d2204bf595.jpeg)
内容选自《程序员的数学基础课》
你好,我是黄申。今天我来说说矩阵。
矩阵由多个长度相等的向量组成,其中的每列或者每行就是一个向量。从数据结构的角度来看,我们可以把向量看作一维数组,把矩阵看作二维数组。
具有了二维数组的特性,矩阵就可以表达二元关系了,例如图中结点的邻接关系,或者是用户对物品的评分关系。而通过矩阵上的各种运算操作,我们就可以挖掘这些二元关系,在不同的应用场景下达到不同的目的。今天我就从图的邻接矩阵出发,展示如何使用矩阵计算来实现 PageRank 算法。
回顾 PageRank 链接分析算法
在讲马尔科夫模型的时候,我已经介绍了 PageRank 链接分析算法。所以,在展示这个算法和矩阵操作的关系之前,我们快速回顾一下它的核心思想。
PageRank 是基于马尔科夫链的。它假设了一个“随机冲浪者”模型,冲浪者从某张网页出发,根据 Web 图中的链接关系随机访问。在每个步骤中,冲浪者都会从当前网页的链出网页中,随机选取一张作为下一步访问的目标。此外,PageRank 还引入了随机的跳转操作,这意味着冲浪者不是按 Web 图的拓扑结构走下去,只是随机挑选了一张网页进行跳转。
基于之前的假设,PageRank 的公式定义如下:
![1f3bc1a027d40e135b5ca0f30c2030af.png](https://i-blog.csdnimg.cn/blog_migrate/7a3c9d5d3e26772d3b5d730854bbef3c.jpeg)
其中,pi 表示第 i 张网页,Mi 是 pi 的入链接集合,pj 是 Mi 集合中的第 j 张网页。PR(pj) 表示网页 pj 的 PageRank 得分,L(pj) 表示网页 pj 的出链接数量,1/L(pj) 就表示从网页 pj 跳转到 pi 的概率。α是用户不进行随机跳转的概率,N 表示所有网页的数量。
PageRank 的计算是采样迭代法实现的:一开始所有网页结点的初始 PageRank 值都可以设置为某个相同的数,例如 1,然后我们通过上面这个公式,得到每个结点新的 PageRank 值。每当一张网页的 PageRank 发生了改变,它也会影响它的出链接所指向的网页,因此我们可以再次使用这个公式,循环地修正每个网页结点的值。由于这是一个马尔科夫过程,所以我们能从理论上证明,所有网页的 PageRank 最终会达到一个稳定的数值。整个证明过程很复杂,这里我们只需要知道这个迭代计算的过程就行了。
简化 PageRank 公式
那么,这个计算公式和矩阵操作又有什么联系呢?为了把问题简化,我们暂时不考虑随机跳转的情况,而只考虑用户按照网页间链接进行随机冲浪。那么 PageRank 的公式就简化为:
![10a615fdd823f773f31ea1b024fe80a5.png](https://i-blog.csdnimg.cn/blog_migrate/3d6b1bbbbf04907ffaf7350921b60caa.jpeg)
这个公式只包含了原公式中的Σ(PR(pj)/L(pj)) 部分。我们再来对比看看矩阵点乘的计算公式。
![3f2fdfc7e05499a884b73cefcc380edc.png](https://i-blog.csdnimg.cn/blog_migrate/1d85ece8fe05af1b1e78982b5ec2c041.jpeg)
以上两个公式在形式上是基本一致的。因此,我们可以把Σ(PR(pj)/L(pj)) 的计算,分解为两个矩阵的点乘。一个矩阵是当前每张网页的 PageRank 得分,另一个矩阵就是邻接矩阵。所谓邻接矩阵,其实就是表示图结点相邻关系的矩阵。
假设 xi,j 是矩阵中第 i 行、第 j 列的元素,那么我们就可以使用 xi,j 表示从结点 i 到结点 j 的连接,放到 PageRank 的应用场景,xi,j 就表示网页 pi 到网页 pj 的链接。最原始的邻接矩阵所包含的元素是 0 或 1,0 表示没有链接,而 1 表示有链接。
考虑到 PageRank 里乘积是 1/L(pj),我们可以对邻接矩阵的每一行进行归一化,用原始的值(0 或 1)除以 L(pj),而 L(pj) 表示有某张网页 pj 的出链接,正好是矩阵中 pj 这一行的和。所以,我们可以对原始的邻接矩阵,进行基于行的归一化,这样就能得到每个元素为 1/L(pj) 的矩阵,其中 j 表示矩阵的第 j 行。注意,这里的归一化是指让所有元素加起来的和为 1。
为了方便你理解,我用下面这个拓扑图作为例子给你详细解释。
![099687dda951ed78e7f278e5aa98f51d.png](https://i-blog.csdnimg.cn/blog_migrate/cb65e7103796ed5049dfde387c618265.jpeg)
基于上面这个图,原始矩阵为:
![175c9eedd2d75aaeb87632d2ba0a8a4d.png](https://i-blog.csdnimg.cn/blog_migrate/d68fce8c1fef103c48f8be61d17777bb.jpeg)
其中第 i 行、第 j 列的元素值表示从结点 i 到 j 是不是存在链接。如果是,那么这个值为 1;否则就为 0。
按照每一行的和,分别对每一行进行归一化之后的矩阵就变为:
![f56e5fb51efe8be7d77721ab56e9f6c7.png](https://i-blog.csdnimg.cn/blog_migrate/fddac7a8966e3d0e8489c38a9b6a2531.jpeg)
有了上述这个邻接矩阵,我们就可以开始最简单的 PageRank 计算。PageRank 的计算是采样迭代法实现的。这里我把初始值都设为 1,并把第一次计算的结果列在这里。
![b54f448c506f16eeb94a64b5e3ba2b6f.png](https://i-blog.csdnimg.cn/blog_migrate/b6753682ead1a9d04cdb187de2fdeabc.jpeg)
好了,我们已经成功迈出了第一步,但是还需要考虑随机跳转的可能性。
考虑随机跳转
经过上面的步骤,我们已经求得Σ(PR(pj)/L(pj)) 部分。不过,PageRank 引入了随机跳转的机制。这一部分其实也是可以通过矩阵的点乘来实现的。我们把Σ(PR(pj)/L(pj)) 部分用 A 表示,那么完整的 PageRank 公式就可以表示为:
![7efa77bcc7faa6c727f4634cf8b62946.png](https://i-blog.csdnimg.cn/blog_migrate/7eb623d5fbf36e41d7bfbdd6267b0f6a.jpeg)
于是,我们可以把上述公式分解为如下两个矩阵的点乘:
![59f526b1fedad3e5467fb271025f72d5.png](https://i-blog.csdnimg.cn/blog_migrate/1325802d0a8d78fef26f9b3c68197d7a.jpeg)
我们仍然使用前面的例子,来看看经过随机跳转之后,PageRank 值变成了多少。这里α取 0.9。
![0666808674b57848d77935b81e591cba.png](https://i-blog.csdnimg.cn/blog_migrate/bc8b4848c094ef58b303bbd84b496fd2.jpeg)
我们前面提到,PageRank 算法需要迭代式计算。为了避免计算后的数值越来越大甚至溢出,我们可以进行归一化处理,保证所有结点的数值之和为 1。经过这个处理之后,我们得到第一轮的 PageRank 数值,也就是下面这个行向量:
[0.37027027 0.24864865 0.37027027 0.00540541 0.00540541]
接下来,我们只需要再重复之前的步骤,直到每个结点的值趋于稳定就可以了。
使用 Python 进行实现
说到这里,我已经把如何把整个 PageRank 的计算,转换成多个矩阵的点乘这个过程讲完了。这样一来,我们就可以利用 Python 等科学计算语言提供的库,来完成基于 PageRank 的链接分析。为了展示具体的代码,我以之前的拓扑图为例,给你详细讲述每一步。
首先,我们要进行一些初始化工作,包括设置结点数量、确定随机跳转概率的α、代表拓扑图的邻接矩阵以及存放所有结点 PageRank 值的数组。下面是一段示例代码,在代码中我提供了注释供你参考。
复制代码
import numpy as np # 设置确定随机跳转概率的 alpha、网页结点数alpha = 0.9N = 5 # 初始化随机跳转概率的矩阵jump = np.full([2,1], [[alpha], [1-alpha]], dtype=float) # 邻接矩阵的构建adj = np.full([N,N], [[0,0,1,0,0],[1,0,1,0,0],[1,0,0,0,0],[0,0,0,0,0],[0,1,0,0,0]], dtype=float) # 对邻接矩阵进行归一化row_sums = adj.sum(axis=1) # 对每一行求和row_sums[row_sums == 0] = 0.1 # 防止由于分母出现 0 而导致的 Nanadj = adj / row_sums[:, np.newaxis] # 除以每行之和的归一化 # 初始的 PageRank 值,通常是设置所有值为 1.0pr = np.full([1,N], 1, dtype=float)
之后,我们就能采用迭代法来计算 PageRank 值。一般我们通过比较每个结点最近两次计算的值是否足够接近,来确定数值是不是已经稳定,以及是不是需要结束迭代。这里为简便起见,我使用了固定次数的循环来实现。如果你的拓扑图比较复杂,需要更多次迭代,我把示例代码和注释列在这里。
复制代码
# PageRank 算法本身是采样迭代方式进行的,当最终的取值趋于稳定后结束。for i in range(0, 20): # 进行点乘,计算Σ(PR(pj)/L(pj)) pr = np.dot(pr, adj) # 转置保存Σ(PR(pj)/L(pj)) 结果的矩阵,并增加长度为 N 的列向量,其中每个元素的值为 1/N,便于下一步的点乘。 pr_jump = np.full([N, 2], [[0, 1/N]]) pr_jump[:,:-1] = pr.transpose() # 进行点乘,计算α(Σ(PR(pj)/L(pj))) + (1-α)/N) pr = np.dot(pr_jump, jump) # 归一化 PageRank 得分 pr = pr.transpose() pr = pr / pr.sum() print("round