现有图神经网络皆基于邻居聚合的框架,即为每个目标节点通过聚合其邻居刻画结构信息,进而学习目标节点的表示
- 谱域方法:利用图上卷积定理从谱域定义图卷积。
- 空间域方法:从节点域出发,通过在节点层面定义聚合函数来聚合每个中心节点和其邻近节点。
谱域图卷积神经网络
谱图理论和图卷积
卷积的傅里叶变换
卷积定理:信号卷积的傅立叶变换等价于信号傅立叶变换的乘积
其中的 f,g 表示两个原始信号,F(f) 表示 f 的傅立叶变换, ⋅ 表示乘积算子, ∗ 表示卷积算子
对上面的公式做傅立叶逆变换,可以得到
利用卷积定理,我们可以对谱空间的信号做乘法,再利用傅里叶逆变换将信号转换到原空间来实现图卷积,从而避免了图数据不满足平移不变性而造成的卷积定义困难问题
图傅里叶变换
图傅立叶变换依赖于图上的拉普拉斯矩阵 L。对 L 做谱分解,我们可以得到
其中 Λ 是特征值矩阵, U 是对应的特征向量矩阵。如下图所示
图上傅立叶变换的定义依赖于拉普拉斯矩阵的特征向量。以特征向量作为谱空间下的一组基底,图上信号 x 的傅立叶变换为:
其中 x 指信号在节点域的原始表示。x^ 指信号 x 变换到谱域后的表示
图卷积
先将图进行傅里叶变化,在谱域完成卷积操作,然后再将频域信号转换回原域。我们将卷积核定义为 gθ(Λ),那么卷积的频域表示可以写为
先对输入 x (其输出为 y) 进行傅里叶变换得到 UTx (以及 UTy),然后对其应用卷积核得到
最后,再利用傅里叶逆变换得到
以上就是最重要的图卷积操作。它一般可以被简化为
谱卷积神经网络
该方法完全按照上述得到的卷积操作,堆叠多层得到。我们可以将上面公式的 x 和 y 替换为图上的节点特征 H(l) 和 H(l+1),那么第 l 层的结构如下
σ 表示非线性激活函数。
切比雪夫网络
谱卷积神经网络其缺点为:
(1)难以从卷积形式中保证节点的信息更新由其邻居节点贡献,因此无法保证局部性
(2)谱卷积神经网络的计算复杂度比较大,难以扩展到大型图网络结构中
切比雪夫网络对卷积核 gθ 进行参数化
其中 θk 是需要学的系数,并定义切比雪夫多项式是可以通过递归求解,递归表达式为
其中初始值 T0(x)=1,T1(x)=x
切比雪夫网络第 l 层的结构定义如下:
当且仅当两个节点满足 K 跳可达时,其拉普拉斯矩阵中这一项不为 0,这一性质使得当 K 较小时,切比雪夫网络具有局部性
图卷积神经网络
图卷积神经网络(GCN)对切比雪夫网络进行了简化,只取 0 阶和 1 阶,形式如下
令 θ=θ0=−θ1,然后应用一个重新标准化的trick,在应用于图神经网络的第 l 层时
图卷积神经网络代码
# 获得图的一些统计特征
print(f'Number of nodes: {data.num_nodes}')
print(f'Number of edges: {data.num_edges}')
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}')
print(f'Has isolated nodes: {data.has_isolated_nodes()}')
print(f'Has self-loops: {data.has_self_loops()}')
print(f'Is undirected: {data.is_undirected()}')
Number of nodes
: 图中节点的数量。Number of edges
: 图中边的数量。Average node degree
: 平均节点度,即每个节点连接的平均边数。Has isolated nodes
: 是否存在孤立节点,即没有与其他节点连接的节点。Has self-loops
: 是否存在自环,即节点与自身相连的边。Is undirected
: 图是否是无向图,即边没有方向性。
from torch_geometric.loader import DataLoader
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
批处理对于图数据比较复杂和麻烦。PyTorch Geometric 选择了一种和常见图像数据集不同的方法来实现多个示例的并行化。 在这里,邻接矩阵以对角方式堆叠(创建一个包含多个孤立子图的巨型图),并且节点和目标特征在节点维度中简单地连接——[num_nodes_total, num_node_features + num_target_features]
与其他批处理程序相比,该程序具有一些关键优势:(1)依赖于消息传递方案的 GNN 算子不需要修改,因为属于不同图的两个节点之间不会交换消息;(2)由于邻接矩阵以稀疏方式保存,仅保存非零条目(即边),因此不存在计算或内存开销
训练 GNN 进行图分类通常遵循一个简单的方法:
- 通过执行多轮消息传递来嵌入每个节点。
- 将节点嵌入聚合为统一的图嵌入(读出层)。
- 在图嵌入上训练最终分类器。
最常见的一种读出层是简单地取节点嵌入的平均值:
PyTorch Geometric 通过 torch_geometric.nn.global_mean_pool
提供此功能,它接受小批量中所有节点的节点嵌入和分配向量批量,以计算批量中每个图的大小为 [batch_size, hide_channels]
的图嵌入
from torch.nn import Linear
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
from torch_geometric.nn import global_mean_pool
class GCN(torch.nn.Module):
def __init__(self, hidden_channels):
super(GCN, self).__init__()
torch.manual_seed(12345)
# 使用 GCNConv
# 为了让模型更稳定我们也可以使用带有跳跃链接的 GraphConv
# from torch_geometric.nn import GraphConv
self.conv1 = GCNConv(dataset.num_node_features, hidden_channels)
self.conv2 = GCNConv(hidden_channels, hidden_channels)
self.conv3 = GCNConv(hidden_channels, hidden_channels)
self.lin = Linear(hidden_channels, dataset.num_classes)
def forward(self, x, edge_index, batch):
# 1. 获得节点的嵌入
x = self.conv1(x, edge_index)
x = x.relu()
x = self.conv2(x, edge_index)
x = x.relu()
x = self.conv3(x, edge_index)
# 2. 读出层
x = global_mean_pool(x, batch) # [batch_size, hidden_channels]
# 3. 应用最后的分类器
x = F.dropout(x, p=0.5, training=self.training)
x = self.lin(x)
return x
model = GCN(hidden_channels=64)
print(model)
空间域图卷积神经网络
图卷积神经网络的空域理解
从邻居节点信息聚合的角度出发,应该做的如下两件事情
- 对节点的信息进行转换(Message Transformation)
- 对节点信息进行聚合 (Message Aggregation)
其中第一项表示邻居节点信息的转换和聚合,第二项表示自身节点信息的变换
上述的公式的矩阵表达可以写为
空域图卷积的统一范式和GraphSAGE
图卷积的统一范式
其中 TRANSu(l) 表示对邻居节点信息的转换,TRANSv(l) 表示对自身节点信息的转换,AGG1l 表示对邻居节点信息的聚合,AGG2l 表示对自身节点信息的聚合
可以看出来 GCN
在进行聚合的时候是没有考虑边的权重的而当作 1
进行简单的加和
GraphSAGE
- 在训练时,采样方式将 GCN 的全图采样优化到部分以节点为中心的邻居抽样,这使得大规模图数据的分布式训练成为可能,并且使得网络可以学习没有见过的节点,这也使得 GraphSAGE 可以做 Inductive Learning。
- GraphSAGE 研究了若干种邻居聚合的方式,及其 AGG 聚合函数可以使用
- 平均
- Max Pooling
- LSTM
在 GraphSAGE 之前的 GCN 模型中,都是采用的全图的训练方式,也就是说每一轮的迭代都要对全图的节点进行更新
GraphSAGE 提出了一个mini-batch解决方案:
- 对邻居进行随机采样,每一跳抽样的邻居数不多于 Sk 个;
- 生成目标节点的 embedding:先聚合二跳邻居的特征,生成一跳邻居的embedding,再聚合一跳的 embedding,生成目标节点的 embedding;
- 将目标节点的 embedding 输入全连接网络得到目标节点的预测值。
GraphSAGE代码
# 分离负样本
adj = sp.coo_matrix((np.ones(len(u)), (u.numpy(), v.numpy())))
adj_neg = 1 - adj.todense() - np.eye(g.number_of_nodes())
neg_u, neg_v = np.where(adj_neg != 0)
uv表示一条边,创建一个adj稀疏矩阵,通过1-得到不存在边的地方
from dgl.nn import SAGEConv
# 构建一个两层的 GraphSAGE 模型
class GraphSAGE(nn.Module):
def __init__(self, in_feats, h_feats):
super(GraphSAGE, self).__init__()
self.conv1 = SAGEConv(in_feats, h_feats, 'mean')
self.conv2 = SAGEConv(h_feats, h_feats, 'mean')
def forward(self, g, in_feat):
h = self.conv1(g, in_feat)
h = F.relu(h)
h = self.conv2(g, h)
return h
# 构建正样本和负样本的图
train_pos_g = dgl.graph((train_pos_u, train_pos_v), num_nodes=g.number_of_nodes())
train_neg_g = dgl.graph((train_neg_u, train_neg_v), num_nodes=g.number_of_nodes())
test_pos_g = dgl.graph((test_pos_u, test_pos_v), num_nodes=g.number_of_nodes())
test_neg_g = dgl.graph((test_neg_u, test_neg_v), num_nodes=g.number_of_nodes())
正负样本是通过节点对表示的,在这里把它们变成图,使得正负样本可以直接作为图的结构输入到模型中
import dgl.function as fn
class DotPredictor(nn.Module):
def forward(self, g, h):
with g.local_scope():
g.ndata['h'] = h
# 通过点积计算一个新的边的分数
g.apply_edges(fn.u_dot_v('h', 'h', 'score'))
# u_dot_v 返回了一个 1-element 的向量,所以需要压平它
return g.edata['score'][:, 0]
class MLPPredictor(nn.Module):
def __init__(self, h_feats):
super().__init__()
self.W1 = nn.Linear(h_feats * 2, h_feats)
self.W2 = nn.Linear(h_feats, 1)
def apply_edges(self, edges):
"""
Computes a scalar score for each edge of the given graph.
Parameters
----------
edges :
Has three members ``src``, ``dst`` and ``data``, each of
which is a dictionary representing the features of the
source nodes, the destination nodes, and the edges
themselves.
Returns
-------
dict
A dictionary of new edge features.
"""
h = torch.cat([edges.src['h'], edges.dst['h']], 1)
return {'score': self.W2(F.relu(self.W1(h))).squeeze(1)}
def forward(self, g, h):
with g.local_scope():
g.ndata['h'] = h
g.apply_edges(self.apply_edges)
return g.edata['score']
两个图神经网络中常用的预测器类:DotPredictor
和 MLPPredictor
。
DotPredictor
类:图上两点的特征做点积MLPPredictor
类:源节点和目标节点的特征拼接后过MLP
optimizer = torch.optim.Adam(itertools.chain(model.parameters(), pred.parameters()), lr=0.01)
# 训练
all_logits = []
for e in range(100):
# 前向传播
h = model(train_g, train_g.ndata['feat'])
pos_score = pred(train_pos_g, h)
neg_score = pred(train_neg_g, h)
loss = compute_loss(pos_score, neg_score)
# 更新参数
optimizer.zero_grad()
loss.backward()
optimizer.step()
if e % 5 == 0:
print('In epoch {}, loss: {}'.format(e, loss))
# 计算AUC
with torch.no_grad():
pos_score = pred(test_pos_g, h)
neg_score = pred(test_neg_g, h)
print('AUC', compute_auc(pos_score, neg_score))
SAGE提供特征,预测器打分,优化器同时更新model和pred