玩命加载中 . . .

Graph Attention Networks论文笔记


Methods

我们来看看论文中的图注意力机制。对于图$\mathcal{G}(V,E)$,图中结点数量为$N=|V|$。我们把所有结点的特征向量存入集合,记作

而每一个结点特征维度为$F$,因此$\forall i \in \{1,2,\dots, N\}, \vec{h}_i \in \mathbb{R}^F$。

  1. 将特征映射到高层特征,因此需要在最开始加入一个线性变换。对第$i$个结点的特征向量$\vec{h}_i$,施加一个矩阵$\mathbf{W}\in \mathbb{R}^{F’\times F}$(所有结点共享),做矩阵乘法

  2. 计算自注意力,引入映射$a:\mathbb{R}^{F’} \times \mathbb{R}^{F’} \to \mathbb{R}$(权值共享)。比如,第$j$个结点对于第$i$个结点的重要度(关注度)就是

    在图注意力机制中,对于第$i$个结点,我们仅仅计算那些与之邻接的结点对于其的重要度

  3. 归一化注意力值。记与结点$i$邻接的结点集合为$\mathcal{N}_i$,则注意力系数

  4. 计算最终输出的新特征向量

论文中接下来指出了$a$具体是什么:实验中$a$其实就是一个简单的前馈神经网络层,并接上激活函数LeakyReLU。具体描述如下

其中$\mathbf{a}\in \mathbb{R}^{2F’}$;由上文可知,$\mathbf{W}\vec{h}_i, \mathbf{W}\vec{h}_j \in \mathbb{R}^{F’}$,而我们将它们竖向拼接起来(以下用||简记)。

至此,将注意力系数的完整计算过程表示为

同样地,图注意力中也有多头注意力机制。设置有$K$个头,在上述第4步计算输出时,变为

同样地,|| 表示拼接操作。显然,$\vec{h}_i’ \in \mathbb{R}^{KF’}$。

代码实现

具体到代码实现图注意力时,我们将所有结点的特征向量存入矩阵$\mathbf{h}$,它便是注意力层的输入

而经过矩阵乘法,$\mathbf{W}\in \mathbb{R}^{F\times F’}$,得

而计算注意力值的映射中用到的线性变换矩阵为$\mathbf{a} \in \mathbb{R}^{2F’\times 1}$,分别计算

于是将计算注意力系数的过程转化为

其中存在矩阵广播加法,正是巧妙用到了这个特性实现了这个计算注意力系数过程。

下面简单证明上述过程的正确性。

为了方便表示,我们把上面出现过的一些矩阵式展开写明:

其中$\vec{h}_i, i\in \{1,2,\dots,N\}$是第$i$个结点的特征向量。

我们进而将矩阵$\mathbf{a}_{2F’\times1}$写为

因此,

故有

故第$j$个结点对于第$i$个结点的关键度为

class GraphAttentionLayer(nn.Module):
    """
    Simple GAT layer, similar to https://arxiv.org/abs/1710.10903
    in_features: 结点原始特征向量维度
    out_features: 经过映射后的特征向量维度
    concat: 默认为True,表示将特征向量进行拼接
    """
    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.transpose(0, 1)
        return self.leakyrelu(e)

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

文章作者: 鹿卿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 鹿卿 !
评论
  目录