Pytorch实现GAT(基于PyTorch实现)

Posted 海洋.之心

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Pytorch实现GAT(基于PyTorch实现)相关的知识,希望对你有一定的参考价值。

文章目录


前言

大家好,我是阿光。

本专栏整理了《图神经网络代码实战》,内包含了不同图神经网络的相关代码实现(PyG以及自实现),理论与实践相结合,如GCN、GAT、GraphSAGE等经典图网络,每一个代码实例都附带有完整的代码。

正在更新中~ ✨

🚨 我的项目环境:

  • 平台:Windows10
  • 语言环境:python3.7
  • 编译器:PyCharm
  • PyTorch版本:1.11.0
  • PyG版本:2.1.0

💥 项目专栏:【图神经网络代码实战目录】


本文我们将使用PyTorch来简易实现一个GAT(图注意力网络),不使用PyG库,让新手可以理解如何PyTorch来搭建一个简易的图网络实例demo。

一、导入相关库

本项目是采用自己实现的GAT,并没有使用 PyG 库,原因是为了帮助新手朋友们能够对GAT的原理有个更深刻的理解,如果熟悉之后可以尝试使用PyG库直接调用 GATConv 这个图层即可。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from scipy.sparse import coo_matrix
from torch_geometric.datasets import Planetoid

二、加载Cora数据集

本文使用的数据集是比较经典的Cora数据集,它是一个根据科学论文之间相互引用关系而构建的Graph数据集合,论文分为7类,共2708篇。

  • Genetic_Algorithms
  • Neural_Networks
  • Probabilistic_Methods
  • Reinforcement_Learning
  • Rule_Learning
  • Theory

这个数据集是一个用于图节点分类的任务,数据集中只有一张图,这张图中含有2708个节点,10556条边,每个节点的特征维度为1433。

# 1.加载Cora数据集
dataset = Planetoid(root='./data/Cora', name='Cora')

三、定义GAT网络

3.1 定义GAT层

这里我们就不重点介绍GCN网络了,相信大家能够掌握基本原理,本文我们使用的是PyTorch定义网络层。

对于GATConv的常用参数:

  • in_channels:每个样本的输入维度,就是每个节点的特征维度
  • out_channels:经过注意力机制后映射成的新的维度,就是经过GAT后每个节点的维度长度
  • add_self_loops:为图添加自环,是否考虑自身节点的信息
  • bias:训练一个偏置b

我们在实现时也是考虑这几个常见参数

对于GAT的传播公式为:

x i ′ = α i , i θ x i + ∑ j ∈ N ( i ) α i , j θ x j x_i'=\\alpha_i,i\\theta x_i+ \\sum_j\\in N(i)\\alpha_i,j\\theta x_j xi=αi,iθxi+jN(i)αi,jθxj

上式子的意思就是对自己和邻居的特征进行按照权重聚合,其中的 α \\alpha α 代表注意力分数, θ \\theta θ 代表可学习参数, x j x_j xj 代表邻居节点的特征向量。

其中注意力分数的计算方式如下:

α i , j = e x p ( L e a k y R e L U ( α T [ θ x i ∣ ∣ θ x j ] ) ) ∑ k ∈ N ( i ) ∪ i e x p ( L e a k y R e L U ( α T [ θ x i ∣ ∣ θ x j ] ) ) \\alpha_i,j=\\fracexp(LeakyReLU(\\alpha^T[\\theta x_i||\\theta x_j]))\\sum_k\\in N(i)\\cupiexp(LeakyReLU(\\alpha^T[\\theta x_i||\\theta x_j])) αi,j=kN(i)iexp(LeakyReLU(αT[θxi∣∣θxj]))exp(LeakyReLU(αT[θxi∣∣θxj]))

所以我们的任务无非就是获取这几个变量,然后进行传播计算即可

3.1.1 将节点信息进行空间映射

在注意力公式中,它是首先对邻居节点先进行空间上的映射,实现代码如下:

# 1.计算wh,进行节点空间映射
wh = torch.mm(x, self.weight_w)

3.1.2 注意力分数

第二步就是计算注意力分数,注意一点这个分数并没有被激活,实现的部分就是 LeakyReLU 括号内的部分。

# 2.计算注意力分数
e = torch.mm(wh, self.weight_a[: self.out_channels]) + torch.matmul(wh, self.weight_a[self.out_channels:]).T

3.1.3 获取邻接矩阵

由于我们使用的是内置数据集 Cora,他给出的数据集并没有给出对应的邻接矩阵,所以我们需要手动实现获取该图对应的邻接矩阵。

