Gat Note


layout: post title: GAT categories: MachineLearning tags: —

这是官方给出的GAT的实现,在一个小数据集上进行。

先介绍下数据集: cora 是一个论文分类任务,每篇论文都由一个1433维的词向量表示,所以,每个样本点具有1433个特征。词向量的每个元素都对应一个词,且该元素只有0或1两个取值。取0表示该元素对应的词不在论文中,取1表示在论文中。所有的词来源于一个具有1433个词的字典。数据集共2708篇论文,每篇论文都被划分为7个类别中的一类。 这也是个多分类问题。

加载数据

在代码中,数据集的相关变量如下;

‘x’, 训练集特征矩阵,1401433 ‘y’, 训练集label,1407 ‘tx’, 测试集特征矩阵,10001433 ‘ty’,测试集label,10007 ‘allx’, 所有有标注和未标注的训练集样本的特征矩阵<1708,1433> ‘ally’, 所有有标注和未标注的训练集样本的label,1708*7 ‘graph’ 图结构,存储每个节点的邻居,格式为 {index:[index_of_neighbor_nodes]}

allx 和tx组成features特征矩阵。

label是one hot格式的。

test_idx_reorder应该是测试集样本shuffle后的顺序,最小值为1708。 代码中将allx和tx存储在同一个矩阵中,作为整个数据集的表示,记为featuers,ally和y也存储在一起,记为labels。 然后按照test_idx_reorder的顺序对矩阵中的测试集进行排列。

训练集的下标范围为[0,140) 验证集的下标范围为[140,140+500) 测试集的下标范围为[1708,1708+1000)

然后,根据各个数据集的下标范围,生成各自的mask向量,维度为[2708]。如果样本属于该数据集,则mask中的该值为true,否则为false。

然后根据各自的mask向量获取各自的label矩阵。

预处理(归一化)

然后对features进行归一化,归一化的方法是每个样本都除以该样本的非零取值的特征个数。即如果一个样本有5个特征非0, 那么这5个特征值都除以5,值都变为0.2。 实现时,先求每个样本的非零取值个数,即按行维度求和,然后取倒数,记为r_inv,如果该样本的所有特征取值都为0,那么和也置为0。 然后生成以r_inv中的元素为对角元素的对角矩阵,记为r_mat_inv。最后r_mat_inv和features进行矩阵乘法,即得到归一化后的特征矩阵。

为啥按照行来归一化呢?

生成k阶邻接矩阵

然后,根据邻接矩阵生成k阶邻接矩阵。

生成邻接矩阵的代码如下:

    for _ in range(nhood):

        mt[g] = np.matmul(mt[g], (adj[g] + np.eye(adj.shape[1])))

nhood为邻居跳数。 mt[g]第第g个图的k阶邻接矩阵,初始化为单位对角阵。 adj[g]为邻接矩阵。

这里是求图g的k阶邻接矩阵。由于adj[g]加上了单位对角阵,所以这里每个节点都可以自循环。

mt[g]中的元素大于0,表明两个节点可以在k步到达,此时将其置为1,表示可达。等于0的元素不变,表示不可达。

最后返回-1e9 * (1.0 - mt),即可达的两个节点之间的值置为0,不可达的节点之间置为-e9。

这样是为了后面attention归一化,不可达的两个节点之间置为-e9,这样计算softmax时attention权重就是0。

这时候我们就有图的结构即临接矩阵了,下面就是在图上进行GAT了。

GAT

由于代码用的是小模型,所以每个step都是用的整个图,即batch_size=1。 整个GAT 是使用multi-head attention 机制来聚合所有的邻居节点的信息的。

attention

输入维度为[batch_size=1,node_num=2708, fea_num=1443],conv1d后变成[graph_num=1,node_num=2708, hidden_num=8],即对每个节点做了次线性变换,记为seq_fts。 然后再对每个节点分别进行两次conv1d,变成[graph_num=1,node_num=2708, hidden_num=1],分别记为f1/f2。 f2转置后加上f1,得到graph_num=1,node_num=2708,node_num=2708]

这个就是attention矩阵。这个矩阵是图中每个节点和图中所有节点的attention得分。

