心得体会

(1)这次学习了超大图上的节点表征学习面临的挑战——目前基于SGD的算法要么面临着随GCN层数呈指数增长的高计算成本,要么面临着保存整个图形和每个节点的embedding到内存的巨大空间需求;应对这一挑战的,基于图聚类结构且适合于基于SGD训练的GCN算法——Cluster-GCN方法,并且学习使用Cluster-GCN方法进行超大图节点分类的方法。

(2)Cluster-GCN的大致思想:在每个步骤中,它对一个与通过用图聚类算法来区分的密集子图相关联的一组节点进行采样,并限制该子图中的邻居搜索。
效果:显著提高内存和计算效率,同时能够达到与以前算法相当的测试精度。

(3)一些论文细节还要学习​​[论文笔记]Cluster-GCN: An Efficient Algorithm for Training Deep and Large Graph Convolutional Networks​​​和对应论文原文​​Cluster-GCN: An Efficient Algorithm for Training Deep and Large Graph Convolutional Network​​。

文章目录

基于SGD的图神经网络的训练方法:

(1)面临着随着图神经网络层数增加,计算成本呈指数增长的问题,
(2)面临着保存整个图的信息和每一层每个节点的表征到内存(显存)而消耗巨大内存(显存)空间的问题。

虽然已经有一些论文提出了无需保存整个图的信息和每一层每个节点的表征到GPU内存(显存)的方法,但这些方法可能会损失预测精度或者对提高内存的利用率并不明显
于是论文​​​Cluster-GCN: An Efficient Algorithm for Training Deep and Large Graph Convolutional Network​​提出了一种新的图神经网络的训练方法
下面的任务:
(1)对Cluster-GCN论文中提出的方法(后文中简称为Cluster-GCN方法)做简单概括,
(2)深入分析超大图上的节点表征学习面临的挑战,
(3)对Cluster-GCN方法做深入分析。

一、Cluster-GCN方法简单概括

为了解决普通训练方法无法训练超大图的问题,Cluster-GCN论文提出:

  • 利用图节点聚类算法将一个图的节点划分为【GNN】task5-超大图上的节点表征学习_GNN_03个簇,每一次选择几个簇的节点和这些节点对应的边构成一个子图,然后对子图做训练。
  • 由于是利用图节点聚类算法将节点划分为多个簇,所以簇内边的数量要比簇间边的数量多得多,所以可以提高表征利用率,并提高图神经网络的训练效率。
  • 每一次随机选择多个簇来组成一个batch,这样不会丢失簇间的边,同时也不会有batch内类别分布偏差过大的问题。
  • 基于小图进行训练,不会消耗很多内存空间,于是我们可以训练更深的神经网络,进而可以达到更高的精度。

二、节点表征学习回顾

给定一个图【GNN】task5-超大图上的节点表征学习_GNN_04,它由【GNN】task5-超大图上的节点表征学习_深度学习_05个节点和【GNN】task5-超大图上的节点表征学习_神经网络_06条边组成,其邻接矩阵记为【GNN】task5-超大图上的节点表征学习_GNN_07,其节点属性记为【GNN】task5-超大图上的节点表征学习_聚类算法_08【GNN】task5-超大图上的节点表征学习_邻接矩阵_09表示节点属性的维度。一个【GNN】task5-超大图上的节点表征学习_深度学习_10层的图卷积神经网络由【GNN】task5-超大图上的节点表征学习_深度学习_10个图卷积层组成,每一层都通过聚合邻接节点的上一层的表征来生成中心节点的当前层的表征:
【GNN】task5-超大图上的节点表征学习_邻接矩阵_12

上面公式的符号含义

(1)【GNN】task5-超大图上的节点表征学习_深度学习_13表示第【GNN】task5-超大图上的节点表征学习_邻接矩阵_14【GNN】task5-超大图上的节点表征学习_神经网络_15个节点的表征,并且有【GNN】task5-超大图上的节点表征学习_邻接矩阵_16
(2)【GNN】task5-超大图上的节点表征学习_神经网络_17是归一化和规范化后的邻接矩阵,
(3)【GNN】task5-超大图上的节点表征学习_邻接矩阵_18是权重矩阵,也就是要训练的参数。
(4)为了简单起见,我们假设所有层的表征维度都是一样的,即 【GNN】task5-超大图上的节点表征学习_聚类算法_19
(5)激活函数【GNN】task5-超大图上的节点表征学习_神经网络_20通常被设定为​​​ReLU​​。

