GAT浅入

前言

在之前介绍的GraphSAGE[3]文章中,通过融合当前节点的邻居节点来获得这个节点的特征表示,从而将GCN扩展到了归纳学习的领域。在GraphSAGE中,各个邻居节点被等同的看待,然而在实际场景中,不同的邻居节点可能对核心节点起着不同的作用。这一部分要介绍的GAT(Graph Attention Network)[1]就是通过自注意力机制(self-attention)[2] 来对邻居节点进行聚合,实现了对不同邻居的权值自适应匹配,从而提高了模型的准确率。GAT在归纳学习和转导学习的任务中均取得了不错的效果。源代码参考:https://github.com/PetarV-/GAT

1. GAT详解

和很多深度学习方法类似,GAT由若干个功能相同的block组成,这个block叫做Graph Attention Layer,首先我们先介绍Graph Attention Layer的结构。

1.1 图注意力层

图注意力层(Graph attention layer)的输入时节点的特征值 [公式] ,其中 [公式] 是节点的个数, [公式] 是节点特征的维度。经过一个Graph Attention Layer后输出一个新的特征向量,假设这个特征向量的节点特征的维度为 [公式] (可以为任意值),这个特征可以表示为 [公式] ,如图1所示。

img图1:GAT中的注意力层

这里使用Self-attention的目的就是提高 [公式] 的表达能力。在Graph Attention Layer中,首先使用一个权值矩阵 [公式] 作用到每个节点,然后对每个节点使用self-attention来计算一个attention系数,这里使用的共享的self-attention机制,表示为 [公式]

[公式]

[公式] 表示节点 [公式] 对于节点 [公式] 的重要性。理论上我们可以计算图中任意一个节点到中心节点的权值,GAT中为了简化计算,将节点限制在了中心节点的一跳邻居内,另外节点也将自己作为邻居节点考虑了进去。

[公式] 的选择有多种方式,论文中作者选择了一个参数为 [公式] 的单层前馈神经网络,然后使用了 [公式] 做非线性化,因此 [公式] 可以写做:

[公式]

最后使用了 [公式] 对中心节点的邻居节点做了归一化:

[公式]

最终通过对输入特征的加权得到输出特征 [公式]

[公式]

在源码中,作者添加了Dropout以及残差结构的超参供结构调整,这一部分的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def attn_head(seq, out_sz, bias_mat, activation, in_drop=0.0, coef_drop=0.0, residual=False):
with tf.name_scope('my_attn'):
if in_drop != 0.0:
seq = tf.nn.dropout(seq, 1.0 - in_drop)
seq_fts = tf.layers.conv1d(seq, out_sz, 1, use_bias=False)
# simplest self-attention possible
f_1 = tf.layers.conv1d(seq_fts, 1, 1)
f_2 = tf.layers.conv1d(seq_fts, 1, 1)
logits = f_1 + tf.transpose(f_2, [0, 2, 1])
coefs = tf.nn.softmax(tf.nn.leaky_relu(logits) + bias_mat)
if coef_drop != 0.0:
coefs = tf.nn.dropout(coefs, 1.0 - coef_drop)
if in_drop != 0.0:
seq_fts = tf.nn.dropout(seq_fts, 1.0 - in_drop)
vals = tf.matmul(coefs, seq_fts)
ret = tf.contrib.layers.bias_add(vals)
# residual connection
if residual:
if seq.shape[-1] != ret.shape[-1]:
ret = ret + conv1d(seq, ret.shape[-1], 1) # activation
else:
ret = ret + seq
return activation(ret) # activation

我们对上面的重点几行代码进行解读。首先是这个函数的三个输入:

  • seq: 输入节点的特征矩阵,维度为[num_graph, num_node, fea_size]
  • out_sz: 输出特征的维度,也就是 [公式] 的维度;
  • bias_mat:图经过过变换后的掩码矩阵,维度为[num_node, num_node]

第5行是对原始的节点特征seq利用卷积核大小为1的一维卷积得到维度为[num_graph, num_node, out_sz]的特征向量。

接着第7,8行对得到的seq_fts分别使用两个独立的卷积核大小为1的卷积核进行一维卷积,得到节点本身的投影f_1以及其邻居的投影f_2。这里对应的是公式1中的 [公式]

第9行是使用广播机制将f_2转置后与f_1叠加,得到注意力矩阵 [公式]

最后通过第10行的softmax归一化便得到了注意力的权重。需要注意的是在计算softmax之前加了一个bias_mat矩阵,那么这个bias_mat是个什么东西呢?它的作用是让非互为邻居的注意力 [公式] 不要进入softmax计算。

当进行权值加权时,一个最简单的思想便是使用一个只有0,1的邻接矩阵和得到的矩阵进行单位乘的运算。但是因为softmax有exp指数运算,这种运算方式会有问题。例如一个节点的邻接向量为 [公式] ,权值向量为 [公式] ,经过mask得到 [公式] ,再送入到softmax归一化,变为 [公式] ,这里需要被mask掉的1.2变成了 [公式] 。这个非邻居节点还是参与到了权值的计算。所以我们需要将非邻居节点的权值变为0,即加上了bias_mat矩阵。

这个矩阵的生成方式为utils/process.pyadj_to_bias函数,这个函数的解析见下面代码的注释部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def adj_to_bias(adj, sizes, nhood=1):
nb_graphs = adj.shape[0] # num_graph个图
mt = np.empty(adj.shape) # 输出矩阵的形状和adj相同

