1. 写在前面

这个系列整理的关于GNN的相关基础知识, 图深度学习是一个新兴的研究领域,将深度学习与图数据连接了起来,推动现实中图预测应用的发展。 之前一直想接触这一块内容,但总找不到能入门的好方法,而这次正好Datawhale有组队学习课,有大佬亲自带队学习入门,不犹豫,走起(感谢组织)。所以这个系列是参加GNN组队学习的相关知识沉淀, 希望能对GNN有一个好的入门吧 😉

这一篇内容呢叫做图神经网络的节点表征学习,节点表征是图节点预测或边预测任务中的第一步。高质量节点表征应该能用于衡量节点的相似性,然后基于节点表征可以实现高准确性的节点预测或边预测。基于图神经网络的节点表征学习可以理解为对图神经网络进行基于监督学习的训练,使得图神经网络学会产生高质量的节点表征。

自己的理解,这篇内容呢,是对上一篇内容的再次深入, 上一篇内容中主要的核心是如何用一个一层的图神经网络去学习到节点的特征向量,最重要的是GNN的前向传播过程, 就像神经网络那样, 理解了一层神经网络的运算方式, 多层神经网络就感觉是非常自然的过程, 图这里也是一样,所以简单学习完了一层的图神经网络,这篇内容,就是搭建比较复杂的图神经去学习较好的节点表征问题了。 这篇文章介绍了两个GNN里面比较经典的两个网络GCN和GAT,当然,涉及到太细节的东西, 这篇文章还是以代码为主, 当然,由于自身是小白,并且实习原因,也根本没法去读原论文了解太细,所有我只能尽量的争取过一遍大佬们整理的文档,然后加上自己的理解了,具体细节补充应该不是很多,这里面觉得有两点是比较重要的:

  1. 代码部分的话还是应该好好学,现在的编码能力太弱,如何搭建图神经网络去完成节点预测问题是个重点, 而上一节里面,我就隐约的感觉,相邻节点对于当前节点的重要性应该不同,所以可能会有Attention出现,而这里的GAT,正好是考虑了这一点(这个可没有偷窥,随着慢慢学习可能有感觉了吧),所以如何搭建带ATT的图神经网络完成预测也是重点,这是代码层面需要掌握的
  2. 理论层面上, 对于我这样的小白来说,首先,先明白GCN的原理,不要求具体细节,然后学习GCN目前的不足,然后就是GAT的原理以及存在的问题等, 感觉这才是比较重要的知识。

所以我这边学习目标还是比较明确的,那么,就开始啦。

大纲如下

  • 图神经网络里面的节点预测问题
  • MLP在图节点分类的应用问题
  • GCN及其在图节点分类任务中的应用
  • GraphSAGE在图节点分类任务中的应用
  • GAT及其在图节点分类任务中的应用

2. 图神经网络里面的节点预测问题

这里主要是先了解下图神经网络的节点预测问题到底是个啥意思? 之前我也是一脸懵逼,节点怎么还能预测呢? 看过文档之后,哦,原来是这样:

在节点预测任务中,我们拥有一个图,图上有很多节点,部分节点的标签已知,剩余节点的标签未知。将节点的属性(x)、边的端点信息(edge_index)、边的属性(edge_attr,如果有的话)输入到多层图神经网络,经过图神经网络每一层的一次节点间信息传递,图神经网络为节点生成节点表征。有了节点表征,我们就可以对节点本身进行一些预测问题了。

所以,这里面能得到的图节点预测问题就是:根据节点的属性(可以是类别型、也可以是数值型)、边的信息、边的属性(如果有的话)、已知的节点预测标签,对未知标签的节点做预测, 而这个过程中,我们也能得到节点的表征

这里又让我想起了NLP里面的语言模型, 训练语言模型是根本,而这个过程中也能顺带着把词的embedding给学习出来。 这里又发现有点相通了,节点预测这里, 以训练预测节点类别的模型为根本,而顺带着,又能把节点的embedding给学习出来,异曲同工了。

