图神经网络实战(19)——异构图神经网络
0. 前言
我们已经学习了如何生成包含不同类型节点(原子)和边(键)的分子结构,这种技术在其它应用中也具有广泛用途,例如推荐系统(用户和商品)、社交网络(关注者和被关注者)或网络安全(路由器和服务器)。我们将这类图称为异构图 (heterogeneous graph
),与同构图 (homogeneous graph
) 相对,后者只涉及一种类型的节点和一种类型的边。在本节中,我们将回顾关于同构图神经网络 (Graph Neural Networks, GNN) 与消息传递神经网络框架的相关概念,以扩展 GNN
架构适用于异构图。首先,我们将创建自定义异构数据集。然后,将同构架构转化为异构架构。
1. 异构图
1.1 异构图基本概念
异构图 (heterogeneous graph
)是表示不同实体间关系的强大工具,拥有不同类型的节点和边会创建更复杂但也更难学习的图结构。同时,异构图的一个主要问题是,来自不同类型节点或边的特征不一定具有相同的意义或维度。
因此,合并不同的特征会破坏大量信息。而同构图 (homogeneous graph
) 则不同,在同构图中,每个节点或边的每个维度都具有完全相同的含义。
异构图是一种更通用的网络,可以表示不同类型的节点和边。从形式上看,异构图定义为由节点集
V
V
V 和边集
E
E
E 组成的图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E),在异构图中,包括节点类型映射函数
ϕ
:
V
→
A
ϕ :V→A
ϕ:V→A (其中
A
A
A 表示节点类型集),以及边类型映射函数
ψ
:
E
→
R
ψ:E→R
ψ:E→R (其中
R
R
R 表示边类型集)。下图是一个具有三种节点类型和三种边类型的异构图。
在上图中,我们可以看到三种类型的节点(用户、游戏和开发者)和三种类型的边(关注、游戏和开发)。它代表了一个涉及人员(用户和开发者)和游戏的网络,可用于游戏推荐等各种应用。如果这个图包含数百万个元素,它就可以用作图结构的知识数据库或知识图谱。知识图谱能够用来回答查询,比如“谁玩 Dev 1
开发的游戏?”。
类似的查询可以提取有用的同质图。例如,我们可能只想考虑玩 Game 1
的用户,输出结果为 User 1
和 User 2
。我们也可以创建更复杂的查询,例如“谁是玩 Dev 1
开发的游戏的用户?”结果是相同的,但遍历了两个关系来获得用户,这种查询称为元路径 (meta-path
)。
在第一个例子中,元路径是 User → Game → User
(通常表示为 UGU
),而在第二个例子中,我们的元路径是 User → Game → Dev → Game → User
(或表示为 UGDGU
)。需要注意的是,起点节点类型和终点节点类型是相同的。元路径是异构图中的一个基本概念,通常用于衡量不同节点的相似性。
1.2 构建异构图数据集
接下来,我们使用 PyTorch Geometric
(PyG
) 实现异构图,使用数据对象 HeteroData
创建一个数据对象来存储上示异构图。
(1) 从 torch_geometric.data
中导入 HeteroData
类,并创建变量 data
:
from torch_geometric.data import HeteroData
data = HeteroData()
(2) 首先,存储节点特征。例如,可以使用 data['user'].x
访问用户特征。我们使用一个维度为 [num_users, num_features_users]
的张量作为输入,其中 num_users
表示用户数量,num_features_users
表示用户特征数量。在本例中,内容并不重要,因此我们将创建一个用 1
表示 user 1
、用 2
表示 user 2
、用 3
表示 user 3
的特征向量:
data['user'].x = torch.Tensor([[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]) # [num_users, num_features_users]
(3) 我们对游戏特征和开发者特征重复以上过程。需要注意的是,特征向量的维度并不相同;这是异构图在处理不同表示时的一个重要优势:
data['game'].x = torch.Tensor([[1, 1], [2, 2]])
data['dev'].x = torch.Tensor([[1], [2]])
(4) 接下来,在节点之间建立联系。连接具有不同的含义,因此我们将创建三组边索引。我们可以使用三元组(源节点类型、边缘类型、目标节点类型)来声明每组边索引,例如 data['user','follows','user'].edge_index
。然后,将连接存储在一个维数为 [2, num_edge]
的张量中,其中 num_edge
表示边的数量:
data['user', 'follows', 'user'].edge_index = torch.Tensor([[0, 1], [1, 2]]) # [2, num_edges_follows]
data['user', 'plays', 'game'].edge_index = torch.Tensor([[0, 1, 1, 2], [0, 0, 1, 1]])
data['dev', 'develops', 'game'].edge_index = torch.Tensor([[0, 1], [0, 1]])
(5) 边也可以具有特征,例如,边 plays
可以包括用户玩相应游戏的小时数。我们假设 user 1
玩了 2
小时 game 1
,user 2
玩了半小时 game 1
和 10
小时 game 2
,user 3
玩了 12
小时 game 2
:
data['user', 'plays', 'game'].edge_attr = torch.Tensor([[2], [0.5], [10], [12]])
(6) 最后,打印 data
对象来验证结果:
print(data)
'''
HeteroData(
user={ x=[3, 4] },
game={ x=[2, 2] },
dev={ x=[2, 1] },
(user, follows, user)={ edge_index=[2, 2] },
(user, plays, game)={
edge_index=[2, 4],
edge_attr=[4, 1]
},
(dev, develops, game)={ edge_index=[2, 2] }
)
'''
从以上实现中可以看出,不同类型的节点和边并不共享相同的张量,甚至它们的维度也并不相同。因此,我们需要思考如何使用图神经网络 (Graph Neural Networks, GNN) 聚合来自多个张量的信息。
在同构图中,我们只关注单一类型的节点,权重矩阵的大小适合与预定义的维度相乘。然而,当具有不同维度的输入时,该如何实现 GNN
?
2. 将同构图神经网络转换为异构图神经网络
2.1 数据集介绍
为了更好地理解如何将同构图神经网络 (Graph Neural Networks
, GNN
) 转换为异构 GNN
,我们以一个真实的数据集为例。DBLP
计算机科学文献提供了一个包含四种节点类型的数据集,分别是论文(papers
,14328
篇)、术语(terms
,7723
个)、作者(authors
,4057
个)和会议(conferences
,20
个)。该数据集的目标是将作者正确地分为四类研究领域——数据库 (database
)、数据挖掘 (data mining
)、人工智能 (artificial intelligence
) 和信息检索 (information retrieval
)。作者的节点特征是他们在论文中可能使用的 334
个关键词组成的词袋( “0
” 或 “1
”),不同节点类型之间的关系如下所示。
这些节点类型的维度和语义关系并不相同。在异构图中,节点之间的关系至关重要,这也是需要考虑节点对的原因。例如,不需要向 GNN
层输入作者节点,而是考虑 (作者、论文)
这种节点对。这意味着我们现在需要为每个关系建立一个 GNN
层;在这种情况下,“to
” 关系是双向的,因此我们需要建立六个层。
这些新层具有独立的权重矩阵,适用于每种节点类型的正确维度。现在我们有了六个不共享任何信息的不同层,可以通过引入跳跃连接 (skip-connections
)、共享层 (shared layers
)、跳转知识 (jumping knowledge
) 等方法来解决信息共享问题。
在将同构模型转化为异构模型之前,我们先在 DBLP
数据集上实现经典的图注意力网络 (Graph Attention Networks,GAT) 模型。GAT
无法考虑不同的关系;我们必须给它一个唯一的邻接矩阵,将作者相互连接起来。可以通过使用元路径技术生成这种邻接矩阵,如作者-论文-作者,将同一篇论文的作者连接起来。
也可以通过随机游走构建一个良好的邻接矩阵。即使图是异构的,也可以进行探索,并连接经常出现在相同序列中的节点。
2.2 同构图注意力网络
接下来,使用 PyTorch Geometric
(PyG
) 在 DBLP
数据集上实现经典图注意力网络 (Graph Attention Networks,GAT) 架构。
(1) 导入所需的库:
from torch import nn
import torch.nn.functional as F
import torch_geometric.transforms as T
from torch_geometric.datasets import DBLP
from torch_geometric.nn import GAT
(2) 使用特定语法定义要使用的元路径:
metapaths = [[('author', 'paper'), ('paper', 'author')]]
(3) 使用 AddMetaPaths
转换函数自动计算元路径。使用 drop_orig_edge_types=True
从数据集中移除其他关系( GAT
只能考虑一种关系):
transform = T.AddMetaPaths(metapaths=metapaths, drop_orig_edge_types=True)
(4) 加载 DBLP
数据集并打印相关信息:
dataset = DBLP('.', transform=transform)
data = dataset[0]
print(data)
输出结果如下所示,可以看到转换函数创建的 (author, metapath_0, author)
关系:
(5) 创建一个单层 GAT
模型,其中 in_channels=-1
用于执行懒初始化(模型将自动计算值),out_channels=4
用于将作者节点分为四种类别:
model = GAT(in_channels=-1, hidden_channels=64, out_channels=4, num_layers=1)
(6) 实例化 Adam
优化器并尝试将模型和数据转移到GPU中:
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=0.001)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
data, model = data.to(device), model.to(device)
(7) 定义 test()
函数用于评估模型预测的准确性:
@torch.no_grad()
def test(mask):
model.eval()
pred = model(data.x_dict['author'], data.edge_index_dict[('author', 'metapath_0', 'author')]).argmax(dim=-1)
acc = (pred[mask] == data['author'].y[mask]).sum() / mask.sum()
return float(acc)
(8) 创建训练循环:
for epoch in range(101):
model.train()
optimizer.zero_grad()
out = model(data.x_dict['author'], data.edge_index_dict[('author', 'metapath_0', 'author')])
mask = data['author'].train_mask
loss = F.cross_entropy(out[mask], data['author'].y[mask])
loss.backward()
optimizer.step()
if epoch % 20 == 0:
train_acc = test(data['author'].train_mask)
val_acc = test(data['author'].val_mask)
print(f'Epoch: {epoch:>3} | Train Loss: {loss:.4f} | Train Acc: {train_acc*100:.2f}% | Val Acc: {val_acc*100:.2f}%')
(9) 在测试集上对训练后的模型进行了测试:
test_acc = test(data['author'].test_mask)
print(f'Test accuracy: {test_acc*100:.2f}%')
# Test accuracy: 73.29%
使用元路径将异构数据集缩减为同构数据集,并应用了经典 GAT
架构。模型的测试准确率为 73.29%
,这可以作为其他技术进行比较的基准。
2.3 异构图神经网络
接下来,构建图注意力网络 (Graph Attention Networks
,GAT
) 模型的异构版本。如前所示,我们需要六个(而不再是一个) GAT
层。在 PyTorch Geometric
可以使用 to_hetero()
或 to_hetero_bases()
函数自动完成。to_hetero()
函数需要三个重要参数:
module
: 要转换的同构模型metadata
: 有关图的异构性质的信息,用元组(node_types, edge_types)
表示,其中node_types
和edge_types
分别表示节点类型和边类型aggr
:聚合算子,用于聚合由不同关系(例如,求和、最大值或均值)生成的节点嵌入
同构 GAT
(左图)和使用 to_hetero()
得到的异构版本(右图)如下所示。
异构 GAT
的实现过程于同构 GAT
模型相似。
(1) 首先,从 PyTorch Geometric
中导入 GNN
层:
from torch_geometric.nn import GATConv, Linear, to_hetero
(2) 加载 DBLP
数据集:
dataset = DBLP(root='.')
data = dataset[0]
(3) 当我们打印这个数据集的信息时,注意到会议节点没有任何特征。这于我们的架构假设(每个节点类型都有自己的特征)相违背,可以通过生成零值作为特征来解决此问题:
data['conference'].x = torch.zeros(20, 1)
(4) 创建 GAT
类,其中包含 GAT
层和线性层,使用 (-1, -1)
元组再次进行懒初始化:
class GAT(torch.nn.Module):
def __init__(self, dim_h, dim_out):
super().__init__()
self.conv = GATConv((-1, -1), dim_h, add_self_loops=False)
self.linear = nn.Linear(dim_h, dim_out)
def forward(self, x, edge_index):
h = self.conv(x, edge_index).relu()
h = self.linear(h)
return h
(5) 实例化 GAT
模型,并使用 to_hetero()
进行转换:
model = GAT(dim_h=64, dim_out=4)
model = to_hetero(model, data.metadata(), aggr='sum')
print(model)
(5) 实例化 Adam
优化器,并尝试将模型和数据转移到 GPU
上:
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=0.001)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
data, model = data.to(device), model.to(device)
(6) 编写 test()
函数,不需要指定任何关系,因为模型会考虑所有关系:
@torch.no_grad()
def test(mask):
model.eval()
pred = model(data.x_dict, data.edge_index_dict)['author'].argmax(dim=-1)
acc = (pred[mask] == data['author'].y[mask]).sum() / mask.sum()
return float(acc)
(7) 实现训练循环:
for epoch in range(101):
model.train()
optimizer.zero_grad()
out = model(data.x_dict, data.edge_index_dict)['author']
mask = data['author'].train_mask
loss = F.cross_entropy(out[mask], data['author'].y[mask])
loss.backward()
optimizer.step()
if epoch % 20 == 0:
train_acc = test(data['author'].train_mask)
val_acc = test(data['author'].val_mask)
print(f'Epoch: {epoch:>3} | Train Loss: {loss:.4f} | Train Acc: {train_acc*100:.2f}% | Val Acc: {val_acc*100:.2f}%')
'''
...
Epoch: 60 | Train Loss: 0.5049 | Train Acc: 98.00% | Val Acc: 73.25%
Epoch: 80 | Train Loss: 0.2687 | Train Acc: 99.25% | Val Acc: 76.75%
Epoch: 100 | Train Loss: 0.1574 | Train Acc: 100.00% | Val Acc: 76.50%
'''
(8) 训练后模型在测试数据集上的测试准确率如下:
test_acc = test(data['author'].test_mask)
print(f'Test accuracy: {test_acc*100:.2f}%')
# Test accuracy: 78.39%
异构 GAT
的测试准确率为 78.39%
,比之同构版本有了较大提高 (+5.10%
)。
小结
在本节中,我们扩展了消息传递神经网络 (Message Passing Neural Network, MPNN) 框架,以考虑由不同类型的节点和边组成的异构图。这种特殊的图可以表示实体之间的各种关系,这比单一类型的连接具有更高的表达能力。此外,我们还介绍了如何利用 PyTorch Geometric
将同构图神经网络 (Graph Neural Networks, GNN) 转换为异构 GNN
,描述了异构图注意力网络 (Graph Attention Networks,GAT) 中的不同层,将节点对作为输入来模拟它们之间的关系。
系列链接
图神经网络实战(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)——经典链接预测算法
图神经网络实战(14)——基于节点嵌入预测链接
图神经网络实战(15)——SEAL链接预测算法
图神经网络实战(16)——经典图生成算法
图神经网络实战(17)——深度图生成模型
图神经网络实战(18)——消息传播神经网络