图注意力网络(Graph Attention Network, GAT) 模型解读与代码实现(tensorflow2.0)

2023-11-10

前面的文章,我们讲解了图神经网络三剑客GCN、GraphSAGE、GAT中的两个:

  1. 图卷积神经网络(GCN)理解与tensorflow2.0代码实现
  2. GraphSAGE 模型解读与tensorflow2.0代码实现

本要讲的是GAT(Graph Attention Network),它使用 Attention 机制来对邻居节点进行加权求和,和一般的Attention 机制一样,分为计算注意力系数和加权求和两个步骤。

GAT中的 Attention 机制

先来看看每一层的输入与输出:
 input  : h = { h ⃗ 1 , h ⃗ 2 , … , h ⃗ N } , h ⃗ i ∈ R F  output  : h ′ = { h ⃗ 1 ′ , h ⃗ 2 ′ , … , h ⃗ N ′ } , h ⃗ i ′ ∈ R F \text { input }: \mathbf{h}=\left\{\vec{h}_{1}, \vec{h}_{2}, \ldots, \vec{h}_{N}\right\}, \vec{h}_{i} \in \mathrm{R}^{F} \\ \text { output }: \mathbf{h}^{\prime}=\left\{\vec{h}_{1}^{\prime}, \vec{h}_{2}^{\prime}, \ldots, \vec{h}_{N}^{\prime}\right\}, \vec{h}_{i}^{\prime} \in \mathrm{R}^{F}  input :h={h 1,h 2,,h N},h iRF output :h={h 1,h 2,,h N},h iRF

1. 计算注意力系数

首先计算顶点 i i i 与周围邻居节点 j ∈ N i j \in \mathcal N_i jNi 的相似度:
e i j = a ( W h ⃗ i , W h ⃗ j ) e_{i j}=a\left(\mathbf{W} \vec{h}_{i}, \mathbf{W} \vec{h}_{j}\right) eij=a(Wh i,Wh j)
公式中的 ( W h ⃗ i , W h ⃗ j ) \left(\mathbf{W} \vec{h}_{i}, \mathbf{W} \vec{h}_{j}\right) (Wh i,Wh j) 可以看出特征 h i , h j h_i, h_j hi,hj 共享了参数矩阵 W W W,都使用 W W W 对特征进行线性变换, ( ⋅ , ⋅ ) (·, ·) (,)在公式中表示横向拼接。而公式最外层的 a a a 表示单层前馈神经网络(使用 L e a k y R e L U LeakyReLU LeakyReLU作为激活函数),输出为一个数值。

有了相关系数,接下来就要进行归一化了,作者使用了 S o f t m a x Softmax Softmax,于是注意力系数的计算过程全貌为:
α i j = softmax ⁡ j ( e i j ) = exp ⁡ ( e i j ) ∑ k ∈ N i exp ⁡ ( e i k ) = exp ⁡ ( LeakyReLU ⁡ ( a → T [ W h ⃗ i ∥ W h ⃗ j ] ) ) ∑ k ∈ N i exp ⁡ ( LeakyReLU ⁡ ( a → T [ W h ⃗ i ∥ W h ⃗ k ] ) ) \begin{aligned} \alpha_{i j}&=\operatorname{softmax}_{j}\left(e_{i j}\right)\\ &=\frac{\exp \left(e_{i j}\right)}{\sum_{k \in N_{i}} \exp \left(e_{i k}\right)}\\ &=\frac{\exp \left(\operatorname{LeakyReLU}\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{j}\right]\right)\right)}{\sum_{k \in N_{i}} \exp \left(\operatorname{LeakyReLU}\left(\overrightarrow{\mathbf{a}}^{T}\left[\mathbf{W} \vec{h}_{i} \| \mathbf{W} \vec{h}_{k}\right]\right)\right)} \end{aligned} αij=softmaxj(eij)=kNiexp(eik)exp(eij)=kNiexp(LeakyReLU(a T[Wh iWh k]))exp(LeakyReLU(a T[Wh iWh j]))
其中, ∥ \| 表示向量拼接,以上的计算过程如下图所示:

在这里插入图片描述

2. 加权求和

