Press "Enter" to skip to content

用插图和PyTorch实现解释的“图注意力网络论文”

《Graph Attention Networks》一文的详细图解,由Veličković等人撰写,包含了所提出模型的PyTorch实现。

图解:Graph Attention Networks中的消息传递层 — 图片作者:原文

引言

图神经网络(GNNs)是一类在图结构数据上操作的强大神经网络。它们通过聚合节点局部邻域的信息来学习节点表示(嵌入)。在图表示学习文献中,这个概念被称为“消息传递”。

消息(嵌入)在图中的节点之间通过多层GNN进行传递。每个节点通过聚合其邻居的消息来更新其表示。这个过程在层之间重复进行,使得节点可以获得编码有关图的更丰富信息的表示。一些重要的GNN变体包括GraphSAGE [2]、图卷积网络[3]等。您可以在此处探索更多的GNN变体。

图解:消息传递的单个步骤 — 图片作者:原文

图注意力网络(GAT)[1]是一类特殊的GNN,旨在改进这种消息传递机制。它引入了可学习的“注意力机制”,使得节点在聚合其局部邻域的消息时可以决定哪些邻居节点更重要,而不是像以前那样使用相等权重聚合所有邻居节点的信息。

从实证角度来看,图注意力网络在节点分类、链接预测和图分类等任务上表现出色。它们在几个基准图数据集上展示了最先进的性能。

在本文中,我们将详细解读Veličković等人原始的《Graph Attention Networks》论文的关键部分,并使用PyTorch框架实现其中提出的概念,以更好地理解GAT方法的本质。

您还可以在此GitHub存储库中访问本文中使用的完整代码,包括训练和验证代码。

论文概述

第一节 — 引言

在第一节“引言”中,对图表示学习文献中的现有方法进行了广泛回顾,并介绍了图注意力网络(GAT)。作者提到:

  1. 注意力机制的整体视图。
  2. GAT具有的三个特性,即高效计算、适用于所有节点以及在归纳学习中的可用性。
  3. 他们评估GAT性能的基准和数据集。
原始GAT论文的选定部分

然后,与一些现有方法进行比较,提及它们的一般相似性和差异性,并继续论文的下一节。

第二节 — GAT架构

在本节中,详细介绍了图注意力网络的架构,这是论文的主要部分。为了进一步解释,假设所提出的架构在具有N个节点(V = {vᵢ};i=1,…,N)的图上执行,并且每个节点用F个元素的向量hᵢ表示,节点之间存在任意设置的边。

示例输入图表 — 作者提供的图像

作者首先对单个图注意力层(Graph Attention Layer)进行了表征,并说明了其运行方式,这成为了构建图注意力网络的基本组件。一般来说,单个GAT层应该接收一个带有给定节点嵌入(表示)的图作为输入,向本地邻居节点传播信息,并输出节点的更新表示。

原始GAT论文的选定部分

如上所述,为了实现此目的,首先,他们指出GA层的所有输入节点特征向量(hᵢ)都要进行线性变换(即乘以权重矩阵W),在PyTorch中,一般是这样做的:

节点特征的线性变换 — 作者提供的图像
import torchfrom torch import nn# in_features -> F and out_feature -> F'in_features = ...out_feature = ...# 实例化可学习的权重矩阵W(FxF'矩阵)W = nn.Parameter(torch.empty(size=(in_features, out_feature)))# 初始化权重矩阵Wnn.init.xavier_normal_(W)# 将W和h相乘(h是所有节点的输入特征 -> NxF矩阵)h_transformed = torch.mm(h, W)

现在我们已经得到了输入节点特征(嵌入)的转换版本,我们向前迈出了几步,观察和理解在GAT层中我们的最终目标是什么。

正如论文中所描述的,在图注意力层的末尾,对于每个节点i,我们需要获得一个更加结构和上下文感知的新特征向量。

这是通过计算邻居节点特征的加权和,并在之后应用非线性激活函数σ来实现的。在一般的GNN层操作中,这个加权和也被称为“聚合步骤”,这是图机器学习文献中的术语。

这些权重αᵢⱼ ∈ [0, 1]是通过一个注意力机制进行学习和计算的,它表示了在消息传递和聚合过程中邻居节点j对节点i的特征的重要性。

用插图和PyTorch实现解释的“图注意力网络论文” 四海 第7张

原始GAT论文的选定部分

