图(Graph),一般用 G=(V,E)G=(V,E)G=(V,E) 表示,这里的VVV是图中节点的集合,EEE 为边的集合,节点的个数用NNN表示。在一个图中,有三个比较重要的矩阵:

  1. 特征矩阵XXX:维度为 N×DN\times DN×D ,表示图中有N个节点,每个节点的特征个数是D。
  2. 邻居矩阵AAA:维度为 N×NN\times NN×N ,表示图中N个节点之间的连接关系。
  3. 度矩阵 DDD:维度为 N×NN\times NN×N ,是一个对角矩阵,即只有对角线上不为零,其他位置元素都是 0 ,表示图中N个节点与其他节点相连的边的个数。对于无权图而言,Dii=∑jAijD_{ii}=\sum_j A_{ij}Dii=jAij

邻接矩阵与度矩阵例子如下图所示:

对于图像(Image)数据,我们可以用卷积核来提取特征,无论卷积核覆盖在图像的哪个部分,其内部结构都是一样的,这是因为图片结构具有平移不变性,如下图左半部分所示:

但是对于图(Graph)数据而言,其形状是不规则的,不具有平移不变性。于是 GCN,也就是图卷积神经网络,其目标就是设计一种特征提取器,进而完成节点分类、变预测等任务,还顺便可以得到每个节点的 embedding 表示。

上面展示了一个简单的 3x3 的卷积核,每次自左向右,自上而下扫描图片(Image)时,都是将 3x3 的像素进行加权求和,即:∑i=19wixi\sum_{i=1}^9 w_i x_ii=19wixi,然后将求和的结果作为该 3x3 区域的特征。

那么在图(graph)中要怎么提取特征?这里给出两种思路。

图卷积

思路一

CNN加权求和的思想也可以应用到图(Graph)的特征提取上,如下图所示:

对于节点 iii,我们可以用其邻接节点加权求和的结果来表示当前节点,这个操作我们称为“聚合(aggregate)”:
agg(Xi)=∑j∈neighbor(i)AijXjagg(X_i) = \sum_{j \in neighbor(i)} A_{ij} X_j agg(Xi)=jneighbor(i)AijXj
考虑到与节点 iii 没有边连接的节点 jjj ,对应的权重 AijA_{ij}Aij 为 0 ,因此上面的公式又可以改写为:
agg(Xi)=∑j∈NAijXjagg(X_i) = \sum_{j \in N} A_{ij} X_j agg(Xi)=jNAijXj
那么,对于所有的节点而言,其聚合的结果可以用下面的公式表示:
agg(X)=AXagg(X) = AX agg(X)=AX
上面的公式只考虑了邻居加权求和的结果,很多情况下,节点自身的信息是不可忽略的,因此一般情况下会把自身的特征也加回来:
agg(Xi)=∑j∈NAijXj+Xiagg(X_i) = \sum_{j \in N} A_{ij} X_j + X_i agg(Xi)=jNAijXj+Xi
于是有:
agg(X)=AX+X=(A+I)Xagg(X) = AX + X = (A+I)X agg(X)=AX+X=(A+I)X
其中,III 是单位矩阵,令:
A~=A+I\tilde A = A+I A~=A+I
则有:
agg(X)=A~Xagg(X) = \tilde AX agg(X)=A~X
也就是说把单位矩阵 III 加到邻接矩阵 AAA 上,即可在聚合操作中加入自身特征了。

现在有个问题,只能用自身节点以及邻居节点加权求和的结果来表示某个节点的特征吗?其实还有另一种思路。

思路二

在某些情况下,我们更关注节点之间的差值,因此可以对差值进行加权求和:
agg(Xi)=∑j∈NAij(Xi−Xj)=DiiXi−∑j∈NAijXj\begin{aligned} agg(X_i) & = \sum_{j \in N} A_{ij} (X_i - X_j) \\ &= D_{ii}X_i- \sum_{j \in N} A_{ij}X_j \\ \end{aligned} agg(Xi)=jNAij(XiXj)=DiiXijNAijXj
其中,D 表示度矩阵,表示节点与其他节点相连的边的个数,对于无权图而言,Dii=∑jAijD_{ii}=\sum_j A_{ij}Dii=jAij

对于整个图的节点而言,上面的公式可以转换为矩阵化的表示:
agg(X)=DX−AX=(D−A)X\begin{aligned} agg(X) &= DX - AX \\ &= (D-A)X \end{aligned} agg(X)=DXAX=(DA)X