得到注意力系数之后,就是对邻居节点的特征进行加权求和:
h ⃗ i ′ = σ ( ∑ j ∈ N i α i j W h ⃗ j ) \vec{h}_{i}^{\prime}=\sigma\left(\sum_{j \in N_{i}} \alpha_{i j} \mathbf{W} \vec{h}_{j}\right) h i=σjNiαijWh j
不过为了更好的学习效果,作者使用了 “multi-head attention”,也就是使用 K 个注意力。对于 K 个注意力又可以使用两种方法对邻居节点进行聚合。

一种方法是横向拼接的方式,这样聚合到的特征维度就是原来的K倍:
h ⃗ i ′ = ∥ k = 1 K σ ( ∑ j ∈ N i α i j k W k h ⃗ j ) \vec{h}_{i}^{\prime}=\|_{k=1}^{K} \sigma\left(\sum_{j \in N_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) h i=k=1KσjNiαijkWkh j
另一种方法是把K个注意力机制得到的结果取平均值:
h ⃗ i ′ = σ ( 1 K ∑ k = 1 K ∑ j ∈ N i α i j k W k h ⃗ j ) \vec{h}_{i}^{\prime}=\sigma\left(\frac{1}{K} \sum_{k=1}^{K} \sum_{j \in N_{i}} \alpha_{i j}^{k} \mathbf{W}^{k} \vec{h}_{j}\right) h i=σK1k=1KjNiαijkWkh j
下图可以辅助理解上面的计算过程:

在这里插入图片描述

尽管使用了多个 head ,但是不同的 head 是可以并行训练的。

GAT 优点

  1. 为邻接节点分配不同的权重,考虑到节点特征之间的相关性。
  2. 不需要事先得到整个图结构或所有顶点的特征(只需访问目标节点的领接节点)。
  3. 能够运用于inductive任务中。

代码实现(tensorflow2.0)

参数配置:

# training params
batch_size = 1
nb_epochs = 100000
patience = 100
lr = 0.005  
l2_coef = 0.0005  # l2 正则化系数
hid_units = [8] # 每一个attention head中每一层的隐藏单元个数
n_heads = [8, 1] # 每层使用的注意力头个数
residual = False
# ...

单个 Attention Head 实现:

class attn_head(tf.keras.layers.Layer):
  def __init__(self,hidden_dim, nb_nodes = None,in_drop=0.0, coef_drop=0.0,activation = tf.nn.elu,residual = False):    
    super(attn_head,self).__init__()    
    self.activation = activation
    self.residual = residual
    self.in_dropout = tf.keras.layers.Dropout(in_drop)
    self.coef_dropout = tf.keras.layers.Dropout(coef_drop)    
    self.conv_no_bias = tf.keras.layers.Conv1D(hidden_dim,1,use_bias=False)
    self.conv_f1 = tf.keras.layers.Conv1D(1,1)
    self.conv_f2 = tf.keras.layers.Conv1D(1,1)
    self.conv_residual = tf.keras.layers.Conv1D(hidden_dim,1)
    self.bias_zero = tf.Variable(tf.zeros(hidden_dim))
    
  def __call__(self,seq,bias_mat,training):
    # seq: 输入的节点特征
    seq = self.in_dropout(seq,training = training)
    # 使用 hidden_dim=8 个1维卷积,卷积核大小为1
    # 每个卷积核的参数相当于邻居节点的权重
    # 整个卷积的过程相当于公式中的 Wh
    # seq_fts.shape: (num_graph, num_nodes, hidden_dim)
    seq_fts = self.conv_no_bias(seq)
    # 1x1 卷积可以理解为按hidden_dim这个通道进行加权求和,但参数共享
    # 相当于单输出全连接层1
    # f_1.shape: (num_graph, num_nodes, 1)
    f_1 = self.conv_f1(seq_fts)
    # 相当于单输出全连接层2
    f_2 = self.conv_f2(seq_fts)
    
    # 广播机制计算(num_graph,num_nodes,1)+(num_graph,1,num_nodes)
    # logits.shape: (num_graph, num_nodes, num_nodes)
    # 相当于计算了所有节点的 [e_ij]
    logits = f_1 + tf.transpose(f_2,[0,2,1])
    # 得到邻居节点的注意力系数:[alpha_ij]
    # bias_mat 中非邻居节点为极大的负数,softmax之后变为0
    # coefs.shape: (num_graph, num_nodes, num_nodes)
    coefs = tf.nn.softmax(tf.nn.leaky_relu(logits)+bias_mat)
    # dropout
    coefs = self.coef_dropout(coefs,training = training)
    seq_fts = self.in_dropout(seq_fts,training = training)
    # 计算:[alpha_ij] x Wh
    # vals.shape: (num_graph, num_nodes, num_nodes)
    vals = tf.matmul(coefs, seq_fts)
    vals = tf.cast(vals, dtype=tf.float32)
    # 最终结果再加上一个 bias 
    ret = vals + self.bias_zero
    # 残差
    if self.residual:
      if seq.shape[-1] != ret.shape[-1]:
        ret = ret + self.conv_residual(seq)        
      else:
        ret = ret + seq
    # 返回 h' = σ([alpha_ij] x Wh)
    # shape: (num_graph, num_nodes, hidden_dim)
    return self.activation(ret)

Multi-head Attention 代码:

chosen_attention = attn_head

class inference(tf.keras.layers.Layer):
  def __init__(self,n_heads,hid_units,nb_classes, nb_nodes,Sparse,ffd_drop=0.0, attn_drop=0.0,activation = tf.nn.elu,residual = False):    
    super(inference,self).__init__()
    attned_head = choose_attn_head(Sparse)
    self.attns = []
    self.sec_attns = []
    self.final_attns = []
    self.final_sum = n_heads[-1]
    # 构造 n_heads[0] 个 attention
    for i in range(n_heads[0]):
      self.attns.append(attned_head(hidden_dim = hid_units[0], nb_nodes = nb_nodes,
                                  in_drop = ffd_drop, coef_drop = attn_drop, 
                                  activation = activation,
                                  residual = residual))
    
    # hid_units表示每一个attention head中每一层的隐藏单元个数
    # 若给定hid_units = [8], 表示使用单个全连接层
    # 因此,不执行下面的代码
    for i in range(1, len(hid_units)):
      h_old = h_1
      sec_attns = []
      for j in range(n_heads[i]):        
        sec_attns.append(attned_head(hidden_dim = hid_units[i], nb_nodes = nb_nodes,
                                   in_drop = ffd_drop, coef_drop = attn_drop, 
                                   activation = activation,
                                   residual = residual))
        self.sec_attns.append(sec_attns)
        
    # 加上输出层
    for i in range(n_heads[-1]):
      self.final_attns.append(attned_head(hidden_dim = nb_classes, nb_nodes = nb_nodes,
                              in_drop = ffd_drop, coef_drop = attn_drop, 
                              activation = lambda x: x,
                              residual = residual))        

  def __call__(self,inputs,bias_mat,training):    
    first_attn = []
    out = []
    # 计算 n_heads[0] 个 attention
    for indiv_attn in self.attns:
      first_attn.append(indiv_attn(seq = inputs, bias_mat = bias_mat,training = training))
    # h_1.shape: (num_graph, num_nodes, hidden_dim*n_heads[0])
    h_1 = tf.concat(first_attn,axis = -1)   
    # 如果 attention 使用了多层网络,则依次计算
    for sec_attns in self.sec_attns:
      next_attn = []
      for indiv_attns in sec_attns:
        next_attn.append(indiv_attn(seq = h_1,bias_mat = bias_mat,training = training))
      h_1 = tf.concat(next_attns,axis = -1)
    # 得到最终的预测结果
    for indiv_attn in self.final_attns:
      out.append(indiv_attn(seq=h_1,bias_mat = bias_mat,training = training))
    # 将结果在最后一个维度取均值
    # logits.shape: (num_graph, num_nodes, nb_classes)
    logits = tf.add_n(out)/self.final_sum
    return logits

GAT 模型:

class GAT(tf.keras.Model):
  def __init__(self, hid_units,n_heads, nb_classes, nb_nodes,Sparse,ffd_drop = 0.0,attn_drop = 0.0,activation = tf.nn.elu,residual=False):  
    super(GAT,self).__init__()
    '''
    hid_units: 隐藏单元个数
    n_heads: 每层使用的注意力头个数
    nb_classes: 类别数,7
    nb_nodes: 节点的个数,2708
    activation: 激活函数
    residual: 是否使用残差连接
    '''            
    self.hid_units = hid_units     #[8]
    self.n_heads = n_heads       #[8,1]
    self.nb_classes = nb_classes
    self.nb_nodes = nb_nodes
    self.activation = activation
    self.residual = residual    
    
    self.inferencing = inference(n_heads,hid_units,nb_classes,nb_nodes,Sparse = Sparse,ffd_drop = ffd_drop,attn_drop = attn_drop, activation = activation,residual = residual)
    
  

  def masked_softmax_cross_entropy(self,logits, labels, mask):
    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)

  def masked_accuracy(self,logits, labels, mask):
    correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1))
    accuracy_all = tf.cast(correct_prediction, tf.float32)
    mask = tf.cast(mask, dtype=tf.float32)
    mask /= tf.reduce_mean(mask)
    accuracy_all *= mask
    return tf.reduce_mean(accuracy_all)

  
  def __call__(self,inputs,training,bias_mat,lbl_in,msk_in):   
    # logits.shape: (num_graph, num_nodes, nb_classes)     
    logits = self.inferencing(inputs = inputs, bias_mat = bias_mat,training = training)    
    
    log_resh = tf.reshape(logits, [-1, self.nb_classes])    
    lab_resh = tf.reshape(lbl_in, [-1, self.nb_classes])
    msk_resh = tf.reshape(msk_in, [-1])    
    
    loss = self.masked_softmax_cross_entropy(log_resh, lab_resh, msk_resh)
    
    lossL2 = tf.add_n([tf.nn.l2_loss(v) for v in self.trainable_variables if v.name not
                           in ['bias', 'gamma', 'b', 'g', 'beta']]) * l2_coef
        
    loss = loss+lossL2
    accuracy = self.masked_accuracy(log_resh, lab_resh, msk_resh)
    
    return logits,accuracy,loss
  

