图卷积神经网络(GCN)理解与 tensorflow2.0 代码实现
图(Graph),一般用 表示,这里的是图中节点的集合, 为边的集合,节点的个数用表示。在一个图中,有三个比较重要的矩阵:
- 特征矩阵:维度为
- 邻居矩阵:维度为
- 度矩阵 :维度为 ,是一个对角矩阵,即只有对角线上不为零,其他位置元素都是 0 ,表示图中 N 个节点与其他节点相连的边的个数。对于无权图而言,
邻接矩阵与度矩阵例子如下图所示:
对于图像(Image)数据,我们可以用卷积核来提取特征,无论卷积核覆盖在图像的哪个部分,其内部结构都是一样的,这是因为图片结构具有平移不变性,如下图左半部分所示:
但是对于图(Graph)数据而言,其形状是不规则的,不具有平移不变性。于是 GCN,也就是图卷积神经网络,其目标就是设计一种特征提取器,进而完成节点分类、变预测等任务,还顺便可以得到每个节点的 embedding 表示。
上面展示了一个简单的 3x3 的卷积核,每次自左向右,自上而下扫描图片(Image)时,都是将 3x3 的像素进行加权求和,即:,然后将求和的结果作为该 3x3 区域的特征。
那么在图(graph)中要怎么提取特征?这里给出两种思路。
图卷积
思路一
CNN 加权求和的思想也可以应用到图(Graph)的特征提取上,如下图所示:
对于节点 ,我们可以用其邻接节点加权求和的结果来表示当前节点,这个操作我们称为“聚合(aggregate)”:
考虑到与节点 没有边连接的节点 ,对应的权重
那么,对于所有的节点而言,其聚合的结果可以用下面的公式表示:
上面的公式只考虑了邻居加权求和的结果,很多情况下,节点自身的信息是不可忽略的,因此一般情况下会把自身的特征也加回来:
于是有:
其中,
则有:
也就是说把单位矩阵 加到邻接矩阵
现在有个问题,只能用自身节点以及邻居节点加权求和的结果来表示某个节点的特征吗?其实还有另一种思路。
思路二
在某些情况下,我们更关注节点之间的差值,因此可以对差值进行加权求和:
其中,D 表示度矩阵,表示节点与其他节点相连的边的个数,对于无权图而言,
对于整个图的节点而言,上面的公式可以转换为矩阵化的表示:
实际上,上面公式中的 是拉普拉斯矩阵(用
拉普拉斯矩阵如下图所示:
如果想更多地了解拉普拉斯矩阵在 GCN 中的作用,可以参考:如何理解 Graph Convolutional Network(GCN)?
归一化
无论是思路一的 还是思路二的 ,与 CNN 的卷积相似之处都是局部数据的聚合操作,只不过 CNN 中卷积的局部连接数是固定的。但是在 Graph 中每个节点的邻居个数都可能不同,进行聚合操作后,对于度较大的节点,得到的特征比较大,度较少的节点得到的特征就比较小,因此还需要进行归一化的处理。
归一化的思路有两种:
(1)算数平均
(2)几何平均
几何平均受极端值影响较小,因此是 GCN 中比较常用的归一化方法,于是有:
当然也可以是:
在实际的 GCN 代码实现中,会对聚合结果进行一些变换,第 层到第
其中:
- ,也可以是
- 是 的度矩阵,每个元素为:
- 是每一层的特征,对于输入层而言, 就是
- σ 是 sigmoid 函数
由于 D 是在矩阵 A 的基础上得到的,因此在给定矩阵 A 之后,
代码实现
Cora 数据集介绍
Cora 数据集由机器学习论文组成,是近年来图深度学习很喜欢使用的数据集。整个数据集有 2708 篇论文,所有样本点被分为 8 个类别,类别分别是 1)基于案例;2)遗传算法;3)神经网络;4)概率方法;5)强化学习;6)规则学习;7)理论。每篇论文都由一个 1433 维的词向量表示,所以,每个样本点具有 1433 个特征。词向量的每个元素都对应一个词,且该元素只有 0 或 1 两个取值。取 0 表示该元素对应的词不在论文中,取 1 表示在论文中。
训练模型
# 计算标准化的邻接矩阵:根号D * A * 根号D
def preprocess_graph(adj):
# _A = A + I
_adj = adj + sp.eye(adj.shape[0])
# _dseq:各个节点的度构成的列表
_dseq = _adj.sum(1).A1
# 构造开根号的度矩阵
_D_half = sp.diags(np.power(_dseq, -0.5))
# 计算标准化的邻接矩阵, @ 表示矩阵乘法
adj_normalized = _D_half @ _adj @ _D_half
return adj_normalized.tocsr()
if __name__ == "__main__":
# 读取数据
# A_mat:邻接矩阵,以scipy的csr形式存储
# X_mat:特征矩阵,以scipy的csr形式存储
# z_vec:label
# train_idx,val_idx,test_idx: 要使用的节点序号
A_mat, X_mat, z_vec, train_idx, val_idx, test_idx = load_data_planetoid(FLAGS.dataset)
# 邻居矩阵标准化
An_mat = preprocess_graph(A_mat)
# 节点的类别个数
K = z_vec.max() + 1
# 构造GCN模型
gcn = GCN(An_mat, X_mat, [FLAGS.hidden1, K])
# 训练
gcn.train(train_idx, z_vec[train_idx], val_idx, z_vec[val_idx])
# 测试
test_res = gcn.evaluate(test_idx, z_vec[test_idx], training=False)
print("Dataset {}".format(FLAGS.dataset),
"Test loss {:.4f}".format(test_res[0]),
"test acc {:.4f}".format(test_res[1]))
GCN 小结
GCN 的优点: 可以捕捉 graph 的全局信息,从而很好地表示 node 的特征。
GCN 的缺点:属于直推式(transductive)的学习方式,需要把所有节点都参与训练才能得到 node embedding,无法快速得到新 node 的 embedding。如果要得到新节点的表示,很可能就需要重新训练所有节点的表示。
参考文章:
如何理解 Graph Convolutional Network(GCN)?
GCN(Graph Convolutional Network)的理解