# 4.获取邻接矩阵
if self.adj == None:
    self.adj = to_dense_adj(edge_index).squeeze()
    
    # 5.添加自环,考虑自身加权
    if self.add_self_loops:
        self.adj += torch.eye(x.shape[0])

3.1.4 获得注意力分数矩阵

在上述GAT的传播公式中我们可以看到,每次加权的节点信息为自身和其邻居节点,所以为了实现非邻居节点不参与加权,我们需要对注意力分数矩阵非邻居节点的位置将其置为一个很小的值,这样在矩阵乘法时就不会发挥什么作用。

# 6.获得注意力分数矩阵
attention = torch.where(self.adj > 0, e, -1e9 * torch.ones_like(e))

该代码的意思就是如果邻接矩阵中位置大于0,也就是该条边存在,那么注意力矩阵对应的位置分数不变,否则将其置为 -1e9 这个很小的数。

3.1.5 加权融合特征

这个部分就是将获得的注意力分数进行归一化,然后将这个矩阵和映射后的特征矩阵进行相乘,实现聚合操作,最终在结果上面添加偏置信息。

# 7.归一化注意力分数
attention = F.softmax(attention, dim=1)

# 8.加权融合特征
output = torch.mm(attention, wh)

# 9.添加偏置
if self.bias != None:
    return output + self.bias.flatten()
else:
    return output         

3.1.6 GATConv层

接下来就可以定义GATConv层了,该层实现了2个函数,分别是 init_parameters()forward()

  • init_parameters():初始化可学习参数
  • forward():这个函数定义模型的传播过程,也就是上面公式的 x i ′ = α i , i θ x i + ∑ j ∈ N ( i ) α i , j θ x j x_i'=\\alpha_i,i\\theta x_i+ \\sum_j\\in N(i)\\alpha_i,j\\theta x_j xi=αi,iθxi+jN(i)αi,jθxj,如果设置了偏置在加上偏置返回即可
# 2.定义GATConv层
class GATConv(nn.Module):
    def __init__(self, in_channels, out_channels, heads=1, add_self_loops=True, bias=True):
        super(GATConv, self).__init__()
        self.in_channels = in_channels # 输入图节点的特征数
        self.out_channels = out_channels # 输出图节点的特征数
        self.adj = None
        self.add_self_loops = add_self_loops
        
        
        # 定义参数 θ
        self.weight_w = nn.Parameter(torch.FloatTensor(in_channels, out_channels))
        self.weight_a = nn.Parameter(torch.FloatTensor(out_channels * 2, 1))
        
        if bias:
            self.bias = nn.Parameter(torch.FloatTensor(out_channels, 1))
        else:
            self.register_parameter('bias', None)
        
        self.leakyrelu = nn.LeakyReLU()
        self.init_parameters()
    
    # 初始化可学习参数
    def init_parameters(self):
        nn.init.xavier_uniform_(self.weight_w)
        nn.init.xavier_uniform_(self.weight_a)
        
        if self.bias != None:
            nn.init.zeros_(self.bias)
        
    def forward(self, x, edge_index):
        # 1.计算wh,进行节点空间映射
        wh = torch.mm(x, self.weight_w)
                                  
        # 2.计算注意力分数
        e = torch.mm(wh, self.weight_a[: self.out_channels]) + torch.matmul(wh, self.weight_a[self.out_channels:]).T

        # 3.激活
        e = self.leakyrelu(e)
        
        # 4.获取邻接矩阵
        if self.adj == None:
            self.adj = to_dense_adj(edge_index).squeeze()
            
            # 5.添加自环,考虑自身加权
            if self.add_self_loops:
                self.adj += torch.eye(x.shape[0])
        
        # 6.获得注意力分数矩阵
        attention = torch.where(self.adj > 0, e, -1e9 * torch.ones_like(e))
        
        # 7.归一化注意力分数
        attention = F.softmax(attention, dim=1)
        
        # 8.加权融合特征
        output = torch.mm(attention, wh)
        
        # 9.添加偏置
        if self.bias != None:
            return output + self.bias.flatten()
        else:
            return output                                   

对于我们实现这个网络的实现效率上来讲比PyG框架内置的 GCNConv 层稍差一点,因为我们是按照公式来一步一步利用矩阵计算得到,没有对矩阵计算以及算法进行优化,不然初学者可能看不太懂,不利于理解GCN公式的传播过程,有能力的小伙伴可以看下官方源码学习一下。

3.2 定义GAT网络

