当前位置:   article > 正文

GAT源码分析——从DEBUG走起_gat代码

gat代码

近期的学习中,总会遇到一个问题:好不容易弄懂了一个个数学公式,但是真正在实现的时候不知道代码怎么去写。因此,参考已有的模型代码区复现分析变得重要起来,今天选择了一个相对简单的GAT模型,结合已经掌握的GAT相关知识,从源码级别DEBUG走起。

前置工作

代码结构树

  1. pyGAT
  2. ├── data
  3. │ └── cora
  4. │ ├── README
  5. │ ├── cora.cites
  6. │ └── cora.content
  7. ├── LICENSE
  8. ├── README.md
  9. ├── layers.py
  10. ├── models.py
  11. ├── requirements.txt
  12. ├── train.py
  13. ├── utils.py

Cora数据集简介

cora.content中包含了七类机器学习的论文,共计2708行,每行以ID起始,中间是1433维的one-hot特征,最后一列是对应的类别

cora.cites中每一行由两个数字构成,第一个数字代表被引用论文的ID。第二个数字代表引用前面论文的那篇论文的ID

加载数据

1a1fc7ff1fac4460b58e8948edd0f381.png

adj是全部文章的引用关系构成的图做过归一化后的邻接矩阵(2708*2708),features为2708篇文章每篇的1433个特征,idx_test,idx_train,idx_val分别是划分出来的测试集训练集与验证集,labels为文章的分类标签

模型定义

GAT初始化

在model处单步步入,看GAT模型类构建的方法

models.py

  1. class GAT(nn.Module):
  2. def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads):
  3. """Dense version of GAT."""
  4. super(GAT, self).__init__()
  5. self.dropout = dropout
  6. self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)]
  7. for i, attention in enumerate(self.attentions):
  8. self.add_module('attention_{}'.format(i), attention)
  9. self.out_att = GraphAttentionLayer(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False) # 第二层(最后一层)的attention layer
  10. def forward(self, x, adj):
  11. x = F.dropout(x, self.dropout, training=self.training)
  12. x = torch.cat([att(x, adj) for att in self.attentions], dim=1) # 将每层attention拼接
  13. x = F.dropout(x, self.dropout, training=self.training)
  14. x = F.elu(self.out_att(x, adj)) # 第二层的attention layer
  15. return F.log_softmax(x, dim=1)

定义第一层GraphAttentionLayer

在第7行,建立了第一个层GraphAttentionLayer,其中nheads参数表示了这是一个多头的GAT

fc38c00bfc8349479aeb7f511d680439.png

接着定义了第一层的GraphAttentionLayer,步入里面,看其中的init方法

layers.py

  1. def __init__(self, in_features, out_features, dropout, alpha, concat=True):
  2. super(GraphAttentionLayer, self).__init__()
  3. self.dropout = dropout
  4. self.in_features = in_features
  5. self.out_features = out_features
  6. self.alpha = alpha
  7. self.concat = concat
  8. self.W = nn.Parameter(torch.empty(size=(in_features, out_features)))
  9. nn.init.xavier_uniform_(self.W.data, gain=1.414)
  10. self.a = nn.Parameter(torch.empty(size=(2*out_features, 1))) # concat(V,NeigV)
  11. nn.init.xavier_uniform_(self.a.data, gain=1.414)
  12. self.leakyrelu = nn.LeakyReLU(self.alpha)
'
运行

首先是对公式中的W的定义,in_features, out_features分别是(1433,8),也就是说W是将1433维的特征转换为8维

接着定义了α向量,作为Whi与Whj向量拼接后的系数,因为是横向拼接所以2*out_features

最后定义了leakyrelu激活函数

6c685a94a27e46ceb502112407deadaf.png

877d101a0aae4f3790a8dd2f395224d5.png

步出GraphAttentionLayer,进入class GAT(nn.Module)line8的循环,因为样例中定义了8个head,因此循环了8次定义了8个GraphAttentionLayer

self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)]

903d5b665bf3459c87ed1e60ecdc9e0b.png

定义第二层GraphAttentionLayer

回到class GAT(nn.Module)的init中line11,对第二层(最后一层)GraphAttentionLayer进行定义

self.out_att = GraphAttentionLayer(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False)

这里的nhid * nheads为8*8,也就是隐层维度与注意力头数相乘,nclass=7是最后要映射到的7个类别上,对于这一层的GraphAttentionLayer内部同上,不再赘述