然后对attention矩阵应用激活函数leaky_relu。 再加上bias矩阵。 因为bias矩阵中可达的两个节点之间的值为0,不可达的两个节点之间置为-e9,这样加上之后,可达的节点之间的attention不变,不可达的两个节点之间的值变为-e9。 最后,应用softmax计算最终的attention得分时,不可达之间的两个节点之间的attention得分就是0。 就达到了只对邻居节点计算attention的目的。 然后分别对attention矩阵和seq_fts进行dropout。 最后,attention矩阵和seq_fts进行矩阵乘法,对节点进行加权。得到加权后的节点表示,记为ret。

最后,再执行residual操作,将ret和输入矩阵相加后返回。 如果输入矩阵的维度和ret的维度不同,输入矩阵需要再次conv1d,转换为相同维度后再相加。最后执行激活函数,返回。

此时,即为attention的全过程。

multi-head attention

将上面的attention过程执行多次,将每次的结果concat起来 作为每个节点聚合后的表示。

然后,可以将上面的multi-head attention 执行多次,类似于Transformer中的encoder层的堆叠,前一层到的输出作为后一层的输入。

假设第一层multi-head attention 聚合到了一跳邻居的信息,得到了每个节点的表示。 在这个表示上再次进行multi-head attention ,此时,应该是聚合到了二跳邻居的信息。 如此多次,就可以聚合到多跳邻居的信息。

最后一层的输出即节点最终的表示,然后计算每个节点的概率分布。

概率分布的计算也是使用attention得到的,和前面的attention过程的不同在于 1.激活函数是线性的,因为attention只是先得到logit 2.没有residual 3.输出维度是类别个数

最后,计算损失和准确率。

计算损失时 使用mask向量来求loss的均值和准确率的均值。

由于训练时 输入模型的是整个图,包含了所有的样本,而训练集只占一部分,所以需要用mask向量选出来训练集中的样本。但是代码里面的实现很奇怪

 def masked_softmax_cross_entropy(logits, labels, mask):
        """Softmax cross-entropy loss with masking."""
        #loss的维度是[batch_size,],表示每个样本的损失
        loss = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=labels)
        mask = tf.cast(mask, dtype=tf.float32)
        mask /= tf.reduce_mean(mask) #求均值是为了啥
        loss *= mask
        return tf.reduce_mean(loss)

mask除以均值之后再和loss相乘,那么相当于非0的mask的样本的loss 都乘以了总样本数再除以mask为1的样本数,最后再除以总共样本数,相当于loss除以了非0的样本数,通过这种方法来求loss均值。 但是直接和mask相乘再取均值不是也可以吗?

训练时重复进行多个epoch,每次选取一个batch进行训练,并计算损失和准确率。

训练完一个epoch 之后,再在验证集上进行验证。

代码中采用了early stop策略。

验证集上的准确率或者损失 二者之一 有正向的话 则更新对应的指标; 如果两个指标都正向的话,则记录下来,作为当前最优的指标结果 并把curr_step置为0,表示最优结果是在当前step取得的 如果没有一个指标正向,curr_step加1,并判断是否到达阈值patience, 表示连续经过patience步,指标都没有取得正向进展,提前结束训练

最后,在测试集上测试。

三种稀疏矩阵存储格式:

1.LIL

List of Lists Format

a. An array (self.rows) of rows, each of which is a sorted list of column indices of non-zero elements. b. The corresponding nonzero values are stored in similar fashion in self.data

2.Coordinate Format (COO)

three NumPy arrays: row, col, data data[i] is value at (row[i], col[i]) position

3.Compressed Sparse Row Format (CSR)

indices is array of column indices data is array of corresponding nonzero values indptr points to row starts in indices and data

https://scipy-lectures.org/advanced/scipy_sparse/storage_schemes.html

cora 数据集是一个分类数据集,所以在上面的gat 模型预测每个节点属于每个类的概率

有几个问题

1.attention的计算过程为啥和一般的attention实现不一样

2.masked_softmax_cross_entropy 中为啥对mask求均值后再乘以loss

3.对feature进行预处理时为啥要进行归一化

4.基于临接矩阵求k阶邻居的原理是啥

5.为啥训练时对所有样本进行采样,只是在最后算loss和accuracy时才mask出所有的训练样本,这样在训练时可能会用到验证集或者测试集样本。不过由于loss 只在训练样本上计算,所以验证集和测试集样本虽然在训练过程中有计算,但是并不会参与到对参数的更新。

参考:

https://zhuanlan.zhihu.com/p/161827463

https://blog.csdn.net/u012856866/article/details/107227491

宁雨 /
Published under (CC) BY-NC-SA in categories tagged with
comments powered by Disqus