训练目标

当图神经网络应用于半监督节点分类任务时,训练的目标是通过最小化损失函数来学习公式(1)中的权重矩阵:
【GNN】task5-超大图上的节点表征学习_深度学习_21
其中,【GNN】task5-超大图上的节点表征学习_邻接矩阵_22是节点类别;【GNN】task5-超大图上的节点表征学习_GNN_23【GNN】task5-超大图上的节点表征学习_神经网络_24的第【GNN】task5-超大图上的节点表征学习_神经网络_25行,表示对节点【GNN】task5-超大图上的节点表征学习_神经网络_25的预测,节点【GNN】task5-超大图上的节点表征学习_神经网络_25的真实类别为【GNN】task5-超大图上的节点表征学习_GNN_28

三、Cluster-GCN方法详细分析

1.Full-batch gradient descent

(1)以往的训练方法需要同时计算所有节点的表征以及训练集中所有节点的损失产生的梯度(后文我们直接称为完整梯度)。这种训练方式需要非常巨大的计算开销和内存(显存)开销:在内存(显存)方面,计算公式(2)的完整梯度需要存储所有的节点表征矩阵【GNN】task5-超大图上的节点表征学习_GNN_29,这需要【GNN】task5-超大图上的节点表征学习_聚类算法_30的空间;
(2)在收敛速度方面,由于神经网络在每个epoch中只更新一次,所以训练需要更多的epoch才能达到收敛

2.Mini-batch gradient descent

最近的一些工作证明,采用mini-batch SGD的方式训练,可以提高图神经网络的训练速度并减少内存(显存)需求
(1)在参数更新中,SGD不需要计算完整梯度,而只需要基于mini-batch计算部分梯度。我们使用【GNN】task5-超大图上的节点表征学习_GNN_31来表示一个batch,其大小为【GNN】task5-超大图上的节点表征学习_深度学习_32。SGD的每一步都将计算梯度估计值【GNN】task5-超大图上的节点表征学习_聚类算法_33来进行参数更新。
(2)尽管在epoches数量相同的情况下,采用SGD方式进行训练,收敛速度可以更快,但此种训练方式会引入额外的时间开销,这使得相比于全梯度下降的训练方式,此种训练方式每个epoch的时间开销要大得多

问题:为什么采用最简单的mini-batch SGD进行训练,每个epoch用时更多?

邻域扩展

我们将节点【GNN】task5-超大图上的节点表征学习_神经网络_25的梯度的计算表示为【GNN】task5-超大图上的节点表征学习_神经网络_35,它依赖于节点【GNN】task5-超大图上的节点表征学习_神经网络_25【GNN】task5-超大图上的节点表征学习_深度学习_10层的表征,而节点【GNN】task5-超大图上的节点表征学习_聚类算法_38的非第【GNN】task5-超大图上的节点表征学习_GNN_39层的表征都依赖于各自邻接节点的前一层的表征,这被称为邻域扩展

假设一个图神经网络有【GNN】task5-超大图上的节点表征学习_邻接矩阵_40层,节点的平均的度为【GNN】task5-超大图上的节点表征学习_深度学习_41为了得到节点【GNN】task5-超大图上的节点表征学习_聚类算法_38的梯度,平均我们需要聚合图上【GNN】task5-超大图上的节点表征学习_神经网络_43的节点的表征。也就是说,我们需要获取节点的距离为【GNN】task5-超大图上的节点表征学习_神经网络_44的邻接节点的信息来进行一次参数更新。由于要与权重矩阵【GNN】task5-超大图上的节点表征学习_聚类算法_45相乘,所以计算任意节点表征的时间开销是【GNN】task5-超大图上的节点表征学习_神经网络_46。所以平均来说,一个节点的梯度的计算需要【GNN】task5-超大图上的节点表征学习_神经网络_47的时间

节点表征的利用率可以反映出计算的效率。
考虑到一个batch有多个节点,时间与空间复杂度的计算就不是上面那样简单了,因为不同的节点同样距离远的邻接节点可以是重叠的,于是计算表征的次数可以小于最坏的情况【GNN】task5-超大图上的节点表征学习_邻接矩阵_48

注意:上面说的【GNN】task5-超大图上的节点表征学习_深度学习_41是每个点度数,【GNN】task5-超大图上的节点表征学习_深度学习_10是GCN的层数。

