图神经网络实战(14)——基于节点嵌入预测链接
- 0. 前言
- 1. 图自编码器
- 2. 变分图自编码器
- 3. 实现变分图自编码器
- 小结
- 系列链接
0. 前言
我们已经了解了如何使用图神经网络 (Graph Neural Networks, GNN) 生成节点嵌入,我们可以使用这些嵌入执行矩阵分解 (matrix factorization
) 完成链接预测任务。本节将介绍两种用于链接预测的 GNN 架构
——图自编码器 (Graph Autoencoder
, GAE
) 和变分图自编码器 (Variational Graph Autoencoder
, VGAE
)。
1. 图自编码器
图自编码器 (Graph Autoencoder
, GAE
) 和变分图自编码器 (Variational Graph Autoencoder
, VGAE
) 架构都是 Kipf
和 Welling
于 2016
年所提出的。它们分别对应于两种流行的神经网络架构——自编码器 (Autoencoder) 和变分自编码器 (Variational Autoencoder, VAE)。为了便于理解,我们将首先介绍 GAE
。GAE
由两个模块组成:
- 编码器 (
encoder
):一个经典的双层图卷积网络 (Graph Convolutional Network, GCN),使用以下方式计算节点嵌入:
Z = G C N ( X , A ) Z=GCN(X,A) Z=GCN(X,A) - 解码器 (
decoder
):使用矩阵分解 (matrix factorization) 和sigmoid
函数 σ σ σ 来近似邻接矩阵 A ^ \hat A A^,从而输出概率:
A ^ = σ ( Z T Z ) \hat A=\sigma(Z^TZ) A^=σ(ZTZ)
需要注意的是,我们并不是要对节点或图进行分类,而是预测邻接矩阵 A ^ \hat A A^ 中每个元素的概率(介于 0
和 1
之间),因此使用两个邻接矩阵元素之间的二进制交叉熵损失(负对数似然)来训练 GAE
:
L B C E = ∑ i ∈ V , j ∈ V − A i j l o g ( A ^ i j ) − ( 1 − A i j ) l o g ( 1 − A ^ i j ) \mathcal L_{BCE}=\sum_{i\in V,j\in V}-A_{ij}log(\hat A_{ij})-(1-A_{ij})log(1-\hat A_{ij}) LBCE=i∈V,j∈V∑−Aijlog(A^ij)−(1−Aij)log(1−A^ij)
然而,邻接矩阵通常非常稀疏,这会使 GAE
偏向于预测零值。有两种简单的方法可以修正这一偏差。首先,可以在上述损失函数中增加一个权重,使偏向于 A i i = 1 A_{ii}=1 Aii=1。其次,可以在训练过程中采样较少的零值,使标签更加均衡。
这种架构非常灵活,编码器可以换成其它类型的图神经网络 (Graph Neural Networks, GNN) (如 GraphSAGE、图同构网络 (Graph Isomorphism Network, GIN) 等),多层感知机 (Multilayer Perceptron
, MLP
) 也可以作为解码器,另一种改进方法是将 GAE
转换为变分图自编码器。
2. 变分图自编码器
图自编码器 (Graph Autoencoder
, GAE
) 和变分图自编码器 (Variational Graph Autoencoder
, VGAE
) 之间的区别与自编码器 (Autoencoder) 和变分自编码器 (Variational Autoencoder, VAE) 之间的区别相同。VGAE 不直接学习节点嵌入,而是学习正态分布,然后通过采样生成嵌入。VGAE
也由两个模块组成:
- 编码器 (
encoder
):由共享第一层的两个图卷积网络 (Graph Convolutional Network
,GCN
) 组成。其目标是学习每个潜正态分布的参数,均值 μ μ μ (由 G C N μ GCN_μ GCNμ 学习)和方差 σ 2 σ^2 σ2 (在实践中通过 G C N σ GCN_σ GCNσ 学习其对数形式) - 解码器 (
decoder
):使用重参数化技巧 (reparametrization trick
),从学习到的分布 ( μ , σ 2 ) (μ, σ^2) (μ,σ2) 中采样嵌入值 z i z_i zi, 。然后,它使用潜变量之间的内积来近似邻接矩阵 A ^ = σ ( Z T Z ) \hat A= σ(Z^TZ) A^=σ(ZTZ)。
对于 VGAE
,确保编码器的输出服从正态分布非常重要,因此需要在损失函数中添加一个新项,Kullback-Leibler 散度
(KL 散度
),它用于测量两个分布之间的差异。VGAE
的总体损失如下,也称为证据下界 (evidence lower bound
, ELBO
):
L E L B O = L B C E − K L [ q ( Z ∣ X , A ) ∣ ∣ p ( Z ) ] \mathcal L_{ELBO}=\mathcal L_{BCE}-KL[q(Z|X,A)||p(Z)] LELBO=LBCE−KL[q(Z∣X,A)∣∣p(Z)]
其中, q ( Z ∣ X , A ) q(Z|X,A) q(Z∣X,A) 表示编码器, p ( Z ) p(Z) p(Z) 是 Z Z Z 的先验分布。通常可以使用ROC 曲线下面积 (area under the ROC
, AUROC
) 和平均精度 (average precision
, AP
) 这两个指标来评估模型的性能。
接下来,我们使用 PyTorch Geometric
实现 VGAE
。
3. 实现变分图自编码器
变分图自编码器 (Variational Graph Autoencoder
, VGAE
) 与其它类型的图神经网络 (Graph Neural Networks, GNN) (如 GraphSAGE、图同构网络 (Graph Isomorphism Network, GIN) 等)实现有两个主要区别:
- 对数据集进行预处理,随机删除一些链接以进行预测
- 创建一个编码器模型,并将其添加到
VGAE
类中,而不是直接从头开始实现VGAE
接下来,使用 PyTorch Geometric
(PyG
) 构建 VGAE
模型。
(1) 首先,导入所需的库,并定义设备:
import numpy as np
import torch
import matplotlib.pyplot as plt
import torch_geometric.transforms as T
from torch_geometric.datasets import Planetoiddevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
(2) 创建一个 transform
对象,对输入特征进行归一化处理,将张量转移到预定义的设备中,并随机分割链接(在本节中,我们按照 85: 5:10
的比例进行拆分),将 add_negative_train_samples
参数设置为 False
,因为模型已经执行了负采样,所以数据集中不需要负采样:
transform = T.Compose([T.NormalizeFeatures(),T.ToDevice(device),T.RandomLinkSplit(num_val=0.05, num_test=0.1, is_undirected=True, split_labels=True, add_negative_train_samples=False),
])
(3) 使用定义的 transform
对象加载 Cora 数据集:
dataset = Planetoid('.', name='Cora', transform=transform)
(4) RandomLinkSplit
方法会按预定比例拆分生成训练/验证/测试集,并存储这些数据集:
train_data, val_data, test_data = dataset[0]
(5) 接下来,实现编码器。首先,需要导入 GCNConv 和 VGAE
:
from torch_geometric.nn import GCNConv, VGAE
声明一个新类,在这个类中,需要三个图卷积网络 (Graph Convolutional Network
, GCN
) 层,一个作为共享层、一个用于近似均值 μ μ μ,第三个用于近似方差值(实践中使用对数标准差, log σ \log\sigma logσ):
class Encoder(torch.nn.Module):def __init__(self, dim_in, dim_out):super().__init__()self.conv1 = GCNConv(dim_in, 2 * dim_out)self.conv_mu = GCNConv(2 * dim_out, dim_out)self.conv_logstd = GCNConv(2 * dim_out, dim_out)def forward(self, x, edge_index):x = self.conv1(x, edge_index).relu()return self.conv_mu(x, edge_index), self.conv_logstd(x, edge_index)
(6) 初始化 VGAE
并将编码器作为输入,默认情况下,VGAE
使用内积作为解码器:
model = VGAE(Encoder(dataset.num_features, 16)).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
(7) 在 train()
方法中,首先使用 model.encode()
计算嵌入矩阵 Z Z Z,此函数从学习到的分布中对样本嵌入进行采样。然后,使用 model.recon_loss()
(二进制交叉熵损失)和 model.kl_loss()
(KL 散度
) 计算 ELBO
损失。解码器会被隐式调用来计算交叉熵损失:
def train():model.train()optimizer.zero_grad()z = model.encode(train_data.x, train_data.edge_index)loss = model.recon_loss(z, train_data.pos_edge_label_index) + (1 / train_data.num_nodes) * model.kl_loss()loss.backward()optimizer.step()return float(loss)
(8) test()
函数只需调用 VGAE
的专用方法:
@torch.no_grad()
def test(data):model.eval()z = model.encode(data.x, data.edge_index)return model.test(z, data.pos_edge_label_index, data.neg_edge_label_index)
(9) 对模型进行 301
个 epoch
的训练,并打印 AUC
和 AP
指标:
for epoch in range(301):loss = train()val_auc, val_ap = test(test_data)if epoch % 50 == 0:print(f'Epoch {epoch:>2} | Loss: {loss:.4f} | Val AUC: {val_auc:.4f} | Val AP: {val_ap:.4f}')
输出结果如下所示:
Epoch 0 | Loss: 3.4412 | Val AUC: 0.6842 | Val AP: 0.7043
Epoch 50 | Loss: 1.3321 | Val AUC: 0.6628 | Val AP: 0.6881
Epoch 100 | Loss: 1.1690 | Val AUC: 0.7512 | Val AP: 0.7526
Epoch 150 | Loss: 1.0348 | Val AUC: 0.8173 | Val AP: 0.8128
Epoch 200 | Loss: 0.9980 | Val AUC: 0.8415 | Val AP: 0.8364
Epoch 250 | Loss: 0.9698 | Val AUC: 0.8576 | Val AP: 0.8457
Epoch 300 | Loss: 0.9339 | Val AUC: 0.8727 | Val AP: 0.8620
(10) 在测试集上对模型进行评估:
test_auc, test_ap = test(test_data)
print(f'Test AUC: {test_auc:.4f} | Test AP {test_ap:.4f}')# Test AUC: 0.8727 | Test AP 0.8620
(11) 手动计算近似邻接矩阵 A ^ \hat A A^:
z = model.encode(test_data.x, test_data.edge_index)
Ahat = torch.sigmoid(z @ z.T)
print(Ahat)
'''
tensor([[0.8468, 0.5072, 0.7254, ..., 0.7016, 0.8674, 0.8545],[0.5072, 0.8120, 0.7991, ..., 0.4572, 0.6988, 0.6898],[0.7254, 0.7991, 0.8623, ..., 0.5731, 0.8622, 0.8496],...,[0.7016, 0.4572, 0.5731, ..., 0.6582, 0.6973, 0.6925],[0.8674, 0.6988, 0.8622, ..., 0.6973, 0.9259, 0.9155],[0.8545, 0.6898, 0.8496, ..., 0.6925, 0.9155, 0.9051]],device='cuda:0', grad_fn=<SigmoidBackward0>)
'''
VGAE
的训练速度很快,输出结果也很容易理解,但我们已经知道 GCN
并不是最具表达能力的运算符。为了提高模型的表达能力,我们需要采用更好的技术。
小结
链接预测可以帮助我们发现隐藏的关联规律,从而为网络分析、推荐系统等问题提供有效的解决方案。在本节中,介绍了如何使用图神经网络 (Graph Neural Networks
, GNN
) 实现链接预测,学习了基于节点嵌入的链接预测技术,包括图自编码器 (Graph Autoencoder
, GAE
) 和变分图自编码器 (Variational Graph Autoencoder
, VGAE
),并使用边级随机分割和负采样在 Cora
数据集上实现了 VGAE
模型。
系列链接
图神经网络实战(1)——图神经网络(Graph Neural Networks, GNN)基础
图神经网络实战(2)——图论基础
图神经网络实战(3)——基于DeepWalk创建节点表示
图神经网络实战(4)——基于Node2Vec改进嵌入质量
图神经网络实战(5)——常用图数据集
图神经网络实战(6)——使用PyTorch构建图神经网络
图神经网络实战(7)——图卷积网络(Graph Convolutional Network, GCN)详解与实现
图神经网络实战(8)——图注意力网络(Graph Attention Networks, GAT)
图神经网络实战(9)——GraphSAGE详解与实现
图神经网络实战(10)——归纳学习
图神经网络实战(11)——Weisfeiler-Leman测试
图神经网络实战(12)——图同构网络(Graph Isomorphism Network, GIN)
图神经网络实战(13)——经典链接预测算法