实际上,上面公式中的 D−AD-ADA 是拉普拉斯矩阵(用 LLL 表示):
L=D−AL = D - A L=DA
拉普拉斯矩阵如下图所示:

如果想更多地了解拉普拉斯矩阵在GCN中的作用,可以参考:如何理解 Graph Convolutional Network(GCN)?

归一化

无论是思路一的 A~\tilde AA~ 还是思路二的 LLL,与CNN的卷积相似之处都是局部数据的聚合操作,只不过CNN 中卷积的局部连接数是固定的。但是在Graph中每个节点的邻居个数都可能不同,进行聚合操作后,对于度较大的节点,得到的特征比较大,度较少的节点得到的特征就比较小,因此还需要进行归一化的处理。

归一化的思路有两种:

(1)算数平均
Lrw=D−1LL^{rw}=D^{-1}L Lrw=D1L
(2)几何平均
Lsym=D−0.5LD−0.5L^{sym}=D^{-0.5}LD^{-0.5} Lsym=D0.5LD0.5
几何平均受极端值影响较小,因此是GCN中比较常用的归一化方法,于是有:
agg(X)=LsymX=D−0.5LD−0.5X=D−0.5(D−A)D−0.5X\begin{aligned} agg(X) &= L^{sym} X \\ &= D^{-0.5}LD^{-0.5}X \\ &= D^{-0.5}(D-A)D^{-0.5} X \end{aligned} agg(X)=LsymX=D0.5LD0.5X=D0.5(DA)D0.5X
当然也可以是:
agg(X)=D−0.5A~D−0.5X=D−0.5(A+I)D−0.5X\begin{aligned} agg(X) & = D^{-0.5}\tilde A D^{-0.5} X\\ & = D^{-0.5}(A+I)D^{-0.5} X \end{aligned} agg(X)=D0.5A~D0.5X=D0.5(A+I)D0.5X
在实际的GCN代码实现中,会对聚合结果进行一些变换,第 lll 层到第 l+1l+1l+1 层的传播方式为:
H(l+1)=σ(D~−12A~D~−12H(l)W(l))H^{(l+1)}=\sigma\left(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)}\right) H(l+1)=σ(D~21A~D~21H(l)W(l))
其中:

  • A~=A+I\tilde A=A+IA~=A+I ,也可以是 A~=D−A\tilde A = D - AA~=DA
  • D~\tilde DD~A~\tilde AA~ 的度矩阵,每个元素为:D~ii=∑jA~ij\tilde D_{ii}=\sum_j \tilde A_{ij}D~ii=jA~ij
  • HHH 是每一层的特征,对于输入层而言,HHH 就是 XXX
  • σ 是 sigmoid 函数

由于 D 是在矩阵 A 的基础上得到的,因此在给定矩阵 A 之后,D~−12A~D~−12\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}}D~21A~D~21 就可以事先计算好。

代码实现

相关代码可以在文末获取。

Cora 数据集介绍

Cora数据集由机器学习论文组成,是近年来图深度学习很喜欢使用的数据集。整个数据集有2708篇论文,所有样本点被分为8个类别,类别分别是1)基于案例;2)遗传算法;3)神经网络;4)概率方法;5)强化学习;6)规则学习;7)理论。每篇论文都由一个1433维的词向量表示,所以,每个样本点具有1433个特征。词向量的每个元素都对应一个词,且该元素只有0或1两个取值。取0表示该元素对应的词不在论文中,取1表示在论文中。

定义图卷积层

import tensorflow as tf
from tensorflow.keras import activations, regularizers, constraints, initializersclass GCNConv(tf.keras.layers.Layer):def __init__(self,units,activation=lambda x: x,use_bias=True,kernel_initializer='glorot_uniform',bias_initializer='zeros',**kwargs):super(GCNConv, self).__init__()self.units = unitsself.activation = activations.get(activation)self.use_bias = use_biasself.kernel_initializer = initializers.get(kernel_initializer)self.bias_initializer = initializers.get(bias_initializer)def build(self, input_shape):""" GCN has two inputs : [shape(An), shape(X)]"""fdim = input_shape[1][1]  # feature dim# 初始化权重矩阵self.weight = self.add_weight(name="weight",shape=(fdim, self.units),initializer=self.kernel_initializer,trainable=True)if self.use_bias:# 初始化偏置项self.bias = self.add_weight(name="bias",shape=(self.units, ),initializer=self.bias_initializer,trainable=True)def call(self, inputs):""" GCN has two inputs : [An, X]"""self.An = inputs[0]self.X = inputs[1]# 计算 XWif isinstance(self.X, tf.SparseTensor):h = tf.sparse.sparse_dense_matmul(self.X, self.weight)else:h = tf.matmul(self.X, self.weight)# 计算 AXWoutput = tf.sparse.sparse_dense_matmul(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 模型

