图网络 | Graph Attention Networks | ICLR 2018 | 代码讲解

【前言】:之前断断续续看了很多图网络、图卷积网络的讲解和视频。现在对于图网络的理解已经不能单从文字信息中加深了,所以我们要来看代码部分。现在开始看第一篇图网络的论文和代码,来正式进入图网络的科研领域。

  • 论文名称:‘GRAPH ATTENTION NETWORKS’
  • 文章转自:微信公众号“机器学习炼丹术”
  • 笔记作者:炼丹兄
  • 联系方式:微信cyx645016617(欢迎交流,共同进步)
  • 论文传送门:https://arxiv.org/pdf/1710.10903.pdf

0

1 代码实现

  • 代码github:https://github.com/Diego999/pyGAT
  • 评价:这个github简洁明了,下载好cora数据集后,直接修改一下路径就可以运行了。我这里的代码讲解也是基于这个github的内容。

1.1 实验结果

因为这是我第一次看GNN的论文,所以我也不知道2018年之后的发展如何(不过估计爆发式发展吧),Graph Attention Network时这样的结果:

可以看到,cora的精度时0.83左右,而我用官方代码测试的结果为:

说着至少这是一个比较solid的研究了。

1.2 数据读取

Cora数据集由机器学习论文组成,是近年来图深度学习很喜欢使用的数据集。在数据集中,每一个论文就是一个样本,每一样论文的特征就是某一个单词是否包含在这个论文当中。也就是一个0/1的向量。论文的标签就是论文的类别,总共有7个类别:

  • 基于案例
  • 遗传算法
  • 神经网络
  • 概率方法
  • 强化学习
  • 规则学习
  • 理论

论文是一个节点,那么这个节点的邻居有谁那?引用关系。论文的选择方式是,在最终语料库中,每篇论文引用或被至少一篇其他论文引用。整个语料库中有2708篇论文。

在词干堵塞和去除词尾后,只剩下1433个独特的单词。文档频率小于10的所有单词都被删除。

下面是从txt的数据文件中读取,得到每一个样本的标签、特征,以及样本和样本之间的邻接矩阵的函数。

import numpy as np
import scipy.sparse as sp
import torch


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


def load_data(path="./data/cora/", dataset="cora"):
    """Load citation network dataset (cora only for now)"""
    print('Loading {} dataset...'.format(dataset))

    idx_features_labels = np.genfromtxt("{}/{}.content".format(path, dataset), dtype=np.dtype(str))
    features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
    labels = encode_onehot(idx_features_labels[:, -1])

    # build graph
    idx = np.array(idx_features_labels[:, 0], dtype=np.int32)
    idx_map = {j: i for i, j in enumerate(idx)}
    edges_unordered = np.genfromtxt("{}/{}.cites".format(path, dataset), dtype=np.int32)
    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]))

    idx_train = range(140)
    idx_val = range(200, 500)
    idx_test = range(500, 1500)

    adj = torch.FloatTensor(np.array(adj.todense()))
    features = torch.FloatTensor(np.array(features.todense()))
    labels = torch.LongTensor(np.where(labels)[1])

    idx_train = torch.LongTensor(idx_train)
    idx_val = torch.LongTensor(idx_val)
    idx_test = torch.LongTensor(idx_test)

    return adj, features, labels, idx_train, idx_val, idx_test


def normalize_adj(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))
    r_inv_sqrt = np.power(rowsum, -0.5).flatten()
    r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0.
    r_mat_inv_sqrt = sp.diags(r_inv_sqrt)
    return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt)


def normalize_features(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))
    r_inv = np.power(rowsum, -1).flatten()
    r_inv[np.isinf(r_inv)] = 0.
    r_mat_inv = sp.diags(r_inv)
    mx = r_mat_inv.dot(mx)
    return mx


def accuracy(output, labels):
    preds = output.max(1)[1].type_as(labels)
    correct = preds.eq(labels).double()
    correct = correct.sum()
    return correct / len(labels)

其中,关键的函数就是:

  1. sp是scipy的sparse库函数,稀疏矩阵操作;
  2. sp.coo_matrix(a,b,c,shape,dtype)这个函数就是构建一个技术矩阵。b是矩阵的行,c是矩阵的列,a是b行c列的那个数字,shape是构建的稀疏矩阵的尺寸。这个函数不清楚可以百度去。这样我们得到的返回值,就是一个矩阵,里面的元素是从被引用文献id指向引用文献的id。
  3. adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)
    这个方法,就是让有方向的指向变成双向的邻接矩阵。加的第一个因子会重复加上自己引用自己的情况(这种情况再论文中不会出现,但是再其他图网络中可能出现节点连接自己的情况)。而减去的因子就是避免上述重复计算自己连接自己的情况。
  4. normalize_feature就是很简单的让每一个样本的特征除以他们的和。使得,每一个样本的特征值的和都是1.
  5. normalia_adj类似上面的过程,是让样本的行和列都进行标准化,具体逻辑很难讲清楚,自己体会。