模型训练

进行初始化

在train.py中有对cuda的定义,如果设置了CUDA使用GPU算的时要转换为Variable,因为variable是一种可以不断变化的变量,符合反向传播 而tensor不能反向传播

Variable计算时,会逐步生成计算图,这个图将所有的计算节点都连接起来,最后进行loss反向传播时,一次性将所有Variable里的梯度计算出来,然而tensor没有这个能力

train.py

  1. if args.cuda:
  2. model.cuda()
  3. features = features.cuda()
  4. adj = adj.cuda()
  5. labels = labels.cuda()
  6. idx_train = idx_train.cuda()
  7. idx_val = idx_val.cuda()
  8. idx_test = idx_test.cuda()
  9. features, adj, labels = Variable(features), Variable(adj), Variable(labels)

跟进train()=>model(features, adj)

train.py

  1. def train(epoch):
  2. t = time.time()
  3. model.train()
  4. optimizer.zero_grad()
  5. output = model(features, adj) # GAT模块
  6. loss_train = F.nll_loss(output[idx_train], labels[idx_train])
  7. acc_train = accuracy(output[idx_train], labels[idx_train])
  8. loss_train.backward()
  9. optimizer.step()
'
运行

再跟进model(features, adj)中的forward

models.py

  1. def forward(self, x, adj):
  2. x = F.dropout(x, self.dropout, training=self.training)
  3. x = torch.cat([att(x, adj) for att in self.attentions], dim=1) # 将每层attention拼接
  4. x = F.dropout(x, self.dropout, training=self.training)
  5. x = F.elu(self.out_att(x, adj)) # 第二层的attention layer
  6. return F.log_softmax(x, dim=1)
'
运行

也就是在定义好的模型上先进行前向传播,先是第一层的GraphAttentionLayer,在上面line3的self.attentions中步入

走进第一层

layers.py

  1. def forward(self, h, adj):
  2. Wh = torch.mm(h, self.W) # h.shape: (N, in_features), Wh.shape: (N, out_features)
  3. a_input = self._prepare_attentional_mechanism_input(Wh) # 每一个节点和所有节点,特征。(Vall, Vall, feature)
  4. e = self.leakyrelu(torch.matmul(a_input, self.a).squeeze(2))
  5. # 之前计算的是一个节点和所有节点的attention,其实需要的是连接的节点的attention系数
  6. zero_vec = -9e15*torch.ones_like(e)
  7. attention = torch.where(adj > 0, e, zero_vec) # 将邻接矩阵中小于0的变成负无穷
  8. attention = F.softmax(attention, dim=1) # 按行求softmax。 sum(axis=1) === 1
  9. attention = F.dropout(attention, self.dropout, training=self.training)
  10. h_prime = torch.matmul(attention, Wh) # 聚合邻居函数
  11. if self.concat:
  12. return F.elu(h_prime) # elu-激活函数
  13. else:
  14. return h_prime
'
运行

先看上面line2的h和W,不难看出是通过W参数矩阵对1433维度特征的压缩

5086e52021c64c499559f192311741dd.png

再看上面line3处的self._prepare_attentional_mechanism_input,跟进后发现比较复杂,还好作者给了解释和说明

layers.py

  1. def _prepare_attentional_mechanism_input(self, Wh):
  2. N = Wh.size()[0] # number of nodes
  3. # Below, two matrices are created that contain embeddings in their rows in different orders.
  4. # (e stands for embedding)
  5. # These are the rows of the first matrix (Wh_repeated_in_chunks):
  6. # e1, e1, ..., e1, e2, e2, ..., e2, ..., eN, eN, ..., eN
  7. # '-------------' -> N times '-------------' -> N times '-------------' -> N times
  8. #
  9. # These are the rows of the second matrix (Wh_repeated_alternating):
  10. # e1, e2, ..., eN, e1, e2, ..., eN, ..., e1, e2, ..., eN
  11. # '----------------------------------------------------' -> N times
  12. #
  13. Wh_repeated_in_chunks = Wh.repeat_interleave(N, dim=0) # 复制
  14. Wh_repeated_alternating = Wh.repeat(N, 1)
  15. # Wh_repeated_in_chunks.shape == Wh_repeated_alternating.shape == (N * N, out_features)
  16. # The all_combination_matrix, created below, will look like this (|| denotes concatenation):
  17. # e1 || e1
  18. # e1 || e2
  19. # e1 || e3
  20. # ...
  21. # e1 || eN
  22. # e2 || e1
  23. # e2 || e2
  24. # e2 || e3
  25. # ...
  26. # e2 || eN
  27. # ...
  28. # eN || e1
  29. # eN || e2
  30. # eN || e3
  31. # ...
  32. # eN || eN
  33. all_combinations_matrix = torch.cat([Wh_repeated_in_chunks, Wh_repeated_alternating], dim=1)
  34. # all_combinations_matrix.shape == (N * N, 2 * out_features)
  35. return all_combinations_matrix.view(N, N, 2 * self.out_features)