那么, 图神经网络究竟如何被训练来预测节点的类别呢? 又是怎么能在这个过程中学习出节点表征呢? 后面会整理到, 但是整理之前, 得先思考下,如果是普通的神经网络,是怎么解决这个问题的呢? 这里由普通的全连接,去慢慢的延伸和类比,在这里,我参考了一篇大佬的文章,对类比细节做些补充。

这里会偏实践一点了,通过之前的那个数据案例进行的串联,Cora是一个论文引用网络,节点代表论文,如果两篇论文存在引用关系,那么认为对应的两个节点之间存在边,每个节点由一个1433维的词包特征向量描述。我们的任务是推断每个文档的类别(共7类)。所以在写神经网络之前,先把数据导入,然后了解下数据集:

from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures

dataset = Planetoid(root='data/Planetoid', name='Cora', transform=NormalizeFeatures())

具体的属性打印,这里就不打印了,之前在第一篇文章就打印过了,需要对这个数据集再回忆下:

Cora图拥有2,708个节点和10,556条边,平均节点度为3.9。我们仅使用140个有真实标签的节点(每类20个)用于训练。有标签的节点的比例只占到5%。进一步我们可以看到,这个图是无向图,不存在孤立的节点(即每个文档至少有一个引文)。

这里发现的不一样的地方, 就是后面比之前多了一个transform参数,这个是做数据转换的,数据转换在将数据输入到神经网络之前修改数据,这一功能可用于实现数据规范化或数据增强。在此例子中,我们使用NormalizeFeatures,进行节点特征行归一化,使各节点特征总和为1。其他数据转换方法请参阅torch-geometric-transforms

对于这个分类任务,首先我们知道data.x就是一个矩阵,每一行表示一个样本,而每一列表示特征, 这样的话,其实我们可以建立一个简单的神经网络跑这个任务,无非就是不考虑边与边之间的关系罢了, 所以,先来个神经网络看看效果。

3. MLP在图节点分类的应用问题

这里是搭建一个普通的全连接网络来实现节点的分类问题,代码如下:

class MLP(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(MLP, self).__init__()
        torch.manual_seed(12345)
        self.lin1 = Linear(dataset.num_features, hidden_channels)
        self.lin2 = Linear(hidden_channels, dataset.num_classes)

    def forward(self, x):
        x = self.lin1(x)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.lin2(x)
        return x

model = MLP(hidden_channels=16)

比较简单的全连接网络,MLP由两个线程层、一个ReLU非线性层和一个dropout操作。第一线程层将1433维的特征向量嵌入(embedding)到低维空间中(hidden_channels=16),第二个线性层将节点表征嵌入到类别空间中(num_classes=7)。 这里有个疑问就是为啥最后输出的时候不用softmax多分类,而是直接线性映射,由于这两天我学校校园网出现问题了,没法进行代码实验,再加上公司的事情实在太多,所以实验部分,统一周末的时候做和补充

利用交叉熵损失Adam优化器来训练这个简单的MLP网络

model = MLP(hidden_channels=16)
criterion = torch.nn.CrossEntropyLoss()  # Define loss criterion.
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)  # Define optimizer.

def train():
    model.train()
    optimizer.zero_grad()  # Clear gradients.
    out = model(data.x)  # Perform a single forward pass.
    loss = criterion(out[data.train_mask], data.y[data.train_mask])  # Compute the loss solely based on the training nodes.
    loss.backward()  # Derive gradients.
    optimizer.step()  # Update parameters based on gradients.
    return loss

for epoch in range(1, 201):
    loss = train()
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

测试的代码:

def test():
    model.eval()
    out = model(data.x)
    pred = out.argmax(dim=1)  # Use the class with highest probability.
    test_correct = pred[data.test_mask] == data.y[data.test_mask]  # Check against ground-truth labels.
    test_acc = int(test_correct.sum()) / int(data.test_mask.sum())  # Derive ratio of correct predictions.
    return test_acc

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