1.3 模型部分

output = model(features, adj)
loss_train = F.nll_loss(output[idx_train], labels[idx_train])

可以看到,模型是把特征和临界矩阵都放进去了,然后输出的output,应该就是每一个样本的分类概率了。之后再通过交叉熵计算得到loss。

model = GAT(nfeat=features.shape[1], 
                nhid=8, 
                nclass=int(labels.max()) + 1, 
                dropout=0.6, 
                nheads=8, 
                alpha=0.2)

构建GAT的时候,nfeat表示每一个样本的特征数目,这里是1433个,nhid待定含义,nclass就是分类的类别,nheads待定含义,alpha=0.2待定含义。

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

        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)

上面就是模型构建的pytorch模型类。可以发现:

  • 有几个nhead,self.attentions中就会有几个GraphAttentionLayer。最后再加一个self.out_att的GraphAttentionLayer,就构成了全部的网络。
  • forward阶段,特征先进行随机的dropout,dropout率这么大不知道是不是图网络都是这样的,六个悬念把
  • 经过dropout的模型,分别经过之前不同的nheads定义的GraphAttentionLayer,然后把所有的结果都concat起来;
  • 再进行一次dropout后,就进行sefl.out_att就行了。最后用softmax一下就好。

现在其中的关键就是GraphAttentionLayer的构建了

1.4 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
        self.alpha = alpha
        self.concat = concat

        self.W = nn.Parameter(torch.empty(size=(in_features, out_features)))
        nn.init.xavier_uniform_(self.W.data, gain=1.414)
        self.a = nn.Parameter(torch.empty(size=(2*out_features, 1)))
        nn.init.xavier_uniform_(self.a.data, gain=1.414)

        self.leakyrelu = nn.LeakyReLU(self.alpha)

    def forward(self, h, adj):
        Wh = torch.mm(h, self.W) # h.shape: (N, in_features), Wh.shape: (N, out_features)
        e = self._prepare_attentional_mechanism_input(Wh)

        zero_vec = -9e15*torch.ones_like(e)
        attention = torch.where(adj > 0, e, zero_vec)
        attention = F.softmax(attention, dim=1)
        attention = F.dropout(attention, self.dropout, training=self.training)
        h_prime = torch.matmul(attention, Wh)

        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)
        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) + ')'

这个GraphAttentionLayer(GAL)中的forward函数,h就是features,shape应该是(2708,1433),adj是节点的邻接矩阵,shape是(2708,2708)

  1. 先用h通过torch.mm得到隐含变量,类似于一个全连接层,把1433个特征缩小到8个特征(nhid=8);
  2. e = self._prepare_attentional_mechanism_input(Wh)这一段应该是这篇论文创新的地方了。这一段里面实在是太抽象了,要看论文才能理解它的含义把可坑,反正这个函数返回的e的shape是(2708,2708)
  3. torch.where这是一个新的函数。在使用A[A>x] = 1这样的in-place操作是不可导的,所以我要使用torch.where(condiciton,B,A)函数。满足条件的A会被对应位置的B替代。所以代码中就是,zero_vec的邻接矩阵大于0的位置的值会被替换成刚刚计算出来的e的对应位置的值。这个就是atteniton,表示临界的节点对于这个节点的不同的重要性的概念把。然后就是dropout,然后就是attention和W相乘。结束了。

【总结一下】,首先经过全连接层讲1433特征压缩成8个特征,然后通过某种机制得到一个注意力权重,然后根据邻接矩阵选出来一部分的权重值,然后在一开始的8个特征进行相乘即可。

1.5 疑惑

这一行代码:

# init部分
self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)]
# forward部分
x = torch.cat([att(x, adj) for att in self.attentions], dim=1)

为什么要构建8个一摸一样的GraphAttentionLayer呢?我感觉就是你用8个一摸一样的卷积层并列起来,其实并不能起到增强特征的效果。

所以我这里使用了不同的nheads来进行实验,看看是否对实验结果有影响:

nheadstest acc
80.84
40.848
20.841
10.8450
120.8480

实验结果表明,其实nheads的个数,对实际的影响并不大,不过既然都做到这里了,我们再来看一下nhid对于实验结果的影响,这里选择nheads为1

nhidtest acc
80.84
160.8500
320.8400
640.8350
40.7940

实验结果表明,nhid太少造成特征缺失,太多又容易过拟合。所以要选择始中才好

