文章目录
前言
现实世界中,大量的数据集是以异构图的形式存在,因为它们存储了有关不同类型实体以及不同类型关系的信息。异构图带有附加到节点和边的不同类型的信息。因此,由于类型和维度的差异,单个节点或边特征张量不能包含整个图的所有节点或边特征,因此我们需要针对异构图构建专门的网络架构。
一、传递消息的GNN架构
在探索异构图之前,让我们回顾一下我们对GNN的了解。在前面的章节中,我们看到了聚合和组合来自不同节点的特征的不同函数。在VNN中最简单的GNN层是将邻近节点(包括目标节点本身)的特征线性组合与权重矩阵相加。然后,前一个和的输出替换之前的目标节点嵌入。嵌入过程表示如下:
h
i
‘
=
∑
j
∈
N
i
h
j
W
T
h_{i}^{‘}=\sum_{j\in N_i}{h_jW^T}
hi‘=j∈Ni∑hjWT
其中,
N
N
N是邻近节点的集合,
h
h
h是第
i
i
i个节点的嵌入,
W
W
W是加权矩阵。
GCN和GAT层为节点特征添加了固定和动态权重,但保持了相同的思想。甚至GraphSAGE的LSTM算子或GIN的max聚合器也没有改变GNN层的主要概念。如果我们查看所有这些变体,我们可以将GNN层推广到一个称为消息传递神经网络(MPNN或MP-GNN)的公共框架中。
- 信息:每个节点使用一个函数为每个邻居创建一条消息,它可以由自己的特征组成,也可以考虑相邻节点的特征和边缘特征。
- 聚合:每个节点使用置换函数聚合来自邻接节点的信息。
- 更新:每个节点使用一个函数来更新其功能,将其当前功能与聚合的消息结合起来。
上述步骤可以总结为:
h
i
′
=
γ
(
h
i
,
⨁
j
∈
N
i
ϕ
(
h
i
,
h
j
,
e
j
,
i
)
)
h_{i}^{\prime}=\gamma\left(h_{i}, \bigoplus_{j \in \mathcal{N}_{i}} \phi\left(h_{i}, h_{j}, e_{j, i}\right)\right)
hi′=γ
hi,j∈Ni⨁ϕ(hi,hj,ej,i)
式中,
ϕ
\phi
ϕ为信息函数,
⨁
\bigoplus
⨁是聚合函数,
γ
\gamma
γ是更新函数。
二、异构图
异构图是表示不同实体之间一般关系的强大工具。拥有不同类型的节点和边会创建更复杂但也更难学习的图结构。特别是,异构网络的主要问题之一是来自不同类型的节点或边缘的特征不一定具有相同的含义或维度。
1、同质网络转换为异质网络的应用
1.1 数据集说明
为了更好地理解这个问题,让我们以一个真实的数据集为例。DBLP计算机科学参考书目提供了一个数据集,其中包含四种类型的节点-论文(14,328),术语(7,723),作者(4,057)和会议(20)。该数据集的目标是将作者正确地分为四类——数据库、数据挖掘、人工智能和信息检索。作者的节点特征是一个由334个关键词组成的词袋(“0”或“1”),这些关键词可能在他们的出版物中使用过。不同节点类型之间的关系如下图所示。
这些节点类型不具有相同的维数和语义关系。在异构图中,节点之间的关系是必不可少的,这就是为什么我们要考虑节点对。在上图中,作者和论文是一对节点对,这就意味着我们需要两个同质网络结构来表述两者之间的对应关系。想要完成上图的异质网络,我们总共需要六个同质网络结构。
这些新层具有独立的权重矩阵,其大小适合每种节点类型。实际上,我们现在有六个不同的层,它们不共享任何信息。我们可以通过引入跳跃连接、共享层、跳跃知识等来解决这个问题。
1.2 导入数据库
import torch
import torch.nn.functional as F
import torch_geometric.transforms as T
import os
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
from torch import nn
from torch_geometric.datasets import DBLP
from torch_geometric.nn import GAT
from torch_geometric.data import HeteroData
from torch_geometric.nn import GATConv, Linear, to_hetero
torch.manual_seed(42)
torch.cuda.manual_seed(42)
torch.cuda.manual_seed_all(42)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
1.3 导入数据集
# Input Dataset
path = os.path.abspath(os.path.dirname(os.getcwd())) + '\MyCode\data\DBLP'
dataset = DBLP(path)
data = dataset[0]
data['conference'].x = torch.zeros(20, 1)
d a t a data data变量的输出如下。
Out[5]:
HeteroData(
author={
x=[4057, 334],
y=[4057],
train_mask=[4057],
val_mask=[4057],
test_mask=[4057]
},
paper={ x=[14328, 4231] },
term={ x=[7723, 50] },
conference={
num_nodes=20,
x=[20, 1]
},
(author, to, paper)={ edge_index=[2, 19645] },
(paper, to, author)={ edge_index=[2, 19645] },
(paper, to, term)={ edge_index=[2, 85810] },
(paper, to, conference)={ edge_index=[2, 14328] },
(term, to, paper)={ edge_index=[2, 85810] },
(conference, to, paper)={ edge_index=[2, 14328] }
)
通过上述输出内容我们可以发现,只有作者( a u t h o r author author)存在 y y y变量,而我们的主要目的也是预测该 y y y变量。同时针对不同的节点,有不同大小的特征向量。另外,节点与节点之间存在不同的连接关系( e d g e _ i n d e x edge\_index edge_index)。由于会议( c o n f e r e n c e conference conference)没有特征向量,我们将其幅值为0,这样在模型运行的过程中它将不起决定作用。
1.4 GAT类
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
model = GAT(dim_h=64, dim_out=4)
model = to_hetero(model, data.metadata(), aggr='sum')
print(model)
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)
接下来,我们将对上述类仔细分析,分析其内部的运行过程,以及它是如何实现同质和异质的。
首先,对于上述同质结构,我们建立了隐藏层层数 d i m _ h = 64 dim\_h=64 dim_h=64,输出层层数 d i m _ o u t = 4 dim\_out=4 dim_out=4分别对应于四种不同类型的概率。 t o _ h e t e r o to\_hetero to_hetero表示通过同质结构构建异质结构, d a t a . m e t a d a t a ( ) data.metadata() data.metadata()表示节点之间的对应关系, a g g r aggr aggr表示将异质结构的结果等于同质结构的输出结果求和。
那么它是如何具体实现的呢?
想要解决这个问题,我们首先得知道模型是如何实现同构的。在实现同构的过程中,我们建立了
G
A
T
C
o
n
v
GATConv
GATConv函数,该函数考虑节点的自注意力过程,详细可看GATs。GATs可以表述为:
h
i
=
∑
j
∈
N
i
α
i
j
W
x
j
α
i
j
=
exp
(
W
a
t
t
T
L
e
a
k
y
R
e
L
U
(
[
W
x
i
∥
W
x
j
]
)
)
∑
k
∈
N
i
exp
(
W
a
t
t
T
L
e
a
k
y
R
e
L
U
(
[
W
x
i
∥
W
x
k
]
)
)
h_i=\sum_{j\in N_i}{\alpha _{ij}Wx_j} \\ \alpha _{ij}=\frac{\exp \left( W_{att}^{T}Leaky\mathrm{Re}LU\left( \left[ \left. Wx_i \right\| Wx_j \right] \right) \right)}{\sum_{k\in N_i}{\exp \left( W_{att}^{T}Leaky\mathrm{Re}LU\left( \left[ \left. Wx_i \right\| Wx_k \right] \right) \right)}}
hi=j∈Ni∑αijWxjαij=∑k∈Niexp(WattTLeakyReLU([Wxi∥Wxk]))exp(WattTLeakyReLU([Wxi∥Wxj]))
也就是说,GATs先将不同大小的节点特征转换到相同层数的隐藏层中,在构建注意力系数
α
\alpha
α。在
G
A
T
C
o
n
v
GATConv
GATConv代码函数中,我们可以发现。
x_src, x_dst = x
assert x_src.dim() == 2, "Static graphs not supported in 'GATConv'"
x_src = self.lin_src(x_src).view(-1, H, C)
if x_dst is not None:
x_dst = self.lin_dst(x_dst).view(-1, H, C)
节点特征向量首先通过线性变换嵌入到隐藏层。
x
_
s
r
c
x\_src
x_src大小为(起始节点数目,多层注意力数,隐藏层数)。
x
_
d
s
t
x\_dst
x_dst大小为(终端节点数目,多层注意力数,隐藏层数)。
alpha_src = (x_src * self.att_src).sum(dim=-1)
alpha_dst = None if x_dst is None else (x_dst * self.att_dst).sum(-1)
alpha = (alpha_src, alpha_dst)
在经过学习矩阵
W
a
t
t
W_{att}
Watt和求和后转化为初始的
α
\alpha
α。
a
l
p
h
a
_
s
r
c
alpha\_src
alpha_src大小为(起始节点数目,1)。
a
l
p
h
a
_
d
s
t
alpha\_dst
alpha_dst大小为(终端节点数目,1)。
alpha = self.edge_updater(edge_index, alpha=alpha, edge_attr=edge_attr)
在 e d g e _ u p d a t e r edge\_updater edge_updater函数中,主要做下面的事情:
- 构建 a l p h a _ i alpha\_i alpha_i和 a l p h a _ j alpha\_j alpha_j向量,该向量主要是 a l p h a _ s r c alpha\_src alpha_src和 a l p h a _ d s t alpha\_dst alpha_dst在 e d g e _ i n d e x edge\_index edge_index上的映射。 a l p h a _ j alpha\_j alpha_j为 a l p h a _ d s t alpha\_dst alpha_dst在索引为 e d g e _ i n d e x [ 1 ] edge\_index[1] edge_index[1]上的值, a l p h a _ i alpha\_i alpha_i为 a l p h a _ s r c alpha\_src alpha_src在索引为 e d g e _ i n d e x [ 0 ] edge\_index[0] edge_index[0]上的值。
- 将 a l p h a _ i alpha\_i alpha_i和 a l p h a _ j alpha\_j alpha_j相加,经过激活函数( L e a k y R e L U LeakyReLU LeakyReLU)和 S o f t m a x Softmax Softmax函数后转换为注意力系数 α \alpha α。
a
l
p
h
a
alpha
alpha大小为(边数目,1)。
该注意力系数将会被下面的代码应用。
out = self.propagate(edge_index, x=x, alpha=alpha, size=size)
通过
p
r
o
p
a
g
a
t
e
propagate
propagate函数,将目标节点的邻接节点聚合。也就完成了隐藏层嵌入部分(即自注意力过程)。
之后,隐藏层的嵌入会通过
R
e
L
U
ReLU
ReLU函数并再次经过线性层后输出四个类型的预测概率,最终完成一个同质结构。
同质结构完成后,我们根据不同节点的连接关系建立多个同质结构,在此基础上构建异质结构。
在前文的
t
o
_
h
e
t
e
r
o
to\_hetero
to_hetero函数中我们将超参数
a
g
g
r
=
s
u
m
aggr=sum
aggr=sum,即异质结构的输出是同质结构输出之和。
例如,根据图1我们可以知道
P
a
p
e
r
Paper
Paper同另外三个节点都存在同质关系,那么经过三个以
P
a
p
e
r
Paper
Paper为输出的同质结构后,会输出三个形状为(4,1)的预测概率,将这三个概率求和后得到输出,这就是异质结构。
1.5 训练模型及可视化结果
@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)
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}%')
test_acc = test(data['author'].test_mask)
print(f'Test accuracy: {test_acc*100:.2f}%')
def visualize(h, color):
h = torch.Tensor.cpu(h)
color = torch.Tensor.cpu(color)
z = TSNE(n_components=2).fit_transform(h.detach().cpu().numpy())
plt.figure(figsize=(10,10))
plt.xticks([])
plt.yticks([])
plt.scatter(z[:, 0], z[:, 1], s=70, c=color, cmap="Set2")
plt.show()
return z
Dimensionality_distruption = visualize(out, color=data['author'].y)
模型的训练结果如下。
Epoch: 0 | Train Loss: 1.3839 | Train Acc: 32.75% | Val Acc: 26.00%
Epoch: 20 | Train Loss: 1.1783 | Train Acc: 89.50% | Val Acc: 62.75%
Epoch: 40 | Train Loss: 0.8273 | Train Acc: 96.50% | Val Acc: 69.00%
Epoch: 60 | Train Loss: 0.4740 | Train Acc: 98.75% | Val Acc: 73.25%
Epoch: 80 | Train Loss: 0.2424 | Train Acc: 99.75% | Val Acc: 76.25%
Epoch: 100 | Train Loss: 0.1371 | Train Acc: 100.00% | Val Acc: 76.25%
Test accuracy: 77.59%
将预测的结果与真实结果相比较,利用 T S N E TSNE TSNE算法进行降维处理,得到的结果如下图所示。
在上图中横纵坐标为降维后的二维数据,颜色为不同的标签。可以发现不同的标签在分布上占据不同的位置,但是在分布的边界处颜色的混合较多,说明模型在区分这部分数据时准确率较低。
2、实现分层自关注网络
在本节中,我们将实现一个用于处理异构图的GNN模型——层次自关注网络(HAN)。HAN在两个不同的层面上使用自我关注:
- 节点关注:以了解给定元路径中相邻节点的重要性(例如同构设置中的GAT)
- 语义层面上的关注:这是HAN的主要功能,允许我们自动为给定任务选择最佳元路径——例如,在某些任务中,游戏-用户-游戏的元路径可能比游戏-开发-游戏的元路径在预测玩家数量上更关键。
该体系结构图如下。
2.1 HAN原理
与GAT一样,第一步包括将节点投影到每个元路径的统一特征空间中。然后,我们用第二个权重矩阵计算同一元路径中节点对(两个投影节点的连接)的权重。对这个结果应用一个非线性函数,然后用
s
o
f
t
m
a
x
softmax
softmax函数将其归一化。
j
j
j节点对
i
i
i节点的归一化注意分数(重要性)计算如下:
α
i
j
Φ
=
exp
(
σ
(
a
Φ
T
[
W
Φ
h
i
∥
W
Φ
h
j
]
)
)
∑
k
∈
N
i
Φ
exp
(
σ
(
a
Φ
T
[
W
Φ
h
i
∥
W
Φ
h
k
]
)
)
\alpha_{i j}^{\Phi}=\frac{\exp \left(\sigma\left(\boldsymbol{a}_{\Phi}^{\boldsymbol{T}}\left[\boldsymbol{W}_{\Phi} \mathbf{h}_{i} \| \boldsymbol{W}_{\Phi} \boldsymbol{h}_{j}\right]\right)\right)}{\sum_{k \in \mathcal{N}_{i}^{\Phi}} \exp \left(\sigma\left(\boldsymbol{a}_{\Phi}^{T}\left[\boldsymbol{W}_{\Phi} \mathbf{h}_{i} \| \boldsymbol{W}_{\Phi} \boldsymbol{h}_{k}\right]\right)\right)}
αijΦ=∑k∈NiΦexp(σ(aΦT[WΦhi∥WΦhk]))exp(σ(aΦT[WΦhi∥WΦhj]))
多层注意力得到最终的嵌入向量为:
z
i
=
∥
k
=
1
K
σ
(
∑
j
∈
N
i
α
i
j
Φ
⋅
W
Φ
h
j
)
z_i=\parallel _{k=1}^{K}\sigma \left( \sum_{j\in N_i}{\alpha _{ij}^{\varPhi}}\cdot W_{\varPhi}h_j \right)
zi=∥k=1Kσ
j∈Ni∑αijΦ⋅WΦhj
具体请查看GATs。
对于语义级注意力,我们对每个元路径的注意力得分重复类似的过程(表示为
β
Φ
1
\beta _{\varPhi _1}
βΦ1,
β
Φ
2
\beta _{\varPhi _2}
βΦ2,…,
β
Φ
p
\beta _{\varPhi _p}
βΦp)。嵌入在给定元路径(表示为
Z
Φ
p
\Z _{\varPhi _p}
ZΦp)中的每个节点被馈送到应用非线性转换的MLP。我们将这个结果与一个新的注意力向量
q
q
q作为相似性度量进行比较。我们平均这个结果来计算给定元路径的重要性:
W
Φ
p
=
1
∣
V
∣
∑
i
∈
V
q
T
⋅
tanh
(
W
⋅
z
i
Φ
p
+
b
)
W_{\varPhi _p}=\frac{1}{\left| V \right|}\sum_{i\in V}{q^T}\cdot \tanh \left( W\cdot z_{i}^{\varPhi _p}+b \right)
WΦp=∣V∣1i∈V∑qT⋅tanh(W⋅ziΦp+b)
式中,
W
W
W是
M
L
P
MLP
MLP权重矩阵,
b
b
b是偏置,
q
q
q是语义级关注向量,该向量在元路径中共享。
我们必须对这一结果进行规范化,以比较不同语义层面的注意力得分。我们使用
s
o
f
t
m
a
x
softmax
softmax函数来获得最终的权重:
β
Φ
p
=
exp
(
w
Φ
p
)
∑
k
=
1
P
exp
(
w
Φ
p
)
\beta _{\varPhi _p}=\frac{\exp \left( w_{\varPhi _p} \right)}{\sum\nolimits_{k=1}^P{\exp \left( w_{\varPhi _p} \right)}}
βΦp=∑k=1Pexp(wΦp)exp(wΦp)
最终得到节点级关注和语义级关注相结合的嵌入
Z
Z
Z:
Z
=
∑
p
=
1
P
β
Φ
p
⋅
X
Φ
p
Z=\sum_{p=1}^P{\beta _{\varPhi _p}}\cdot X_{\varPhi _p}
Z=p=1∑PβΦp⋅XΦp
2.2 HAN应用
import torch
import torch.nn.functional as F
import torch_geometric.transforms as T
import os
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
from torch import nn
from torch_geometric.datasets import DBLP
from torch_geometric.nn import HANConv, Linear
path = os.path.abspath(os.path.dirname(os.getcwd())) + '\MyCode\data\DBLP'
dataset = DBLP(path)
data = dataset[0]
print(data)
data['conference'].x = torch.zeros(20, 1)
class HAN(nn.Module):
def __init__(self, dim_in, dim_out, dim_h=128, heads=8):
super().__init__()
self.han = HANConv(dim_in, dim_h,
heads=heads,
dropout=0.6,
metadata=data.metadata())
self.linear = nn.Linear(dim_h, dim_out)
def forward(self, x_dict, edge_index_dict):
out = self.han(x_dict, edge_index_dict)
# choose the node type 'author' to predict
out = self.linear(out['author'])
return out
model = HAN(dim_in=-1, dim_out=4)
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)
HAN同第一种GAT架构的唯一区别在于 H A N C o n v HANConv HANConv函数。
计算嵌入向量 α \alpha α,以及通过聚合函数得到最终嵌入向量 z z z的过程,此处不在详细说明。详写一些HAN同GAT架构不同的部分。
for node_type, outs in out_dict.items():
# group: semantic-level attention
out, attn = group(outs, self.q, self.k_lin)
out_dict[node_type] = out
semantic_attn_dict[node_type] = attn
对于不同节点的不同连接会产生不同的嵌入向量 z z z,该向量保存在 o u t _ d i c t out\_dict out_dict中,通过函数 g r o u p group group实现HAN过程。
num_edge_types = len(xs)
out = torch.stack(xs)
if out.numel() == 0:
return out.view(0, out.size(-1)), None
# one reuslt for each edge type
attn_score = (q * torch.tanh(k_lin(out)).mean(1)).sum(-1)
attn = F.softmax(attn_score, dim=0)
out = torch.sum(attn.view(num_edge_types, 1, -1) * out, dim=0)
return out, attn
在 g r o u p group group中可以明显的观察到语义级关注的实现过程。
2.3 训练模型及可视化结果
@torch.no_grad()
def test(mask):
model.eval()
pred = model(data.x_dict, data.edge_index_dict).argmax(dim=-1)
acc = (pred[mask] == data['author'].y[mask]).sum() / mask.sum()
return float(acc)
for epoch in range(101):
model.train()
optimizer.zero_grad()
out = model(data.x_dict, data.edge_index_dict)
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}%')
test_acc = test(data['author'].test_mask)
print(f'Test accuracy: {test_acc*100:.2f}%')
模型的输出如下。
Epoch: 0 | Train Loss: 1.3842 | Train Acc: 24.50% | Val Acc: 26.50%
Epoch: 20 | Train Loss: 1.1397 | Train Acc: 89.75% | Val Acc: 65.50%
Epoch: 40 | Train Loss: 0.7855 | Train Acc: 95.25% | Val Acc: 68.75%
Epoch: 60 | Train Loss: 0.4735 | Train Acc: 98.00% | Val Acc: 74.25%
Epoch: 80 | Train Loss: 0.2920 | Train Acc: 99.50% | Val Acc: 79.25%
Epoch: 100 | Train Loss: 0.2319 | Train Acc: 100.00% | Val Acc: 78.50%
Test accuracy: 81.36%
HAN的检测准确率为81.36%,高于异质GAT(77.59%)。它显示了聚合不同类型节点关系的重要性。
比较图4可以发现,HAN在标签边界的预测上更加准确,预测的准确率更高。
总结
在本文中,我们通过三个步骤来推广GNN层——消息、聚合和更新。在本章的其余部分,我们扩展了这个框架来考虑由不同类型的节点和边组成的异构网络。这种特殊的图形允许我们表示实体之间的各种关系,这比单一类型的连接更有洞察力。此外,我们还看到了如何将同构GNN转换为异构GNN,这要归功于PyTorch Geometric。我们描述了异构GAT中的不同层,这些层将节点对作为输入来建模它们之间的关系。最后,我们使用HAN实现了一个异构特定架构,并比较了两种技术在DBLP数据集上的结果。这证明了开发这种网络中所代表的异构信息的重要性。