MLP表现相当糟糕,只有大约59%的测试准确性。

为什么MLP没有表现得更好呢? 其中一个重要原因是,用于训练此神经网络的有标签节点数量过少,此神经网络应该是欠拟合状态(这里文档上说的是过拟合, 但不明白为啥是过拟合这个具体得通过做实验看)。另一个原因,就是浪费了节点与节点的连接信息。 这里注意下,Pytorch中交叉熵损失已经实现了softmax计算,所以最后不要再加个softmax了, 之前和tf有点混了,在群里问到了这样一个不太好的问题。

那么图神经网络为啥能做到效果好一些呢? 目前我觉得,图神经网络相比神经网络的优势就是考虑上了节点之间的连接信息,这个上一篇文章已经介绍过, 从神经网络到图神经网络,大致上是一个这样的过程:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数


而这里的A呢? 就是上一篇见识过的邻接矩阵了, 回顾下上一篇里面的图神经网络的前向传播,就会发现, 图神经网络和神经网络的一大区别,就在于节点的信息传递那里, 图神经网络的节点信息传递,就是考虑了节点信息的embedding,然后通过聚合的方式得到节点最终的embedding向量。而邻近节点是怎么得到的呢? 就是这个A了,所以这里也比较好理解。

下面就看下图神经网络的"开山之作"GCN是怎么做上面的节点分类任务的。

4. GCN及其在图节点分类任务中的应用

4.1 理论介绍

这里还是先介绍点GCN的理论吧,GCN是首次将图像处理中的卷积操作简单的用到图结构数据处理中来,并且给出了具体的推导,这里面涉及到复杂的谱图理论,具体推导过程还是很复杂的。

但结论的话会比较舒服,其数学定义为,
图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_02
其中图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_03表示插入自环的邻接矩阵,图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_04表示其对角线度矩阵。邻接矩阵可以包括不为图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_05的值,当邻接矩阵不为{0,1}值时,表示邻接矩阵存储的是边的权重。图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_06对称归一化矩阵。左乘一个这东西,是相当于把邻居节点的特征向量也进行了一个求和聚合操作。

如果你说,这是个什么鬼? 那么你还记得这个东西吗?
图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_07
似曾相识呀感觉, 这个就是对于某个节点的参数更新公式,就是聚合邻居节点的特征然后做一个线性变换。如果这个还是不好理解, 文档上的节点公式也可以:
图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_08
其中,图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_09图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_10表示从源节点图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_11到目标节点图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_12的边的对称归一化系数(默认值为1.0)。

如果你说,这几个都看不懂, 那抱歉, 感觉还是得先去第二篇学学前向传播的原理。

为了使得GCN能够捕捉到K-hop的邻居节点的信息,作者还堆叠多层GCN layers,这时候的计算公式就变成了:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_13


那么GCN如何用于节点的分类任务呢?

4.2 PyG中GCNConv 模块

GCNConv构造函数接口:

GCNConv(in_channels: int, out_channels: int, improved: bool = False, cached: bool = False, add_self_loops: bool = True, normalize: bool = True, bias: bool = True, **kwargs)

其中:

  • in_channels:输入数据维度;
  • out_channels:输出数据维度;
  • improved:如果为true图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_14,其目的在于增强中心节点自身信息;
  • cached:是否存储图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_15的计算结果以便后续使用,这个参数只应在归纳学习(transductive learning)的景中设置为true
  • add_self_loops:是否在邻接矩阵中增加自环边;
  • normalize:是否添加自环边并在运行中计算对称归一化系数;
  • bias:是否包含偏置项。

详细内容参阅GCNConv官方文档

上面提到了一个transductive learning, 这个感觉不应该叫做归纳学习,网上叫直推式学习的多,为此我还特意查了查这些概念,感觉这个大佬讲的比较清晰: 这里的inductive learning才是归纳学习吧。

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_16