现在让我们看看如何为每对节点i和其邻居j计算这些注意力权重αᵢⱼ

简而言之,注意力权重αᵢⱼ的计算如下所示:

原始GAT论文的选定部分

其中eᵢⱼ注意力分数,并且应用了Softmax函数,以确保所有权重都在[0, 1]区间内并且总和为1。

注意力得分 eᵢⱼ 是通过注意力函数 a(…) 在每个节点 i 和其邻居 j ∈ N 之间计算得出的:

原始GAT论文中的选定部分

这里 || 表示两个转换后的节点嵌入的串联a 是一个大小为 2 * F’(转换后嵌入的两倍大小)的可学习参数向量(即注意力参数)。

其中 (aᵀ) 是向量 a转置,从而整个表达式 aᵀ [Whᵢ|| Whⱼ] 是“a” 与转换后嵌入的串联之间的点乘(内积)

整个操作如下所示:

GAT中注意力得分的计算—作者提供的图像

在PyTorch中,为了计算这些得分,我们采用了稍微不同的方法。因为在计算节点之间的所有可能对的 eᵢⱼ 时,计算所有对的效率更高,然后只选择那些代表节点之间存在边的得分。为了计算所有的 eᵢⱼ:

# 实例化可学习的注意力参数向量 `a`a = nn.Parameter(torch.empty(size=(2 * out_feature, 1)))# 初始化参数向量 `a`nn.init.xavier_normal_(a)# 我们在前面的代码片段中获得了 `h_transformed`# 计算所有节点嵌入和注意力向量参数前半部分(对应邻居信息)的点积source_scores = torch.matmul(h_transformed, self.a[:out_feature, :])# 计算所有节点嵌入和注意力向量参数后半部分(对应目标节点)的点积target_scores = torch.matmul(h_transformed, self.a[out_feature:, :])# 广播加法 e = source_scores + target_scores.Te = self.leakyrelu(e)