上面我们已经实现好了 GATConv 的网络层,之后就可以调用这个层来搭建 GAT 网络。

# 3.定义GAT网络
class GAT(nn.Module):
    def __init__(self, num_node_features, num_classes):
        super(GAT, self).__init__()
        self.conv1 = GATConv(in_channels=num_node_features,
                                    out_channels=16,
                                    heads=2)
        self.conv2 = GATConv(in_channels=16,
                                    out_channels=num_classes,
                                    heads=1)
        
    def forward(self, data):
        x, edge_index = data.x, data.edge_index
        
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, training=self.training)
        x = self.conv2(x, edge_index)
        
        return F.log_softmax(x, dim=1)

上面网络我们定义了两个GATConv层,第一层的参数的输入维度就是初始每个节点的特征维度,输出维度是16。

第二个层的输入维度为16,输出维度为分类个数,因为我们需要对每个节点进行分类,最终加上softmax操作。

四、定义模型

下面就是定义了一些模型需要的参数,像学习率、迭代次数这些超参数,然后是模型的定义以及优化器及损失函数的定义,和pytorch定义网络是一样的。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
epochs = 10 # 学习轮数
lr = 0.003 # 学习率
num_node_features = dataset.num_node_features # 每个节点的特征数
num_classes = dataset.num_classes # 每个节点的类别数
data = dataset[0].to(device) # Cora的一张图

# 3.定义模型
model = GAT(num_node_features, num_classes).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr) # 优化器
loss_function = nn.NLLLoss() # 损失函数

五、模型训练

模型训练部分也是和pytorch定义网络一样,因为都是需要经过前向传播、反向传播这些过程,对于损失、精度这些指标可以自己添加。

# 训练模式
model.train()

for epoch in range(epochs):
    optimizer.zero_grad()
    pred = model(data)
    
    loss = loss_function(pred[data.train_mask], data.y[data.train_mask]) # 损失
    correct_count_train = pred.argmax(axis=1)[data.train_mask].eq(data.y[data.train_mask]).sum().item() # epoch正确分类数目
    acc_train = correct_count_train / data.train_mask.sum().item() # epoch训练精度
    
    loss.backward()
    optimizer.step()
    
    if epoch % 20 == 0:
        print("【EPOCH: 】%s" % str(epoch + 1))
        print('训练损失为::.4f'.format(loss.item()), '训练精度为::.4f'.format(acc_train))

print('【Finished Training!】')

六、模型验证

下面就是模型验证阶段,在训练时我们是只使用了训练集,测试的时候我们使用的是测试集,注意这和传统网络测试不太一样,在图像分类一些经典任务中,我们是把数据集分成了两份,分别是训练集、测试集,但是在Cora这个数据集中并没有这样,它区分训练集还是测试集使用的是掩码机制,就是定义了一个和节点长度相同纬度的数组,该数组的每个位置为True或者False,标记着是否使用该节点的数据进行训练。

# 模型验证
model.eval()
pred = model(data)