'
运行

为了计算两个相邻节点的attention,这里面并没有提取相邻节点,而是把全局所有的N个节点都一一配对,出了N*N个对,得到了如下的向量

c69db7917f7944b3ad8473825f3b8ef5.png

也就是N*N,2*output_feature,也就是所有节点两两拼接的feature,return到forward(self, h, adj)的line3的a_input

a_input = self._prepare_attentional_mechanism_input(Wh)

但是我们要明确的是,需要求的是相邻节点之间的attention,刚刚上面的只是一个全局的初始化

那么,怎么定义相邻节点的attention呢?

先别着急,接着看,line4其实就是eij

e = self.leakyrelu(torch.matmul(a_input, self.a).squeeze(2))

1cb0c8d19654499ab5937ed10355fcd0.png

接着初始化了一个负无穷的zero_vec矩阵

zero_vec = -9e15*torch.ones_like(e)

因为之前定义过一个叫adj的邻接矩阵,储存了节点的相连关系,所以对attention是这样定义的

attention = torch.where(adj > 0, e, zero_vec)

通过这一步,将邻接矩阵adj中大于0(有边的)置为刚刚定义的eij,小于0的变成负无穷,可以看一下效果

e09fa727aaa44cc0a409b1cc3adbf13c.png

但是这并不是最终attention的系数,还要通过softmax归一化

63593b90c04a40eab7039514a258f29d.png

归一化之后的attention,每一个节点对行求和之后系数是1,也就很好的说明了相邻节点对于目标节点的权重系数之和为1

e3fbeb02139844ec882da79710dc50f1.png

求完了attention之后,来求

924e399ecf2546fbb8c44dea644eaaff.png

也就是

h_prime = torch.matmul(attention, Wh) # 聚合邻居函数

得到了表示2708个节点隐层的矩阵

a12f0734568f46ec8f72bf3476f6c9e1.png

又因为定义了8个attention的head,这里循环了8次使用

x = torch.cat([att(x, adj) for att in self.attentions], dim=1) # 将8层attention拼接

也就是

af895bbc164b4323a38d7a5afd0af74e.png

好,Dropout之后,到此,第一层就结束了

走进第二层

再回到model(features, adj)中的forward的line5

x = F.elu(self.out_att(x, adj))

也就是第二层的GraphAttentionLayer,h是刚刚拼好的x

281b52fbba084157a2acc41a3e325c2d.png

因为最后是7分类,所以这里W的维度有所不同

91a4c1d72a804164891acf9ab7fea238.png

其他部分没有什么变化,第二层最终输出的就是,接上一个softmax输出为output

b074fed08de24dbbaa3e792e5b92d396.png

再贴一遍train

train.py

  1. def train(epoch):
  2. t = time.time()
  3. model.train()
  4. optimizer.zero_grad()
  5. output = model(features, adj) # GAT模块
  6. loss_train = F.nll_loss(output[idx_train], labels[idx_train])
  7. acc_train = accuracy(output[idx_train], labels[idx_train])
  8. loss_train.backward()
  9. optimizer.step()
'
运行

上面的line6的nll_loss怎么理解?其实就是nn.CrossEntropyLoss

b41e3c0534284100b015f9684dc5ac37.png

再loss_train.backward()进行反向传播

打印output,在7列中哪个值最大,也就最后可能是哪一类标签的节点

6577d523d40747198b738b0c01b81ac9.png

这大概也就把GAT的源码部分撸完了,周末快乐!

参考:

Cora数据集介绍 - 知乎

GAT源码剖析_行走天涯的豆沙包的博客-CSDN博客_gat源码

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/980239
推荐阅读
相关标签
  

闽ICP备14008679号