代码片段的最后一部分(# 广播加法)将所有一对一的源和目标得分相加,从而得到一个包含所有 eᵢⱼ 得分的 NxN 矩阵(如下图所示)。

GAT中计算所有节点之间注意力得分的向量化并行计算—作者提供的图像

到目前为止,我们假设图是完全连接的,并计算了所有可能节点对之间的注意力得分。为了解决这个问题,在对注意力得分应用LeakyReLU激活函数之后,基于图中现有边缘对注意力得分进行了屏蔽,意味着我们只保留与现有边缘对应的得分。

可以通过使用图的邻接矩阵来实现。邻接矩阵是一个NxN矩阵,如果节点i和j之间存在边,则在第i行和第j列上为1,其他位置为0。因此,我们通过将邻接矩阵的零元素分配为-∞,其他位置分配为0来创建遮罩。然后,将遮罩添加到我们的得分矩阵中,并在其行上应用softmax函数。

connectivity_mask = -9e16 * torch.ones_like(e)# adj_mat是N乘以N的邻接矩阵
e = torch.where(adj_mat > 0, e, connectivity_mask) # 通过softmax函数计算注意力系数
attention = F.softmax(e, dim=-1)

最后,根据论文的描述,获取注意力分数并将其与现有的边进行遮罩处理,通过对得分矩阵的行进行softmax操作,得到注意力权重αᵢⱼ。

选自原始GAT论文的部分
应用连通性遮罩和softmax到注意力分数以获得注意力系数的示意图-作者的图片。

如前所述,我们计算节点嵌入的加权和:

# 最终节点嵌入是其邻居特征的加权平均值
h_prime = torch.matmul(attention, h_transformed)

最后,论文引入了“多头”注意力的概念,其中所有讨论的操作都通过多个并行流进行,最终的结果头部要么被平均,要么被连接。

选自原始GAT论文的部分

多头注意力和聚合过程如下所示:

节点1在其邻域中进行多头注意力的示意图(K = 3个头)。不同的箭头样式和颜色表示独立的注意力计算。来自每个头的聚合特征被连接或平均以获得h’。-原始论文中的图片

为了以更清晰的模块化形式(作为PyTorch模块)进行实现,并引入多头注意力功能,整个图注意力层的实现如下:

import torchfrom torch import nnimport torch.nn.functional as F###################################  GAT LAYER定义    ###################################class GraphAttentionLayer(nn.Module):    def __init__(self, in_features: int, out_features: int,                 n_heads: int, concat: bool = False, dropout: float = 0.4,                 leaky_relu_slope: float = 0.2):        super(GraphAttentionLayer, self).__init__()        self.n_heads = n_heads # 注意力头的数量        self.concat = concat # 是否将最终的注意力头连接起来        self.dropout = dropout # Dropout率        if concat: # 连接注意力头            self.out_features = out_features # 每个节点的输出特征数量            assert out_features % n_heads == 0 # 确保out_features是n_heads的倍数            self.n_hidden = out_features // n_heads        else: # 对注意力头的输出进行平均(在主论文中使用)            self.n_hidden = out_features        # 对每个节点的特征应用共享的线性变换,由权重矩阵W参数化        # 初始化权重矩阵W         self.W = nn.Parameter(torch.empty(size=(in_features, self.n_hidden * n_heads)))        # 初始化注意力权重a        self.a = nn.Parameter(torch.empty(size=(n_heads, 2 * self.n_hidden, 1)))        self.leakyrelu = nn.LeakyReLU(leaky_relu_slope) # LeakyReLU激活函数        self.softmax = nn.Softmax(dim=1) # softmax激活函数,用于注意力系数        self.reset_parameters() # 重置参数    def reset_parameters(self):        nn.init.xavier_normal_(self.W)        nn.init.xavier_normal_(self.a)    def _get_attention_scores(self, h_transformed: torch.Tensor):                source_scores = torch.matmul(h_transformed, self.a[:, :self.n_hidden, :])        target_scores = torch.matmul(h_transformed, self.a[:, self.n_hidden:, :])        # 广播相加         # (n_heads, n_nodes, 1) + (n_heads, 1, n_nodes) = (n_heads, n_nodes, n_nodes)        e = source_scores + target_scores.mT        return self.leakyrelu(e)    def forward(self,  h: torch.Tensor, adj_mat: torch.Tensor):        n_nodes = h.shape[0]        # 对节点特征进行线性变换 -> W h        # 输出形状 (n_nodes, n_hidden * n_heads)        h_transformed = torch.mm(h, self.W)        h_transformed = F.dropout(h_transformed, self.dropout, training=self.training)        # 通过重新调整张量并将头部维度放在第一位来划分注意力头        # 输出形状 (n_heads, n_nodes, n_hidden)        h_transformed = h_transformed.view(n_nodes, self.n_heads, self.n_hidden).permute(1, 0, 2)                # 获取注意力分数        # 输出形状 (n_heads, n_nodes, n_nodes)        e = self._get_attention_scores(h_transformed)        # 将不存在的边的注意力分数设置为-9e15(遮罩不存在的边)        connectivity_mask = -9e16 * torch.ones_like(e)        e = torch.where(adj_mat > 0, e, connectivity_mask) # 遮罩处理后的注意力分数                # 通过对得分矩阵的行进行softmax操作,计算注意力系数        attention = F.softmax(e, dim=-1)        attention = F.dropout(attention, self.dropout, training=self.training)        # 最终节点嵌入是其邻居特征的加权平均值        h_prime = torch.matmul(attention, h_transformed)        # 连接/平均注意力头        # 输出形状 (n_nodes, out_features)        if self.concat:            h_prime = h_prime.permute(1, 0, 2).contiguous().view(n_nodes, self.out_features)        else:            h_prime = h_prime.mean(dim=0)        return h_prime

接下来,作者对GAT和其他一些现有的GNN方法/架构进行了比较。他们认为:

  1. GAT比某些现有方法在计算上更高效,因为它能够并行计算注意权重和执行局部聚合。
  2. GAT在汇总消息时可以为节点的邻居分配不同的重要性,这可以提高模型的容量和可解释性。
  3. GAT考虑节点的完整邻域(不需要从邻居中进行采样),而且不假设节点内部有任何顺序。
  4. GAT可以通过将伪坐标函数设置为u(x, y) = f(x)||f(y)(其中f(x)表示节点x的(可能是MLP变换的)特征,||表示连接),将其重新构造为MoNet(Monti等人,2016)的特定实例;将权重函数设置为wj(u) = softmax(MLP(u))。

第三部分 – 评估

在论文的第三部分中,首先,作者描述了用于评估GAT的基准、数据集和任务。然后他们呈现了他们对模型的评估结果。

推导学习 vs. 归纳学习本文中用作基准的数据集分为两种类型的任务,即推导学习和归纳学习

  • 归纳学习:这是一种只在一组标记的训练示例上训练模型,并在训练期间完全未观察到的示例上评估和测试训练模型的监督学习任务。这是一种常见的监督学习类型。
  • 推导学习:在这种类型的任务中,所有数据,包括训练、验证和测试实例,都在训练期间使用。但在每个阶段,模型只能访问相应集合的标签。这意味着在训练期间,模型只使用从训练实例和标签得到的损失进行训练,但测试和验证特征用于消息传递。这主要是因为示例中存在的结构和上下文信息。

数据集在论文中,使用了四个基准数据集来评估GAT,其中三个对应于推导学习,另一个用作归纳学习任务。

推导学习数据集,即CoraCiteseerPubmed(Sen等,2008),都是引文图,其中节点是发表的文档,边缘(连接)是它们之间的引用,节点特征是文档的词袋表示的元素。归纳学习数据集是一个包含不同人体组织的蛋白质相互作用(PPI)数据集(Zitnik和Leskovec,2017)。数据集的详细描述如下:

我们实验中使用的数据集摘要-来自原始论文。

设置和结果

  • 对于三个推导任务,训练使用的设置为:使用2个GAT层-第1层使用- K = 8个注意力头- F’ = 8个每个头的输出特征维度- ELU激活函数和第二层[Cora和Citeseer | Pubmed]- [1 | 8]个注意力头C个类别的输出维度- Softmax激活函数进行分类概率输出和整个网络- Dropout使用p = 0.6L2正则化使用λ = [0.0005 | 0.001]
  • 对于三个推导任务,训练使用的设置为:三个层-– 第1层和第2层:K = 4 | F’ = 256 | ELU – 第3层:K = 6 | F’ = C个类别 | Sigmoid(多标签)无正则化和dropout

使用PyTorch在下面完成了第一个设置的实现,使用了我们之前定义的层:

class GAT(nn.Module):    def __init__(self,        in_features,        n_hidden,        n_heads,        num_classes,        concat=False,        dropout=0.4,        leaky_relu_slope=0.2):        super(GAT, self).__init__()        # 定义图注意力层        self.gat1 = GraphAttentionLayer(            in_features=in_features, out_features=n_hidden, n_heads=n_heads,            concat=concat, dropout=dropout, leaky_relu_slope=leaky_relu_slope            )                self.gat2 = GraphAttentionLayer(            in_features=n_hidden, out_features=num_classes, n_heads=1,            concat=False, dropout=dropout, leaky_relu_slope=leaky_relu_slope            )    def forward(self, input_tensor: torch.Tensor , adj_mat: torch.Tensor):        # 应用第一个图注意力层        x = self.gat1(input_tensor, adj_mat)        x = F.elu(x) # 对第一层的输出应用ELU激活函数        # 应用第二个图注意力层        x = self.gat2(x, adj_mat)        return F.softmax(x, dim=1) # 应用softmax激活函数

在测试后,作者报告了四个基准测试的性能,显示了GAT与现有GNN方法相比的可比结果。

用于Cora、Citeseer和Pubmed的分类准确度总结——来自原始论文。
PPI数据集的微平均F1分数总结——来自原始论文。

结论

总之,在本博文中,我试图采用详细易懂的方法来解释Veličković等人的“图注意力网络”论文,通过使用插图帮助读者理解这些网络背后的主要思想,以及它们在处理复杂的图结构数据(例如社交网络或分子)中的重要性。此外,本博文还包括使用流行的编程框架PyTorch对模型进行实际实现的实例。通过阅读博文并尝试代码,我希望读者能够对GAT的工作原理和在实际场景中的应用有一个扎实的理解。希望本博文对您有所帮助,并激发您进一步探索这个令人兴奋的研究领域。

另外,您可以访问本文中使用的完整代码,其中包含了GitHub存储库中的训练和验证代码。

我很乐意听取任何意见或对本文的建议/修改。

参考文献

[1] — 图注意力网络(2017年),Petar Veličković, Guillem Cucurull, Arantxa Casanova, Adriana Romero, Pietro Liò, Yoshua Bengio. arXiv:1710.10903v3

[2] — 大规模图上的归纳表示学习(2017年),William L. Hamilton, Rex Ying, Jure Leskovec. arXiv:1706.02216v4

[3] — 具有图卷积网络的半监督分类(2016年),Thomas N. Kipf, Max Welling. arXiv:1609.02907v4

Leave a Reply

Your email address will not be published. Required fields are marked *