GCN输入了整个图,训练节点收集邻居节点信息的时候,用到了测试和验证集的样本,我们把这个称为Transductive learning。 其实这样也导致了GCN的一个问题,后面会说, 首先先看GCN是如何进行节点分类的呢?

4.3 GCN节点分类

通过将torch.nn.Linear layers 替换为PyG的GNN Conv Layers,我们可以轻松地将MLP模型转化为GNN模型。在下方的例子中,我们将MLP例子中的linear层替换为GCNConv层。

from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(GCN, self).__init__()
        torch.manual_seed(12345)
        self.conv1 = GCNConv(dataset.num_features, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, dataset.num_classes)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

model = GCN(hidden_channels=16)
# print(model)

可视化我们的未训练的GCN网络的节点表征。

model = GCN(hidden_channels=16)
model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

这里用到了一个可视化节点表征的方式: 先利用TSNE将高维节点表征嵌入到二维平面空间,然后在二维平面空间画出节点。

import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

def visualize(h, color):
    z = TSNE(n_components=2).fit_transform(out.detach().cpu().numpy())
    plt.figure(figsize=(10,10))
    plt.xticks([])
    plt.yticks([])

    plt.scatter(z[:, 0], z[:, 1], s=70, c=color, cmap="Set2")
    plt.show()

这时候节点是比较乱的:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_17


下面进行训练:

model = GCN(hidden_channels=16)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.CrossEntropyLoss()

def train():
      model.train()
      optimizer.zero_grad()  # Clear gradients.
      out = model(data.x, data.edge_index)  # Perform a single forward pass.
      loss = criterion(out[data.train_mask], data.y[data.train_mask])  # Compute the loss solely based on the training nodes.
      loss.backward()  # Derive gradients.
      optimizer.step()  # Update parameters based on gradients.
      return loss

for epoch in range(1, 201):
    loss = train()
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

在训练过程结束后,我们检测GCN节点分类器在测试集上的准确性:

def test():
      model.eval()
      out = model(data.x, data.edge_index)
      pred = out.argmax(dim=1)  # Use the class with highest probability.
      test_correct = pred[data.test_mask] == data.y[data.test_mask]  # Check against ground-truth labels.
      test_acc = int(test_correct.sum()) / int(data.test_mask.sum())  # Derive ratio of correct predictions.
      return test_acc

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

通过简单地将线性层替换成GCN层,我们可以达到81.4%的测试准确率!与前面的仅获得59%的测试准确率的MLP分类器相比,现在的分类器准确性要高得多。这表明节点的邻接信息在取得更好的准确率方面起着关键作用。

最后我们还可以通过可视化我们训练过的模型输出的节点表征来再次验证这一点,现在同类节点的聚集在一起的情况更加明显了。

model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

这时候训练完了之后再看:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_18


这个感觉直接可以做聚类了呀, 之前有个比较好的哥们,问我有没有好的衡量节点相似度的方法, 他说目前只在相似度上进行了创新,当时我给的回复是能不能基于图节点训练出embedding来,这样能够看相似。 只可惜当时我也不知道怎么训练embedding, 如果当时,能看到这篇文章就好了。

但是,对于GCN,我们还是要持着辩证的态度去看,相比于传统方法提升还是很显著的,这很有可能是得益于GCN善于编码图的结构信息,能够学习到更好的节点表示。 当然,GCN的缺点也是很显然易见的,第一,GCN需要将整个图放到内存和显存,这将非常耗内存和显存,处理不了大图;第二,GCN在训练时需要知道整个图的结构信息(包括待预测的节点), 这在现实某些任务中也不能实现(比如用今天训练的图模型预测明天的数据,那么明天的节点是拿不到的)。 此外, 就像上一篇文章我考虑到的那样, GCN聚合邻居节点的时候也没有考虑到不同的邻居节点重要性不同的问题, 而是对周围节点的影响一视同仁, 这在现实中,可能并不符合实际。

