1.GCN概述
CNN处理的图像或者视频数据中像素点(pixel)是排列成成很整齐的矩阵
GCN 中的 Graph 是指数学(图论)中的用顶点和边建立相应关系的拓扑图
GCN的本质目的就是用来提取拓扑图的空间特征
1.1.拉普拉斯矩阵
拉普拉斯矩阵(Laplacian matrix) ,主要应用在图论中,作为一个图的矩阵表示。对于图 G=(V,E),其Laplacian 矩阵的定义为 L=D-A,其中L 是Laplacian 矩阵, D=diag(d)是顶点的度矩阵(对角矩阵),d=rowSum(A),对角线上元素依次为各个顶点的度, A 是图的邻接矩阵。
普通形式的拉普拉斯矩阵:
L
=
D
−
A
L=D-A
L=D−A
为什么GCN要用拉普拉斯矩阵?
- 拉普拉斯矩阵是对称矩阵,可以进行特征分解(谱分解)
- 由于卷积在傅里叶域的计算相对简单,为了在graph上做傅里叶变换,需要找到graph的连续的正交基对应于傅里叶变换的基,因此要使用拉普拉斯矩阵的特征向量。
1.2.相关定义
邻居:一个节点 n 是一个节点 v 的邻居只有在存在一个从 v 到 n 的边时。
邻接矩阵:邻接矩阵 A 为 n×n 矩阵,其中 n 为图中节点数,Aij=1 表示节点 i 和节点 j 之间有边相连
度矩阵D:D 是对角矩阵,即除了对角线其他元素都为 0,对角值如下计算,
D
i
i
D_{ii}
Dii表示与节点
i
i
i 相连的节点数
节点特征向量矩阵:的节点 v 带有特征向量,用矩阵 X 保存图节点的特征向量,特征向量维度是 d:
GCN 主要是将卷积操作应用到图结构上,如下图所示,GCN 输入的 chanel 为 C(即节点 Xi 特征向量的维度), GCN 输出的 chanel 为 F,即每个节点 (Zi) 的特征向量维度为 F,最后用节点的特征对节点进行分类预测等:
1.3.GCN的输入
给定一个图G=(E,V) ,一个GCN的输入包括:
- 一个输入特征矩阵 X X X,其维度是 N × F 0 N \times F^{0} N×F0 ,其中 N N N是节点的数目, F 0 F^{0} F0是每个节点输入特征的数目
- 一个 N × N N \times N N×N的对于图结构的表示的矩阵,例如 G G G的邻接矩阵 A A A
2.一个简单的 Propagation Rule
一个最简单的传播规则是:
f
(
H
i
,
A
)
=
σ
(
A
H
i
W
i
)
f(H^{i},A)=σ(AH^{i}W^{i})
f(Hi,A)=σ(AHiWi)
其中Wi是第 i层的权重并且σ是一个非线性激活函数例如ReLU 函数。权重矩阵的维度是
F
i
×
F
i
+
1
F_{i}×F_{i}+1
Fi×Fi+1;也就是说权重矩阵的第二个维度决定了在下一层的特征的数目。如果你对卷积神经网络熟悉,这个操作类似于 filtering operation 因为这些权重被图上节点共享。
H i + 1 = f ( H i , A ) = σ ( A H i W i ) H^{i+1}=f(H^{i},A)=σ(AH^{i}W^{i}) Hi+1=f(Hi,A)=σ(AHiWi)
A
A
A:(N,N)邻接矩阵
F
i
F^{i}
Fi:一维数值,第
i
i
i层的特征数目
H
i
H^{i}
Hi:
(
N
,
F
i
)
(N,F^{i})
(N,Fi),每一行都是一个节点的特征表示
X
=
H
0
X=H^{0}
X=H0:
(
N
,
F
0
)
(N,F^{0})
(N,F0),输入向量
W
i
W^{i}
Wi:
(
f
i
,
F
i
+
1
)
(f^{i},F^{i+1})
(fi,Fi+1)第
i
i
i层的权值矩阵
加入权重和激活函数后完整的计算公式:
3.一个例子,由浅入深
定义一个图:
3.1.按照简单的传播规则计算
step 1:这个图的邻接矩阵表示为:
import numpy as np
A = np.matrix([ ##邻接矩阵
[0,1,0,0],
[0,0,1,1],
[0,1,0,0],
[1,0,1,0]
],dtype=float)
step 2:定义特征向量X:
X = np.matrix([
[i,-i]
for i in range(A.shape[0])
],dtype=float)
X:
matrix([[ 0., 0.],
[ 1., -1.],
[ 2., -2.],
[ 3., -3.]])
step 3:计算 A ∗ X A*X A∗X:
A*X
A ∗ X A*X A∗X:
matrix([[ 1., -1.],
[ 5., -5.],
[ 1., -1.],
[ 2., -2.]])
3.2.出现的问题
这时我们会发现一般的传播规则存在如下的问题:
- A ∗ X A*X A∗X 的结点表示中,并没有加自己的特征值。一个节点的聚集表示不包括它自己的特征!这个表示只是它的邻居节点特征的聚集。
- 邻接结点多的结点的特征值会大,少的特征值就小。具有很大度数的节点将会有很大的值在它们的特征表示中,而具有很小度数的节点将会有很小的值。这些可能造成梯度消失或者梯度爆炸。这对于随机梯度下降也可能是有问题的,随机梯度下降通常被用来训练这样的网络,而且对于每个输入特征的值的范围是敏感的。
3.3.自循环
step 4:单位矩阵 I I I:
I = np.matrix(np.eye(A.shape[0]))
I I I:
matrix([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
step 5:加入自循环后与特征向量 X X X相乘:
A_hat = A + I
A_hat * X
结果:
matrix([[ 1., -1.],
[ 6., -6.],
[ 3., -3.],
[ 5., -5.]])
3.4.归一化
特征表示可以通过节点的度来进行归一化,方法是将邻接矩阵 A 转换为 A 和度矩阵D的逆的乘积。因此我们简化的传播规则看起来向这样:
step 6:度矩阵:
D = np.array(np.sum(A,axis=0))[0]
D = np.matrix(np.diag(D))
D D D:
matrix([[1., 0., 0., 0.],
[0., 2., 0., 0.],
[0., 0., 2., 0.],
[0., 0., 0., 1.]])
step 7:归一化操作:
D**-1 * A
计算后:
matrix([[0. , 1. , 0. , 0. ],
[0. , 0. , 0.5, 0.5],
[0. , 0.5, 0. , 0. ],
[1. , 0. , 1. , 0. ]])
step 8:归一化之后与特征向量 X X X相乘:
D**-1 * A * X
计算后:
matrix([[ 1. , -1. ],
[ 2.5, -2.5],
[ 0.5, -0.5],
[ 2. , -2. ]])
3.5.加入权重和激活函数
step 9:加入权重:
w = np.matrix([
[1,-1],
[-1,1]
])
A_hat = A + I
D_hat = np.array(np.sum(A_hat,axis=0))[0]
D_hat = np.matrix(np.diag(D_hat))
D_hat:
matrix([[2., 0., 0., 0.],
[0., 3., 0., 0.],
[0., 0., 3., 0.],
[0., 0., 0., 2.]])
D_hat**-1 * A_hat*X*w
计算后:
matrix([[ 1., -1.],
[ 4., -4.],
[ 2., -2.],
[ 5., -5.]])
step 10:加入激活函数:
def relu(x):
return (abs(x) + x) / 2.0
relu(D_hat**-1 * A_hat * X * w)
计算结果:
matrix([[1., 0.],
[4., 0.],
[2., 0.],
[5., 0.]])
4.空手道案例分析
4.1.案例介绍
Zachary 空手道俱乐部是一个被广泛使用的社交网络,其中的节点代表空手道俱乐部的成员,边代表成员之间的相互关系。当年,Zachary 在研究空手道俱乐部的时候,管理员和教员发生了冲突,导致俱乐部一分为二。下图显示了该网络的图表征,其中的节点标注是根据节点属于俱乐部的哪个部分而得到的,「A」和「I」分别表示属于管理员和教员阵营的节点。
4.2.代码实现
import numpy as np
from networkx import to_numpy_matrix
import networkx as nx
zkc = nx.karate_club_graph() # 导入空手道俱乐部的社交网络
order = sorted(list(zkc.nodes())) #对所有点进行排序 # sorted() 函数对所有可迭代的对象进行升序排序操作。
A = to_numpy_matrix(zkc, nodelist=order) #邻接矩阵
I = np.eye(zkc.number_of_nodes()) #单位矩阵
A_hat = A + I
D_hat = np.array(np.sum(A_hat, axis=0))[0] #sum(a, axis = None) : 依给定轴axis计算数组a相关元素之和,axis为整数或者元组
D_hat = np.matrix(np.diag(D_hat)) #对角线
def plot_graph(G, weight_name=None):
'''
G: a networkx G
weight_name: name of the attribute for plotting edge weights (if G is weighted)
'''
import matplotlib.pyplot as plt
%matplotlib notebook
plt.figure()
pos = nx.spring_layout(G) #采用spring布局方式定义一个布局
edges = G.edges() #获取边
weights = None
if weight_name:
weights = [ int(G[u][v][weight_name] ) for u,v in edges]
labels = nx.get_edge_attributes(G,weight_name)
nx.draw_networkx_edge_labels(G,pos,edge_labels=labels)
nx.draw_networkx(G, pos, edges=edges, width=weights);
else:
nodelist1 = []
nodelist2 = []
for i in range (34): #管理员和教员阵营分类
if zkc.nodes[i]['club'] == 'Mr. Hi':
nodelist1.append(i)
else:
nodelist2.append(i)
#nx.draw_networkx(G, pos, edges=edges);
nx.draw_networkx_nodes(G, pos, nodelist=nodelist1, node_size=300, node_color='r',alpha = 0.8)
nx.draw_networkx_nodes(G, pos, nodelist=nodelist2, node_size=300, node_color='b',alpha = 0.8)
nx.draw_networkx_edges(G, pos, edgelist=edges,alpha =0.4)
plot_graph(zkc)
#定义权重
#正态分布;loc:正态分布的均值; scale:正态分布的标准差; size(int 或者整数元组):输出的值赋在shape里
W_1 = np.random.normal(loc=0, scale=1, size=(zkc.number_of_nodes(), 4))
W_2 = np.random.normal(loc=0, size=(W_1.shape[1], 2))
W_1.shape
(34, 4)
W_2.shape
(4, 2)
def relu(x): #定义relu激活函数
return (abs(x) + x) / 2.0
def gcn_layer(A_hat, D_hat, X, W): #定义 D_hat**-1 * A_hat * X * W公式
return relu(D_hat**-1 * A_hat * X * W)
#前向传播:
H_1 = gcn_layer(A_hat, D_hat, I, W_1)
H_2 = gcn_layer(A_hat, D_hat, H_1, W_2)
output = H_2
feature_representations = {
node: np.array(output)[node] for node in zkc.nodes()}
feature_representations
{0: array([0.3646146 , 0.26904721]),
1: array([0.2848063 , 0.29515183]),
2: array([0.51791888, 0.18827742]),
3: array([0.31797502, 0.23169222]),
4: array([0.2265585 , 0.71748199]),
5: array([0.20798158, 0.54375024]),
6: array([0.23193104, 0.46542201]),
7: array([0.18881706, 0.31602282]),
8: array([0.41123805, 0. ]),
9: array([0.41832314, 0.12669272]),
10: array([0.17721712, 0.6640156 ]),
11: array([0.47274409, 0. ]),
12: array([0.43774438, 0.15015674]),
13: array([0.2456919, 0.261067 ]),
14: array([0.93199074, 0. ]),
15: array([0.9236548, 0. ]),
16: array([0.23525499, 0.37571568]),
17: array([0.25711395, 0.27326112]),
18: array([1.16095606, 0. ]),
19: array([0.28237279, 0.03967677]),
20: array([1.05828531, 0. ]),
21: array([0.22964731, 0.27950972]),
22: array([1.07827656, 0. ]),
23: array([0.97721974, 0. ]),
24: array([1.17417171, 0.0783619 ]),
25: array([1.15124332, 0.07776736]),
26: array([0.7745391, 0. ]),
27: array([0.85109117, 0.05471966]),
28: array([0.75985656, 0.31224247]),
29: array([0.81969178, 0. ]),
30: array([0.4580584, 0. ]),
31: array([0.86383816, 0.19264988]),
32: array([1.24950088, 0. ]),
33: array([1.1516554, 0. ])}
#把运行结果画出来
import matplotlib.pyplot as plt
plt.figure()
plt.xlim(-0.3,1)
plt.ylim(-0.3,1)
for i in range (34):
if zkc.nodes[i]['club'] == 'Mr. Hi':
plt.scatter(np.array(output)[i,0], np.array(output)[i,1] ,color = 'r',alpha=0.5,s = 20) #alpha透明度,s点的大小
else:
plt.scatter(np.array(output)[i,0], np.array(output)[i,1] ,color = 'b',alpha=0.5,s = 20)
#plt.scatter(np.array(output)[:,0],np.array(output)[:,1])
plt.show()