# 训练集(使用了掩码)
correct_count_train = pred.argmax(axis=1)[data.train_mask].eq(data.y[data.train_mask]).sum().item()
acc_train = correct_count_train / data.train_mask.sum().item()
loss_train = loss_function(pred[data.train_mask], data

图注意网络GAT理解及Pytorch代码实现PyGAT代码详细注释

文章目录

GAT

题:Graph Attention Networks
摘要:
提出了图形注意网络(GAT) ,这是一种基于图结构数据的新型神经网络结构,利用掩蔽的自我注意层来解决基于图卷积或其近似的先前方法的缺点。通过叠加层,节点能够参与其邻域的特征,我们能够(隐式地)为邻域中的不同节点指定不同的权重,而不需要任何代价高昂的矩阵操作(如反演) ,或者依赖于预先知道图的结构。通过这种方法,我们同时解决了基于谱的图形神经网络的几个关键问题,并使我们的模型容易地适用于归纳和转导问题。我们的 GAT 模型已经实现或匹配了四个已建立的转导和归纳图基准的最新结果: Cora,Citeseer 和 Pubmed 引用网络数据集,以及protein-protein interaction dataset(其中测试图在训练期间保持不可见)。

Paper with code 网址,可找到对应论文和github源码,原论文使用TensorFlow实现,本篇主要对Pytorch版本的 PyGAT附详细注释帮助理解和测试。

GitHUb: keras版本实现
Pytorch版本实现 PyGAT


截图及下文代码注释参考自视频:GAT详解及代码实现

视频中的eij的实现与源码不同,视频中是先拼接两个W,再与a乘
源码在_prepare_attentional_mechanism_input()函数中先分别与a乘,再拼接

代码实现【PyGAT】

在PyGAT :

  • layers.py中定义Simple GAT layer实现(GraphAttentionLayer)和Sparse version GAT layer实现(SpGraphAttentionLayer)。
  • models.py 实现两个版本加入Multi-head机制
  • trains.py 使用model定义的GAT构建模型进行训练,使用cora数据集

GraphAttentionLayer【一个图注意力层实现】

class GraphAttentionLayer(nn.Module):
    """
    Simple GAT layer, similar to https://arxiv.org/abs/1710.10903
    """
    def __init__(self, in_features, out_features, dropout, alpha, concat=True):
        super(GraphAttentionLayer, self).__init__()
        self.dropout = dropout
        self.in_features = in_features#结点向量的特征维度
        self.out_features = out_features#经过GAT之后的特征维度
        self.alpha = alpha#dropout参数
        self.concat = concat#LeakyReLU参数

        # 定义可训练参数,即论文中的W和a
        self.W = nn.Parameter(torch.empty(size=(in_features, out_features)))
        nn.init.xavier_uniform_(self.W.data, gain=1.414)# xavier初始化
        self.a = nn.Parameter(torch.empty(size=(2*out_features, 1)))
        nn.init.xavier_uniform_(self.a.data, gain=1.414)# xavier初始化

        # 定义leakyReLU激活函数
        self.leakyrelu = nn.LeakyReLU(self.alpha)

    def forward(self, h, adj):
        '''
        adj图邻接矩阵,维度[N,N]非零即一
        h.shape: (N, in_features), self.W.shape:(in_features,out_features)
        Wh.shape: (N, out_features)
        '''
        Wh = torch.mm(h, self.W) # 对应eij的计算公式
        e = self._prepare_attentional_mechanism_input(Wh)#对应LeakyReLU(eij)计算公式

        zero_vec = -9e15*torch.ones_like(e)#将没有链接的边设置为负无穷
        attention = torch.where(adj > 0, e, zero_vec)#[N,N]
        # 表示如果邻接矩阵元素大于0时,则两个节点有连接,该位置的注意力系数保留
        # 否则需要mask设置为非常小的值,因为softmax的时候这个最小值会不考虑
        attention = F.softmax(attention, dim=1)# softmax形状保持不变[N,N],得到归一化的注意力全忠!
        attention = F.dropout(attention, self.dropout, training=self.training)# dropout,防止过拟合
        h_prime = torch.matmul(attention, Wh)#[N,N].[N,out_features]=>[N,out_features]

        # 得到由周围节点通过注意力权重进行更新后的表示
        if self.concat:
            return F.elu(h_prime)
        else:
            return h_prime

    def _prepare_attentional_mechanism_input(self, Wh):
        # Wh.shape (N, out_feature)
        # self.a.shape (2 * out_feature, 1)
        # Wh1&2.shape (N, 1)
        # e.shape (N, N)
        # 先分别与a相乘再进行拼接
        Wh1 = torch.matmul(Wh, self.a[:self.out_features, :])
        Wh2 = torch.matmul(Wh, self.a[self.out_features:, :])
        # broadcast add
        e = Wh1 + Wh2.T
        return self.leakyrelu(e)

    def __repr__(self):
        return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')'

用上面实现的单层网络测试

x = torch.randn(6,10)
adj=torch.tensor([[0,1,1,0,0,0],
                  [1,0,1,0,0,0],
                  [1,1,0,1,0,0],
                  [0,0,1,0,1,1],
                  [0,0,0,1,0,0,],
                  [0,0,0,1,1,0]])
my_gat = GraphAttentionLayer(10,5,0.2,0.2)
print(my_gat(x,adj))
输出:
tensor([[-0.2965,  2.8110, -0.6680, -0.9643, -0.9882],
        [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
        [-0.4981, -0.7515,  1.1159,  0.3546,  1.3592],
        [ 0.4679,  1.7208,  0.3084, -0.5331, -0.1291],
        [-0.4375, -0.8778,  1.1767, -0.5869,  1.5154],
        [-0.2164, -0.5897,  0.4988, -0.3125,  0.6423]], grad_fn=<EluBackward>)

加入Multi-head机制的GAT

用不同head捕捉不同特征,使模型有更好的拟合能力。

class GAT(nn.Module):
    def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads):
        """Dense version of GAT."""
        super(GAT, self).__init__()
        self.dropout = dropout

        # 加入Multi-head机制
        self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)]
        for i, attention in enumerate(self.attentions):
            self.add_module('attention_'.format(i), attention)

        self.out_att = GraphAttentionLayer(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False)

    def forward(self, x, adj):
        x = F.dropout(x, self.dropout, training=self.training)
        x = torch.cat([att(x, adj) for att in self.attentions], dim=1)
        x = F.dropout(x, self.dropout, training=self.training)
        x = F.elu(self.out_att(x, adj))
        return F.log_softmax(x, dim=1)