所以说, 后面的一些图神经网络,开始对其进行改进。首先,先看看GraphSAGE。

5. GraphSAGE在图节点分类任务中的应用

5.1 原理理解

Graph Sample and Aggregate(GraphSAGE)的提出,是为了解决GCN的前两个问题的。

我们知道图数据和其他类型数据的不同,图数据中的每一个节点可以通过边的关系利用其他节点的信息。而上面提到GCN是Transductive learning的学习方式,也就是输入了整个图,训练节点收集邻居节点信息的时候,用到了测试和验证集的样本。然而,我们所处理的大多数的机器学习问题都是Inductive learning,因为我们刻意的将样本集分为训练/验证/测试,并且训练的时候只用训练样本。这样对图来说有个好处,可以处理图中新来的节点,可以利用已知节点的信息为未知节点生成embedding,GraphSAGE就是这么干的。

GraphSAGE是一个Inductive Learning框架,具体实现中,训练时它仅仅保留训练样本到训练样本的边,然后包含Sample和Aggregate两大步骤,Sample是指如何对邻居的个数进行采样,Aggregate是指拿到邻居节点的embedding之后如何汇聚这些embedding以更新自己的embedding信息。下图展示了GraphSAGE学习的一个过程:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_19


这个过程主要分为三步, 其实也是比较好理解, 感觉只要打通了GNN的前向传播过程,这些网络的思路理解起来就会容易很多

  1. 邻居采样: 这个和之前GCN不同的是, 这个训练的时候, 不是用所有邻居的信息了,而是在邻居里面进行采样, 用部分邻居的信息
  2. 对于每个节点,拿到它采样后的邻居节点的embedding,然后进行聚合,这个方式就很多了
  3. 利用聚合函数聚合邻居节点的信息,并结合自身embedding通过一个非线性变换更新自身的embedding表示

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_20


注意到算法里面的 图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_21,它是指聚合器的数量,也是指权重矩阵的数量,还是网络的层数。

网络的层数可以理解为需要最大访问的邻居的跳数(hops),比如上图中,红色节点的更新拿到了它一、二跳邻居的信息,那么网络层数就是2。为了更新红色节点,首先在第一层(图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_22),我们会将蓝色节点的信息聚合到红色解节点上,将绿色节点的信息聚合到蓝色节点上。在第二层(图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_23)红色节点的embedding被再次更新,不过这次用到的是更新后的蓝色节点embedding,这样就保证了红色节点更新后的embedding包括蓝色和绿色节点的信息,也就是两跳信息

每一层网络中聚合器和权重矩阵是共享的。下图这个A节点和B节点的更新过程,就看出了共享到底是啥意思。

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_24


那么GraphSAGE Sample是怎么做的呢?GraphSAGE是采用定长抽样的方法,具体来说,定义需要的邻居个数图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_25 ,然后采用有放回的重采样/负采样方法达到图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_25保证每个节点(采样后的)邻居个数一致,这样是为了把多个节点以及它们的邻居拼接成Tensor送到GPU中进行批训练。

然后就是用的聚合器,论文里面给了三种:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_27


这里面,第一个的话,比较好理解, 邻居向量求平均,然后过一个神经网络层得到隐向量。 第二个的话,这里作者强调

与平均聚合器相比,lstm具有更大的表达能力。然而,需要注意的是,lstm并非天生对称(即,它们不是排列不变的),因为它们以顺序的方式处理它们的输入。我们通过简单地将lstm应用于节点邻居的随机排列,使lstm对一个无序集进行操作。

这里看看第三个, 这个的意思是先每个邻居向量先单独的过一个一层的全连接然后线性变换一下,这样每个邻居向量还是每个邻居向量, 接下来,再从每个邻居向量的横向维度上,进行最大池化操作,也就是在向量的每个元素层级取最大值得到最终的向量,作者说:

原则上,在最大池化之前应用的函数可以是任意深度的多层感知器,但在本工作中我们只关注简单的单层架构。

直观地说,多层感知器可以被认为是一组函数,用于计算邻居集合中每个节点表示的特征。通过对每个计算特征应用最大池运算符,该模型有效地捕获邻域集的不同方面。

并且这个地方,作者验证了平均池化和最大池化的区别并不是很大

模型上的细节完事之后,就是如何训练了, 也就是GraphSAGE是如何学习聚合器的参数以及权重矩阵 图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_28呢?

  • 如果是有监督的情况下,可以使用每个节点的预测lable和真实lable的交叉熵作为损失函数。
  • 如果是在无监督的情况下,可以假设相邻的节点的embedding表示尽可能相近,因此可以设计出如下的损失函数

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_29


这里又是不自觉的串联下,如果了解W2V的负采样的训练方式,就会对这里的损失函数有种似曾相识之感。其实是一样的意思,这里假设的是相邻的节点embedding尽可能相近, 那里假设的是相邻的词之间节点embedding尽可能相似。 而这种无监督的方式的好处就是即使样本并不是很多的情况下,往往也能通过这种方式训练出不错的embedding。 这个我是来公司之后,慢慢体会到的, 现在有些NLP的模型, 比如著名的BERT这种, 在进行预训练或者微调的时候, 样本数量不够怎么办? 现在往往常用的方式叫做对比学习,这是一种自监督学习的方式, 在有监督的基础上,可以加入无监督对模型综合训练,已达到训练出良好表示的效果。 有监督的训练方式我们都懂,那么怎么进行无监督学习呢?

这里面有个核心叫做学习算法并不一定要关注到样本的每一个细节,只要学到的特征能够使其和其他样本区别开来就行,这啥意思呢? 核心就是要学习一个映射函数图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_30,把样本图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_31 编码成其表示图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_32,对比学习的核心就是使得这个图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_32 满足下面这个式子:
图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_34
这里的 图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_35就是和 图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_31类似的样本,图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_37就是和图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_31不相似的样本,图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_39这是一个度量样本之间相似程度的函数,一个比较典型的 score 函数就是就是向量内积,即优化下面这一期望:
图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_40
好吧,如果还是不懂,还想看看相关论文吧,这里感觉有点偏了呀, 由于我也是这两天刚发现还能这么玩的,所以顺便整理下,自监督的话,数据就会很多了,就拿我这边NLP的一个例子来说, 假设我们训练bert这样的模型,这个模型在衡量句子语义相似度方面并不是很好,所以往往我们需要在原来的基础上进行fine-tuning, 让它更加适合下游的任务,那么如果我们此时,fine-tuning的语料不是很足呢? 这时候自监督的优势就出来了

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_41


这里由于是整理GNN,有点扯远了,所以简单说说自监督是怎么玩的,因为这个东西发现还真是好思路。 所谓自监督,和上面GraphSAGE里面的无监督非常像, 比如我这里有两个句子,首先拿到它的embedding,这里的embedding就是本身各个词的embedding加上位置embedding之后的了。 然后进行数据增强, 这里增强的方式,下面的四种都可以:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络_42


比如随机打乱位置, 随机遮盖某些embedding的某些token, 或者随机删除一行或者一列,这个从句子的角度,就是随机删除了某个字,或者字的某些维度信息,或者句子中词的顺序随机打乱等。但注意,本身还是这个句子, 与原句子的相似程度上,往往要比不是这个句子的更加相似嘛。所以,两个句子各自通过两种数据增强的方式,得到了相当于两倍的embedding了, 然后过bert编码器, 这样得到的还是两倍的embedding,不过融合了其他词的信息,然后从embedding的维度做平均池化,就得到了最终两个句子表示(一个句子就能得到两个句子表示),这样的话,相当于最终得到了4个句子, 这4个句子里面,来自同一个句子的算作一对正样本, 来自不同句子的算作一对负样本,这样就出来正负样本对了, 这样再用无监督的那个损失函数就能训练了。 这就是自监督的一个典型例子了,是不是很爽? 两个句子,通过数据增强的方式就能造出这么多样本来。但设计出合理的正例和负例 pair,并且尽可能提升 pair 能够 cover 的 semantic relation,才能让得到的表示在 downstream task 表现的更好,不同的任务设计方式是不同的

