近期买了一本图神经网络的入门书,最近几篇博客对书中的一些实战案例进行整理,具体的理论和原理部分可以自行查阅该书,该书购买链接:《深入浅出的图神经网络》。
本节我们通过代码来介绍GraphSAGE以加深读者对相关知识的理解,如书中介绍的那样,GraphSAGE包括两方面,一是对邻居的采样;二是对邻居的聚合操作。
目录
1. 对邻居采样
首先来看下对邻居的采样方法,为了实现更高效低地采样,可以将节点及其邻居存放在一起,即维护一个节点与其邻居对应关系的表。可以通过两个函数sampling和multihop_sampling来实现采样的具体操作。其中sampling是进行一阶采样,根据源节点采样指定数量的邻居节点,multihop_sampling则是利用sampling实现多阶采样的功能。
def sampling(src_nodes, sample_num, neighbor_table):
"""根据源节点采样指定数量的邻居节点,注意使用的是有放回的采样;
某个节点的邻居节点数量少于采样数量时,采样结果出现重复的节点
Arguments:
src_nodes {list, ndarray} -- 源节点列表
sample_num {int} -- 需要采样的节点数
neighbor_table {dict} -- 节点到其邻居节点的映射表,邻接矩阵
Returns:
np.ndarray -- 采样结果构成的列表
"""
results = []
for sid in src_nodes:
# 从节点的邻居中进行有放回地进行采样
res = np.random.choice(neighbor_table[sid], size=(sample_num,))
results.append(res)
return np.asarray(results).flatten() #拉伸为1维
def multihop_sampling(src_nodes, sample_nums, neighbor_table):
"""根据源节点进行多阶采样
Arguments:
src_nodes {list, np.ndarray} -- 源节点id
sample_nums {list of int} -- 每一阶需要采样的个数
neighbor_table {dict} -- 节点到其邻居节点的映射 /邻接矩阵
Returns:
[list of ndarray] -- 每一阶采样的结果
"""
sampling_result = [src_nodes] #首先包含源节点
for k, hopk_num in enumerate(sample_nums): #先对源节点进行1阶采样 在与源节点距离为1的节点中采样hopk_num个节点; 再对源节点进行2阶采样,即对源节点的所有1阶邻居进行1阶采样
hopk_result = sampling(sampling_result[k], hopk_num, neighbor_table)
sampling_result.append(hopk_result) #追加源节点的1阶邻居 和 2阶邻居(2层网络,代表采样到2阶)
return sampling_result
这样采样得到的结果仅仅是节点的ID,还需要根据节点ID去查询每个节点的特征(向量),以进行聚合操作更新特征。
2. 对邻居聚合
接下里根据两种聚类算子对邻居的状态(特征向量)进行聚合:
1)平均/加和聚合算子
2)池化聚合算子
计算过程定义在forward函数中,输入neighbor_feature表示每个节点需要聚合的邻接节点的特征(向量),维度为,其中
表示源节点的数量,
表示邻居节点的数量,
表示输入的特征维度。将这些邻居节点的特征经过一个线性变换得到隐层特征,就可以沿着
维度进行聚合操作了,包括求和、均值和最大值,得到维度
的输出。
class NeighborAggregator(nn.Module):
def __init__(self, input_dim, output_dim,
use_bias=False, aggr_method="mean"):
"""聚合节点邻居
Args:
input_dim: 输入特征的维度
output_dim: 输出特征的维度
use_bias: 是否使用偏置 (default: {False})
aggr_method: 邻居聚合方式 (default: {mean})
"""
super(NeighborAggregator, self).__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.use_bias &