表征利用率

为了反映mini-batch SGD的计算效率,Cluster-GCN论文提出了"表征利用率"的概念来描述计算效率。在训练过程中,如果节点【GNN】task5-超大图上的节点表征学习_神经网络_25【GNN】task5-超大图上的节点表征学习_邻接矩阵_14层的表征【GNN】task5-超大图上的节点表征学习_聚类算法_53被计算并在【GNN】task5-超大图上的节点表征学习_GNN_54层的表征计算中被重复使用【GNN】task5-超大图上的节点表征学习_深度学习_55次,那么我们说【GNN】task5-超大图上的节点表征学习_聚类算法_53的表征利用率为【GNN】task5-超大图上的节点表征学习_深度学习_55对于随机抽样的mini-batch SGD,【GNN】task5-超大图上的节点表征学习_邻接矩阵_58非常小,因为图通常是大且稀疏的。

邻域扩展问题

假设【GNN】task5-超大图上的节点表征学习_深度学习_55是一个小常数(节点间同样距离的邻接节点重叠率小),那么mini-batch SGD的训练方式对每个batch需要计算【GNN】task5-超大图上的节点表征学习_邻接矩阵_48的表征,于是每次参数更新需要【GNN】task5-超大图上的节点表征学习_邻接矩阵_61的时间,每个epoch需要【GNN】task5-超大图上的节点表征学习_GNN_62的时间,这被称为邻域扩展问题

相反的是,全梯度下降训练具有最大的表征利用率——每个节点表征将在上一层被重复使用平均节点度次。因此,全梯度下降法在每个epoch中只需要计算【GNN】task5-超大图上的节点表征学习_聚类算法_63的表征,这意味着平均下来只需要【GNN】task5-超大图上的节点表征学习_GNN_64的表征计算就可以获得一个节点的梯度。

【GNN】task5-超大图上的节点表征学习_神经网络_65

图1 - 邻域扩展问题说明:过去的方法和Cluster-GCN方法之间的邻域扩展差异。红色节点是邻域扩展的起始节点。过去的方法需要做指数级的邻域扩展(图左),而Cluster-GCN的方法可以避免巨大范围的邻域扩展(图右)。

3.简单的Cluster-GCN方法

Cluster-GCN方法是由这样的问题驱动的:我们能否找到一种将节点分成多个batch的方式,对应地将图划分成多个子图,使得表征利用率最大?我们通过将表征利用率的概念与图节点聚类的目标联系起来来回答这个问题。

一.将SGD图神经网络训练的效率与图聚类算法联系起来

考虑到在每个batch中,我们计算一组节点(记为【GNN】task5-超大图上的节点表征学习_神经网络_66)从第【GNN】task5-超大图上的节点表征学习_神经网络_67层到第【GNN】task5-超大图上的节点表征学习_深度学习_10层的表征。由于图神经网络每一层的计算都使用相同的子图【GNN】task5-超大图上的节点表征学习_神经网络_69【GNN】task5-超大图上的节点表征学习_神经网络_66内部的边),所以表征利用率就是这个batch内边的数量,记为【GNN】task5-超大图上的节点表征学习_聚类算法_71。因此,为了最大限度地提高表征利用率,理想的划分batch的结果是,batch内的边尽可能多,batch之间的边尽可能少。基于这一点,我们将SGD图神经网络训练的效率与图聚类算法联系起来。

现在我们正式学习Cluster-GCN方法。对于一个图【GNN】task5-超大图上的节点表征学习_深度学习_72,我们将其节点划分为【GNN】task5-超大图上的节点表征学习_深度学习_73个簇:【GNN】task5-超大图上的节点表征学习_深度学习_74,其中【GNN】task5-超大图上的节点表征学习_邻接矩阵_75由第【GNN】task5-超大图上的节点表征学习_深度学习_76个簇中的节点组成,对应的我们有【GNN】task5-超大图上的节点表征学习_深度学习_73个子图:

【GNN】task5-超大图上的节点表征学习_聚类算法_78

其中【GNN】task5-超大图上的节点表征学习_邻接矩阵_79只由【GNN】task5-超大图上的节点表征学习_邻接矩阵_75中的节点之间的边组成。经过节点重组,邻接矩阵被划分为大小为【GNN】task5-超大图上的节点表征学习_聚类算法_81的块矩阵,如下所示
【GNN】task5-超大图上的节点表征学习_深度学习_82
其中
【GNN】task5-超大图上的节点表征学习_聚类算法_83