class GCN():def __init__(self, An, X, sizes, **kwargs):self.with_relu = Trueself.with_bias = Trueself.lr = FLAGS.learning_rateself.dropout = FLAGS.dropoutself.verbose = FLAGS.verboseself.An = Anself.X = Xself.layer_sizes = sizesself.shape = An.shapeself.An_tf = sp_matrix_to_sp_tensor(self.An)self.X_tf = sp_matrix_to_sp_tensor(self.X)self.layer1 = GCNConv(self.layer_sizes[0], activation='relu')self.layer2 = GCNConv(self.layer_sizes[1])self.opt = tf.optimizers.Adam(learning_rate=self.lr)def train(self, idx_train, labels_train, idx_val, labels_val):K = labels_train.max() + 1train_losses = []val_losses = []# use adam to optimizefor it in range(FLAGS.epochs):tic = time()with tf.GradientTape() as tape:_loss = self.loss_fn(idx_train, np.eye(K)[labels_train])# optimize over weightsgrad_list = tape.gradient(_loss, self.var_list)grads_and_vars = zip(grad_list, self.var_list)self.opt.apply_gradients(grads_and_vars)# evaluate on the trainingtrain_loss, train_acc = self.evaluate(idx_train, labels_train, training=True)train_losses.append(train_loss)val_loss, val_acc = self.evaluate(idx_val, labels_val, training=False)val_losses.append(val_loss)toc = time()if self.verbose:print("iter:{:03d}".format(it),"train_loss:{:.4f}".format(train_loss),"train_acc:{:.4f}".format(train_acc),"val_loss:{:.4f}".format(val_loss),"val_acc:{:.4f}".format(val_acc),"time:{:.4f}".format(toc - tic))return train_lossesdef loss_fn(self, idx, labels, training=True):if training:# .nnz 是获得X中元素的个数_X = sparse_dropout(self.X_tf, self.dropout, [self.X.nnz])else:_X = self.X_tfself.h1 = self.layer1([self.An_tf, _X])if training:_h1 = tf.nn.dropout(self.h1, self.dropout)else:_h1 = self.h1self.h2 = self.layer2([self.An_tf, _h1])self.var_list = self.layer1.weights + self.layer2.weights# calculate the loss base on idx and labels_logits = tf.gather(self.h2, idx)_loss_per_node = tf.nn.softmax_cross_entropy_with_logits(labels=labels,logits=_logits)_loss = tf.reduce_mean(_loss_per_node)# 加上 l2 正则化项_loss += FLAGS.weight_decay * sum(map(tf.nn.l2_loss, self.layer1.weights))return _lossdef evaluate(self, idx, true_labels, training):K = true_labels.max() + 1_loss = self.loss_fn(idx, np.eye(K)[true_labels], training=training).numpy()_pred_logits = tf.gather(self.h2, idx)_pred_labels = tf.argmax(_pred_logits, axis=1).numpy()_acc = accuracy_score(_pred_labels, true_labels)return _loss, _acc

训练模型

# 计算标准化的邻接矩阵:根号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_halfreturn 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 小结

本文使用到的代码与数据集地址:https://github.com/zxxwin/tf2_gcn

GCN的优点: 可以捕捉graph的全局信息,从而很好地表示node的特征。

GCN的缺点:

  1. 属于直推式(transductive)的学习方式,模型学习的权重W与图的邻接矩阵A和度矩阵D息息相关,一旦图的结构发生变化,那么A与D也就变化了,模型就得重新训练。
  2. 需要把所有节点都参与训练才能得到node embedding,当图的节点很多,图的结构很复杂时,训练成本非常高,难以快速适应图结构的变化。

参考文章:

如何理解 Graph Convolutional Network(GCN)?

2020年,我终于决定入门GCN

GCN(Graph Convolutional Network)的理解

