HAN论文阅读与代码详解( 二 )


语义级别的注意力
异构图中的每个节点通常包含多种类型的语义信息 , 语义特定的节点嵌入只能从一个方面反映节点 。为了学习一个更全面的节点嵌入 , 我们需要融合多种语义 , 这些语义可以通过元路径来揭示 。为了应对异构图中的元路径选择和语义融合挑战 , 作者提出了一种新颖的语义级注意力机制 , 用于自动学习不同元路径的重要性 , 并将它们融合到特定任务中 。
将节点级注意力得到的 P P P组语义特定的节点嵌入作为输入 , 每个元路径的权重可以被描述如下公式6:
( β Φ 1 , … , β Φ P ) = a t t s e m ( Z Φ 1 , … , Z Φ P ) (\beta_{\Phi_1},\ldots,\beta_{\Phi_P})=att_{sem}(\{Z}_{\Phi_1},\ldots,\{Z}_{\Phi_P}) (βΦ1??,…,βΦP??)=?(ZΦ1??,…,ZΦP??)
为了学习每个元路径的重要性 , 首先通过非线性变换(例如 , 单层MLP)变换语义特定的嵌入向量
然后 , 我们通过变换嵌入与语义级注意向量 q \ q q的相似性度量语义特定嵌入的重要性 。此外 , 对所有语义特定节点嵌入的重要性进行平均 , 这可以解释为每个元路径的重要性 。如下公式7
w Φ p = 1 ∣ V ∣ ∑ i ∈ V q T ? tanh ? ( W ? z i Φ p + b ) w_{\Phi_{p}}=\frac{1}{|\{V}|}\sum_{i\in\{V}}\{q}^{\{T}}\cdot\tanh(\{W}\cdot\{z}_{i}^{\Phi_{p}}+\{b}) wΦp??=∣V∣1?i∈V∑?qT?tanh(W?ziΦp??+b)

HAN论文阅读与代码详解

