目录
[1、GAT- 标准图注意力网络](#1、GAT- 标准图注意力网络)
[2、HeteGAT_multi( HAN核心模型(当前使用的)](#2、HeteGAT_multi( HAN核心模型(当前使用的))
[2.1 attn_head:节点级别注意力](#2.1 attn_head:节点级别注意力)
[2.2 SimpleAttLayer:语义级别注意力](#2.2 SimpleAttLayer:语义级别注意力)
图神经网络概览:图神经网络分享系列-概览
上一篇文章:图神经网络分享系列-HAN(Heterogeneous Graph Attention Network)(二)
本章内容主要进行实战
github 链接:https://github.com/Jhy1993/HAN
一、模型
1、对比
| 模型 | 元路径注意力 | 节点注意力系数 | 用途 |
|---|---|---|---|
| GAT | ❌ | ❌ | 同构图/作为基础组件 |
| HeteGAT_multi | ✅ | ❌ | 异构图分类(当前使用) |
| HeteGAT_no_coef | ✅ | ❌ | 轻量级异构图分类 |
| HeteGAT | ✅ | ✅(可选) | 异构图分类 + 可解释性分析 |
GAT:
输入 → [注意力] → 输出
HeteGAT_multi/HeteGAT_no_coef:
输入1 → [节点注意力] → 嵌入1
输入2 → [节点注意力] → 嵌入2
↓
语义注意力\] → 输出 HeteGAT: 输入1 → \[节点注意力(含系数)\] → 嵌入1 输入2 → \[节点注意力(含系数)\] → 嵌入2 ↓ \[语义注意力\] → 输出 ↓ 可选返回节点级注意力系数
1、GAT- 标准图注意力网络
这个部分之前分享过,本次不做过多赘述,可参考:
图神经网络分享系列-GAT(GRAPH ATTENTION NETWORKS) (四)-实战篇
python
class GAT(BaseGAttN):
"""
GAT (Graph Attention Network) - 标准同构图注意力网络
这是一个基础的GAT实现,用于处理单一类型节点的同构图。
在HAN中,我们使用多个GAT分别处理不同的元路径。
架构:
输入特征 → 多头注意力层 → 输出层 → 分类结果
"""
def inference(inputs, nb_classes, nb_nodes, training, attn_drop, ffd_drop,
bias_mat, hid_units, n_heads, activation=tf1.nn.elu, residual=False):
"""
GAT前向传播
参数:
inputs: 节点特征 [batch_size, nb_nodes, feature_size]
nb_classes: 分类类别数
nb_nodes: 节点数量
training: 是否训练模式
attn_drop: 注意力系数dropout率
ffd_drop: 前馈层dropout率
bias_mat: 邻接矩阵偏置
hid_units: 隐藏层单元数列表
n_heads: 注意力头数列表
activation: 激活函数
residual: 是否使用残差连接
返回:
logits: 分类logits
"""
attns = []
# 第一层:多头注意力
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 = tf1.concat(attns, axis=-1)
# 后续层(如果有的话)
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 = tf1.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 = tf1.add_n(out) / n_heads[-1]
return logits
2、HeteGAT_multi( HAN核心模型(当前使用的)
python
class HeteGAT_multi(BaseGAttN):
"""
HeteGAT_multi - HAN的核心模型(多注意力头版本)
这是HAN的主要实现,包含双层注意力机制:
1. 节点级注意力 (Node-level Attention):
- 对每个元路径分别应用多头注意力机制
- 学习如何聚合同一元路径下邻居节点的信息
2. 语义级注意力 (Semantic Attention):
- 使用SimpleAttLayer融合不同元路径的嵌入
- 自动学习每个元路径的重要性权重
示例(ACM数据集):
- 元路径1: PAP (Paper-Author-Paper) - 通过作者连接的论文
- 元路径2: PLP (Paper-Label-Paper) - 通过标签连接的论文
"""
def inference(inputs_list, nb_classes, nb_nodes, training, attn_drop, ffd_drop,
bias_mat_list, hid_units, n_heads, activation=tf1.nn.elu, residual=False,
mp_att_size=128):
"""
HAN前向传播
参数:
inputs_list: 多个元路径的节点特征列表
每个元素形状: [batch_size, nb_nodes, feature_size]
nb_classes: 分类类别数
nb_nodes: 节点数量
training: 是否训练模式
attn_drop: 注意力系数dropout率 (训练时设为0.6,推理时设为0)
ffd_drop: 前馈层dropout率 (训练时设为0.6,推理时设为0)
bias_mat_list: 多个元路径的邻接矩阵偏置列表
每个元素形状: [batch_size, nb_nodes, nb_nodes]
hid_units: 隐藏层单元数列表, 如 [8] 表示一层8个隐藏单元
n_heads: 注意力头数列表, 如 [8, 1] 表示第一层8个头,输出层1个头
activation: 激活函数 (默认ELU)
residual: 是否使用残差连接
mp_att_size: 元路径注意力层的大小 (默认128)
返回:
logits: 节点分类logits, 形状 [batch_size, nb_nodes, nb_classes]
final_embed: 最终的节点嵌入, 形状 [batch_size, nb_nodes, hidden_dim]
att_val: 元路径注意力权重, 形状 [batch_size, num_meta_paths]
"""
embed_list = [] # 存储每个元路径的嵌入
# =================================================================
# 第一层:节点级注意力 (Node-level Attention)
# =================================================================
# 对每个元路径分别进行处理
for inputs, bias_mat in zip(inputs_list, bias_mat_list):
# inputs: 节点特征 [batch, nb_nodes, ft_size]
# bias_mat: 邻接矩阵的偏置 [batch, nb_nodes, nb_nodes]
attns = []
jhy_embeds = []
# 多头注意力机制 (Multi-Head Attention)
# n_heads[0]=8,表示使用8个注意力头
# 每个头学习不同的注意力权重
for _ in range(n_heads[0]):
# 对每个元路径分别做注意力卷积
# attn_head函数实现了GAT的核心注意力机制
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))
# 拼接多头输出
# 形状: [batch, nb_nodes, hid_units[0] * n_heads[0]] = [1, N, 64]
h_1 = tf1.concat(attns, axis=-1)
# 如果有更多隐藏层,继续处理
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 = tf1.concat(attns, axis=-1)
# 将该元路径的嵌入添加到列表
# 形状调整: [batch, nb_nodes, hidden_dim] -> [batch, 1, nb_nodes, hidden_dim]
embed_list.append(tf1.expand_dims(tf1.squeeze(h_1), axis=1))
# =================================================================
# 第二层:语义级注意力 (Semantic Attention)
# =================================================================
# 将多个元路径的嵌入拼接
# 形状: [batch, num_mp, nb_nodes, hidden_dim]
multi_embed = tf1.concat(embed_list, axis=1)
# 使用SimpleAttLayer对元路径进行加权融合
# 这是一个可学习的注意力机制,自动学习每个元路径的重要性
# mp_att_size=128 是注意力层的隐藏维度
final_embed, att_val = layers.SimpleAttLayer(multi_embed, mp_att_size,
time_major=False,
return_alphas=True)
# =================================================================
# 输出层
# =================================================================
out = []
for i in range(n_heads[-1]):
# 使用全连接层进行分类
# 这里可以继续使用注意力头,或者直接用dense层
out.append(tf1.layers.dense(final_embed, nb_classes, activation=None))
# 取平均(如果有多个注意力头)
logits = tf1.add_n(out) / n_heads[-1]
print('de') # 调试输出
# 调整输出形状以匹配标签格式
logits = tf1.expand_dims(logits, axis=0)
# 返回: logits, 最终嵌入, 元路径注意力权重
return logits, final_embed, att_val
2.1 attn_head:节点级别注意力
python
def attn_head(seq, out_sz, bias_mat, activation, in_drop=0.0, coef_drop=0.0, residual=False,
return_coef=False):
"""
attn_head - 节点级注意力层 (Node-level Attention)
这是GAT的核心实现,实现了对邻居节点的信息聚合。
核心思想是:不同邻居节点对目标节点的贡献是不同的,通过注意力机制学习这种重要性差异。
注意力机制计算流程:
1. 特征变换: 将输入特征通过线性层映射到低维空间
2. 注意力分数计算: 使用LeakyReLU激活函数计算节点对之间的注意力分数
3. 掩码操作: 通过邻接矩阵掩码,屏蔽非邻居节点
4. Softmax归一化: 将注意力分数归一化为概率分布
5. 特征聚合: 使用注意力系数对邻居特征进行加权求和
参数:
seq: 输入序列/节点特征
形状: [batch_size, nb_nodes, feature_size]
例如: [1, 3025, 1870] 表示3025个节点,每个节点1870维特征
out_sz: 输出特征维度
如果hid_units[0]=8, n_heads[0]=8,则输出维度为8*8=64
bias_mat: 邻接矩阵偏置
形状: [batch_size, nb_nodes, nb_nodes]
用于屏蔽非邻居节点,非邻居位置为-1e9,邻居位置为0
activation: 激活函数 (通常使用ELU)
in_drop: 输入特征的dropout率 (训练时使用,推理时为0)
coef_drop: 注意力系数的dropout率 (训练时使用,推理时为0)
residual: 是否使用残差连接
return_coef: 是否返回注意力系数 (用于可视化和分析)
返回:
activation(ret): 经过激活函数处理的聚合特征
形状: [batch_size, nb_nodes, out_sz]
coefs: 注意力系数 (可选,只有return_coef=True时返回)
形状: [batch_size, nb_nodes, nb_nodes]
示例:
假设节点A有两个邻居B和C
- 注意力系数 α_AB = 0.7, α_AC = 0.3
- 节点A的新表示 = 0.7 * W·h_B + 0.3 * W·h_C
"""
with tf1.name_scope('my_attn'):
# =================================================================
# 步骤1: 输入Dropout
# =================================================================
# 在训练时随机丢弃部分输入特征,防止过拟合
if in_drop != 0.0:
seq = tf1.nn.dropout(seq, 1.0 - in_drop)
# =================================================================
# 步骤2: 特征变换
# =================================================================
# 使用1x1卷积(等价于线性变换)对输入特征进行映射
# 将特征从 ft_size 维映射到 out_sz 维
# seq_fts形状: [batch, nb_nodes, out_sz]
seq_fts = tf1.layers.conv1d(seq, out_sz, 1, use_bias=False)
# =================================================================
# 步骤3: 计算注意力分数
# =================================================================
# 使用两个独立的线性变换 f_1 和 f_2
# 将特征映射到1维,得到注意力分数
# 这允许模型学习不同的"查询"和"键"
f_1 = tf1.layers.conv1d(seq_fts, 1, 1) # 形状: [batch, nb_nodes, 1]
f_2 = tf1.layers.conv1d(seq_fts, 1, 1) # 形状: [batch, nb_nodes, 1]
# =================================================================
# 步骤4: 计算节点对之间的注意力分数
# =================================================================
# logits[i][j] 表示节点i对节点j的注意力分数
# 通过 f_1 + f_2^T 实现: a^T [W·h_i || W·h_j]
# 这种方式是GAT论文中推荐的做法,比直接拼接更高效
# transpose: [1, 3, 1] -> [1, 1, 3]
# 相加后: [1, 3, 1] + [1, 1, 3] -> [1, 3, 3]
logits = f_1 + tf1.transpose(f_2, [0, 2, 1])
# logits形状: [batch, nb_nodes, nb_nodes]
# =================================================================
# 步骤5: LeakyReLU激活 + 邻接矩阵掩码 + Softmax
# =================================================================
# LeakyReLU: 允许负值存在但梯度较小,常用于注意力机制
# bias_mat: 邻接矩阵偏置,非邻居位置为-1e9,邻居位置为0
# Softmax: 将每个节点的注意力分数归一化为概率分布
coefs = tf1.nn.softmax(tf1.nn.leaky_relu(logits) + bias_mat)
# coefs形状: [batch, nb_nodes, nb_nodes]
# coefs[i][j] 表示节点i对节点j的注意力权重
# =================================================================
# 步骤6: 注意力系数Dropout
# =================================================================
# 训练时随机丢弃部分注意力连接,增加模型鲁棒性
if coef_drop != 0.0:
coefs = tf1.nn.dropout(coefs, 1.0 - coef_drop)
# 特征Dropout(第二次)
if in_drop != 0.0:
seq_fts = tf1.nn.dropout(seq_fts, 1.0 - in_drop)
# =================================================================
# 步骤7: 特征聚合
# =================================================================
# 使用注意力系数对邻居特征进行加权求和
# vals[i][j] = Σ(coefs[i][j] * seq_fts[j])
# 即:节点i的新表示 = Σ(注意力权重 × 邻居节点特征)
vals = tf1.matmul(coefs, seq_fts)
# vals形状: [batch, nb_nodes, out_sz]
# TF2.x兼容:移除bias_add,直接使用vals
ret = vals
# =================================================================
# 步骤8: 残差连接 (可选)
# =================================================================
# 残差连接有助于深层网络的训练
# 如果输出维度与输入维度相同,则直接相加
# 否则,通过线性变换后再相加
if residual:
if seq.shape[-1] != ret.shape[-1]:
ret = ret + conv1d(seq, ret.shape[-1], 1) # 激活
else:
seq_fts = ret + seq
# =================================================================
# 返回结果
# =================================================================
if return_coef:
# 同时返回聚合特征和注意力系数(用于分析)
return activation(ret), coefs
else:
# 只返回聚合特征
return activation(ret)
2.2 SimpleAttLayer:语义级别注意力
python
def SimpleAttLayer(inputs, attention_size, time_major=False, return_alphas=False):
"""
SimpleAttLayer - 语义级注意力层 (Semantic Attention)
这是HAN中第二层注意力机制,用于融合不同元路径的嵌入表示。
核心思想:
不同元路径对于下游任务的贡献是不同的,
通过可学习的注意力机制自动学习每个元路径的重要性权重。
例如在ACM数据集中:
- PAP (Paper-Author-Paper): 通过作者连接的论文
- PLP (Paper-Label-Paper): 通过标签连接的论文
模型会学习到:对于论文分类任务,PLP路径可能比PAP路径更重要
注意力机制计算流程:
1. 将输入通过一个带激活的全连接层
2. 计算每个元路径的注意力分数
3. 通过Softmax归一化
4. 使用归一化的权重对输入进行加权求和
参数:
inputs: 多个元路径的嵌入
形状: [batch_size, num_meta_paths, nb_nodes, hidden_dim]
例如: [1, 2, 3025, 64] 表示2个元路径,3025个节点,64维嵌入
attention_size: 注意力层的隐藏维度
用于计算注意力分数的中间维度,默认128
time_major: 是否时间序列优先(此场景固定为False)
return_alphas: 是否返回注意力权重(用于分析)
返回:
output: 融合后的嵌入
形状: [batch_size, nb_nodes, hidden_dim]
alphas: 注意力权重 (可选)
形状: [batch_size, num_meta_paths]
表示每个元路径的重要性
示例:
假设有2个元路径:
- 嵌入1: PAP路径的表示, shape [1, 3025, 64]
- 嵌入2: PLP路径的表示, shape [1, 3025, 64]
SimpleAttLayer会学习权重:
- α_1 = 0.3 (PAP的重要性)
- α_2 = 0.7 (PLP的重要性)
最终嵌入 = 0.3 × 嵌入1 + 0.7 × 嵌入2
"""
# 如果输入是元组(双向RNN等情况),进行拼接
if isinstance(inputs, tuple):
inputs = tf1.concat(inputs, 2)
# 时间序列优先时的维度转换(此场景不使用)
if time_major:
inputs = tf1.transpose(inputs, [1, 0, 2])
# 获取输入的隐藏维度
hidden_size = inputs.shape[2].value # D value - hidden size
# =================================================================
# 可学习的注意力参数
# =================================================================
# w_omega: 将输入映射到注意力空间的权重矩阵
# 形状: [hidden_dim, attention_size]
# 例如: [64, 128]
w_omega = tf1.Variable(tf1.random_normal([hidden_size, attention_size], stddev=0.1))
# b_omega: 注意力空间的偏置
# 形状: [attention_size]
b_omega = tf1.Variable(tf1.random_normal([attention_size], stddev=0.1))
# u_omega: 用于计算注意力权重的向量
# 形状: [attention_size]
u_omega = tf1.Variable(tf1.random_normal([attention_size], stddev=0.1))
# =================================================================
# 计算注意力分数
# =================================================================
# 第一步:将输入映射到注意力空间
# v = tanh(W·h + b)
# v形状: [batch, num_mp, attention_size]
with tf1.name_scope('v'):
v = tf1.tanh(tf1.tensordot(inputs, w_omega, axes=1) + b_omega)
# 第二步:计算每个元路径的注意力分数
# vu = v · u^T
# vu形状: [batch, num_mp]
# 这是一个标量分数,表示每个元路径的"重要性"
vu = tf1.tensordot(v, u_omega, axes=1, name='vu')
# 第三步:Softmax归一化
# alphas形状: [batch, num_mp]
# 每个元路径的注意力权重,归一化为概率分布
alphas = tf1.nn.softmax(vu, name='alphas')
# =================================================================
# 加权求和
# =================================================================
# output = Σ(α_i × h_i)
# 使用注意力权重对输入进行加权求和
# output形状: [batch, hidden_dim]
output = tf1.reduce_sum(inputs * tf1.expand_dims(alphas, -1), 1)
# =================================================================
# 返回结果
# =================================================================
if not return_alphas:
return output
else:
# 同时返回融合后的嵌入和注意力权重
# 注意力权重可以用于分析每个元路径的贡献
return output, alphas
3、HeteGAT_no_coef
python
class HeteGAT_no_coef(BaseGAttN):
"""
HeteGAT_no_coef - 不返回注意力系数的版本
这是HeteGAT的简化版本,不返回节点级别的注意力系数。
适用于只需要最终分类结果,不需要分析注意力分布的场景。
"""
def inference(inputs, nb_classes, nb_nodes, training, attn_drop, ffd_drop,
bias_mat_list, hid_units, n_heads, activation=tf1.nn.elu, residual=False,
mp_att_size=128):
"""
前向传播(不返回注意力系数)
参数与HeteGAT_multi相同
"""
embed_list = []
for bias_mat in bias_mat_list:
attns = []
head_coef_list = []
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,
return_coef=False))
h_1 = tf1.concat(attns, axis=-1)
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 = tf1.concat(attns, axis=-1)
embed_list.append(tf1.expand_dims(tf1.squeeze(h_1), axis=1))
# 元路径级别的注意力融合
multi_embed = tf1.concat(embed_list, axis=1)
final_embed, att_val = layers.SimpleAttLayer(multi_embed, mp_att_size,
time_major=False,
return_alphas=True)
# 输出层
out = []
for i in range(n_heads[-1]):
out.append(tf1.layers.dense(final_embed, nb_classes, activation=None))
logits = tf1.add_n(out) / n_heads[-1]
print('de')
logits = tf1.expand_dims(logits, axis=0)
return logits, final_embed, att_val
4、HeteGAT
python
class HeteGAT(BaseGAttN):
"""
HeteGAT - 完整版本,支持返回注意力系数
这是最完整的HAN实现,除了返回分类结果外,
还可以返回节点级别的注意力系数,用于可解释性分析。
返回的coef_list存储了每个元路径的节点注意力分布,
可以帮助理解模型是如何关注不同邻居节点的。
"""
def inference(inputs, nb_classes, nb_nodes, training, attn_drop, ffd_drop,
bias_mat_list, hid_units, n_heads, activation=tf1.nn.elu, residual=False,
mp_att_size=128,
return_coef=False):
"""
前向传播(可选择返回注意力系数)
参数:
return_coef: 是否返回节点级别的注意力系数
返回:
logits: 分类logits
final_embed: 最终嵌入
att_val: 元路径注意力权重
coef_list: 节点注意力系数列表 (可选)
"""
embed_list = []
coef_list = [] # 存储每个元路径的注意力系数
for bias_mat in bias_mat_list:
attns = []
head_coef_list = []
# 第一层:多头注意力
for _ in range(n_heads[0]):
if return_coef:
# 同时返回注意力系数和聚合后的特征
a1, a2 = 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,
return_coef=return_coef)
attns.append(a1)
head_coef_list.append(a2)
else:
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,
return_coef=return_coef))
# 对多头的注意力系数取平均
head_coef = tf1.concat(head_coef_list, axis=0)
head_coef = tf1.reduce_mean(head_coef, axis=0)
coef_list.append(head_coef)
h_1 = tf1.concat(attns, axis=-1)
# 后续隐藏层
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 = tf1.concat(attns, axis=-1)
embed_list.append(tf1.expand_dims(tf1.squeeze(h_1), axis=1))
# 语义级注意力融合
multi_embed = tf1.concat(embed_list, axis=1)
final_embed, att_val = layers.SimpleAttLayer(multi_embed, mp_att_size,
time_major=False,
return_alphas=True)
# 输出层
out = []
for i in range(n_heads[-1]):
out.append(tf1.layers.dense(final_embed, nb_classes, activation=None))
logits = tf1.add_n(out) / n_heads[-1]
logits = tf1.expand_dims(logits, axis=0)
if return_coef:
return logits, final_embed, att_val, coef_list
else:
return logits, final_embed, att_val
tensorflow的核心部分讲完了,下面会讲解torch版本