1.符号表示

(1)对角线上的块【GNN】task5-超大图上的节点表征学习_聚类算法_84是大小为【GNN】task5-超大图上的节点表征学习_邻接矩阵_85的邻接矩阵,它由【GNN】task5-超大图上的节点表征学习_深度学习_86内部的边构成。
(2)【GNN】task5-超大图上的节点表征学习_深度学习_87是图【GNN】task5-超大图上的节点表征学习_聚类算法_88的邻接矩阵。
(3)【GNN】task5-超大图上的节点表征学习_邻接矩阵_89由两个簇【GNN】task5-超大图上的节点表征学习_深度学习_90【GNN】task5-超大图上的节点表征学习_邻接矩阵_75之间的边构成。
(4)【GNN】task5-超大图上的节点表征学习_聚类算法_92是由【GNN】task5-超大图上的节点表征学习_GNN_07的所有非对角线块组成的矩阵。
(5)同样,我们可以根据【GNN】task5-超大图上的节点表征学习_邻接矩阵_94划分节点表征矩阵【GNN】task5-超大图上的节点表征学习_深度学习_95和类别向量【GNN】task5-超大图上的节点表征学习_邻接矩阵_96,得到【GNN】task5-超大图上的节点表征学习_聚类算法_97【GNN】task5-超大图上的节点表征学习_邻接矩阵_98,其中【GNN】task5-超大图上的节点表征学习_神经网络_99【GNN】task5-超大图上的节点表征学习_邻接矩阵_100分别由【GNN】task5-超大图上的节点表征学习_邻接矩阵_101中节点的表征和类别组成。

2.近似邻接矩阵

接下来我们用块对角线邻接矩阵【GNN】task5-超大图上的节点表征学习_邻接矩阵_102去近似邻接矩阵【GNN】task5-超大图上的节点表征学习_GNN_103,这样做的好处是,完整的损失函数(公示(2))可以根据batch分解成多个部分之和。以【GNN】task5-超大图上的节点表征学习_邻接矩阵_104表示归一化后的【GNN】task5-超大图上的节点表征学习_深度学习_87,最后一层节点表征矩阵可以做如下的分解:
【GNN】task5-超大图上的节点表征学习_邻接矩阵_106
由于【GNN】task5-超大图上的节点表征学习_深度学习_87是块对角形式(【GNN】task5-超大图上的节点表征学习_聚类算法_108【GNN】task5-超大图上的节点表征学习_邻接矩阵_104的对角线上的块),于是损失函数可以分解为
【GNN】task5-超大图上的节点表征学习_深度学习_110
基于公式(6)和公式(7),在训练的每一步中,Cluster-GCN首先采样一个簇【GNN】task5-超大图上的节点表征学习_GNN_111,然后根据【GNN】task5-超大图上的节点表征学习_深度学习_112的梯度进行参数更新。这种训练方式,只需要用到子图【GNN】task5-超大图上的节点表征学习_聚类算法_84, 【GNN】task5-超大图上的节点表征学习_神经网络_99, 【GNN】task5-超大图上的节点表征学习_邻接矩阵_100以及神经网络权重矩阵【GNN】task5-超大图上的节点表征学习_神经网络_116。 实际中,主要的计算开销在神经网络前向过程中的矩阵乘法运算(公式(6)的一个行)和梯度反向传播

3.图节点聚类算法来划分图

我们使用图节点聚类算法来划分图。图节点聚类算法将图节点分成多个簇,划分结果是簇内边的数量远多于簇间边的数量。如前所述,每个batch的表征利用率相当于簇内边的数量。直观地说,每个节点和它的邻接节点大部分情况下都位于同一个簇中,因此【GNN】task5-超大图上的节点表征学习_神经网络_117跳(L-hop)远的邻接节点大概率仍然在同一个簇中。由于我们用块对角线近似邻接矩阵【GNN】task5-超大图上的节点表征学习_深度学习_87代替邻接矩阵【GNN】task5-超大图上的节点表征学习_GNN_07,产生的误差与簇间的边的数量【GNN】task5-超大图上的节点表征学习_聚类算法_92成正比,所以簇间的边越少越好。综上所述,使用图节点聚类算法对图节点划分多个簇的结果,正是我们希望得到的。