好了,拉回来了,再说就收不住,扯NLP里面去了。这样,GraphSAGE就差不多原理完事了, 总结下优缺点:
优点:

  • 利用采样机制,很好的解决了GCN必须要知道全部图的信息问题,克服了GCN训练时内存和显存的限制,即使对于未知的新节点,也能得到其表示
  • 聚合器和权重矩阵的参数对于所有的节点是共享的
  • 模型的参数的数量与图的节点个数无关,这使得GraphSAGE能够处理更大的图
  • 既能处理有监督任务也能处理无监督任务

在公司里面,发现这些大佬们特别喜欢的就是这种方法简洁,又能解决实际问题的模型,下面摘一段张俊林老师的原话,也作为自己以后追求的一种目标吧:

我一直在内心里比较排斥那些看着特别复杂,堆砌了各种说不清道不明作用构件的模型,比较喜欢简洁有效的算法,可能是在公司里待久了造成的后遗症。就我的经验,如果一个算法构成复杂,效果又没有比基线模型有特别大的提升,除非它能通过消融实验证明每个构件都是必须存在的,否则,这种算法大概率可以直接忽略掉,因为这通常是为了所谓的“创新性”,被强制塞进了看着很高大上但是其实没啥用的东西。“如无必要,勿增实体”,这个原则同样适用于算法领域,而满足这种条件的模型,其实,是很少的。我觉得吧,公司里讨生活的算法工程师,应该尽量且尽早,养成对这种复杂但作用不大的复杂模型发自心底的排斥感,这绝对能极大提升你获取新知识的效率。

当然,GraphSAGE也有一些缺点,还是之前提到的那个,每个节点那么多邻居,GraphSAGE的采样没有考虑到不同邻居节点的重要性不同,而且聚合计算的时候邻居节点的重要性和当前节点也是不同的

那么,这个问题怎么克服呢? 这才过渡到了GAT!

5.2 实践

关于实践这块,也打算用GraphSAGE跑一下上面节点分类的任务,但由于刚学, 这个目前还无法复现, 并且我这边服务器出现了些问题,打算放在周末休息的时候补上这一块。

6. GAT及其在图节点分类任务中的应用

6.1 原理理解

为了解决GNN聚合邻居节点的时候没有考虑到不同的邻居节点重要性不同的问题,GAT(Graph Attention Networks)借鉴了Transformer的idea,引入masked self-attention机制,在计算图中的每个节点的表示的时候,会根据邻居节点特征的不同来为其分配不同的权值。

这啥意思? 这个其实就是我之前考虑过的那个问题, 每个邻居节点对于当前节点的重要性应该是不同的呀,直接无脑求平均或者求和,是不是有点鲁莽? 所以加Att是比较好的一种方式:

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_43


其数学定义为,

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_44

其中注意力系数图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GAT_45的计算方法为,

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GCN_46

这个求注意力的方法如果感觉比较复杂(也不是很复杂,注意力的实现类似于过了一个全连接神经网络), 还有简单的方式,就可以理解为之前的那种, 邻居节点与当前节点直接求内积,然后进行softmax操作也能得到图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_47。当然,如果想变得复杂,提高模型拟合能力,那就引入多头的注意力机制(transformer), 也就是注意力的方法其实是多种多样的。

图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_GraphSAGE_48

