从0到1用PyG创建异构图
在现实中需要对 多种类型的节点以及这些 节点之间多种类型的边进行处理,这就需要
异构图
的概念,在异构图中,
不同类型的边
描述不同类型节点之间
不同的关系
,
异构图神经网络的任务就是在这种图结构上学习出节点或者整个异构图的特征表示。
异构图
异构图的准确定义如下:异构图(Heterogeneous Graphs):一个异构图 G G G有一组节点 V = v 1 , v 2 , . . . , v n V=v_1,v_2,...,v_n V=v1,v2,...,vn和一组边 E = e 1 , e 2 , . . . , e m E=e_1,e_2,...,e_m E=e1,e2,...,em组成,其中每个节点和每条边都对应着一种类型,用 T v T_v Tv表示节点类型的集合, T e T_e Te表示边类型的集合,一个异构图有两个映射函数,分别将每个节点映射到其对应的类型 ϕ v : V → T v \phi_v:V\rightarrow T_v ϕv:V→Tv,每条边映射到其对应的类型: ϕ e : E → T e \phi_e:E\rightarrow T_e ϕe:E→Te。
创建异构图
电影评分数据集MovieLens
这里以一个电影评分数据集MovieLens为例,逐行示例如何创建异构图。MovieLens包含了600个用户对于电影的评分,利用这个数据集构建一个二分图
,包含电影
、用户
两种类型的节点,一种类型的边(含有多种类型节点
,所以可以看作一个异质图)。
- MovieLens中的
movies.csv
文件描述了电影的信息,包括电影在数据集中唯一的ID
,电影名
,电影所属的类型
:
- MovieLens中的ratings.csv包含了用户对于电影的评分:
建立二分图数据集
首先下载一个Python库:sentence_transformers
。SentenceTransformers 是一个可以用于句子、文本和图像嵌入的Python库。 可以为 100 多种语言计算文本的嵌入并且可以轻松地将它们用于语义文本相似性、语义搜索和同义词挖掘等常见任务。该框架基于 PyTorch 和 Transformers,并提供了大量针对各种任务的预训练模型。 还可以很容易根据自己的模型进行微调。
pip install -U sentence-transformers
首先,导入依赖库:
import os.path as osp
import torch
import pandas as pd
from sentence_transformers import SentenceTransformer
from torch_geometric.data import HeteroData, download_url, extract_zip
from torch_geometric.transforms import ToUndirected, RandomLinkSplit
然后,设定数据集:
利用Pandas查看数据集
(1)利用嵌入模型将每个电影名用向量表示(Embedding)
# 将电影名那列用嵌入模型将每个电影名用向量表示(Embedding)
class SequenceEncoder(object):
# 初始化,指定使用的embedding module和设备
def __init__(self, model_name='all-MiniLM-L6-v2', device=None):
# 使用的设备
self.device = device
# 使用的嵌入模型名
self.model = SentenceTransformer(model_name, device=device)
# 嵌入模型不参与后续图神经网络的训练
@torch.no_grad()
def __call__(self, df):
x = self.model.encode(
# 要进行嵌入的值
df.values,
# 显示处理进度
show_progress_bar=True,
# 转换为Pytorch的张量
convert_to_tensor=True,
# 使用的设备
device=self.device
)
return x.cpu()
(2)将电影类型也进行嵌入表示
# 将电影类型那列也进行嵌入表示
class GenresEncoder(object):
# 默认分隔符为"|"
def __init__(self, sep="|"):
self.sep = sep
def __call__(self, df):
genres = set(g for col in df.values for g in col.split(self.sep))
# 将电影类型用数字表示
mapping = {genre: i for i, genre in enumerate(genres)}
# 用multi-hot形式表示电影的类型
x = torch.zeros(len(df), len(mapping))
for i, col in enumerate(df.values):
for genre in col.split(self.sep):
x[i, mapping[genre]]=1
return x
(3)从CSV文件中读取信息,建立二分图中的节点信息
# 从csv中读取信息,建立二分图中节点的信息
def load_node_csv(path, index_col, encoders=None, **kwargs):
"""
:param path: CSV文件路径
:param index_col: 文件中的索引列,也就是节点所在的列
:param encoders: 节点嵌入器
:param kwargs
:return:
"""
df = pd.read_csv(path, index_col=index_col, **kwargs)
# 将索引用数字表示
mapping = {index: i for i, index in enumerate(df.index.unique())}
# 节点属性向量矩阵
x = None
# 如果嵌入器非空
if encoders is not None:
# 对相应的列进行嵌入,获取嵌入向量表示
xs = [encoder(df[col]) for col, encoder in encoders.items()]
x = torch.cat(xs, dim=-1)
return x, mapping
(4)获取节点信息
# 处理movies.csv表,将"电影名","电影类型"列转换为嵌入向量的表示形式
movie_x, movie_mapping = load_node_csv(movie_path, index_col = 'movieId', encoders = {'title':SequenceEncoder(),'genres':GenresEncoder()})
# 处理rating.csv表,将用户ID用PyTorch中的张量表示
user_x, user_mapping = load_node_csv(rating_path, index_col='userId')
# 建立异质图,这里是一个二分图
data = HeteroData() # HeteroData()是PyG中内置的一个表示异质图的数据结构
# 加入不同类型节点的信息
# 加入用户信息,用户没有属性信息, 只需要告诉PyG有多少个用户节点就可以
data['user'].num_nodes = len(user_mapping)
# 告诉PyG 电影的属性向量矩阵,PyG会根据x推断出电影节点的个数
data['movie'].x = movie_x
print(data)
(5)建立用户和电影之间的边的信息
# 将用户对电影的评分转换为PyTorch的张量
class IdentityEncoder(object):
def __init__(self, dtype=None):
self.dtype = dtype
def __call__(self, df):
return torch.from_numpy(df.values).view(-1, 1).to(self.dtype)
(6)建立二分图边的连接信息
# 建立二分图边的链接信息
def load_edge_csv(path, src_index_col, src_mapping, dst_index_col, dst_mapping, encoders=None, **kwargs):
"""
:param path: CSV表的路径
:param src_index_col: 二分图左边节点来源于CSV表的哪一列,比如'user_id'这列
:param src_mapping:将user_id映射为节点编号,我们前面定义的user_mapping
:param dst_index_col:同理,二分图右边电影节点
:param dst_mapping:
:param encoders:边的嵌入器
:param kwargs:
:return:
"""
df = pd.read_csv(path, **kwargs)
# 建立连接信息
src = [src_mapping[index] for index in df[src_index_col]]
dst = [dst_mapping[index] for index in df[dst_index_col]]
# 注意这里edge_index维度为[2, 边数]
edge_index = torch.tensor([src, dst])
# 边的属性信息
edge_attr = None
# 如果嵌入器非空
if encoders is not None:
edge_attrs = [encoder(df[col]) for col, encoder in encoders.items()]
edge_attr = torch.cat(edge_attrs, dim=-1)
return edge_index, edge_attr
(7)获取二分图边的信息
# 获取二分图边的信息
edge_index, edge_label = load_edge_csv(
rating_path,
# 二分图左边是用户
src_index_col='userId',
src_mapping=user_mapping,
# 右边是电影
dst_index_col='movieId',
dst_mapping=movie_mapping,
encoders={'rating': IdentityEncoder(dtype=torch.long)}
)
(8)将二分图中的边命名为(‘user’, ‘rates’, ‘movie’)
到此,我们的异构图数据集,实际上是一个二分图,就构建完毕了。下面还要将其转换为一个可以进行训练的数据集
转换为可训练的数据集
这里,我们将构建的异构图数据集转换为一个可训练的无向图数据集。
(1)转换为无向图,同时删除相反方向边的属性信息
data = ToUndirected()(data)
# 删除相反方向边的属性信息,因为没有电影对用户的评分数据
del data['movie', 'rev_rates', 'user'].edge_label
(2)按照比例分割数据集为训练集、测试集、验证集
# 按照一定比例分割数据集为训练集、测试集和验证集
transform = RandomLinkSplit(
num_val=0.05,
num_test=0.1,
# 负采样比率
# 不用负采样,全部输入进行训练
neg_sampling_ratio = 0.0,
# 告诉PyG边的连接关系
edge_types=[('user', 'rates', 'movie')],
rev_edge_types=[('movie', 'rev_rates', 'user')],
)
# 分割数据集
train_data, val_data, test_data = transform(data)
print(train_data)
print(val_data)
print(test_data)
至此,一个可训练的数据集已经构建完毕。
建立异构图神经网络
以OGB数据集为例
在OGB数据集中包含4种类型的节点:author、paper、institution、field of study;4种类型的边:
- writes:author和paper之间的连接关系
- affiliated with:author和institution之间的连接关系
- cites:paper和paper之间的关系
- has topic:paper和field of study之间的关系
OGB数据集上的任务是预测论文在整个关系网中所属的位置。下面代码示例如何表示这个异质图:
from torch_geometric.data import HeteroData
# HeteroData是PyG自带的一个异质图数据结构
data = HeteroData()
# 添加节点的信息
data['paper'].x = ... # [num_papers, num_features_paper]
data['author'].x = ... # [num_authors, num_features_author]
data['institution'].x = ... # [num_institutions, num_features_institution]
data['field_of_study'].x = ... # [num_field, num_features_field]
# 添加边的连接信息
data['paper', 'cites', 'paper'].edge_index = ... # [2, num_edges_cites]
data['author', 'writes', 'paper'].edge_index = ... # [2, num_edges_writes]
data['author', 'affiliated_with', 'institution'].edge_index = ... # [2, num_edges_affiliated]
data['paper', 'has_topic', 'field_of_study'].edge_index = ... # [2, num_edges_topic]
# 添加边的属性信息
data['paper', 'cites', 'paper'].edge_attr = ... # [num_edges_cites, num_features_cites]
data['author', 'writes', 'paper'].edge_attr = ... # [num_edges_writes, num_features_writes]
data['author', 'affiliated_with', 'institution'].edge_attr = ... # [num_edges_affiliated, num_features_affiliated]
data['paper', 'has_topic', 'field_of_study'].edge_attr = ... # [num_edges_topic, num_features_topic]
这样上面的异质图就建立完成了,我们可以将它输入到一个异质图神经网络中:
# 异质图神经网络
model = HeteroGNN(...)
# 获取异构图神经网络的输出
# 注意异质图神经网络的输入是 ..._dict
output = model(data.x_dict, data.edge_index_dict, data.edge_attr_dict)
如果PyG中包含你想用的异质图,可以直接这样导入:
from torch_geometric.datasets import OGB_MAG
# 导入数据集
dataset = OGB_MAG(root='./data', preprocess='metapath2vec')
data = dataset[0]
这样上面的异质图就建立完成了,我们可以将它输入到一个异质图神经网络中
HeteroData中常用的函数
下面介绍一下HeteroData中常用的函数:
- 获取异质图中的某种节点或边
paper_node_data=data['paper']
cites_edge_data=data['paper','cites','paper']
- 如果边的连接节点集合或者边的命名是唯一的,还可以这样写
#使用连接端点获取边
cites_edge_data=data['paper','paper']
#使用边的名字获取
cites_edge_data=data['cites']
- 给节点添加新属性
data['paper'].year=...
- 删除节点的某些属性
def data['field_of_study']
- 通过metadata获取异质图中所有类型的信息
node_types,edge_types=data.metadata()
- 所有类型的节点
print(node_types)
- 所有类型的边
print(edge_types)
- 判断异质图自身的一些属性
print(data.has_isolated_nodes())
- 如果不同类型信息之间维度匹配,还可以将异质图融合为一个简单图
homogeneous_data=data.to_homogeneous()
- 对异质图进行变换
import torch_geometric.transforms as T
#变为无向图
data=T.ToUndirected()(data)
#添加到自身的环
data=T.AddSelfLoops()(data)
将简单图神经网络转换为异质图神经网络
PyG
可以通过torch_geometric.nn.to_hetero()
,或者torch_geometric.nn.to_hetero_with_bases()
将一个简单图神经网络转换成异质图的形式。
import torch_geometric.transforms as T
from torch_geometric.datasets import OGB_MAG
from torch_geometric.nn import SAGEConv, to_hetero
# 导入数据集
dataset = OGB_MAG(root='./data', preprocess='metapath2vec', transform=T.ToUndirected())
data = dataset[0]
# 定义一个普通的图神经网络
class GNN(torch.nn.Module):
def __init__(self, hidden_channels, out_channels):
super().__init__()
self.conv1 = SAGEConv((-1, -1), hidden_channels)
self.conv2 = SAGEConv((-1, -1), out_channels)
def forward(self, x, edge_index):
x = self.conv1(x, edge_index).relu()
x = self.conv2(x, edge_index)
return x
# 实例化我们定义的图神经网络
model = GNN(hidden_channels=64, out_channels=dataset.num_classes)
# 将其转换为异质图形式
model = to_hetero(model, data.metadata(), aggr='sum')
PyG的to_hetero具体工作方式是这样的:
它根据我们的异质图数据结构,自动将我们定义的简单图神经网络结构中的层结构进行了复制,并添加了信息传递路径。
然后,创建的模型可以像往常一样进行训练:
def train():
model.train()
optimizer.zero_grad()
out = model(data.x_dict, data.edge_index_dict)
mask = data['paper'].train_mask
loss = F.cross_entropy(out['paper'][mask], data['paper'].y[mask])
loss.backward()
optimizer.step()
return float(loss)
异构卷积包装器torch_geometry.nn.conv.HeteroConv
允许定义自定义异构消息和更新函数,以从头开始为异构图构建任意MP-GNN。虽然to_hetero()的自动转换器对所有边类型使用相同的运算符,但包装器允许为不同的边类型定义不同的运算符。在这里,HeteroConv将子模块的字典作为输入,图数据中的每个边缘类型都有一个子模块。以下示例显示了如何应用它。
import torch_geometric.transforms as T
from torch_geometric.datasets import OGB_MAG
from torch_geometric.nn import HeteroConv, GCNConv, SAGEConv, GATConv, Linear
dataset = OGB_MAG(root='./data', preprocess='metapath2vec', transform=T.ToUndirected())
data = dataset[0]
class HeteroGNN(torch.nn.Module):
def __init__(self, hidden_channels, out_channels, num_layers):
super().__init__()
self.convs = torch.nn.ModuleList()
for _ in range(num_layers):
conv = HeteroConv({
('paper', 'cites', 'paper'): GCNConv(-1, hidden_channels),
('author', 'writes', 'paper'): SAGEConv((-1, -1), hidden_channels),
('paper', 'rev_writes', 'author'): GATConv((-1, -1), hidden_channels,add_self_loops=False),
}, aggr='sum')
self.convs.append(conv)
self.lin = Linear(hidden_channels, out_channels)
def forward(self, x_dict, edge_index_dict):
for conv in self.convs:
x_dict = conv(x_dict, edge_index_dict)
x_dict = {key: x.relu() for key, x in x_dict.items()}
return self.lin(x_dict['author'])
model = HeteroGNN(hidden_channels=64, out_channels=dataset.num_classes,
num_layers=2)
我们可以通过调用一次来初始化模型(有关延迟初始化的更多详细信息,请参阅此处)
with torch.no_grad(): # Initialize lazy modules.
out = model(data.x_dict, data.edge_index_dict)
GraphGym的使用
PyG 2.0 现在通过 torch_geometric.graphgym 正式支持 GraphGym。总的来说,GraphGym 是一个平台,用于通过高度模块化的 pipeline 从配置文件中设计和评估图神经网络:
- GraphGym 是开始学习标准化 GNN 实现和评估的最佳平台;
- GraphGym 提供了一个简单的接口来并行尝试数千个 GNN 架构,以找到适合特定任务的最佳设计;
- GraphGym 可轻松进行超参数搜索并可视化哪些设计选择更好。
对于GraphGym更多资料,参考https://pytorch-geometric.readthedocs.io/en/latest/modules/graphgym.html
PyG中常用的卷积层