图卷积神经网络(GCN)的核心思想: 学习一个映射 f ( . ) f(.) f(.),通过该映射图中的节点 v i v_i vi可以聚合它自己的特征 x i x_i xi与它的邻居特征 x j ( j ∈ N ( v i ) ) x_j \;(j \in N(v_i)) xj(j∈N(vi))来生成节点的新 v i v_i vi表示。
图卷积本质上是一种aggregation(聚合)操作,是一种局部加权平均运算。在图中"局部"是指他的邻居,简单起见,把有边的权重定义为1,无边的权重定义为0。
基本符号定义
N = 1 , 2 , 3 ⋯ n N=1,2,3\cdots n N=1,2,3⋯n 代表所有节点的编号
X i X_i Xi 代表节点 i i i 的特征向量
A A A 代表邻接矩阵, A i j A_{ij} Aij代表节点 i , j i,j i,j之间的边的权。
D D D 代表图的度的矩阵,是一个对角矩阵(是邻接矩阵的列和或行和),即: D i i = ∑ k = 1 N A i k D_{ii} = \sum_{k=1}^N A_{ik} Dii=∑k=1NAik。
L = D − A L=D-A L=D−A 是图的拉普拉斯矩阵(Laplacian Matrix)。
下面是一个例子(无权图):
图神经网络的公式定义
这是GCN图卷积层的示意图:
1、基本版本
先仔细思考:通过该映射图中的节点 v i v_i vi可以聚合它自己的特征 x i x_i xi与它的邻居特征 x j ( j ∈ N ( v i ) ) x_j \;(j \in N(v_i)) xj(j∈N(vi))来生成节点的新 v i v_i vi表示 如何将这个过程用数学公式实现?主要包括两个部分:
(1)先聚合邻居特征,最简单的是加权平均法:
a
g
g
r
e
g
a
t
e
(
X
i
)
=
∑
j
∈
n
e
i
g
h
b
o
r
(
i
)
A
i
j
X
j
aggregate(\mathbf X_i) = \sum_{j \in neighbor(i)}A_{ij}X_j
aggregate(Xi)=j∈neighbor(i)∑AijXj
注:A是带权邻接矩阵。
将其写成矩阵运算(矩阵式聚合):
a
g
g
r
e
g
a
t
e
(
X
i
)
=
A
X
aggregate(\mathbf X_i) = \mathbf A \mathbf X
aggregate(Xi)=AX
(2)添加自环,加入自己的特征
a
g
g
r
e
g
a
t
e
(
X
)
=
(
A
+
I
)
X
=
A
X
+
X
a
g
g
r
r
e
g
a
t
e
(
X
i
)
=
∑
j
∈
N
A
i
j
X
j
+
X
i
aggregate(X) = (A+I) X = AX +X \\ aggrregate(X_i) = \sum_{j \in N} A_{ij}X_{j} + X_{i}
aggregate(X)=(A+I)X=AX+Xaggrregate(Xi)=j∈N∑AijXj+Xi
这样就实现了将节点本身的特征加回来了。
2、"差分"版本
存在这样一种场景:节点
v
i
v_i
vi的特征与他的邻居节点
x
j
x_j
xj的差距较大,这样的任务可能更加关注相邻节点间的"差分"。一般会用拉普拉斯矩阵
L
=
D
−
A
L = D - A
L=D−A 来实现。
a
g
g
r
e
g
a
t
e
(
X
)
=
(
D
−
A
)
X
=
D
X
−
A
X
a
g
g
r
r
e
g
a
t
e
(
X
i
)
=
∑
j
∈
N
A
i
j
X
i
−
∑
j
∈
N
A
i
j
X
j
=
∑
j
∈
N
A
i
j
(
X
i
−
X
j
)
aggregate(X) =(D-A) X = DX - AX \\ \\ aggrregate(X_i) = \sum_{j \in N} A_{ij}X_{i}- \sum_{j \in N} A_{ij} X_{j} = \sum_{j \in N} A_{ij}(X_{i}- X_{j})
aggregate(X)=(D−A)X=DX−AXaggrregate(Xi)=j∈N∑AijXi−j∈N∑AijXj=j∈N∑Aij(Xi−Xj)
其中,
D
X
DX
DX可以理解为当前节点本来拥有的信息;
A
X
AX
AX 可以理解为本次操作要减少的信息。
3、“带权”归一化版本
无论是 A + I A+I A+I还是 D − A D-A D−A,均是利用邻居节点和自身的节点求和,而不是平均。这会导致离群较远或者度较小的节点在聚合后特征较小,离群较近或者度较大的节点在聚合后特征较大。因此需要进行归一化。
令 A ^ = A + I \hat A = A+I A^=A+I或者 A ^ = D − A \hat A = D-A A^=D−A。 D ^ \hat D D^为 A ^ \hat A A^的度的矩阵。
所以,加权聚合的结果为:
a
g
g
r
e
g
a
t
e
(
X
)
=
D
^
−
1
A
^
X
aggregate(X) = \hat D^{-1} \hat A X
aggregate(X)=D^−1A^X
以下的公式解释了上式如何实现归一化:
a
g
g
r
e
g
a
t
e
(
X
i
)
=
∑
k
=
1
N
D
^
i
k
−
1
∑
j
=
1
N
A
^
i
j
X
j
=
∑
j
=
1
N
D
^
i
i
−
1
A
^
i
j
X
j
=
∑
j
=
1
N
A
^
i
j
D
^
i
i
X
j
=
∑
j
=
1
N
A
^
i
j
∑
k
=
1
N
A
^
i
k
X
j
\begin{aligned} aggregate(X_i) &= \sum_{k=1}^N \hat D_{ik}^{-1}\sum_{j=1}^N \hat A_{ij} X_j \\ &= \sum_{j=1}^{N} \hat D_{ii}^{-1} \hat A_{ij} X_{j} \\ &= \sum_{j=1}^{N} \frac{ \hat A_{ij}}{\hat D_{ii}} X_{j} \\ & = \sum_{j=1}^{N} \frac{ \hat A_{ij}}{\sum_{k=1}^{N}\hat A_{ik}} X_{j} \end{aligned}
aggregate(Xi)=k=1∑ND^ik−1j=1∑NA^ijXj=j=1∑ND^ii−1A^ijXj=j=1∑ND^iiA^ijXj=j=1∑N∑k=1NA^ikA^ijXj
这样通过
D
^
−
1
\hat D^{-1}
D^−1操作,已经将求和变成加权平均求和,权值之和归一化为1。
4、对称归一化版本
“带权”归一化版本只考虑到了节点自身的度,实际上除了应该考虑聚合节点
i
i
i 的度
D
^
j
j
\hat D_{jj}
D^jj ,还应该考虑被聚合节点
j
j
j的度
D
^
j
j
\hat D_{jj}
D^jj,将二者的几何平均
D
^
j
j
D
^
j
j
\sqrt{\hat D_{jj} \hat D_{jj}}
D^jjD^jj 引入:
aggregate
(
X
i
)
=
D
^
−
0.5
A
^
D
^
−
0.5
X
=
∑
k
=
1
N
D
^
i
k
−
0.5
∑
j
=
1
N
A
^
i
j
X
j
∑
l
=
1
N
D
^
i
l
−
0.5
=
∑
j
=
1
N
D
^
i
i
−
0.5
A
^
i
j
X
j
D
^
j
j
−
0.5
=
∑
j
=
1
N
1
D
^
i
i
0.5
A
^
i
j
1
D
^
j
j
0.5
X
j
=
∑
j
=
1
N
A
^
i
j
D
^
i
i
D
^
j
j
X
j
\begin{aligned} \text {aggregate}\left(X_{i}\right) &=\hat{D}^{-0.5} \hat{A} \hat{D}^{-0.5} X \\ &=\sum_{k=1}^{N} \hat{D}_{i k}^{-0.5} \sum_{j=1}^{N} \hat{A}_{i j} X_{j} \sum_{l=1}^{N} \hat{D}_{i l}^{-0.5} \\ &=\sum_{j=1}^{N} \hat{D}_{i i}^{-0.5} \hat{A}_{i j} X_{j} \hat{D}_{j j}^{-0.5} \\ &=\sum_{j=1}^{N} \frac{1}{\hat{D} i i^{0.5}} \hat{A}_{i j} \frac{1}{\hat{D}_{j j}^{0.5}} X_{j} \\ &=\sum_{j=1}^{N} \frac{\hat{A}_{i j}}{\sqrt{\hat{D} i i \hat{D} j j}} X_{j} \end{aligned}
aggregate(Xi)=D^−0.5A^D^−0.5X=k=1∑ND^ik−0.5j=1∑NA^ijXjl=1∑ND^il−0.5=j=1∑ND^ii−0.5A^ijXjD^jj−0.5=j=1∑ND^ii0.51A^ijD^jj0.51Xj=j=1∑ND^iiD^jjA^ijXj
显而易见,通过
D
^
−
0.5
A
^
D
^
−
0.5
\hat{D}^{-0.5} \hat{A} \hat{D}^{-0.5}
D^−0.5A^D^−0.5操作,实现了
D
^
i
i
和
D
^
j
j
\hat D_{ii}和\hat D_{jj}
D^ii和D^jj的集合平均,从而剔除了被聚合节点
j
j
j的度的影响。
这是图卷积的基本操作,这种图卷积是谱图卷积的一阶近似。接着只需要将图卷积层堆积起来就构成了图卷积网络GCN。
GCN基本结构
接下来来看两个GCN结构:
(1)结构一:多层GCN
GCN层通过聚集来自其邻居的特征信息来封装每个节点的隐藏表示。特征聚合后,将非线性变换应用于结果输出。通过堆叠多层,每个节点的最终隐藏表示形式将包含来自其他节点的信息。
(2)结构二:用于分类的FGCN
GCN层后面是池化层,以将图粗化为子图(聚合更多的信息)。因为要计算每个图形标签的概率,输出层是具有SoftMax函数的线性层。
GCN层的tensorflow2.0实现
import tensorflow as tf
from tensorflow.keras import activations, regularizers, constraints, initializers
spdot = tf.sparse.sparse_dense_matmul
dot = tf.matmul
class GCNConv(tf.keras.layers.Layer):
def __init__(self,
units,
activation=lambda x: x,
use_bias=True,
kernel_initializer='glorot_uniform',
kernel_regularizer=None,
kernel_constraint=None,
bias_initializer='zeros',
bias_regularizer=None,
bias_constraint=None,
activity_regularizer=None,
**kwargs):
# 初始化不需要训练的参数
self.units = units
# activation=None 使用线性激活函数(等价不使用激活函数)
self.activation = activations.get(activation)
self.use_bias = use_bias
# 初始化方法定义了对Keras层设置初始化权重(bias)的方法 glorot_uniform
self.kernel_initializer = initializers.get(kernel_initializer)
self.bias_initializer = initializers.get(bias_initializer)
# 加载正则化的方法
self.kernel_regularizer = regularizers.get(kernel_regularizer)
self.bias_regularizer = regularizers.get(bias_regularizer)
self.activity_regularizer = regularizers.get(activity_regularizer)
# 约束:对权重值施加约束的函数。
self.kernel_constraint = constraints.get(kernel_constraint)
self.bias_constraint = constraints.get(bias_constraint)
super(GCNConv, self).__init__()
def build(self, input_shape):
""" GCN has two inputs : [shape(An), shape(X)]
"""
# gsize = input_shape[0][0] # graph size
fdim = input_shape[1][1] # feature dim
# hasattr 检查该对象self是否有某个属性'weight'
if not hasattr(self, 'weight'):
self.weight = self.add_weight(name="weight",
shape=(fdim, self.units),
initializer=self.kernel_initializer,
constraint=self.kernel_constraint,
trainable=True)
if self.use_bias:
if not hasattr(self, 'bias'):
self.bias = self.add_weight(name="bias",
shape=(self.units, ),
initializer=self.bias_initializer,
constraint=self.bias_constraint,
trainable=True)
super(GCNConv, self).build(input_shape)
def call(self, inputs):
""" GCN has two inputs : [An, X]
对称归一化版本的GCN的核心公式计算过程
"""
self.An = inputs[0]
self.X = inputs[1]
# isinstance 函数来判断一个对象是否是一个已知的类型
if isinstance(self.X, tf.SparseTensor):
h = spdot(self.X, self.weight)
else:
# 二维数组矩阵之间的dot函数运算得到的乘积是矩阵乘积
h = dot(self.X, self.weight)
output = spdot(self.An, h)
if self.use_bias:
output = tf.nn.bias_add(output, self.bias)
if self.activation:
output = self.activation(output)
return output
参考文章:
GCN(Graph Convolutional Network)的理解