Methods
我们来看看论文中的图注意力机制。对于图$\mathcal{G}(V,E)$,图中结点数量为$N=|V|$。我们把所有结点的特征向量存入集合,记作
而每一个结点特征维度为$F$,因此$\forall i \in \{1,2,\dots, N\}, \vec{h}_i \in \mathbb{R}^F$。
将特征映射到高层特征,因此需要在最开始加入一个线性变换。对第$i$个结点的特征向量$\vec{h}_i$,施加一个矩阵$\mathbf{W}\in \mathbb{R}^{F’\times F}$(所有结点共享),做矩阵乘法
计算自注意力,引入映射$a:\mathbb{R}^{F’} \times \mathbb{R}^{F’} \to \mathbb{R}$(权值共享)。比如,第$j$个结点对于第$i$个结点的重要度(关注度)就是
在图注意力机制中,对于第$i$个结点,我们仅仅计算那些与之邻接的结点对于其的重要度。
归一化注意力值。记与结点$i$邻接的结点集合为$\mathcal{N}_i$,则注意力系数
计算最终输出的新特征向量
论文中接下来指出了$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) + ')'