网上对图卷积神经网络(Graph Convolutional Networks)的介绍大都说的云里雾里,让人看了不甚明白,无意中找到了篇很好的文章,对图神经网络中 f ( H i , A ) = σ ( A H i W i ) f\left(H^{i}, A\right)=\sigma\left(A H^{i} W^{i}\right) f(Hi,A)=σ(AHiWi)这个式子有很清楚简洁的解释。本文主要参考这篇文章 https://towardsdatascience.com/how-to-do-deep-learning-on-graphs-with-graph-convolutional-networks-7d2250723780,强烈建议大家去读一下原文。
介绍
传统的CNN是在图像上面做卷积运算,假如把图像的每个像素看做一个图的节点,那么图像可以看做一个相互连接的图,但是对于节点不是相互连接的图应该怎么进行卷积操作呢?这就引入了图卷积神经网络。传统的图像卷积是对某个像素领域内的元素进行特征的加权聚合,而图卷积就可以看做是对一个图节点相邻的节点进行特征的加权聚合,具体示意见下图:


给定一个图 G = ( V , E ) G=(V, E) G=(V,E),GCN的输入为
- 特征矩阵 X X X, X X X的维度为 N × F 0 N \times F^{0} N×F0,其中 N N N是节点的数目, F 0 F^{0} F0是每个节点特征的数目
- 图的邻接矩阵 A A A, A A A的维度为 N × N N \times N N×N
GCN的一个隐藏层可以写成 H i = f ( H i − 1 , A ) ) \left.H^{i}=f\left(H^{i-1}, A\right)\right) Hi=f(Hi−1,A)),其中 H 0 = X H^{0}=X H0=X, f f f是网络的传递过程。每一层 H i H^{i} Hi是一个 N × F i N \times F^{i} N×Fi的特征矩阵,每一行都代表图中一个节点的特征向量。每一层这些特征都会通过传递规则 f f f聚合起来,形成下一层的特征向量,采用这种方式特征能随着层数增加变得更加抽象,含有高层的语义信息。下面来介绍传递函数 f f f的实现方法以及整个图神经网络的计算过程。
传递函数
传递函数可以写成如下的形式
f
(
H
i
,
A
)
=
σ
(
A
H
i
W
i
)
f\left(H^{i}, A\right)=\sigma\left(A H^{i} W^{i}\right)
f(Hi,A)=σ(AHiWi)
其中
A
A
A代表邻接矩阵,
H
i
H^{i}
Hi代表第i层的特征,
W
i
W^{i}
Wi代表第i层的权重矩阵,
σ
\sigma
σ代表非线性激活函数,比如ReLU,权重矩阵的维度为
F
i
×
F
i
+
1
F^{i} \times F^{i+1}
Fi×Fi+1,也就是说权重矩阵的第二个维度决定了下一层的特征维度。
A
A
A与
H
i
H^{i}
Hi相乘代表将每个节点周围的节点特征进行聚合,得到更新后的当前节点特征,注意此时特征维度仍然为
N
×
F
i
N \times F^{i}
N×Fi;之后将这个特征与特征矩阵
W
i
W^{i}
Wi相乘,代表将当前特征进行维度变换,从
F
i
F^{i}
Fi个特征变换为
F
i
+
1
F^{i+1}
Fi+1个特征,其中
F
i
+
1
F^{i+1}
Fi+1个特征中的每个特征都是
F
i
F^{i}
Fi个特征的线性组合,最终得到一个
N
×
F
i
+
1
N \times F^{i+1}
N×Fi+1维度的矩阵,通过
σ
\sigma
σ函数以后为第i+1层的特征
H
i
+
1
H^{i+1}
Hi+1。
简单的例子
下面举一个简单的例子以便更好地理解这个过程。首先有如下图这样一张有向图。
下面是这个图的邻接矩阵:
A = np.matrix([
[0, 1, 0, 0],
[0, 0, 1, 1],
[0, 1, 0, 0],
[1, 0, 1, 0]],
dtype=float
)
给每个节点创建一个二维的特征:
In [3]: X = np.matrix([
[i, -i]
for i in range(A.shape[0])
], dtype=float)
X
Out[3]: matrix([
[ 0., 0.],
[ 1., -1.],
[ 2., -2.],
[ 3., -3.]
])
然后应用上面提到的传播规则,首先进行 A A A与 X X X相乘:
In [6]: A * X
Out[6]: matrix([
[ 1., -1.],
[ 5., -5.],
[ 1., -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 [8]: A_hat = A + I
A_hat * X
Out[8]: matrix([
[ 1., -1.],
[ 6., -6.],
[ 3., -3.],
[ 5., -5.]])
针对以上第二个问题解决方案为:对特征进行归一化,具体的方案就是让邻接矩阵乘以它度矩阵的逆。度矩阵为对角矩阵,对角线上元素依次为各个顶点的度。
In [9]: D = np.array(np.sum(A, axis=0))[0]
D = np.matrix(np.diag(D))
D
Out[9]: matrix([
[1., 0., 0., 0.],
[0., 2., 0., 0.],
[0., 0., 2., 0.],
[0., 0., 0., 1.]
])
In [10]: D**-1 * A
Out[10]: matrix([
[0. , 1. , 0. , 0. ],
[0. , 0. , 0.5, 0.5],
[0. , 0.5, 0. , 0. ],
[0.5, 0. , 0.5, 0. ]
])
从上面的结果可以看到邻接矩阵每一行的元素都除以了对应节点的度,这样起到了归一化的作用,下一步就可以对特征进行求解。
In [11]: D**-1 * A * X
Out[11]: matrix([
[ 1. , -1. ],
[ 2.5, -2.5],
[ 0.5, -0.5],
[ 2. , -2. ]
])
在进行乘法时,变换后邻接矩阵的的值就相当于是各个点的权重,计算得到的特征相当于是对相邻节点的特征做了一次加权平均。
最终步骤
上一步讲到了邻接矩阵可以添加自环以及归一化操作,下面就可以把特征矩阵的乘法以及激活函数也加进来形成最终的图神经网络表达式。
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.]
])
其中D_hat是A_hat= A + I A+I A+I的度矩阵, W W W是特征矩阵,代表特征维度的转换关系。本例子中 W W W维度为2X2,因此输出的特征仍然是2维,如果想调整输出的维度可以相应地调整 W W W的列的数目,如下例所示:
In [46]: W = np.matrix([
[1],
[-1]
])
D_hat**-1 * A_hat * X * W
Out[46]: matrix([[1.],
[4.],
[2.],
[5.]]
)
W W W的维度变为2X1,这样输出特征的维度就由2变成了1。最后可以加上激活函数形成最终的表达式:
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.]])
以上就是图神经网络其中一层的计算过程,把多层这样的结构前后级联叠加可以形成一个完整的网络。