当我们在使用Google这种搜素引擎的时候,它往往能以最权威,最完善,最被认同的答案回馈,而且十分安全,让我们不会轻易被钓鱼欺诈的网站所蒙骗。但是对于那么多的网页网站,那么多的答案,它是如何做的这么好的呢?
图论
图论起源于欧拉对哥尼斯堡七桥问题的解决。如上图,意思是如何在不走重复路的情况下走遍七个桥,虽然形式很简单,但欧拉将这种问题表示为点和边,然后用一笔画来解决的思路被后世发展为图论。图的优点在于虽然它表示简洁,但对信息的表达能力很强,毕竟世界上的万事万物都可以主谓宾。
图系统的三大功能:
- 图查询。图查询是对图关联数据的基础查询,直接获取关联信息,包括多阶邻居查询(多阶扩散,邻近分布,关联画像,特定邻居搜索,一般到3阶)、路径查询(两点间的间接关联关系)、子图查询、图数据库,知识图谱(建模、存储、查询)。
- 图计算。图计算是对全图结构进行传播迭代得到点/边的全局属性的过程,如pagerank,图聚类,图分割,图染色,论文中一般称为是graph processing即图处理。
- 图表示学习。图学习是将图中顶点映射到低维向量空间,向量间的距离尽可能反映原顶点在图结构关联强度的相对大小,实现非欧图想欧式空间的转变。
图计算和图表示的不同在于图计算的结果仍然在语义范围内用清晰的解释,而图学习的结果是向量集和图语义没有交集。
这三大功能将不会展开叙述,博主都在其他的文章中有所整理,如知识图谱,Graph Embedding(DeepWalk,LINE,Node2vec),Graph Embedding(SDNE,Graph2vec,GraphGAN)等等,此篇重点整理pagerank。
图特征
基于节点重要性的特征,衡量了某节点在网络中的重要程度
- 节点度(Node Degree)。即入度(in-degree)与出度(out-degree)。
- 节点中心性度量(Node Centrality)。主要有特征向量中心性,中介中心性,接近中心性。特征向量中心性指若一个节点的邻居节点的重要性高,那么它的重要性也越高,计算方法为计算特征矩阵的最大特征值。中介中心性又称为最短路径中心性,即若一个节点在各节点对之间的最短路径上出现次数越多,则其在网络中的重要性越高。接近中心是若一个节点到其他所有节点的最短路径长度越小,那么它在网络中的重要性越高。
基于结构的特征,能够捕捉节点周围领域的拓扑属性
- 节点度(Node Degree)。不同节点的度同样可以反映拓扑结构。
- 聚类系数(Clustering Coefficients)。聚类系数衡量了节点的邻居节点之间的连接性高低,若它的邻居节点之间相互连接的越多,则聚类系数越大。
- 图元度向量(GDV,graphlet degree vector)。对事先指定的图元(graphlet)结构进行了计数统计,从而反映了网络的结构特征。
用networkx可以实现一些小巧的结果
import networkx as nx
import matplotlib.pyplot as plt
import pandas as pd
G = nx.karate_club_graph()
print(nx.info(G))
nx.draw(G, pos=nx.spring_layout(G), with_labels=True)
print("Node Degree")
for v in G:
print(f"{v:4} {G.degree(v):6}")
print("Node centraility")
eigen_dict = nx.eigenvector_centrality(G)
betw_dict = nx.betweenness_centrality(G)
close_dict = nx.closeness_centrality(G)
centrality_df = pd.DataFrame(
{
'eigenvector_centrality': [eigen_dict[v] for v in G],
'betweenness_centrality': [betw_dict[v] for v in G],
'closeness_centrality': [close_dict[v] for v in G],
}
)
centrality_df
print("Node Clustering Coefficients")
nx.clustering(G)
ego = nx.ego_graph(G, 7)
nx.draw(ego, pos=nx.spring_layout(ego), with_labels=True)
PageRank
Google的PageRank算法是根据网站的外部链接和内部链接的数量和质量来衡量网站的价值,这个概念引自学术中一篇论文的被引述的频度–即被别人引述的次数越多,一般判断这篇论文的权威性就越高,所以对于网页来说,如果它被被很多的其它网页所链接,说明它受到普遍的承认和信赖,那么自然的它的排名就高。当然了,如果仅仅是数量多,并不能很客观的看待问题,本身排名高,质量好的网站应该更有发言权,它所引用的网站的重要性应该被提高,赋予更高的权重。
所以一个基本的PageRank算法就出炉了:一个网页的排名应该等于所有链接到该网页的网页的加权排名之和:
P
R
(
i
)
=
∑
(
j
,
i
)
∈
E
P
R
(
j
)
O
j
PR(i) = \sum_{(j,i)\in E} \frac{PR(j)}{O_j}
PR(i)=(j,i)∈E∑OjPR(j)
其中
O
j
O_j
Oj代表该网页在所有网页关系网中的出度,即它的外部链接。但是此时可能会出现两个问题,一是某网页没有出度怎么办?二是某网页故意引用自己怎么办?解决方法是心灵转移(teleporting),即假定每个网页都会有出度,而且会以一定的概率跳到别的网站。所以目标应该变为:
P
R
(
i
)
=
(
1
−
d
)
+
d
∑
(
j
,
i
)
∈
E
P
R
(
j
)
O
j
PR_(i) = (1-d) + d\sum_{(j,i)\in E} \frac{PR(j)}{O_j}
PR(i)=(1−d)+d(j,i)∈E∑OjPR(j)其中d为阻尼因数(damping factor)。阻尼因数是网页链接到另外一个站点时所获得的实际PR分值,一般是0.85。
但是,这里有个问题,若要计算网页的PR值,那就需要知道其他网页的PR值,那就得计算这个网页的PR值,那…这就等同于“是先有鸡还是先有蛋”的怪圈里了。Google 的两个创始人拉里·佩奇 (Larry Page )和谢尔盖·布林(Sergey Brin) 把这个问题变成了一个二维矩阵相乘的问题,并且用迭代的方法解决了这个问题,即他们先假定所有网页的排名是相同的,并且根据这个初始值,算出各个网页的第一次迭代排名,然后再根据第一次迭代排名算出第二次的排名。所以通过一个状态转移矩阵A,即一个跳如网页的概率矩阵来完成:
P
n
+
1
=
A
P
n
P_{n+1} = A P_{n}
Pn+1=APn
此时这个问题就变成了一个马尔可夫( Markov )问题了。那么这个问题真的能够正常收敛,得到最后的结果吗?由马尔可夫过程(Markov process)可以知道,在已知目前状态 (现在)的条件下,它未来的演变 (将来)不依赖于它以往的演变 ( 过去 ) ,所以如果一个马尔可夫过程收敛,需要它的状态矩阵随机,不可约,非周期。而A是设定的网页转移的确定概率矩阵,且非周期,故不论初始值如何选取,一定能收敛到真实值,而且没有任何的人工干预。
理论问题解决了,我们能够计算出正确的PR值,并排名出网页。但互联网发展到今天,网页的数量无法估计,矩阵相乘的计算量太大太大了!不过幸好,大多数网页之间是没有直接的联系的,也就是说这个矩阵是稀疏的,所以利用这一点,便可以很好的简化计算。不过人们在使用Google查询往往是带有主题特征的,而PageRank忽略了这一点,而且从计算方法来看,一个新网页很难得到很高的PR分数值。
import random
N = 8 #八个网页
d = 0.85 #阻尼因子为0.85
delt = 0.00001 #迭代控制变量
#两个矩阵相乘
def matrix_multi(A,B):
result = [[0]*len(B[0]) for i in range(len(A))]
for i in range(len(A)):
for j in range(len(B[0])):
for k in range(len(B)):
result[i][j] += A[i][k]*B[k][j]
return result
#矩阵A的每个元素都乘以n
def matrix_multiN(n,A):
result = [[1]*len(A[0]) for i in range(len(A))]
for i in range(len(A)):
for j in range(len(A[0])):
result[i][j] = n*A[i][j]
return result
#两个矩阵相加
def matrix_add(A,B):
if len(A[0])!=len(B[0]) and len(A)!=len(B):
return
result = [[0]*len(A[0]) for i in range(len(A))]
for i in range(len(A)):
for j in range(len(A[0])):
result[i][j] = A[i][j]+B[i][j]
return result
def pageRank(A):
e = []
for i in range(N):
e.append(1)
norm = 100
New_P = []
for i in range(N):
New_P.append([random.random()])
r = [ [(1-d)*i*1/N] for i in e]
while norm > delt:
P = New_P
New_P = matrix_add(r,matrix_multiN(d,matrix_multi(A,P))) #P=(1-d)*e/n+d*M'P PageRank算法的核心
norm = 0
#求解矩阵一阶范数
for i in range(N):
norm += abs(New_P[i][0]-P[i][0])
print New_P
#根据邻接矩阵求转移概率矩阵并转向
def tran_and_convert(A):
result = [[0]*len(A[0]) for i in range(len(A))]
result_convert = [[0]*len(A[0]) for i in range(len(A))]
for i in range(len(A)):
for j in range(len(A[0])):
result[i][j] = A[i][j]*1.0/sum(A[i])
for i in range(len(result)):
for j in range(len(result[0])):
result_convert[i][j]=result[j][i]
return result_convert
def main():
A = [[0,1,1,0,0,1,0,0],\
[0,0,0,1,1,0,0,0],\
[0,0,0,1,0,1,0,0],\
[0,0,0,0,0,1,0,0],\
[1,0,0,1,0,0,1,1],\
[0,0,0,1,0,0,0,0],\
[0,0,1,0,0,0,0,0],\
[0,0,0,1,0,0,1,0]]
M = tran_and_convert(A)
pageRank(M)
if __name__ == '__main__':
main()