再次来看图1,我们可以看到,Cluster-GCN方法可以避免巨大范围的邻域扩展(图右),因为Cluster-GCN方法将邻域扩展限制在簇内。

【GNN】task5-超大图上的节点表征学习_聚类算法_121


表1显示了两种不同的节点划分策略:随机划分与聚类划分。两者都使用一个分区作为一个batch来进行神经网络训练。我们可以看到,在相同的epoches下,使用聚类分区可以达到更高的精度。

表1:随机分区与聚类分区的对比(采用mini-batch SGD训练)。聚类分区得到更好的性能(就测试F1集得分而言),因为它删除的分区间的边较少。

Dataset

random partition

clustering partition

Cora

78.4

82.5

Pubmed

78.9

79.9

PPI

68.1

92.9

二.时间与空间复杂度分析

(1)时间复杂度

由于簇【GNN】task5-超大图上的节点表征学习_邻接矩阵_75中每个节点只连接到该簇内部的节点,节点的邻域扩展不需要在簇外进行。每个batch的计算将纯粹是矩阵乘积运算(【GNN】task5-超大图上的节点表征学习_神经网络_123)和一些对元素的操作(ReLU),因此每个batch的总体时间复杂度为【GNN】task5-超大图上的节点表征学习_神经网络_124。因此,每个epoch的总体时间复杂度为【GNN】task5-超大图上的节点表征学习_聚类算法_125

平均来说,每个batch只需要计算【GNN】task5-超大图上的节点表征学习_神经网络_126的表征,这是线性的,而不是指数级的。

(2)空间复杂度

在空间复杂度方面,在每个batch中,我们只需要在每一层中存储【GNN】task5-超大图上的节点表征学习_神经网络_127个节点的表征,产生用于存储表征的内存(显存)开销为【GNN】task5-超大图上的节点表征学习_聚类算法_128。因此,此算法也比之前所有的算法的内存效率更高。

此外,我们的算法只需加载子图到内存(显存)中,而不是完整的图(尽管图的存储通常不是内存瓶颈)。表2中总结了详细的时间和内存复杂度。

表2:时间和空间复杂性

【GNN】task5-超大图上的节点表征学习_深度学习_129

4.改进简单Cluster-GCN-随机多分区

尽管简单Cluster-GCN方法可以做到较其他方法更低的计算和内存复杂度,但它仍存在两个潜在问题:

  • 图被分割后,一些边(公式(4)中的【GNN】task5-超大图上的节点表征学习_深度学习_130部分)被移除,性能可能因此会受到影响。
  • 图聚类算法倾向于将相似的节点聚集在一起。因此,单个簇中节点的类别分布可能与原始数据集不同,导致对梯度的估计有偏差。

图2展示了一个类别分布不平衡的例子,该例子使用Reddit数据集,节点聚类由Metis软件包实现。根据各个簇的类别分布来计算熵值。与随机划分相比,采用聚类划分得到的大多数簇熵值都很小,簇熵值小表明簇中节点的标签分布偏向于某一些类别,这意味着不同簇的标签分布有较大的差异,这将影响训练的收敛。

【GNN】task5-超大图上的节点表征学习_聚类算法_131

图2:类别分布熵值柱状图。
类别分布熵越高意味着簇内类别分布越平衡,反之意味着簇内类别分布越不平衡。此图展示了不同熵值的随机分区和聚类分区的簇的数量,大多数聚类分区的簇具有较低的熵,表明各个簇内节点的类别分布存在偏差。相比之下,随机分区会产生类别分布熵很高的簇,尽管基于随机分区的训练的效率较低。在这个例子中,使用了Reddit数据集,进行了300个簇的分区。

随机多簇法

为了解决上述问题,Cluster-GCN论文提出了一种随机多簇方法,此方法首先将图划分为【GNN】task5-超大图上的节点表征学习_神经网络_132个簇,【GNN】task5-超大图上的节点表征学习_聚类算法_133【GNN】task5-超大图上的节点表征学习_神经网络_132是一个较大的值,在构建一个batch时,不是只使用一个簇,而是使用随机选择的【GNN】task5-超大图上的节点表征学习_邻接矩阵_135个簇,表示为【GNN】task5-超大图上的节点表征学习_GNN_136,得到的batch包含节点【GNN】task5-超大图上的节点表征学习_聚类算法_137 、簇内边【GNN】task5-超大图上的节点表征学习_深度学习_138和簇间边【GNN】task5-超大图上的节点表征学习_邻接矩阵_139