对数据集Cora的处理


数据集中两个文件,cites:比如上图11行:编号25和编号1114331的文章
content文件:如下图,每篇文章的id、features及类别

csr_matrix()处理稀疏矩阵

utils.py中对数据进行的处理

#数据是稀疏的,csr_matrix操作从行开始将1的位置取出来,对数据进行压缩

features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
labels = encode_onehot(idx_features_labels[:, -1])

encode_onehot()对label编号

有7个类别,通过classes_dict是7*7的对角阵把每个类别映射成不同向量,对所有label进行编号,再将编号转换为one_hot向量

def encode_onehot(labels):
    # The classes must be sorted before encoding to enable static class encoding.
    # In other words, make sure the first class always maps to index 0.
    classes = sorted(list(set(labels)))
    classes_dict = c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)
    labels_onehot = np.array(list(map(classes_dict.get, labels)), dtype=np.int32)
    return labels_onehot

build graph

见注释:

# build graph
    idx = np.array(idx_features_labels[:, 0], dtype=np.int32)#获取所有文章id
    idx_map = j: i for i, j in enumerate(idx)#按文章数目,对id重新映射
    # 读取数据集中文章和文章直接的引用关系
    edges_unordered = np.genfromtxt(".cites".format(path, dataset), dtype=np.int32)
    # 根据idx_map,将文章引用关系也重新映射
    edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), dtype=np.int32).reshape(edges_unordered.shape)
    adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32)

    # build symmetric adjacency matrix 生成邻接矩阵
    adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

    features = normalize_features(features)
    adj = normalize_adj(adj + sp.eye(adj.shape[0]))

邻接矩阵构造

csr_matrix()只记录了(0,1)1,忽略了(1,0)1。所以需要coo_matrix()操作!才能还原出无向图的邻接矩阵!

本文的一些代码注释及截图还可见视频

一个拓展:

GAT的推广

GAT的推广
GAT仅仅是应用在了单层图结构网络上,我们是否可以将它推广到多层网络结构呢?

这里我们假设一个有N层网络的结构,每层网络都定义了相同的节点,但是节点之间的关系有所差异。举一个简单的例子,假设有一个用户关系型网络,每层网络的节点都被定义成了网络中的用户,网络的第一层视图的关系可以定义为,两个用户之间是否具有好友关系;网络的第二层视图可以定义为,你评论过我的动态;网络的第三层视图可以定义为你转发过我的动态;第四层关系可以定义为,你at过我等等。

通过这样的定义我们就完成了一个多层网络的构建,他们共享相同的节点,但又分别具有不同的邻边,如果我们分别处理每一层视图视图,然后将他们得出的节点表示单纯相加的话,就可能会失去不同视图之间的协作关系,降低分类(预测)的精度。

基于以上观点,我们提出了一种新的方法:首先在每一层单视图中应用GAT进行学习,并计算出每层视图的节点表示。之后再不同视图之间引入attention机制来让网络自行学习不同视图的权重。之后根据学习的权重,将各个视图加权相加得到全局节点表示并进行后续的诸如节点表示,链接预测等任务。

同时,因为不同视图共享同样的节点,即使每一层视图都表示了不同的节点关系,最终得到的每一层的节点嵌入表示应具有一定的相关性。基于以上理论,我们在每层GAT的网络参数间引入正则化项来约束参数,使其向互相相近的方向学习。大致的网络流程图如下:

这部分来源于 链接:https://www.jianshu.com/p/d5d366ba1a57 来源:简书

以上是关于Pytorch实现GAT(基于PyTorch实现)的主要内容,如果未能解决你的问题,请参考以下文章

Pytorch+PyG实现GraphConv

Pytorch+PyG实现MLP

Pytorch+PyG实现GCN(图卷积网络)

PyTorch+PyG实现图神经网络经典模型目录

Pytorch实现MLP(基于PyTorch实现)

基于pytorch使用实现CNN 如何使用pytorch构建CNN卷积神经网络