### 回答1: Graph Attention Networks(GAT)是一种用于神经网络的重要模型。GAT可以对任意大小和结构的进行监督学习和无监督学习。 GAT基于注意力机制,通过计算节点之间的注意力权重来对进行建模。与传统的神经网络不同之处在于,GAT在每个节点与其相邻节点之间引入了注意力权重。这样,每个节点可以根据其邻居节点的特征和注意力权重来更新自身特征表示。通过自适应地学习权重,GAT可以捕捉到不同节点之间的重要性和关联程度。 具体来说,GAT模型主要包括两个关键组件:多头注意力和特征变换。多头注意力允许模型在不同注意力机制下学习到多种节点表示。而特征变换则通过使用多个线性变换层来改变节点特征的维度。 在GAT模型中,每个节点都会与其邻居节点进行信息交互和特征更新。节点会计算与其邻居节点的相似度得分,然后通过softmax函数进行归一化,以得到注意力权重。最后,节点会将邻居节点的特征与对应的注意力权重相乘并求和,从而得到更新后的特征表示。 GAT模型的优点是能够解决不同节点之间的连接关系和重要性差异的建模问题。由于引入了注意力机制,GAT能够对相邻节点的特征进行自适应的加权处理,从而更好地捕捉到有意义的模式和关联。 总之,Graph Attention Networks是一种基于注意力机制的神经网络模型,能够对任意大小和结构的进行监督学习和无监督学习。它通过自适应地计算节点之间的注意力权重,实现了对中节点特征的有效建模。 GAT模型在社交网络、推荐系统和生物信息学等领域具有广泛的应用前景。 ### 回答2: Graph Attention Networks(GAT)是一种用于处理数据的深度学习模型。传统的神经网络模型使用了节点邻居的平均值来更新节点的表示,这种方法忽略了不同节点在中的重要性和关联度。而GAT模型引入了注意力机制,可以在节点之间动态地学习权重,从而更好地捕捉中节点之间的关系。 GAT模型的核心思想是在每个节点层使用自注意力机制来计算节点之间的注意力权重。具体来说,对于每个节点,GAT模型通过计算与之相邻的节点之间的相似度得到一个归一化的注意力权重。这个相似度可以通过神经网络模块学习得到,其中包括一个共享的权重矩阵。然后,通过将相邻节点的表示与对应的注意力权重相乘并求和,得到一个新的节点表示。这个过程可以通过多头注意力机制来并行计算,从而更好地捕捉节点的重要性和关联度。 GAT模型具有许多优点。首先,GAT模型可以自动学习节点之间的关系,并且可以根据节点之间的重要性分配不同的权重。其次,GAT模型具有较强的可解释性,可以通过注意力权重的可视化来解释模型的决策。此外,GAT模型还可以处理不同类型的数据,包括社交网络、生物网络和推荐系统等。最后,GAT模型在一些数据上表现出了较好的性能,在节点分类、链接预测和分类等任务中取得了良好的结果。 总之,Graph Attention Networks是一种用于处理数据的深度学习模型,通过引入注意力机制,可以动态地学习节点之间的权重,从而更好地捕捉中节点之间的关系。该模型具有较好的可解释性和适用性,在许多数据上取得了较好的性能。 ### 回答3: Graph Attention Networks(GAT)是一种基于神经网络的模型。GAT的目标是在数据上进行节点分类或边预测等任务。与传统的神经网络不同,GAT在节点之间引入了注意力机制,以便在中自动学习节点之间的关系。 GAT的核心思想是为每个节点分配不同的注意力权重,以更好地聚焦于重要的邻居节点。这种分配是通过学习每对节点间的注意力系数来实现的,而不是像传统方法一样使用固定的加权平均。 具体地说,GAT中的每个节点都有自己的特征向量表示,在计算节点之间的注意力权重时,GAT通过将节点对的特征向量与学习到的注意力权重相乘来评估节点之间的关系强度。然后,它将这些关系强度进行归一化处理,以产生每个节点对的注意力系数。最后,通过将注意力系数与邻居节点的特征向量相乘并进行加权求和,可以得到每个节点的输出特征。 与其他神经网络方法相比,GAT具有以下优点:1)它能够自动学习节点之间的关系,而不需要手动指定的拓扑结构;2)它能够根据节点之间的重要性自适应地分配注意力权重;3)它具有较强的可解释性,可以通过分析注意力系数来理解节点之间的关系。 GAT已经在许多数据任务上取得了很好的效果,如社交网络分析、推荐系统和药物发现等。由于其良好的性能和可解释性,GAT在学术界和工业界都得到了广泛的应用,并且也有很多相关的改进和扩展方法出现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值