图神经网络实战(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
:
python
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
的特征向量:
python
data['user'].x = torch.Tensor([[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]) # [num_users, num_features_users]
(3) 我们对游戏特征和开发者特征重复以上过程。需要注意的是,特征向量的维度并不相同;这是异构图在处理不同表示时的一个重要优势:
python
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
表示边的数量:
python
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
:
python
data['user', 'plays', 'game'].edge_attr = torch.Tensor([[2], [0.5], [10], [12]])
(6) 最后,打印 data
对象来验证结果:
python
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) 导入所需的库:
python
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) 使用特定语法定义要使用的元路径:
python
metapaths = [[('author', 'paper'), ('paper', 'author')]]
(3) 使用 AddMetaPaths
转换函数自动计算元路径。使用 drop_orig_edge_types=True
从数据集中移除其他关系( GAT
只能考虑一种关系):
python
transform = T.AddMetaPaths(metapaths=metapaths, drop_orig_edge_types=True)
(4) 加载 DBLP
数据集并打印相关信息:
python
dataset = DBLP('.', transform=transform)
data = dataset[0]
print(data)
输出结果如下所示,可以看到转换函数创建的 (author, metapath_0, author)
关系:
(5) 创建一个单层 GAT
模型,其中 in_channels=-1
用于执行懒初始化(模型将自动计算值),out_channels=4
用于将作者节点分为四种类别:
python
model = GAT(in_channels=-1, hidden_channels=64, out_channels=4, num_layers=1)
(6) 实例化 Adam
优化器并尝试将模型和数据转移到GPU中:
python
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()
函数用于评估模型预测的准确性:
python
@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) 创建训练循环:
python
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) 在测试集上对训练后的模型进行了测试:
python
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
层:
python
from torch_geometric.nn import GATConv, Linear, to_hetero
(2) 加载 DBLP
数据集:
python
dataset = DBLP(root='.')
data = dataset[0]
(3) 当我们打印这个数据集的信息时,注意到会议节点没有任何特征。这于我们的架构假设(每个节点类型都有自己的特征)相违背,可以通过生成零值作为特征来解决此问题:
python
data['conference'].x = torch.zeros(20, 1)
(4) 创建 GAT
类,其中包含 GAT
层和线性层,使用 (-1, -1)
元组再次进行懒初始化:
python
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()
进行转换:
python
model = GAT(dim_h=64, dim_out=4)
model = to_hetero(model, data.metadata(), aggr='sum')
print(model)
(5) 实例化 Adam
优化器,并尝试将模型和数据转移到 GPU
上:
python
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()
函数,不需要指定任何关系,因为模型会考虑所有关系:
python
@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) 实现训练循环:
python
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) 训练后模型在测试数据集上的测试准确率如下:
python
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)------消息传播神经网络