torch_geometric (pyg )的介绍和简单使⽤
前⾔
最近做毕业设计,需要⽤到图神经⽹络(以下简称GNN)。由于刚⼊门GNN,不想看⼤段的公式和相关论⽂(然⽽事实证明该看的永远逃不了),所以怎么办?百度上呗!因为⾃⼰平时⽤pytorch⽐较多,所以到了基于pytorch的图神经⽹络库,pytorch_geometric(以下简称pyg)。在⽤这个库的过程中,由于这个库“约定⼤于配置”的⼀些特性,也遇到了许多坑,⽽中⽂资料中,⼤多都是直接翻译⽂档,对⼀些细节没有做解释。因此将整个过程记录下来,供⼤家未来参考。
图神经⽹络基本概念
考虑到有许多同学和我最开始⼀样,就是想知道GNN是⼲什么的,⼤致原理是什么,因此这⼀章将简单介绍GNN
GNN 是⼲什么的
相信⼤家都知道“图”是什么东西,⼀堆的节点(每个节点都有各⾃的特征),中间⽤箭头连起来。那么我们就很⾃然地想,这些相连的节点,可能信息上⽐较相关,可以互补,那我们能不能对这些相关的节点做⼀定的特征聚合操作呢?⽐如求和(这就是GIN),取平均(这就是⼤家常说的GCN),加权求和
(这就是GAN)?做特征聚合时,可以只对⼀阶相邻(也就是直接相邻)节点做聚合,也可以对⼆阶相邻(最多可以通过⼀个中间节点间接相连)节点做聚合,由此衍⽣出各种花⾥胡哨的GNN……
所以我们说,GNN的核⼼操作是节点的特征聚合,具体怎么聚合,各个GNN有⾃⼰的花样。但是这些花样⼀定都是依据邻接关系得到的。毕竟脱离了图结构,GNN也没有存在的意义了,对吧?
看到这个,我们可以联想⼀下CNN,⼀个3x3的卷积核,实际上就是对8邻域做了加权和,对吧?如果把⼆维图像看成是⼀张8邻域连通的图,那实际上就是GNN了。
除了聚合特征之外,GNN们通常还带有⼀个单节点的特征变换运算,这个运算可以是单纯的线性变换,可以带有⾮线性激活函数等,可以认为是对单节点的特征做了增强。所以GNN的⼤致运算过程可以写为
这两种写法并不会影响输⼊输出的维度,重要的是明⽩有⼀次特征聚合,有⼀次特征增强即可。
pyg 库介绍
pyg的功能⽐较强⼤,包括⼀些utils包下的图级别的⼯具函数,nn包下常见的GNN层,data包下封装好的图特征对象,loader包下封装好的图batch的loader,甚⾄还有端到端的GNN模型。⾃⼰搭GNN,主要会⽤到Data、DataLoader、nn、utils等相关⼯具。因此下⾯将从搭建⼀个最简单的GNN,并完成⼀
次输⼊输出运算为主线,介绍这些⼯具的⽤法,以及⼀些暗箱约定(或者说,坑)
以下的内容主要根据梳理,对其中重要的坑会进⾏强调。
那么,想要使⽤三⽅库完成⼀次GNN运算,我们就必须了解以下的⼀些内容
输⼊数据是什么格式?尤其矩阵的维度是什么样的?
DataLoader对数据进⾏了怎样的变换,模型⼜该如何处理⼀个mini-batch?
内置GNN层的输⼊输出⼜是什么?可以直接把mini-batch给它吗?
接下来从探究以上问题出发,我们⼒求把pyg的⽤法讲明⽩
输⼊数据的格式
Data 对象
X =l +1aggregate (X ,A )
l A 表⽰邻接矩阵,X 表⽰第l 层的输出
l X =l +1aggregate (f (X ),A )
l X =l +1f (aggregate (X ,A ))
l
我们知道GNN的输⼊除了各顶点的特征之外,还有邻接矩阵,甚⾄还会有边的特征。pyg内置了Data对象,⽤于封装GNN的输⼊。Data对象中最常使⽤的⼏个属性包括以下三个。如果实际科研⼯作中需要使⽤更复杂的特征,可以回到上⾯源教程。
data.x: 节点特征矩阵,维度是 [num_nodes, num_node_features]
data.edge_index: 图连接关系,也就是之前所说的邻接矩阵。只不过这⾥采⽤了稀疏格式的输⼊,维度是 [2, num_edges] 类型是torch.long。也就是只存储每条边的出发点和终⽌点,⽽不是真正的邻接矩阵(这样的矩阵在顶点多边少的时候,⾮常占内存)。
data.y: 模型的期望输出。如果是完成节点级别任务的GNN,维度⼀般为[num_nodes, *] ;如果是完成图级别任务的GNN,维度⼀般为[1, *]
DataSet对象
DataSet对象通常被我们⽤于原始数据读取和加⼯,将数据转换成DataLoader所能接受的输⼊。
说的现实点,主要就是把我们的数据源转换成⼀系列的Data对象。这个对象本⾝也是pyg封装好的,需要我们削⾜适履,把我们的转换逻辑填进去。以下直接摘⾃官⽅教程
class MyOwnDataset(InMemoryDataset):
def__init__(self, root, transform=None, pre_transform=None):
super().__init__(root, transform, pre_transform)
# 读取已经转换好格式的数据
self.data, self.slices = torch.load(self.processed_paths[0])
# 未处理好的数据,如果pyg发现⽂件不存在,会进⾏下载
@property
def raw_file_names(self):
return['some_file_1','some_file_2',...]
# 处理好的数据,如果pyg发现⽂件不存在,会调⽤self.process()函数
@property
def processed_file_names(self):
return['data.pt']
# 下载原始数据的函数,如果不需要就直接pass
def download(self):
download_url(url, self.raw_dir)
# 将原始数据进⾏处理,转换为Data对象,并且保存下来
def process(self):
# 假设这⾥已经经过了⼀系列处理,得到了包含Data对象的List
data_list =[...]
# 雷打不动的两句话,处理List并存盘
data, slices = llate(data_list)
torch.save((data, slices), self.processed_paths[0])
DataLoader对象
DataLoader打包mini-batch
玩深度学习的同学们都知道,训练模型⼀般要把多个数据打包成⼀个mini-batch,再丢给模型训练(原因我就不解释了)。DataLoader就是完成这个⼯作的。如果你恰好⽤过pytorch,你肯定也知道pytorch默认的DataLoader会把batch_size个样本打包成[batch_size, d1, d2, ..., dn]维度的输⼊,其中[d1, d2, ..., dn]是样本本来的特征维度。
⽽pyg的DataLoader,会把数据打包成[batch_size*num_nodes, num_node_features]的维度(也就是batch_size不会单独成⼀维)。说实话,这⼀点⾮常坑(当然,从性能的⾓度,也可以说“妙”)。pyg官⽅的解释是“为了增加并⾏度”,那么,这⼀步操作是怎么增加并⾏度的?
如何提⾼并⾏度
稍加思考就可以明⽩。之前我们就说过,GNN最重要的操作之⼀,就是进⾏特征聚合。那进⾏特征聚合的代码怎么写?我们以求均值为例,最暴⼒的,当然是对着邻接矩阵,⼀个⼀个把邻居的特征加起来再取平均了eval是做什么的
当然,⼤家都知道拿循环来算加权和,效率⾮常低,因此应该⽤矩阵的形式来表⽰这⼀运算。假设我们已经算出了各个节点加权和的系数,形成⼀个系数矩阵,那上⾯的循环直接就可以⽤⼀个矩阵乘法表⽰了
可以想象,A乘在左边,就是对X做了⾏变换,也就是对X的每⼀⾏进⾏了加权和。
图神经⽹络的计算效率是⽐较低的,多张图之间难以进⾏并⾏化。假如num_nodes 不是很⼤,那进⾏⼀次上述的运算,也不会有太⼤的加速。因此pyg的DadaLoader将数据打包成了[batch_size*num_nodes, num_node_features]维度,相当于⼤⼤提升了参与⼀次图运算的顶点数,因此可以充分利⽤向量运算的优势。
mini-batch 的内容
mini-batch内打包了节点特征、样本标签、连接关系、batch信息等内容
我们现在以最简单的两个图为例,说明打包后的数据长什么样
图1:三个顶点[0, 1, 2],三个顶点两两双向连接,标签是节点维度的
图2:两个顶点[0,1],两个顶点两两双向连接,标签是节点维度的
接下来,特征、标签都正常拼接;但是节点连接关系会进⾏⼀定的运算
data.x : 将两个图的节点特征直接拼接成[5, num_node_features]的矩阵
data.y : 将两个图的节点标签直接拼接成[5, *]的矩阵
data.edge_index : 将两个图混合成⼀张⼤图,形成[2, 10]的矩阵。得到的邻接矩阵⼤概是[[0,1],[1,0],[0,2],[2,0],[1,2],[2,1],[3,4],[4,3]]。这⾥为了看着⽅便,我把稀疏邻接矩阵转置了⼀下,实际上它的维度是[2, num_edges]
欸,哪来的节点3和4呢?这就是这⼀并⾏化算法巧妙的地⽅,它将多个图融合成⼀张⼤图——其实图的编号没有太⼤的实际意义,它只是表达哪⼏个节点需要进⾏信息交换,需要把X的哪⼏⾏进⾏交换罢了。因此整个运算结果是⾮常正确的。
因此,我们再回头看⼀眼,Data对象中,各个元素的维度——知道为什么num_nodes ⼀定作为第⼀维了吗?想要最⼤限度地利⽤pyg库带来的遍历,就必须削⾜适履,迎合它的编码⽅式。在下⼀篇⽂章中,我将讲述如何定制DataLoader从⽽增加⼀些灵活性。
除了以上所述的那些内容,DataLoader还会打包⼀个batch信息。这⼀信息主要是为了从batch中再区分出各个图所⽤,在进⾏⼀些图级别的全局运算,⽐如softmax,⽐如global_average,⽐如global_max,我们肯定希望是在⼀个图样本中进⾏(不然在这张⽤于运算的⼤图上进⾏全局计算,有什么实际意义吗)
⼀个简单的GNN 例⼦
接下来我们搭⼀个最简单的两层GCN⽹络
X =i l +1(a x )/n
j ∑ij ij X =i X [i ,:]
x =ij X [i ,j ]
X =l +1A ⋅X l
A ∈[num _nodes ,num _nodes ]
X ∈[num _nodes ,num _node _features ]
import torch
functional as F
from import GCNConv
class Module):
def__init__(self):
super().__init__()
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = v1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, aining)
x = v2(x, edge_index)
return F.log_softmax(x, dim=1)
接下来是训练过程
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='/tmp/Cora', name='Cora')
device = torch.device('cuda'if torch.cuda.is_available()else'cpu')
model = GCN().to(device)
data = dataset[0].to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
for epoch in range(200):
<_grad()
out = model(data)
loss = F.nll_loss(ain_mask], data.ain_mask])
loss.backward()
optimizer.step()
最后是测试过程
model.eval()
pred = model(data).argmax(dim=1)
correct =(st_mask]== data.st_mask]).sum()
acc =int(correct)/st_mask.sum())
print(f'Accuracy: {acc:.4f}')
好吧,这⼀章很⽔,毕竟这只是⼀个最简单的GNN例⼦,pyg也内置了很多的GNN模型供⼤家调⽤。需要注意的是,在刚刚的例程中,其实pyg内置GNN层的输⼊,并不是data,⽽是分⽴的data.x和data.edge_index,很多其它GNN层也是如此。这是因为通常⼤家先会⽤基本的GNN层搭建⼀些⼩模块,这些模块内可能带着卷积,可能带着池化,⽽对于卷积层来说,它并不需要知道batch信息。
后记
这⼀篇⽂章基本在翻译教程的过程中写完了,加上了⾃⼰在构建DataSet对象和DataLoader对象中踩的坑。但是这不是全部,后续,我将从⼀个交通领域的T-GCN模型出发,讲述如何使⽤pyg库复现这⼀时空图神经⽹络模型。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论