此方法的好处

1)不会丢失簇间的边
2)不会有很大的batch内类别分布的偏差
3)以及不同的epoch使用的batch不同,这可以降低梯度估计的偏差。

图3展示了随机多簇方法,在每个epoch中,随机选择一些簇来组成一个batch,不同的epoch的batch不同。在图4中,我们可以观察到,使用多个簇来组成一个batch可以提高收敛性。最终的Cluster-GCN算法在算法1中呈现。

【GNN】task5-超大图上的节点表征学习_GNN_140


图3:Cluster-GCN提出的随机多分区方法。在每个epoch中,我们(不放回地)随机抽取【GNN】task5-超大图上的节点表征学习_邻接矩阵_135个簇(本例中使用【GNN】task5-超大图上的节点表征学习_邻接矩阵_135=2)及其簇间的边,来构成一个batch(相同颜色的块在同一batch中)

【GNN】task5-超大图上的节点表征学习_神经网络_143

图4:选择一个簇与选择多个簇的比较。前者使用300个簇。后者使用1500个簇,并随机选择5个簇来组成一个batch。该图X轴为epoches,Y轴为F1得分。

【GNN】task5-超大图上的节点表征学习_聚类算法_144

5.训练深层GCNs的问题

(1)将第 l l l层的表征添加到下一层

以往尝试训练更深的GCN的研究似乎表明,增加更多的层是没有帮助的。然而,那些研究的实验使用的图太小,所以结论可能并不正确。例如,其中有一项研究只使用了一个只有几百个训练节点的图,由于节点数量过少,很容易出现过拟合的问题。此外,加深GCN神经网络层数后,训练变得很困难,因为层数多了之后前面的信息可能无法传到后面。有的研究采用了一种类似于残差连接的技术,使模型能够将前一层的信息直接传到下一层。具体来说,他们修改了公式(1),将第【GNN】task5-超大图上的节点表征学习_邻接矩阵_14层的表征添加到下一层,如下所示
【GNN】task5-超大图上的节点表征学习_邻接矩阵_146

(2)放大GCN每一层中使用的邻接矩阵 A A A的对角线部分

在这里,我们提出了另一种简单的技术来改善深层GCN神经网络的训练。在原始的GCN的设置里,每个节点都聚合邻接节点在上一层的表征。然而,在深层GCN的设置里,该策略可能不适合,因为它没有考虑到层数的问题。直观地说,近距离的邻接节点应该比远距离的的邻接节点贡献更大。因此,Cluster-GCN提出一种技术来更好地解决这个问题。

其主要思想是放大GCN每一层中使用的邻接矩阵【GNN】task5-超大图上的节点表征学习_GNN_07的对角线部分。通过这种方式,我们在GCN的每一层的聚合中对来自上一层的表征赋予更大的权重。这可以通过给【GNN】task5-超大图上的节点表征学习_深度学习_87加上一个单位矩阵【GNN】task5-超大图上的节点表征学习_GNN_149来实现,如下所示,
【GNN】task5-超大图上的节点表征学习_神经网络_150
虽然公式(9)似乎是合理的,但对所有节点使用相同的权重而不考虑其邻居的数量可能不合适。此外,它可能会受到数值不稳定的影响,因为当使用更多的层时,数值会呈指数级增长。

(3)第三次改进

因此,Cluster-GCN方法提出了一个修改版的公式(9),以更好地保持邻接节点信息和数值范围。首先给原始的【GNN】task5-超大图上的节点表征学习_GNN_07添加一个单位矩阵【GNN】task5-超大图上的节点表征学习_GNN_149,并进行归一化处理
【GNN】task5-超大图上的节点表征学习_神经网络_153
然后考虑,
【GNN】task5-超大图上的节点表征学习_邻接矩阵_154

6.测试算法的可扩展性

为了测试算法的可扩展性,作者创建了一个新的Amazon2M数据集,它有200万个节点和6100万个边,比以前最大的公开可用数据集(Reddit)大5倍多。在该数据上训练三层GCN,Cluster-GCN比以前最先进的VR-GCN(1523秒vs 1961秒)更快,并且使用的内存更少(2.2GB vs 11.2GB)。此外,在该数据上训练4层GCN,Cluster-GCN可以在36分钟内完成,而所有现有的GCN训练算法由于内存不足而无法训练。