图卷积神经网络(GCN)理解与tensorflow2.0代码实现相关推荐

  1. 深入理解图卷积神经网络(GCN)原理

    深入理解图卷积神经网络(GCN)原理 文章目录 深入理解图卷积神经网络(GCN)原理 前言 一.为什么需要GCN 二.GCN的原理 1.图的定义 2.GCN来了 2.1 矩阵计算公式 2.2 以小规模 ...

  2. DeepLearning | 图卷积神经网络(GCN)解析(论文、算法、代码)

    本篇博客主要讲述三种图卷积网络(Graph Convolutional Network, GCN)的算法原理及python实现,全文阅读时间约10分钟. 博主关于图神经网络的文章 DeepLearni ...

  3. 图卷积神经网络GCN大白话解读!

    何时能懂你的心--图卷积神经网络(GCN) https://zhuanlan.zhihu.com/p/71200936 蝈蝈 把知道的讲清楚.公众号SimpleAI,欢迎来逛逛. 已关注 天雨粟 . ...

  4. (21) 出行需求预测新视角---基于图卷积神经网络GCN的出租车OD需求预测

    交通预见未来(21): 出行需求预测新视角---基于图卷积神经网络GCN的出租车OD需求预测 1.文章信息 <Origin-Destination Matrix Prediction via G ...

  5. 图卷积神经网络(GCN)综述与实现(PyTorch版)

    图卷积神经网络(GCN)综述与实现(PyTorch版) 本文的实验环境为PyTorch = 1.11.0 + cu113,PyG = 2.0.4,相关依赖库和数据集的下载请见链接. 一.图卷积神经网络 ...

  6. 图卷积神经网络GCN的一些理解以及DGL代码实例的一些讲解

    文章目录 前言 GCN 传播公式 例1 例2 DGL中的GCN实例 dgl.DGLGraph.update_all 参考 前言 近些年图神经网络十分火热,因为图数据结构其实在我们的现实生活中更常见,例 ...

  7. tensorflow lstm 预测_图卷积神经网络GCN与递归结构RNN相结合的时间序列预测

    时间序列预测任务可以按照不同的方法执行.最经典的是基于统计和自回归的方法.更准确的是基于增强和集成的算法,我们必须使用滚动周期生成大量有用的手工特性.另一方面,我们可以使用在开发过程中提供更多自由的神 ...

  8. 【深度学习理论】(5) 图卷积神经网络 GCN

    尽管在过去的几年中,神经网络的兴起与成功应用使得许多目标检测.自然语言处理.语音识别等领域都有了飞跃发展,但是将 RNN 或者GCN这样的深度学习模型拓展到任意结构的图上是一个有挑战性的问题.受限于传 ...

  9. 图卷积神经网络GCN中的关键公式推导---干货

    GCN推导(比较关键的部分) GCN公式推导的时候,需要用到线性代数和傅里叶变换的一些定理, 比如拉普拉斯矩阵是实对称矩阵,其标准化都的数值分布在[0,2]之间 比如傅里叶变换的原理就是多个正弦余弦函 ...

最新文章

  1. iPhone开发进阶(9)--- 用SQLite管理数据库
  2. python基础:冒泡和选择排序算法实现
  3. POI的入门:概述和创建EXCEL
  4. ad19电气规则检查_PROTEL DXP电气规则检查
  5. python中文字符串转list
  6. Multimedia (MP3, MPEG-4, AVI, DiVX, etc.) support in Ubuntu 12.04 (Precise)
  7. [HDOJ1897]继续畅通工程
  8. Linux基础——常用命令
  9. spring MVC 及 AOP 原理
  10. 一张图学会python应用到excel-Python | 编写一个简单的Excel处理脚本
  11. Ubuntu16.04.1 安装Redis-Cluster
  12. lammps计算的应力的方法
  13. mysql explain G_MySQL 性能优化神器 Explain 使用分析
  14. 贪心算法(greedy algorithm,又称贪婪算法)详解(附例题)
  15. 360浏览器极速模式pdf文件不能预览问题
  16. 嵌入式研发人员核心竞争力分析
  17. CCNA-思科网络 EIGRP(增强内部网关路由协议)
  18. 台式电脑系统崩掉该如何?
  19. Banana Pi BPI-R2 Pro 开源路由器采用瑞芯微Rockchip RK3568芯片方案设计
  20. 什么是Java?什么是JavaWeb?

热门文章

  1. RStudio修改快捷键确保每次运行都是从头运行所有代码
  2. 统计学习方法例2.1实现(转)
  3. NLTK找出最频繁的名词标记的程序(代码详细解释)
  4. python查询天气
  5. 大话数据结构03:静态链表
  6. 图像工程CH4 频域图像增强
  7. android root 挂载分区,adb — adb disable-verity, adb remount 实现重新挂载system分区为可读写分区...
  8. amazeui学习笔记一(开始使用4)--Web App 相关
  9. linux shell中fork、source、exec的区别
  10. va_start、va_end、va_list的使用