完整代码地址:github.com/zxxwin/Graph-Attention-Networks-tensorflow2.0

参考文章:

图注意力网络(GAT) ICLR2018, Graph Attention Network论文详解

Graph Attention Network (GAT) 的Tensorflow版代码解析

GNN-report.pdf

Attention Model(注意力模型)学习大全

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

图注意力网络(Graph Attention Network, GAT) 模型解读与代码实现(tensorflow2.0) 的相关文章

随机推荐

  • Jmeter快速上手之接口测试

    目录 1 前言 2 简介 3 安装 4 环境变量 4 1 Windows环境 4 2 Mac环境 5 启动程序 6 目录说明 7 操作示例 7 1 Get请求 7 2 Post请求 7 3 依赖请求 1 前言 压测工具 Jmeter 除了可
  • 什么是接口?

    1 什么是接口 接口是一种特殊的内部类 它里面的所有方法都没有实现 2 接口的特点 1 接口中成员默认访问修饰符都是public 即便你不写 2 定义接口必须interface关键字完成 3 接口中可以定义变量 但是变量必须有固定的修饰符修
  • JUC并发编程--------线程安全篇

    目录 什么是线程安全性问题 如何实现线程安全 1 线程封闭 2 无状态的类 3 让类不可变 4 加锁和CAS 并发环境下的线程安全问题有哪些 1 死锁 2 活锁 3 线程饥饿 什么是线程安全性问题 我们可以这么理解 我们所写的代码在并发情况
  • java自引用/类的递归调用问题

    Java的自引用问题 什么是自引用 递归调用 代码示例和分析 自引用情况 类比C 什么是自引用 递归调用 在编写代码的过程中 我们经常看到类中出现该类的声明 示例 class A int data A a 这种情况就被称为自引用 代码示例和
  • Android架构项目代码结构规范--组件化代码

    前言 组件化和插件化有什么区别 虽说网上有很多文章但是讲清的聊聊无几 这也是这篇文章的由来 大方向 组件化是一个项目主管设计管理项目架构方案 而插件化有商务上的合作和局部功能热更换修复等 小方向 如果是公司app合作 组件化也就是插件化作为
  • CLion Bug集合1:windows下导入openssl库方法以及踩过的坑

    系统 win10 64位 工具链 MinGW IDE CLion openssl库下载 下载方法 主要有两种方式 本文主要讲解方式1 方式1 下载地址 2022 6 1补充 该方法的动态库是MinGW64位用的 32位见方法2 方式2 编译
  • linux常用命令案例总结wc,top,free,df -h,head,sed,awk,netstat -antp,ps -aux, ethtool eth0

    1 wc的使用 统计一个目录下的文件个数 root localhost etc cd var log root localhost log ll grep wc l 53 root localhost log 拓展 关于命令wc的使用 wc
  • 使用冻结层进行迁移学习

    使用冻结层进行迁移学习 在yolov5的训练过程中 作者介绍了如何使用冻结层实现迁移学习的策略 具体可以参考官方话题 Transfer Learning with Frozen Layers Issue 1314 ultralytics y
  • JS中的Date数据类型

    JS的Date数据类型 在JavaScript中 Date数据类型用于处理日期和时间 它可以表示自1970年1月1日00 00 00 UTC 协调世界时 以来的毫秒数 Date对象在许多应用程序中都非常有用 例如在Web应用程序中显示当前时
  • Python实现HBA混合蝙蝠智能算法优化循环神经网络回归模型(LSTM回归算法)项目实战

    说明 这是一个机器学习实战项目 附带数据 代码 文档 视频讲解 如需数据 代码 文档 视频讲解可以直接到文章最后获取 1 项目背景 蝙蝠算法是2010年杨教授基于群体智能提出的启发式搜索算法 是一种搜索全局最优解的有效方法 该算法基于迭代优
  • eMMC简介

    eMMC是embedded MultiMediaCard的简称 MultiMediaCard 即MMC 是一种闪存卡 Flash Memory Card 标准 它定义了MMC的架构以及访问Flash Memory的接口和协议 而eMMC则是
  • 全屏dialog

    下面是iOS里面做全屏Dialog的代码 调用show时Dialog会覆盖当前的controller 全屏显示 可以用来做蒙板效果 欢迎转载 转载请注明出处 http blog csdn net tadican article detail
  • Python中的装饰器是什么?装饰器是如何工作的?

    Python很早就引入了装饰器 在PEP 318中 作为一种简化函数和方法定义方式的机制 这些函数和方法在初始定义之后必须进行修改 这样做的最初动机之一是 使用classmethod和staticmethod等函数来转换方法的原始定义 但是
  • hibernate映射继承关系(一):一张表对应一整棵类继承树

    翻译 hibernate映射继承关系 一 一张表对应一整棵类继承树 2人收藏此文章 我要收藏发表于1年前 2012 05 22 16 34 已有 482次阅读 共 0个评论 英文原址 网上这个主题的文章不在少数 这个系列的文章的部分价值在于
  • 【100%通过率 】【华为OD机试 c++】最多等和不相交连续子序列【 2023 Q1

    华为OD机试 题目列表 2023Q1 点这里 2023华为OD机试 刷题指南 点这里 题目描述 最多等和不相交连续子序列 给定一个数组 我们称其中连续的元素为连续子序列 称这些元素的和为连续子序列的和 数组中可能存在几组连续子序列 组内的连
  • NexT主题进阶

    NexT 是 Hexo 框架中最为流行的主题之一 精于心 简于形 NexT 支持多种常见第三方服务 使用 第三方服务 来扩展站点的功能 除了 Markdown 支持的语法之外 NexT 借助 Hexo 提供的 tag 插件 为您提供在书写文
  • EDK2安装教程

    1 1基础搭建 相关文件请自行百度下载 1 安装VS2015到C盘 请勿修改默认目录 否则需要修改C edk2 Conf tools def txt 2 如安装包所示 安装python2 7到C盘并设置环境变量如下 3 将nasm解压到C
  • JavaScript-二分法详解

    文章目录 二分法 二分查找 非递归实现 二分查找 递归实现 二分排序 复杂度分析 推荐文章 经典例题 力扣习题 二分法 二分法又可以被称为二分查找 它描述了在有序集合中搜索特定值的过程 广义的二分查找是将问题的规模尽可能的缩小到原有的一半
  • win10,创建新环境并安装pytorch-gpu=1.7.0版本

    之前装过gpu版本tensorflow 包含cudatoolkit 10 1 创建新环境装gpu版本pytorch时考虑是否再装cudatoolkit 以下是没有再装cudatoolkit情况下 目前正常 文章目录 Anaconda 创建新
  • 图注意力网络(Graph Attention Network, GAT) 模型解读与代码实现(tensorflow2.0)

    前面的文章 我们讲解了图神经网络三剑客GCN GraphSAGE GAT中的两个 图卷积神经网络 GCN 理解与tensorflow2 0代码实现 GraphSAGE 模型解读与tensorflow2 0代码实现 本要讲的是GAT Grap