Cluster-GCN允许在短时间和内存开销的情况下训练更深入的GCN,从而提高了使用5层Cluster-GCN的预测精度,作者在PPI数据集上实现了最先进的test F1 score 99.36,而之前的最佳结果在[16](2018. GaAN: Gated Attention Networks for Learning on Large and Spatiotemporal Graphs. In UAI)上的是98.71。

四、Cluster-GCN实践

PyG为Cluster-GCN提出的训练方式和神经网络的构建提供了良好的支持。我们无需在意图节点是如何被划分成多个簇的,PyG提供的接口允许我们像训练普通神经网络一样在超大图上训练图神经网络。

1.数据集分析

from torch_geometric.datasets import Reddit
from torch_geometric.data import ClusterData, ClusterLoader, NeighborSampler

dataset = Reddit('../dataset/Reddit')
data = dataset[0]
print(dataset.num_classes)
print(data.num_nodes)
print(data.num_edges)
print(data.num_features)# 节点维度

# 41
# 232965
# 114615873
# 602

可以看到该数据集包含41个分类任务,232,965个节点,114,615,873条边,节点维度为602维。

2.图节点聚类与数据加载器生成

cluster_data = ClusterData(data, num_parts=1500, recursive=False, save_dir=dataset.processed_dir)
# 图节点被聚类分成对个簇,train_loader返回一个由多个簇组成的batch
train_loader = ClusterLoader(cluster_data, batch_size=20, shuffle=True, num_workers=12)
# subgraph_loader不对图节点聚类,
# 计算一个batch中的节点表征需要计算该batch中的所有节点的距离从0到L的邻居节点
subgraph_loader = NeighborSampler(data.edge_index, sizes=[-1], batch_size=1024, shuffle=False, num_workers=12)

​train_loader​​,此数据加载器遵循Cluster-GCN提出的方法,图节点被聚类划分成多个簇,此数据加载器返回的一个batch由多个簇组成。

​subgraph_loader​​​,使用此数据加载器不对图节点聚类,计算一个batch中的节点的表征需要计算该batch中的所有节点的距离从【GNN】task5-超大图上的节点表征学习_神经网络_155【GNN】task5-超大图上的节点表征学习_深度学习_10的邻居节点。

3.图神经网络的构建

class Net(torch.nn.Module):
def __init__(self, in_channels, out_channels):
super(Net, self).__init__()
self.convs = ModuleList(
[SAGEConv(in_channels, 128),
SAGEConv(128, out_channels)])

def forward(self, x, edge_index):
for i, conv in enumerate(self.convs):
x = conv(x, edge_index)
if i != len(self.convs) - 1:
x = F.relu(x)
x = F.dropout(x, p=0.5, training=self.training)
return F.log_softmax(x, dim=-1)

# inference应用于推理阶段
def inference(self, x_all):
pbar = tqdm(total=x_all.size(0) * len(self.convs))
pbar.set_description('Evaluating')

# Compute representations of nodes layer by layer, using *all*
# available edges. This leads to faster computation in contrast to
# immediately computing the final representations of each batch.
for i, conv in enumerate(self.convs):
xs = []
# 为了获取更高的预测精度,使用subgraph_loader
for batch_size, n_id, adj in subgraph_loader:
edge_index, _, size = adj.to(device)
x = x_all[n_id].to(device)
x_target = x[:size[1]]
x = conv((x, x_target), edge_index)
if i != len(self.convs) - 1:
x = F.relu(x)
xs.append(x.cpu())

pbar.update(batch_size)

x_all = torch.cat(xs, dim=0)

pbar.close()

return

可以看到此神经网络拥有​​forward​​​和​​inference​​​两个方法。​​forward​​​函数的定义与普通的图神经网络并无区别。​​inference​​​方法应用于推理阶段,为了获取更高的预测精度,所以使用​​subgraph_loader​​。

4.训练、验证与测试

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Net(dataset.num_features, dataset.num_classes).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)

def train():
model.train()

total_loss = total_nodes = 0
for batch in train_loader:
batch = batch.to(device)
optimizer.zero_grad()
out = model(batch.x, batch.edge_index)
loss = F.nll_loss(out[batch.train_mask], batch.y[batch.train_mask])
loss.backward()
optimizer.step()

nodes = batch.train_mask.sum().item()
total_loss += loss.item() * nodes
total_nodes += nodes

