一、图卷积神经网络的定义
图卷积网络(GCN)是一种在图上操作的神经网络。给定一个图G=(E,V), 一个GCN的输入如下:
- 一个N×F的输入特征矩阵X,其中N是图中的节点个数,F是每个节点的输入特征个数;
- 一个N×N的图结构表示矩阵,比如G的邻接矩阵A。 因此,GCN中的隐藏层可以写成,其中,是传播函数。每层Hi对应于的特征矩阵。在每一层,这些特征被聚合后再用传播规则形成下一层的特征。这样,特征在每一个连续的层上都变得越来越抽象。在这个框架中,GCN的变体仅在传播规则的选择上有所不同。
1.1 简单的传播规则的例子
本文将传播规则设置为f(X,A)=AX,对下图进行演示:
1)本文利用每个顶点的入度构建邻接矩阵A,如下所示:
In [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
)
2)为每个节点(0、1、2、3)生成两个整数特征,为了方便确认对应的节点,我们把对应节点的特征数生成对应节点数字,设特征矩阵X为如下形式:
In [2]: X = np.matrix([
[i, -i]
for i in range(A.shape[0])
], dtype=float)
X
Out[2]: matrix([[ 0., 0.],
[ 1., -1.],
[ 2., -2.],
[ 3., -3.]])
3)依据传播规则f(X,A)=AX:
#传播规则f(X,A)= A * X
In [3]: A * X
Out[3]: matrix([
[ 1., -1.],
[ 5., -5.],
[ 1., -1.],
[ 2., -2.]])#分别对应节点0,1,2,3
我们发现每个节点(每一行)的表示现在是其邻居特征的总和。换句话说,图卷积层中每个节点的特征是其邻接节点特征的集合。
例如,第二行的元素为5,表示节点1的两个邻居节点2和3的特征之和。但是这样的特征聚合存在两个问题:
- 节点的聚合表示不包括它自己的特征。这个表示只是其邻居节点特征的集合,因此只有具有自环的节点才会在聚合中包含自己的特征。
- 大度的节点在特征表示中会有较大的值,而小度的节点将具有较小的值。这可能会导致梯度消失或梯度爆炸 [1, 2],也会影响随机梯度下降算法(随机梯度下降算法通常被用于训练这类网络,且对每个输入特征的规模(或值的范围)都很敏感)。
针对这两个问题,对应的解决方法如下是:
1.针对问题1采用的方案是:
向每个节点添加自环。在实践中,通过在应用传播规则之前将对角矩阵I添加到邻接矩阵A来实现的。比如:
#特征矩阵x如下
x=
matrix([[ 0., 0.],#节点0的特征表示
[ 1., -1.],#节点1的特征表示
[ 2., -2.],#节点2的特征表示
[ 3., -3.]])#节点3的特征表示
如何将节点1的特征[1,-1],在利用传播规则的时候也将自己的特征参与到最终的聚合结果中,因此会想到将邻接矩阵与相同维度的单位矩阵相加,因为与单位矩阵相加后,邻接矩阵的对角线上就会出现1的值,然后(Then)在采用传播规则AX,就会将节点1的特征加入进去,加入本节点的特征表示,起到融合信息的作用。因此,程序实现如下:
In [4]:
I = np.matrix(np.eye(A.shape[0]))
I
Out[4]:
matrix([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
In [5]:
A_hat = A + I
A_hat * X
Out[5]:
matrix([[ 1., -1.],
[ 6., -6.],
[ 3., -3.],
[ 5., -5.]])
现在每个节点都将自己的节点看作是自己的邻居,以上程序实现了将其邻居节点的特征进行融合的过程中,节点本身的特征也包括在内。
2、针对问题2采用如下的修正方案。
特征表示归一化。通过将邻接矩阵A与节点的度矩阵D的逆相乘,可以将特征表示按节点度进行归一化。我们将传播规则修改为:
1) 首先计算度矩阵:
In [6]:
D = np.array(np.sum(A, axis=1))
D = np.matrix(np.diag(np.squeeze(D)))
D
Out[6]:
matrix([[1., 0., 0., 0.],
[0., 2., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 2.]])
2) 使用度矩阵对邻接矩阵进行变换:
In [7]:
A = np.matrix([
[0, 1, 0, 0],
[0, 0, 1, 1],
[0, 1, 0, 0],
[1, 0, 1, 0]],
dtype=float
)
D**-1 * A
Out[7]:
matrix([[0. , 1. , 0. , 0. ],
[0. , 0. , 0.5, 0.5],
[0. , 1. , 0. , 0. ],
[0.5, 0. , 0.5, 0. ]])
观察到邻接矩阵的每一行中的权重(值)已被对应行的节点的度所除。
3) 对变换后的邻接矩阵应用传播规则,即:
In [8]:
D**-1 * A * X
Out[8]:
matrix([
[ 1. , -1. ],
[ 2.5, -2.5],
[ 1. , -1. ],
[ 1. , -1. ]])
得到每个节点的相邻节点特征值的均值。
这是因为(变换后)邻接矩阵的权重对应于相邻节点特征加权和的权重。大家可以自己动手验证这个结果。
整合
以上没有考虑权重和激活层的操作,现在,我们将把自环和归一化技巧结合起来。此外,我们还将重新介绍之前为了简化讨论而省略的有关权重和激活函数的操作。
添加权重
首先要做的是应用权重。请注意,这里的 D_hat 是 A_hat = A + I 对应的度矩阵,即具有强制自环的矩阵 A 的度矩阵。
In [45]: W = np.matrix([
[1, -1],
[-1, 1]
])
D_hat**-1 * A_hat * X * W
Out[45]: matrix([
[ 1., -1.],
[ 4., -4.],
[ 2., -2.],
[ 5., -5.]
])
如果我们想要减小输出特征表征的维度,我们可以减小权重矩阵 W 的规模:
In [46]: W = np.matrix([
[1],
[-1]
])
D_hat**-1 * A_hat * X * W
Out[46]: matrix([[1.],
[4.],
[2.],
[5.]]
)
添加激活函数
本文选择保持特征表征的维度,并应用 ReLU 激活函数。
In [51]: W = np.matrix([
[1, -1],
[-1, 1]
])
relu(D_hat**-1 * A_hat * X * W)
Out[51]: matrix([[1., 0.],
[4., 0.],
[2., 0.],
[5., 0.]])
这就是一个带有邻接矩阵、输入特征、权重和激活函数的完整隐藏层!
2、在真实场景下的应用
最后,我们将图卷积网络应用到一个真实的图上。本文将向读者展示如何生成上文提到的特征表征。
Zachary 空手道俱乐部
Zachary 空手道俱乐部是一个被广泛使用的社交网络,其中的节点代表空手道俱乐部的成员,边代表成员之间的相互关系。当年,Zachary 在研究空手道俱乐部的时候,管理员和教员发生了冲突,导致俱乐部一分为二。下图显示了该网络的图表征,其中的节点标注是根据节点属于俱乐部的哪个部分而得到的,「A」和「I」分别表示属于管理员和教员阵营的节点。
构建 GCN
接下来,我们将构建一个图卷积网络。我们并不会真正训练该网络,但是会对其进行简单的随机初始化,从而生成我们在本文开头看到的特征表征。我们将使用 networkx,它有一个可以很容易实现的 Zachary 空手道俱乐部的图表征。然后,我们将计算 A_hat 和 D_hat 矩阵。
from networkx import to_numpy_matrix
zkc = karate_club_graph()
order = sorted(list(zkc.nodes()))
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]#生成度矩阵
D_hat = np.matrix(np.diag(D_hat))#将取出的数值变成矩阵形式
接下来,我们将随机初始化权重。
# Graph一共包含两个隐藏层。因此每个隐藏层会有两个参数
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))
接着,我们会堆叠 GCN 层。这里,我们只使用单位矩阵作为特征表征,即每个节点被表示为一个 one-hot 编码的类别变量。
#定义GCN的卷积层
def gcn_layer(A_hat, D_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()}
你看,这样的特征表征可以很好地将 Zachary 空手道俱乐部的两个社区划分开来。至此,我们甚至都没有开始训练模型!
我们应该注意到,在该示例中由于 ReLU 函数的作用,在 x 轴或 y 轴上随机初始化的权重很可能为 0,因此需要反复进行几次随机初始化才能生成上面的图。
结语
本文中对图卷积网络进行了高屋建瓴的介绍,并说明了 GCN 中每一层节点的特征表征是如何基于其相邻节点的聚合构建的。读者可以从中了解到如何使用 numpy 构建这些网络,以及它们的强大:即使是随机初始化的 GCN 也可以将 Zachary 空手道俱乐部网络中的社区分离开来。
参考文献
[1] Blog post on graph convolutional networks by Thomas Kipf.
[2] Paper called Semi-Supervised Classification with Graph Convolutional Networks by Thomas Kipf and Max Welling.