# 图g的转换
for g in range(nb_graphs):
mt[g] = np.eye(adj.shape[1]) # 与g形状相同的对角矩阵
for _ in range(nhood): # 通过self-loop构建K阶邻接矩阵,即A^(K),这里K=1
mt[g] = np.matmul(mt[g], (adj[g] + np.eye(adj.shape[1])))
# 大于0的置1,小于等于0的保持不变
for i in range(sizes[g]):
for j in range(sizes[g]):
if mt[g][i][j] > 0.0:
mt[g][i][j] = 1.0
# mt中1的位置为0,位置为0的返回很小的负数-1e9
return -1e9 * (1.0 - mt)

1.2 多头图注意力层

为了提高注意力机制的泛化能力,GAT选择使用了多头注意力层,即使用K组相互独立的1.1中的单头注意力层,然后将它们的结果拼接在一起,如图2所示。

[公式]

img图2:GAT中的多头注意力层

其中 [公式] 表示拼接操作, [公式] 表示第 [公式] 组注意力机制计算出来的权重系数, [公式] 是第 [公式] 个模块的权重系数。为了减少特征向量的维度,我们也可以使用平均操作代替拼接操作,如式(6)。在图2中,不同颜色的箭头代表了不同的注意力头,从图中我们可以看出 [公式] 。为了增加多头注意力层的表达能力,我们可以使用不同形式的注意力机制。

[公式]

在作者本人的博客中[4],它提到了在Attention的权重系数 [公式] 上施加Dropout[5]将大大提升模型的泛化能力,尤其是数据集比较小的时候。这种方式本质上其实就是对邻居节点的随机采样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def inference(inputs, nb_classes, nb_nodes, training, attn_drop, ffd_drop, bias_mat, hid_units, n_heads, activation=tf.nn.elu, residual=False):
attns = []
# GAT中预设了8层attention head
for _ in range(n_heads[0]):
attns.append(layers.attn_head(inputs, bias_mat=bias_mat,
out_sz=hid_units[0], activation=activation,
in_drop=ffd_drop, coef_drop=attn_drop, residual=False))
h_1 = tf.concat(attns, axis=-1)
# 隐藏层,hid_units表示每一层attention head中的隐藏单元个数
for i in range(1, len(hid_units)):
h_old = h_1
attns = []
for _ in range(n_heads[i]):
attns.append(layers.attn_head(h_1, bias_mat=bias_mat,
out_sz=hid_units[i], activation=activation,
in_drop=ffd_drop, coef_drop=attn_drop, residual=residual))
h_1 = tf.concat(attns, axis=-1)
out = []
# 输出层
for i in range(n_heads[-1]):
out.append(layers.attn_head(h_1, bias_mat=bias_mat,
out_sz=nb_classes, activation=lambda x: x,
in_drop=ffd_drop, coef_drop=attn_drop, residual=False))
logits = tf.add_n(out) / n_heads[-1]

return logits

推理模块由3个循环组成,第一个循环是对attention head的聚合,输入维度是[batch_size, num_node, fea_size],每个注意力头的输出维度为[batch_size, num_node, out_sz],将所有的节点聚合,得到的输出特征维度为[batch_size, num_node, out_sz * 8]。第二个循环是中间层的更新,层数是len(hid_units)-1,第 [公式] 层有n_heads[i]个注意力头。最后一个循环是输出层,为了使输出维度是[batch_size, num_node, nb_classes],因此使用了平均的聚合方式。

2. GAT的属性

根据我们对GAT算法的分析,我们可以总结出GAT的下述属性:

  • 高效:因为注意力层的参数对于整张图是共享的,因此注意力机制的权值可以并行计算,同样节点的属性值也可以并行计算。同时因为计算中心节点的特征只需要遍历其一阶邻居节点,这也大幅减少了搜索节点需要的时间。
  • 低存储:可以使用稀疏矩阵对GAT的图进行存储,因此需要的最大存储空间为 [公式] 。同时因为GAT使用了参数共享的方式,也大幅减少了存储计算参数需要占用的存储空间。
  • 归纳学习:因为GAT是基于邻居节点的计算方式,因此也是可归纳的(Inductive)。
  • 全图访问:GraphSAGE的采样方式是采样固定数量的邻居,而GAT是采样所有的邻居节点,得到的特征更稳定以及更具表征性。

3. 总结

这篇文章介绍了一个基于Attention机制的图神经网络,和NLP中的Attention机制类似,GAT的Attention也是非常直观的。同GraphSAGE一样,GAT也是一个基于空域的GNN,而且是可以进行归纳学习的。GAT的一个问题是因为只归纳了一阶邻居,导致GAT的感受野必须依赖非常深的网络才能扩展到很大,为了解决这个问题,作者在源码中也添加了一个残差机制。

reference

[1] Veličković, Petar, et al. “Graph attention networks.” arXiv preprint arXiv:1710.10903 (2017).

[2] Vaswani A, Shazeer N, Parmar N, et al. Attention is all you need [C]//Advances in Neural Information Processing Systems. 2017: 5998-6008.

[3] Hamilton, Will, Zhitao Ying, and Jure Leskovec. “Inductive representation learning on large graphs.” Advances in neural information processing systems. 2017.

[4] https://petar-v.com/GAT/

[5] Srivastava, Nitish, et al. “Dropout: a simple way to prevent neural networks from overfitting.” The journal of machine learning research 15.1 (2014): 1929-1958.