return total_loss / total_nodes


@torch.no_grad()
def test(): # Inference should be performed on the full graph.
model.eval()

out = model.inference(data.x)
y_pred = out.argmax(dim=-1)

accs = []
for mask in [data.train_mask, data.val_mask, data.test_mask]:
correct = y_pred[mask].eq(data.y[mask]).sum().item()
accs.append(correct / mask.sum().item())
return accs


for epoch in range(1, 31):
loss = train()
if epoch % 5 == 0:
train_acc, val_acc, test_acc = test()
print(f'Epoch: {epoch:02d}, Loss: {loss:.4f}, Train: {train_acc:.4f}, '
f'Val: {val_acc:.4f}, test: {test_acc:.4f}')
else:
print(f'Epoch: {epoch:02d}, Loss: {loss:.4f}')

可见在训练过程中,我们使用​​train_loader​​​获取batch,每次根据多个簇组成的batch进行神经网络的训练。但在验证阶段,我们使用​​subgraph_loader​​​,在计算一个节点的表征时会计算该节点的距离从【GNN】task5-超大图上的节点表征学习_神经网络_155【GNN】task5-超大图上的节点表征学习_深度学习_10的邻接节点,这么做可以更好地测试神经网络的性能。

五、作业

尝试将数据集切分成不同数量的簇进行实验,然后观察结果并进行比较。

运行上面的代码,电脑配置不太行,把上面的​​Reddit​​​数据集改为​​Cora​​数据集了

【GNN】task5-超大图上的节点表征学习_邻接矩阵_159


可以使用colab pro(内存最大支持25G)选高RAM(大内存)模式(9.9刀/月)动态扩容,如果使用​​Reddit​​数据集的结果正常如下所示:

Epoch: 01, Loss: 1.1573
Epoch: 02, Loss: 0.4642
Epoch: 03, Loss: 0.3904
Epoch: 04, Loss: 0.3572


Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 465930/465930 [01:05<00:00, 7123.90it/s]


Epoch: 05, Loss: 0.3385, Train: 0.9579, Val: 0.9535, test: 0.9520
Epoch: 06, Loss: 0.3139
Epoch: 07, Loss: 0.3060
Epoch: 08, Loss: 0.2950
Epoch: 09, Loss: 0.2933


Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 465930/465930 [01:02<00:00, 7484.08it/s]


Epoch: 10, Loss: 0.2879, Train: 0.9609, Val: 0.9528, test: 0.9509
Epoch: 11, Loss: 0.2902
Epoch: 12, Loss: 0.2871
Epoch: 13, Loss: 0.2803
Epoch: 14, Loss: 0.3023


Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 465930/465930 [01:01<00:00, 7548.35it/s]


Epoch: 15, Loss: 0.2796, Train: 0.9571, Val: 0.9465, test: 0.9436
Epoch: 16, Loss: 0.2711
Epoch: 17, Loss: 0.2619
Epoch: 18, Loss: 0.2726
Epoch: 19, Loss: 0.2710


Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 465930/465930 [01:02<00:00, 7476.75it/s]


Epoch: 20, Loss: 0.2538, Train: 0.9663, Val: 0.9501, test: 0.9499
Epoch: 21, Loss: 0.2532
Epoch: 22, Loss: 0.2457
Epoch: 23, Loss: 0.2470
Epoch: 24, Loss: 0.2340


Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 465930/465930 [01:03<00:00, 7384.29it/s]


Epoch: 25, Loss: 0.2428, Train: 0.9666, Val: 0.9503, test: 0.9491
Epoch: 26, Loss: 0.2469
Epoch: 27, Loss: 0.2340
Epoch: 28, Loss: 0.2511
Epoch: 29, Loss: 0.2444


Evaluating: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 465930/465930 [01:02<00:00, 7445.58it/s]

Epoch: 30, Loss: 0.2278, Train: 0.9684, Val: 0.9516, test: 0.9507

而如果将​​num_parts=1500​​​改成​​num_parts=1000​​进行实验,两者的准确率不相上下,实验结果如下:

Epoch: 01, Loss: 1.3562
Epoch: 02, Loss: 0.5103
......
Epoch: 29, Loss: 0.2154
Evaluating: 100%|██████████| 465930/465930 [17:43<00:00, 441.52it/s]
Epoch: 30, Loss: 0.2126, Train: 0.9741, Val: 0.9542, test: 0.9530

REFERENCE