在学习前可以简单了解前几天刚结束的KDD CUP和OGB共同举办第一届的OGB-LSC(OGB Large-Scale Challenge)图神经网络比赛(KDDCUP是ACM SIGKDD组织的数据挖掘领域最影响力的顶级赛事),该比赛就是提供真实世界的超大规模图数据,完成图学习领域的节点分类、边预测和图回归三大任务,百度在2个赛道上获得冠军,强的一批:
- 大规模节点分类赛道冠军:引入基于异构关系的统一消息传递模型
- 大规模图关系预测赛道冠军:提出 20 层的 NOTE-RPS 知识图谱嵌入模型
学习心得
(1)创建超大规模数据集的步骤
(2)上个task6学习了基于GIN的图表示学习神经网络,这次是将GIN和超大规模数据集类的创建结合起来,构建一种很方便的设置不同参数进行试验的方法,不同试验的过程与结果信息通过简单的操作即可进行比较分析。
(3)第二部分的图预测任务自己渣渣电脑难跑,发现很多人也是(各种原因:显存不够等),另外注意colab是没办法永久安装库的,12小时就会断一次——可以写到谷歌网盘,然后重新运行,再从网盘里读取blabla(小伙伴说的)。
第一部分:超大规模数据集类的创建
- 在前面的学习中我们只接触了数据可全部储存于内存的数据集,这些数据集对应的数据集类在创建对象时就将所有数据都加载到内存。
- 有时数据集规模超级大,我们很难有足够大的内存完全存下所有数据。
因此需要一个按需加载样本到内存的数据集类。在第一部分是学习为一个包含上千万个图样本的数据集构建一个数据集类。
一、Dataset基类简介
在PyG中,我们通过继承torch_geometric.data.Dataset
基类来自定义一个按需加载样本到内存的数据集类。此基类与Torchvision的Dataset
类的概念密切相关,这与第6节中介绍的torch_geometric.data.InMemoryDataset
基类是一样的(可以复习【GNN】task4-数据完整存储与内存的数据集类+节点预测与边预测任务实践)。
继承torch_geometric.data.InMemoryDataset
基类要实现的方法,继承此基类同样要实现,此外还需要实现以下方法:
len()
:返回数据集中的样本的数量。get()
:实现加载单个图的操作。注意:在内部,__getitem__()
返回通过调用get()
来获取Data
对象,并根据transform
参数对它们进行选择性转换。
下面让我们通过一个简化的例子看继承torch_geometric.data.Dataset
基类的规范:
import os.path as osp
import torch
from torch_geometric.data import Dataset, download_url
class MyOwnDataset(Dataset):
def __init__(self, root, transform=None, pre_transform=None):
super(MyOwnDataset, self).__init__(root, transform, pre_transform)
@property
def raw_file_names(self):
return ['some_file_1', 'some_file_2', ...]
@property
def processed_file_names(self):
return ['data_1.pt', 'data_2.pt', ...]
def download(self):
# Download to `self.raw_dir`.
path = download_url(url, self.raw_dir)
...
def process(self):
i = 0
for raw_path in self.raw_paths:
# Read data from `raw_path`.
data = Data(...)
if self.pre_filter is not None and not self.pre_filter(data):
continue
if self.pre_transform is not None:
data = self.pre_transform(data)
torch.save(data, osp.join(self.processed_dir, 'data_{}.pt'.format(i)))
i += 1
def len(self): # 返回数据集中的样本数量
return len(self.processed_file_names)
def get(self, idx): # 实现加载单个图的操作
data = torch.load(osp.join(self.processed_dir, 'data_{}.pt'.format(idx)))
return data
其中,每个Data
对象在process()
方法中单独被保存,并在get()
中通过指定索引进行加载。
1.跳过download/process
对于无需下载数据集原文件的情况,我们不重写(override)download
方法即可跳过下载。对于无需对数据集做预处理的情况,我们不重写process
方法即可跳过预处理。
2.无需定义Dataset类
通过下面的方式,我们可以不用定义一个Dataset
类,而直接生成一个Dataloader
对象,直接用于训练:
from torch_geometric.data import Data, DataLoader
data_list = [Data(...), ..., Data(...)]
loader = DataLoader(data_list, batch_size=32)
我们也可以通过下面的方式将一个列表的Data
对象组成一个batch
:
from torch_geometric.data import Data, Batch
data_list = [Data(...), ..., Data(...)]
loader = Batch.from_data_list(data_list, batch_size=32)
二、图样本封装成批(BATCHING)与DataLoader类
1.合并小图组成大图
图可以有任意数量的节点和边,它不是规整的数据结构,因此对图数据封装成批的操作与对图像和序列等数据封装成批的操作不同。PyTorch Geometric中采用的将多个图封装成批的方式是,将小图作为连通组件(connected component)的形式合并,构建一个大图。于是小图的邻接矩阵存储在大图邻接矩阵的对角线上。大图的邻接矩阵、属性矩阵、预测目标矩阵分别为:
此方法有以下关键的优势:
-
依靠消息传递方案的GNN运算不需要被修改,因为消息仍然不能在属于不同图的两个节点之间交换。
-
没有额外的计算或内存的开销。例如,这个批处理程序的工作完全不需要对节点或边缘特征进行任何填充。请注意,邻接矩阵没有额外的内存开销,因为它们是以稀疏的方式保存的,只保留非零项,即边。
通过torch_geometric.data.DataLoader
类,多个小图被封装成一个大图。torch_geometric.data.DataLoader
是PyTorch的DataLoader
的子类,它覆盖了collate()
函数,该函数定义了一列表的样本是如何封装成批的。因此,所有可以传递给PyTorch DataLoader
的参数也可以传递给PyTorch Geometric的 DataLoader
,例如,num_workers
。
2.小图的属性增值与拼接
(1)将小图存储到大图中时需要对小图的属性做一些修改,一个最显著的例子就是要对节点序号增值。在最一般的形式中,PyTorch Geometric的DataLoader
类会自动对edge_index
张量增值,增加的值为当前被处理图的前面的图的累积节点数量。
(2)比方说,现在对第
k
k
k个图的edge_index
张量做增值,前面
k
−
1
k-1
k−1个图的累积节点数量为
n
n
n,那么对第
k
k
k个图的edge_index
张量的增值
n
n
n。增值后,对所有图的edge_index
张量(其形状为[2, num_edges]
)在第二维中连接起来。
(3)然而,有一些特殊的场景中(如下所述),基于需求我们希望能修改这一行为。PyTorch Geometric允许我们通过覆盖torch_geometric.data.__inc__()
和torch_geometric.data.__cat_dim__()
函数来实现我们希望的行为。在未做修改的情况下,它们在Data
类中的定义如下。
def __inc__(self, key, value):
if 'index' in key or 'face' in key:
return self.num_nodes
else:
return 0
def __cat_dim__(self, key, value):
if 'index' in key or 'face' in key:
return 1
else:
return 0
我们可以看到,__inc__()
定义了两个连续的图的属性之间的增量大小,
而__cat_dim__()
定义了同一属性的图形张量应该在哪个维度上被连接起来。
PyTorch Geometric为存储在Data
类中的每个属性调用此二函数,并以它们各自的key
和值value
作为参数。
在下面的内容中,我们将学习一些对__inc__()
和__cat_dim__()
的修改可能是绝对必要的案例。
(1)图的匹配(Pairs of Graphs)
如果你想在一个Data
对象中存储多个图,例如用于图匹配等应用,我们需要确保所有这些图的正确封装成批行为。例如,考虑将两个图,一个源图
G
s
G_s
Gs和一个目标图
G
t
G_t
Gt,存储在一个Data
类中,即
class PairData(Data):
def __init__(self, edge_index_s, x_s, edge_index_t, x_t):
super(PairData, self).__init__()
self.edge_index_s = edge_index_s
self.x_s = x_s
self.edge_index_t = edge_index_t
self.x_t = x_t
在这种情况中,edge_index_s
应该根据源图
G
s
G_s
Gs的节点数做增值,即x_s.size(0)
,而edge_index_t
应该根据目标图
G
t
G_t
Gt的节点数做增值,即x_t.size(0)
。
class PairData(Data):
def __init__(self, edge_index_s, x_s, edge_index_t, x_t):
super(PairData, self).__init__()
self.edge_index_s = edge_index_s
self.x_s = x_s
self.edge_index_t = edge_index_t
self.x_t = x_t
def __inc__(self, key, value):
if key == 'edge_index_s':
return self.x_s.size(0)
if key == 'edge_index_t':
return self.x_t.size(0)
else:
return super().__inc__(key, value)
我们可以通过设置一个简单的测试脚本来测试我们的PairData批处理行为。
edge_index_s = torch.tensor([
[0, 0, 0, 0],
[1, 2, 3, 4],
])
x_s = torch.randn(5, 16) # 5 nodes.
edge_index_t = torch.tensor([
[0, 0, 0],
[1, 2, 3],
])
x_t = torch.randn(4, 16) # 4 nodes.
data = PairData(edge_index_s, x_s, edge_index_t, x_t)
data_list = [data, data]
loader = DataLoader(data_list, batch_size=2)
batch = next(iter(loader))
print(batch)
# Batch(edge_index_s=[2, 8], x_s=[10, 16], edge_index_t=[2, 6], x_t=[8, 16])
print(batch.edge_index_s)
# tensor([[0, 0, 0, 0, 5, 5, 5, 5], [1, 2, 3, 4, 6, 7, 8, 9]])
print(batch.edge_index_t)
# tensor([[0, 0, 0, 4, 4, 4], [1, 2, 3, 5, 6, 7]])
edge_index_s
和edge_index_t
被正确地封装成批了,即使在为
G
s
G_s
Gs和
G
t
G_t
Gt含有不同数量的节点时也是如此。然而,由于PyTorch Geometric无法识别PairData
对象中实际的图,所以batch
属性(将大图每个节点映射到其各自对应的小图)没有正确工作。此时就需要DataLoader
的follow_batch
参数发挥作用。在这里,我们可以指定我们要为哪些属性维护批信息。
loader = DataLoader(data_list, batch_size=2, follow_batch=['x_s', 'x_t'])
batch = next(iter(loader))
print(batch)
# Batch(edge_index_s=[2, 8], x_s=[10, 16], x_s_batch=[10],
# edge_index_t=[2, 6], x_t=[8, 16], x_t_batch=[8])
print(batch.x_s_batch)
# tensor([0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
print(batch.x_t_batch)
# tensor([0, 0, 0, 0, 1, 1, 1, 1])
可以看到,follow_batch=['x_s', 'x_t']
现在成功地为节点特征x_s
和x_t
分别创建了名为x_s_batch
和x_t_batch
的赋值向量。这些信息现在可以用来在一个单一的Batch
对象中对多个图进行聚合操作,例如,全局池化。
(2)二部图(Bipartite Graphs)
二部图的邻接矩阵定义两种类型的节点之间的连接关系。一般来说,不同类型的节点数量不需要一致,于是二部图的邻接矩阵
A
∈
{
0
,
1
}
N
×
M
A \in \{0,1\}^{N \times M}
A∈{0,1}N×M可能为平方矩阵,即可能有
N
≠
M
N \neq M
N=M。对二部图的封装成批过程中,edge_index
中边的源节点与目标节点做的增值操作应是不同的。我们将二部图中两类节点的特征特征张量分别存储为x_s
和x_t
。
class BipartiteData(Data):
def __init__(self, edge_index, x_s, x_t):
super(BipartiteData, self).__init__()
self.edge_index = edge_index
self.x_s = x_s
self.x_t = x_t
为了对二部图实现正确的封装成批,我们需要告诉PyTorch Geometric,它应该在edge_index
中独立地为边的源节点和目标节点做增值操作。
class BipartiteData(Data):
def __init__(self, edge_index, x_s, x_t):
super(BipartiteData, self).__init__()
self.edge_index = edge_index
self.x_s = x_s
self.x_t = x_t
def __inc__(self, key, value):
if key == 'edge_index':
return torch.tensor([[self.x_s.size(0)], [self.x_t.size(0)]])
else:
return super().__inc__(key, value)
其中,edge_index[0]
(边的源节点)根据x_s.size(0)
做增值运算,而edge_index[1]
(边的目标节点)根据x_t.size(0)
做增值运算。我们可以再次通过运行一个简单的测试脚本来测试我们的实现。
edge_index = torch.tensor([
[0, 0, 1, 1],
[0, 1, 1, 2],
])
x_s = torch.randn(2, 16) # 2 nodes.
x_t = torch.randn(3, 16) # 3 nodes.
data = BipartiteData(edge_index, x_s, x_t)
data_list = [data, data]
loader = DataLoader(data_list, batch_size=2)
batch = next(iter(loader))
print(batch)
# Batch(edge_index=[2, 8], x_s=[4, 16], x_t=[6, 16])
print(batch.edge_index)
# tensor([[0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 1, 2, 3, 4, 4, 5]])
可以看到我们得到我们期望的结果。
(3)在新的维度上做拼接
有时,Data
对象的属性需要在一个新的维度上做拼接(如经典的封装成批),例如,图级别属性或预测目标。具体来说,形状为[num_features]
的属性列表应该被返回为[num_examples, num_features]
,而不是[num_examples * num_features]
。PyTorch Geometric通过在__cat_dim__()
中返回一个None
的连接维度来实现这一点。
class MyData(Data):
def __cat_dim__(self, key, item):
if key == 'foo':
return None
else:
return super().__cat_dim__(key, item)
edge_index = torch.tensor([
[0, 1, 1, 2],
[1, 0, 2, 1],
])
foo = torch.randn(16)
data = MyData(edge_index=edge_index, foo=foo)
data_list = [data, data]
loader = DataLoader(data_list, batch_size=2)
batch = next(iter(loader))
print(batch)
# Batch(edge_index=[2, 8], foo=[2, 16])
正如我们期望的,batch.foo
现在由两个维度来表示,一个批维度,一个特征维度。
三、创建超大规模数据集类实践
PCQM4M-LSC是一个分子图的量子特性回归数据集,它包含了3,803,453个图,具体点击刚才的链接。
注意以下代码依赖于ogb
包,通过pip install ogb
命令可安装此包。ogb
文档可见于Get Started | Open Graph Benchmark (stanford.edu)。
我们定义的数据集类如下:
import os
import os.path as osp
import pandas as pd
import torch
from ogb.utils import smiles2graph
from ogb.utils.torch_util import replace_numpy_with_torchtensor
from ogb.utils.url import download_url, extract_zip
from rdkit import RDLogger
from torch_geometric.data import Data, Dataset
import shutil
RDLogger.DisableLog('rdApp.*')
class MyPCQM4MDataset(Dataset):
def __init__(self, root):
self.url = 'https://dgl-data.s3-accelerate.amazonaws.com/dataset/OGB-LSC/pcqm4m_kddcup2021.zip'
super(MyPCQM4MDataset, self).__init__(root)
filepath = osp.join(root, 'raw/data.csv.gz')
data_df = pd.read_csv(filepath)
self.smiles_list = data_df['smiles']
self.homolumogap_list = data_df['homolumogap']
@property
def raw_file_names(self):
return 'data.csv.gz'
def download(self):
path = download_url(self.url, self.root)
extract_zip(path, self.root)
os.unlink(path)
shutil.move(osp.join(self.root, 'pcqm4m_kddcup2021/raw/data.csv.gz'), osp.join(self.root, 'raw/data.csv.gz'))
def len(self):
return len(self.smiles_list)
def get(self, idx):
smiles, homolumogap = self.smiles_list[idx], self.homolumogap_list[idx]
graph = smiles2graph(smiles)
assert(len(graph['edge_feat']) == graph['edge_index'].shape[1])
assert(len(graph['node_feat']) == graph['num_nodes'])
x = torch.from_numpy(graph['node_fea t']).to(torch.int64)
edge_index = torch.from_numpy(graph['edge_index']).to(torch.int64)
edge_attr = torch.from_numpy(graph['edge_feat']).to(torch.int64)
y = torch.Tensor([homolumogap])
num_nodes = int(graph['num_nodes'])
data = Data(x, edge_index, edge_attr, y, num_nodes=num_nodes)
return data
# 获取数据集划分
def get_idx_split(self):
split_dict = replace_numpy_with_torchtensor(torch.load(osp.join(self.root, 'pcqm4m_kddcup2021/split_dict.pt')))
return split_dict
if __name__ == "__main__":
dataset = MyPCQM4MDataset('dataset2')
from torch_geometric.data import DataLoader
from tqdm import tqdm
dataloader = DataLoader(dataset, batch_size=256, shuffle=True, num_workers=4)
for batch in tqdm(dataloader):
pass
(1)在生成一个该数据集类的对象时,程序首先会检查指定的文件夹下是否存在data.csv.gz
文件,如果不在,则会执行download
方法,这一过程是在运行super
类的__init__
方法中发生的。
(2)然后程序继续执行__init__
方法的剩余部分,读取data.csv.gz
文件,获取存储图信息的smiles
格式的字符串,以及回归预测的目标homolumogap
。
我们将由smiles
格式的字符串转成图的过程在get()
方法中实现,这样我们在生成一个DataLoader
变量时,通过指定num_workers
可以实现并行执行生成多个图。
第二部分:图预测任务实践
将基于GIN的图表示学习神经网络(【GNN】task4-数据完整存储与内存的数据集类+节点预测与边预测任务实践),和上面我们自己定义的数据集来实现分子图的量子性质预测任务。
codes\gin_regression
文件夹
gin_conv.py文件:
gin_conv卷积层模块。
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
gin_graph.py文件:
这个是task6里面提到的模块首先采用GINNodeEmbedding模块对图上每一个节点做嵌入,然后对节点嵌入做池化得到图的嵌入,最后用一层线性变换得到图的最终的表示(graph representation)。
import torch
from torch import nn
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool, GlobalAttention, Set2Set
from 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"):
"""GIN Graph Pooling Module
此模块首先采用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
return torch.clamp(output, min=0, max=50)
main.py文件:
import os
import torch
import argparse
from tqdm import tqdm
from ogb.lsc import PCQM4MEvaluator
from torch_geometric.data import DataLoader
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR
from pcqm4m_data import MyPCQM4MDataset
from gin_graph import GINGraphPooling
from torch.utils.tensorboard import SummaryWriter
def parse_args():
parser = argparse.ArgumentParser(description='Graph data miming with GNN')
parser.add_argument('--task_name', type=str, default='GINGraphPooling',
help='task name')
parser.add_argument('--device', type=int, default=0,
help='which gpu to use if any (default: 0)')
parser.add_argument('--num_layers', type=int, default=5,
help='number of GNN message passing layers (default: 5)')
parser.add_argument('--graph_pooling', type=str, default='sum',
help='graph pooling strategy mean or sum (default: sum)')
parser.add_argument('--emb_dim', type=int, default=256,
help='dimensionality of hidden units in GNNs (default: 256)')
parser.add_argument('--drop_ratio', type=float, default=0.,
help='dropout ratio (default: 0.)')
parser.add_argument('--save_test', action='store_true')
parser.add_argument('--batch_size', type=int, default=512,
help='input batch size for training (default: 512)')
parser.add_argument('--epochs', type=int, default=100,
help='number of epochs to train (default: 100)')
parser.add_argument('--weight_decay', type=float, default=0.00001,
help='weight decay')
parser.add_argument('--early_stop', type=int, default=10,
help='early stop (default: 10)')
parser.add_argument('--num_workers', type=int, default=4,
help='number of workers (default: 4)')
parser.add_argument('--dataset_root', type=str, default="dataset",
help='dataset root')
args = parser.parse_args()
return args
def prepartion(args):
save_dir = os.path.join('saves', args.task_name)
if os.path.exists(save_dir):
for idx in range(1000):
if not os.path.exists(save_dir + '=' + str(idx)):
save_dir = save_dir + '=' + str(idx)
break
args.save_dir = save_dir
os.makedirs(args.save_dir, exist_ok=True)
args.device = torch.device("cuda:" + str(args.device)) if torch.cuda.is_available() else torch.device("cpu")
args.output_file = open(os.path.join(args.save_dir, 'output'), 'a')
print(args, file=args.output_file, flush=True)
def train(model, device, loader, optimizer, criterion_fn):
model.train()
loss_accum = 0
for step, batch in enumerate(tqdm(loader)):
batch = batch.to(device)
pred = model(batch).view(-1,)
optimizer.zero_grad()
loss = criterion_fn(pred, batch.y)
loss.backward()
optimizer.step()
loss_accum += loss.detach().cpu().item()
return loss_accum / (step + 1)
def eval(model, device, loader, evaluator):
model.eval()
y_true = []
y_pred = []
with torch.no_grad():
for _, batch in enumerate(tqdm(loader)):
batch = batch.to(device)
pred = model(batch).view(-1,)
y_true.append(batch.y.view(pred.shape).detach().cpu())
y_pred.append(pred.detach().cpu())
y_true = torch.cat(y_true, dim=0)
y_pred = torch.cat(y_pred, dim=0)
input_dict = {"y_true": y_true, "y_pred": y_pred}
return evaluator.eval(input_dict)["mae"]
def test(model, device, loader):
model.eval()
y_pred = []
with torch.no_grad():
for _, batch in enumerate(loader):
batch = batch.to(device)
pred = model(batch).view(-1,)
y_pred.append(pred.detach().cpu())
y_pred = torch.cat(y_pred, dim=0)
return y_pred
def main(args):
prepartion(args)
nn_params = {
'num_layers': args.num_layers,
'emb_dim': args.emb_dim,
'drop_ratio': args.drop_ratio,
'graph_pooling': args.graph_pooling
}
# automatic dataloading and splitting
dataset = MyPCQM4MDataset(root=args.dataset_root)
split_idx = dataset.get_idx_split()
train_data = dataset[split_idx['train']]
valid_data = dataset[split_idx['valid']]
test_data = dataset[split_idx['test']]
train_loader = DataLoader(train_data, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers)
valid_loader = DataLoader(valid_data, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers)
test_loader = DataLoader(test_data, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers)
# automatic evaluator. takes dataset name as input
evaluator = PCQM4MEvaluator()
criterion_fn = torch.nn.MSELoss()
device = args.device
model = GINGraphPooling(**nn_params).to(device)
num_params = sum(p.numel() for p in model.parameters())
print(f'#Params: {num_params}', file=args.output_file, flush=True)
print(model, file=args.output_file, flush=True)
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=args.weight_decay)
scheduler = StepLR(optimizer, step_size=30, gamma=0.25)
writer = SummaryWriter(log_dir=args.save_dir)
not_improved = 0
best_valid_mae = 9999
for epoch in range(1, args.epochs + 1):
print("=====Epoch {}".format(epoch), file=args.output_file, flush=True)
print('Training...', file=args.output_file, flush=True)
train_mae = train(model, device, train_loader, optimizer, criterion_fn)
print('Evaluating...', file=args.output_file, flush=True)
valid_mae = eval(model, device, valid_loader, evaluator)
print({'Train': train_mae, 'Validation': valid_mae}, file=args.output_file, flush=True)
writer.add_scalar('valid/mae', valid_mae, epoch)
writer.add_scalar('train/mae', train_mae, epoch)
if valid_mae < best_valid_mae:
best_valid_mae = valid_mae
if args.save_test:
print('Saving checkpoint...', file=args.output_file, flush=True)
checkpoint = {
'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(), 'best_val_mae': best_valid_mae, 'num_params': num_params
}
torch.save(checkpoint, os.path.join(args.save_dir, 'checkpoint.pt'))
print('Predicting on test data...', file=args.output_file, flush=True)
y_pred = test(model, device, test_loader)
print('Saving test submission file...', file=args.output_file, flush=True)
evaluator.save_test_submission({'y_pred': y_pred}, args.save_dir)
not_improved = 0
else:
not_improved += 1
if not_improved == args.early_stop:
print(f"Have not improved for {not_improved} epoches.", file=args.output_file, flush=True)
break
scheduler.step()
print(f'Best validation MAE so far: {best_valid_mae}', file=args.output_file, flush=True)
writer.close()
args.output_file.close()
if __name__ == "__main__":
args = parse_args()
main(args)
pcqm4m_data.py文件:
import os
import os.path as osp
import pandas as pd
import torch
from ogb.utils import smiles2graph
from ogb.utils.torch_util import replace_numpy_with_torchtensor
from ogb.utils.url import download_url, extract_zip
from rdkit import RDLogger
from torch_geometric.data import Data, Dataset
import shutil
RDLogger.DisableLog('rdApp.*')
class MyPCQM4MDataset(Dataset):
def __init__(self, root):
self.url = 'https://dgl-data.s3-accelerate.amazonaws.com/dataset/OGB-LSC/pcqm4m_kddcup2021.zip'
super(MyPCQM4MDataset, self).__init__(root)
filepath = osp.join(root, 'raw/data.csv.gz')
data_df = pd.read_csv(filepath)
self.smiles_list = data_df['smiles']
self.homolumogap_list = data_df['homolumogap']
@property
def raw_file_names(self):
return 'data.csv.gz'
def download(self):
path = download_url(self.url, self.root)
extract_zip(path, self.root)
os.unlink(path)
shutil.move(osp.join(self.root, 'pcqm4m_kddcup2021/raw/data.csv.gz'), osp.join(self.root, 'raw/data.csv.gz'))
def len(self):
return len(self.smiles_list)
def get(self, idx):
smiles, homolumogap = self.smiles_list[idx], self.homolumogap_list[idx]
graph = smiles2graph(smiles)
assert(len(graph['edge_feat']) == graph['edge_index'].shape[1])
assert(len(graph['node_feat']) == graph['num_nodes'])
x = torch.from_numpy(graph['node_feat']).to(torch.int64)
edge_index = torch.from_numpy(graph['edge_index']).to(torch.int64)
edge_attr = torch.from_numpy(graph['edge_feat']).to(torch.int64)
y = torch.Tensor([homolumogap])
num_nodes = int(graph['num_nodes'])
data = Data(x, edge_index, edge_attr, y, num_nodes=num_nodes)
return data
def get_idx_split(self):
split_dict = replace_numpy_with_torchtensor(torch.load(osp.join(self.root, 'pcqm4m_kddcup2021/split_dict.pt')))
return split_dict
if __name__ == "__main__":
dataset = MyPCQM4MDataset('dataset')
from torch_geometric.data import DataLoader
from tqdm import tqdm
dataloader = DataLoader(dataset, batch_size=256, shuffle=True, num_workers=4)
for batch in tqdm(dataloader):
pass
通过试验寻找最佳超参数
通过运行以下的命令即可运行一次试验:
#!/bin/sh
python main.py --task_name GINGraphPooling\ # 为当前试验取名
--device 0\
--num_layers 5\ # 使用GINConv层数
--graph_pooling sum\ # 图读出方法
--emb_dim 256\ # 节点嵌入维度
--drop_ratio 0.\
--save_test\ # 是否对测试集做预测并保留预测结果
--batch_size 512\
--epochs 100\
--weight_decay 0.00001\
--early_stop 10\ # 当有`early_stop`个epoches验证集结果没有提升,则停止训练
--num_workers 4\
--dataset_root dataset # 存放数据集的根目录
试验运行开始后,程序会在saves
目录下创建一个task_name
参数指定名称的文件夹用于记录试验过程,当saves
目录下已经有一个同名的文件夹时,程序会在task_name
参数末尾增加一个后缀作为文件夹名称。试验运行过程中,所有的print
输出都会写入到试验文件夹下的output
文件,tensorboard.SummaryWriter
记录的信息也存储在试验文件夹下的文件中。
修改上方的命令再执行,即可试验不同的超参数,所有试验的过程与结果信息都存储于saves
文件夹下。启动TensorBoard
会话,选择saves
文件夹,即可查看所有试验的过程与结果信息。
使用TensorBoard
TensorBoard作用:可以把复杂的神经网络训练过程给可视化,可以更好地理解调试 优化程序。
基于Anaconda可视化tensorboard的步骤:
(1)打开anaconda prompt
(2)打开tensorflow环境,运行Spyder IDE ,执行代码
(3)再打开anaconda prompt
(4)运行tensorflow环境
(5)运行tensorboard --logdir=路径<必须是完全路径>
(6)将终端的网址复制 - 在浏览器中打开
设备不行(其实是脑子不行),用了天国之影大佬跑了5个钟得到的四个文件,在anaconda prompt
中运行tensorboard --logdir=D:\桌面文件\GINGraphPooling\GINGraphPooling
后:
复制其中链接到浏览器得到得到可视化结果,我将Smoothing设置为0.723,Horizontal Axis
设置为RELATIVE(STEP: 迭代步长;RELATIVE: 相对时间(小时,相对于起始点) ; WALL:运行时间(小时) )
train
valid
作业
进行不同超参数的试验,通过观察不同试验的过程与结果信息的差别分析不同超参数对图预测任务的影响。
超参数也是一种参数,它具有参数的特性,比如未知,也就是它不是一个已知常量。
是一种手工可配置的设置,需要为它根据已有或现有的经验,指定“正确”的值,也就是人为为它设定一个值,它不是通过系统学习得到的。
举例:聚类中类的个数、话题模型中话题的数量、模型的学习率、深层神经网络隐藏层数、树的数量或树的深度、矩阵分解中潜在因素的数量、k均值聚类中的簇数
Reference
Dataset
类官方文档:torch_geometric.data.Dataset
- 将图样本封装成批(BATCHING):ADVANCED MINI-BATCHING
- 分子图的量子特性回归数据集:PCQM4M-LSC
- Get Started | Open Graph Benchmark (stanford.edu)
- datawhale course:https://github.com/datawhalechina/team-learning-nlp/tree/master/GNN
- 基于Anaconda的tensorboard可视化
- KDD CUP2021首届图神经网络大赛放榜,百度飞桨PGL获得2金1银
- 天国之影大佬跑了5个钟得到的四个文件