文章插图
在得到每个元路径的重要性后 , 通过函数对它们进行归一化 。如下公式8
β Φ p = exp ? ( w Φ p ) ∑ p = 1 P exp ? ( w Φ p ) \beta_{\Phi_p}=\frac{\exp(w_{\Phi_p})}{\sum_{p=1}^{P}\exp(w_{\Phi_p})} βΦp??=∑p=1P?exp(wΦp??)exp(wΦp??)?
使用学习的权重作为系数 , 我们可以融合这些语义特定的嵌入以获得最终的嵌入 。如下公式9
Z = ∑ p = 1 P β Φ p ? Z Φ p Z=\sum_{p=1}^{P}\beta_{\Phi_{p}}\cdot Z_{\Phi_{p}} Z=p=1∑P?βΦp???ZΦp??
然后 , 我们可以将最终的嵌入应用于特定的任务 , 并设计不同的损失函数 。
比如 , 对于半监督的节点分类任务 , 可以使用交叉熵损失函数 。如下公式10
L = ? ∑ l ∈ Y L Y l ln ? ( C ? Z l ) L=-\sum\{l\in\{Y}_{L}}\{Y}^{l}\ln(\{C}\cdot\{Z}^{l}) L=?l∈YL?∑?Ylln(C?Zl)
节点级别的注意力和语义级别的注意力聚合过程图示:
HAN的整个过程:
代码实现
此模型具体的实现有两种方式:主要区别在于对数据集的处理以及基于元路径邻居的获取 。
实现的具体思路相同 , 下边以第二种方式为例 , 给出代码(注释中详细解释了实现的思路和过程):
下列代码中用到的其他工具类可以在仓库中找到
import dglimport torchimport torch.nn as nnimport torch.nn.functional as Ffrom dgl.nn.pytorch import GATConv"""注释中的字母代表的含义:N : 节点数量M : 元路径数量D : 嵌入向量维度K : 多头注意力总数"""class SemanticAttention(nn.Module):def __init__(self, in_size, hidden_size=128):super(SemanticAttention, self).__init__()# 语义层次的注意力# 对应论文公式(7) , 最终得到每条元路径的重要性权重self.projection = nn.Sequential(nn.Linear(in_size, hidden_size),nn.Tanh(),nn.Linear(hidden_size, 1, bias=False))def forward(self, z):# 输入的z为(N, M, K*D)# 经过映射之后的w形状为 (M , 1)w = self.projection(z).mean(0)# beta (M ,1)beta = torch.softmax(w, dim=0)# beta (N, M, 1)beta = beta.expand((z.shape[0],) + beta.shape)# (N, D*K)return (beta * z).sum(1)class HANLayer(nn.Module):"""meta_paths : list of metapaths, each as a list of edge typesin_size : input feature dimensionout_size : output feature dimensionlayer_num_heads : number of attention headsdropout : Dropout probability"""def __init__(self,meta_paths,in_size,out_size,layer_num_heads,drop_out):super(HANLayer, self).__init__()self.gat_layers = nn.ModuleList()for i in range(len(meta_paths)):# 使用GAT对应的GATConv层 , 完成节点层面的注意力# 之所以能够之间使用GATConv,是因为在forward中生成了每个元路径对应的可达图# 那么在进行节点级注意力的时候 , 节点的所有邻居都是它基于元路径的邻居# 节点级注意力以及聚合的过程就等同于GATConv的过程self.gat_layers.append(GATConv(in_size,out_size,layer_num_heads,drop_out,drop_out,activation=F.elu,allow_zero_in_degree=True))# 语义级注意力层self.semantic_attention = SemanticAttention(in_size=out_size * layer_num_heads)self.meta_paths = list(tuple(meta_path) for meta_path in meta_paths)# 缓存图self._cached_graph = None# 缓存每个元路径对应的可达图self._cached_coalesced_graph = {}def forward(self, g, h):semantic_embeddings = []if self._cached_graph is None or self._cached_graph is not g:self._cached_graph = gself._cached_coalesced_graph.clear()# 存储每个元路径对应的元路径可达图for meta_path in self.meta_paths:self._cached_coalesced_graph[meta_path] = dgl.metapath_reachable_graph(g, meta_path)for i, meta_path in enumerate(self.meta_paths):new_g = self._cached_coalesced_graph[meta_path]semantic_embeddings.append(self.gat_layers[i](new_g, h).flatten(1))# 经过对每个元路径进行节点级聚合# semantic_embeddings 为一个长度为M的列表# 其中的元素为每个节点级注意力的输出 , 形状为(N, D*K)# 将该列表在维度1堆叠 , 得到所有元路径的节点级注意力# 形状为(N, M, D * K)semantic_embeddings = torch.stack(semantic_embeddings, dim=1)# 最终经过语义级注意力聚合不同元路径的表示 , 得到了该层的输出# 形状为(N, D * K)return self.semantic_attention(semantic_embeddings)class HAN(nn.Module):"""参数:meta_paths : 元路径 , 使用边类型列表表示in_size : 输入大小(特征维度)out_size : 输出大小(节点种类数)num_heads : 多头注意力头数(列表形式 , 对应每层的头数)dropout : dropout概率"""def __init__(self, meta_paths, in_size, hidden_size, out_size, num_heads, dropout):super(HAN, self).__init__()self.layers = nn.ModuleList()# 第一个HAN层的输入输出需要单独定义self.layers.append(HANLayer(meta_paths, in_size, hidden_size, num_heads[0], dropout))# 从第二个HAN层开始 , 每一个层的输入都是hidden_size * 上一个层的头数# 输出大小为 hidden_size * 当前层的头数for l in range(1, len(num_heads)):self.layers.append(HANLayer(meta_paths,hidden_size * num_heads[l - 1],hidden_size,num_heads[l],dropout,))# 最终的输出层self.predict = nn.Linear(hidden_size * num_heads[-1], out_size)def forward(self, g, h):for gnn in self.layers:h = gnn(g, h)return self.predict(h)