引言
图表征学习要求在输入节点及边的属性来得到一个向量作为图的表征,基于图表征进一步的我们可以做图的预测。
图同构网络论文:How Powerful are Graph Neural Networks?
基于图同构网络(GIN)的图表征网络的实现
基于图同构网络的图表征学习主要包含以下两个过程:
- 首先计算得到节点表征;
- 其次对图上各个节点的表征做图池化(Graph Pooling),或称为图读出(Graph Readout),得到图的表征(Graph Representation)。
在这里,我们将采用自顶向下的方式,来学习基于图同构模型(GIN)的图表征学习方法。我们首先关注如何基于节点表征计算得到图的表征,而忽略计算结点表征的方法。
基于图同构网络的图表征模块(GINGraphRepr Module)
图表征模块:
- 对图上每一个节点进行节点嵌入,得到节点表征。
- 对节点表征做图池化,得到图表征。
- 用一层线性变换对图表征转换为对图的预测。
import torch
from torch import nn
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool, GlobalAttention, Set2Set
from gin_regression.gin_node import GINNodeEmbedding
#%%
class GINGraphPooling(nn.Module):
def __init__(self, num_tasks=1, num_layers=5, emb_dim=300, residual=False, drop_ratio=0, JK="last", graph_pooling="sum"):
"""此模块首先采用GINNodeEmbedding模块对图上每一个节点做嵌入,然后对节点嵌入做池化得到图的嵌入,最后用一层线性变换得到图的最终的表示(graph representation)
Args:
num_tasks (int, optional): number of labels to be predicted. Defaults to 1 (控制了图表征的维度,dimension of graph representation).
num_layers (int, optional): number of GINConv layers. Defaults to 5.
emb_dim (int, optional): dimension of node embedding. Defaults to 300.
residual (bool, optional): adding residual connection or not. Defaults to False.
drop_ratio (float, optional): dropout rate. Defaults to 0.
JK (str, optional): 可选的值为"last"和"sum"。选"last",只取最后一层的结点的嵌入,选"sum"对各层的结点的嵌入求和。Defaults to "last".
graph_pooling (str, optional): pooling method of node embedding. 可选的值为"sum","mean","max","attention"和"set2set"。 Defaults to "sum".
Out:
graph representation
"""
super(GINGraphPooling, self).__init__()
self.num_layers = num_layers
self.drop_ratio = drop_ratio
self.JK = JK
self.emb_dim = emb_dim
self.num_tasks = num_tasks
if self.num_layers < 2:
raise ValueError("Number of GNN layers must be greater than 1.")
# 对图上的每个节点进行节点嵌入
self.gnn_node = GINNodeEmbedding(num_layers, emb_dim, JK=JK, drop_ratio=drop_ratio, residual=residual)
# Pooling function to generate whole-graph embeddings
if graph_pooling == "sum":
self.pool = global_add_pool
elif graph_pooling == "mean":
self.pool = global_mean_pool
elif graph_pooling == "max":
self.pool = global_max_pool
elif graph_pooling == "attention":
self.pool = GlobalAttention(gate_nn=nn.Sequential(
nn.Linear(emb_dim, emb_dim), nn.BatchNorm1d(emb_dim), nn.ReLU(), nn.Linear(emb_dim, 1)))
elif graph_pooling == "set2set":
self.pool = Set2Set(emb_dim, processing_steps=2)
else:
raise ValueError("Invalid graph pooling type.")
if graph_pooling == "set2set":
self.graph_pred_linear = nn.Linear(2*self.emb_dim, self.num_tasks)
else:
self.graph_pred_linear = nn.Linear(self.emb_dim, self.num_tasks)
def forward(self, batched_data):
h_node = self.gnn_node(batched_data)
h_graph = self.pool(h_node, batched_data.batch)
output = self.graph_pred_linear(h_graph)
if self.training:
return output
else:
# At inference time, relu is applied to output to ensure positivity
# 因为预测目标的取值范围就在 (0, 50] 内
return torch.clamp(output, min=0, max=50)
可以看到可选的基于结点表征计算得到图表征的方法有:
- “sum”:对节点表征求和;
- “mean”: - 对节点表征求平均;
- “max”:取节点表征的最大值,对一个batch中所有节点计算节点表征各个维度的最大值;
- “attention”: 基于Attention对节点表征加权求和;
- “set2set”:
- 另一种基于Attention对节点表征加权求和的方法;
- 使用模块 torch_geometric.nn.glob.Set2Set;
- 来自论文 “Order Matters: Sequence to sequence for sets”。
接下来我们将学习节点嵌入的方法。
基于图同构网络的节点嵌入模块(GINNodeEmbedding Module)
节点嵌入模块(GINNodeEmbeddingModule):
- 用AtomEcoder进行嵌入,得到第0层节点表征
- 逐层计算节点表征
- 感受野越大,节点i的表征最后能捕获到节点i的距离为num_layers的邻接节点的信息
import torch
from gin_regression.mol_encoder import AtomEncoder
from gin_regression.gin_conv import GINConv
import torch.nn.functional as F
# GNN to generate node embedding
class GINNodeEmbedding(torch.nn.Module):
"""
Output:
node representations
"""
def __init__(self, num_layers, emb_dim, drop_ratio=0.5, JK="last", residual=False):
"""GIN Node Embedding Module"""
super(GINNodeEmbedding, self).__init__()
self.num_layers = num_layers
self.drop_ratio = drop_ratio
self.JK = JK
# add residual connection or not
self.residual = residual
if self.num_layers < 2:
raise ValueError("Number of GNN layers must be greater than 1.")
self.atom_encoder = AtomEncoder(emb_dim)
# List of GNNs
self.convs = torch.nn.ModuleList()
self.batch_norms = torch.nn.ModuleList()
for layer in range(num_layers):
self.convs.append(GINConv(emb_dim))
self.batch_norms.append(torch.nn.BatchNorm1d(emb_dim))
def forward(self, batched_data):
x, edge_index, edge_attr = batched_data.x, batched_data.edge_index, batched_data.edge_attr
# computing input node embedding
h_list = [self.atom_encoder(x)] # 先将类别型原子属性转化为原子表征
for layer in range(self.num_layers):
h = self.convs[layer](h_list[layer], edge_index, edge_attr)
h = self.batch_norms[layer](h)
if layer == self.num_layers - 1:
# remove relu for the last layer
h = F.dropout(h, self.drop_ratio, training=self.training)
else:
h = F.dropout(F.relu(h), self.drop_ratio, training=self.training)
if self.residual:
h += h_list[layer]
h_list.append(h)
# Different implementations of Jk-concat
if self.JK == "last":
node_representation = h_list[-1]
elif self.JK == "sum":
node_representation = 0
for layer in range(self.num_layers + 1):
node_representation += h_list[layer]
return node_representation
接下来我们来学习图同构网络的关键组件GINConv
。
GINConv
–图同构卷积层
图同构卷积层的数学定义如下:
x i ′ = h Θ ( ( 1 + ϵ ) ⋅ x i + ∑ j ∈ N ( i ) x j ) \mathbf{x}^{\prime}_i = h_{\mathbf{\Theta}} \left( (1 + \epsilon) \cdot \mathbf{x}_i + \sum_{j \in \mathcal{N}(i)} \mathbf{x}_j \right) xi′=hΘ⎝⎛(1+ϵ)⋅xi+j∈N(i)∑xj⎠⎞
PyG中已经实现了此模块,我们可以通过torch_geometric.nn.GINConv
来使用PyG定义好的图同构卷积层,然而该实现不支持存在边属性的图。在这里我们自己自定义一个支持边属性的GINConv
模块。
由于输入的边属性为类别型,因此我们需要先将类别型边属性转换为边表征。我们定义的GINConv
模块遵循“消息传递、消息聚合、消息更新”这一过程。
- 这一过程随着
self.propagate
的调用开始执行,该函数接收edge_index
,x
,edge_attr
此三个函数。edge_index
是形状为2,num_edges
的张量(tensor)。 - 在消息传递过程中,此张量首先被按行拆分为
x_i
和x_j
张量,x_j
表示了消息传递的源节点,x_i
表示了消息传递的目标节点。 - 接着
message
函数被调用,此函数定义了从源节点传入到目标节点的消息,在这里要传递的消息是源节点表征与边表征之和的relu
。我们在super(GINConv, self).__init__(aggr = "add")
中定义了消息聚合方式为add
,那么传入给任一个目标节点的所有消息被求和得到aggr_out
,它是目标节点的中间过程的信息。 - 接着执行消息更新过程,我们的类
GINConv
继承了MessagePassing
类,因此update
函数被调用。然而我们希望对节点做消息更新中加入目标节点自身的消息,因此在update
函数中我们只简单返回输入的aggr_out
。 - 然后在
forward
函数中我们执行out = self.mlp((1 + self.eps) *x + self.propagate(edge_index, x=x, edge_attr=edge_embedding))
实现消息的更新。
import torch
from torch import nn
from torch_geometric.nn import MessagePassing
import torch.nn.functional as F
from ogb.graphproppred.mol_encoder import BondEncoder
### GIN convolution along the graph structure
class GINConv(MessagePassing):
def __init__(self, emb_dim):
'''
emb_dim (int): node embedding dimensionality
'''
super(GINConv, self).__init__(aggr = "add")
self.mlp = nn.Sequential(nn.Linear(emb_dim, emb_dim), nn.BatchNorm1d(emb_dim), nn.ReLU(), nn.Linear(emb_dim, emb_dim))
self.eps = nn.Parameter(torch.Tensor([0]))
self.bond_encoder = BondEncoder(emb_dim = emb_dim)
def forward(self, x, edge_index, edge_attr):
edge_embedding = self.bond_encoder(edge_attr) # 先将类别型边属性转换为边表征
out = self.mlp((1 + self.eps) *x + self.propagate(edge_index, x=x, edge_attr=edge_embedding))
return out
def message(self, x_j, edge_attr):
return F.relu(x_j + edge_attr)
def update(self, aggr_out):
return aggr_out
Weisfeiler-Lehman Test (WL Test)
图同构性测试
- 迭代聚合节点及其邻接节点的标签
- 将聚合标签散列成新的标签,数学公式:
L u h ← hash ( L u h − 1 + ∑ v ∈ N ( U ) L v h − 1 ) L^{h}_{u} \leftarrow \operatorname{hash}\left(L^{h-1}_{u} + \sum_{v \in \mathcal{N}(U)} L^{h-1}_{v}\right) Luh←hash⎝⎛Luh−1+v∈N(U)∑Lvh−1⎠⎞ - WL子树核衡量图之间相似性:使用不同迭代中的节点标签计数作为图的表征向量
- 详细步骤:
- 聚合自身与邻接节点的标签,得到一串字符串
- 标签散列,将较长的字符串映射到一个简短的标签
- 给节点重新打上标签
- 图相似性评估:
- WL Subtree Kernel方法:用WL Test算法得到节点多层标签,统计图中各类标签出现的次数,使用向量表示,作为图的表征
- 两个图的表征向量内积,作为两个图的相似性估计
- 判断图同构性的必要条件:两个节点自身标签一样且它们的邻接节点一样,将两个节点映射到相同的表征
结语
在此篇文章中,我们学习了基于图同构网络(GIN)的图表征网络,为了得到图表征首先需要做节点表征,然后做图读出。GIN中节点表征的计算遵循WL Test算法中节点标签的更新方法,因此它的上界是WL Test算法。在图读出中,我们对所有的节点表征(加权,如果用Attention的话)求和,这会造成节点分布信息的丢失。
参考资料
-
提出GlobalAttention的论文: “Gated Graph Sequence Neural Networks”
-
提出Set2Set的论文:“Order Matters: Sequence to sequence for sets”
-
PyG中集成的所有的图池化的方法:Global Pooling Layers
-
Weisfeiler-Lehman Test: Brendan L Douglas. The weisfeiler-lehman method and graph isomorphism testing. arXiv preprint arXiv:1101.5211, 2011.