这里的图神经网络有监督学习节点表示损失函数 图神经网络 节点预测_图神经网络有监督学习节点表示损失函数_21是头数。

此外,由于GAT结构的特性,GAT无需使用预先构建好的图,因此GAT既适用于Transductive Learning,又适用于Inductive Learning。

GAT的一些优点:

  • 使用了注意力机制, 使得不同的邻居的重要程度有了一定的区分,还可以多头注意力,可能会学习出更加丰富的embedding
  • 训练GCN无需了解整个图结构,只需知道每个节点的邻居节点即可
  • 计算速度快,可以在不同的节点上进行并行计算
  • 既可以用于Transductive Learning,又可以用于Inductive Learning,可以对未见过的图结构进行处理

6.2 PyG中GATConv 模块说明

GATConv官方文档可以参考,GATConv构造函数接口:

GATConv(in_channels: Union[int, Tuple[int, int]], out_channels: int, heads: int = 1, concat: bool = True, negative_slope: float = 0.2, dropout: float = 0.0, add_self_loops: bool = True, bias: bool = True, **kwargs)

其中:

  • in_channels:输入数据维度;
  • out_channels:输出数据维度;
  • heads:在GATConv使用多少个注意力模型(Number of multi-head-attentions);
  • concat:如为true,不同注意力模型得到的节点表征被拼接到一起(表征维度翻倍),否则对不同注意力模型得到的节点表征求均值;

6.3 基于GAT图神经网络的图节点分类

这一次,我们将MLP例子中的linear层替换为GATConv层,来实现基于GAT的图节点分类神经网络。这里依然是先无脑贴代码了, 这个代码也是还没有来得及好好研究。

from torch_geometric.nn import GATConv

class GAT(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(GAT, self).__init__()
        torch.manual_seed(12345)
        self.conv1 = GATConv(dataset.num_features, hidden_channels)
        self.conv2 = GATConv(hidden_channels, dataset.num_classes)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

这里还差训练测试方式, 等我实战完了再补。

小总

节点的表征学习在GNN中是非常重要的,这篇文章围绕着节点表征学习展开,以一个节点分类任务进行贯穿, 在上一篇的基础上理清了3个更加复杂的GNN网络, 分别是GCN,GraphSAGE和GAT, 并且它们的关系还是层层递进的。

下面是文档中摘录的一些重点:

  • 节点表征中,MLP节点分类器只考虑了节点自身属性,忽略了节点之间的连接关系;而GCN,GraphSAGE,GAT等节点分类器,同时考虑了节点自身属性与周围邻居节点的属性,它们是比MLP要好的,所以邻居节点的信息对于节点分类任务的重要
  • 基于图神经网络的节点表征的学习遵循消息传递范式
  • 在邻居节点信息变换阶段,GCN与GAT都对邻居节点做归一化和线性变换(两个操作不分前后);
  • 在邻居节点信息聚合阶段都将变换后的邻居节点信息做求和聚合;
  • 在中心节点信息变换阶段只是简单返回邻居节点信息聚合阶段的聚合结果。
  • GCN与GAT的区别在于邻居节点信息聚合过程中的归一化方法不同(这种理解方式也感觉挺有意思):
  • 前者根据中心节点与邻居节点的度计算归一化系数,后者根据中心节点与邻居节点的相似度计算归一化系数。
  • 前者的归一化方式依赖于图的拓扑结构,不同节点其自身的度不同、其邻居的度也不同,在一些应用中可能会影响泛化能力。
  • 后者的归一化方式依赖于中心节点与邻居节点的相似度,相似度是训练得到的,因此不受图的拓扑结构的影响,在不同的任务中都会有较好的泛化表现。

好了,这篇文章就到这里了,这次的作业是

使用PyG中不同的图卷积层在PyG的不同数据上实现节点分类或回归任务。

这个作业我周末的时候再做了, 工作日期间找个时间太难了, 这篇文章还差些动手探索的内容, 等后面慢慢补,先打卡。