图被用于目标(节点)间的成对关系(边)的建模。py geomatric的一个图被描述为torch_geomatric.data.Data类的一个实例。 该类具有以下默认属性: (1)data.x:节点特征矩阵(大小:节点数 × \times × 节点特征维度;每行为一个节点的特征) (2)data.edge_index:节点的邻接矩阵,(COO格式的稀疏矩阵,矩阵类型为long,大小:2 × \times ×边数),边的数目是按无向图计算的,因此,是有向图边数的2倍。 (3)data.edge_attr:边的属性矩阵(大小:边数 × \times ×每条边的特征维度) (4)data.y:训练目标,可以为任意形状(节点级目标,大小:节点数$\times$1;图级别任务,大小:1*1 (5)data.pos:节点位置矩阵(大小:节点数 × \times ×位置的维度) 所有属性都不是必须的,属性也不限于这5种。例如,为了表示3D mesh,可扩展一个data.face属性,保存够成曲面的三角形的连接信息。(可构建为long型矩阵,大小:3 × \times ×面数,矩阵每行代表一个三角形面片)
(1)3个节点,4条边的无权重无向图。
import torch from torch_geometric.data import Data # 节点特征:节点数*节点特征维度,这里节点特征为1维。 x = torch.tensor([[-1], [0], [1]], dtype=torch.float) # 节点的邻接信息。可看作边(节点组成的元组)组成的list edge_index = torch.tensor([[0, 1], [1, 0], [1, 2], [2, 1]], dtype=torch.long) # 具有两个属性(节点特征,邻接矩阵)的图 data = Data(x=x, edge_index=edge_index.t().contiguous()) >>> Data(edge_index=[2, 4], x=[3, 1])(2)Data类定义的一些常见效用函数:属性列表、节点数、边数、节点特征维度、是否包含孤立节点、是否包含自环、是否有向等。 完整函数列表在torch_geometric.data.Data。
print(data.keys) >>> ['x', 'edge_index'] print(data['x']) >>> tensor([[-1.0], [0.0], [1.0]]) for key, item in data: print("{} found in data".format(key)) >>> x found in data >>> edge_index found in data 'edge_attr' in data >>> False data.num_nodes >>> 3 data.num_edges >>> 4 data.num_node_features >>> 1 data.contains_isolated_nodes() >>> False data.contains_self_loops() >>> False data.is_directed() >>> False # Transfer data object to GPU. device = torch.device('cuda') data = data.to(device)所有Planetoid数据集(Cora,Citeseer,PubMed), http://graphkernels.cs.tu-dortmund.de的所有数据集及其clean版本, QM7和QM9数据集, 3D 网格或点云数据集(如FAUST、ModelNet10/40、ShapeNet)。 (1) 数据集初始化:自动下载原始数据并将其处理为1提到的Data类格式。 例子1:初始化ENZYMES数据集(600个图,6类图)
from torch_geometric.datasets import TUDataset dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')可直接获取数据集每个Data,即图
data = dataset[0] >>> Data(edge_index=[2, 168], x=[37, 3], y=[1])可以使用切片对数据集划分,如将数据集划分为90/10的训练集/测试集。
# 600*0.9=540 train_num=len(dataset)*0.9 train_dataset = dataset[:train_num] >>> ENZYMES(540) test_dataset = dataset[train_num:] >>> ENZYMES(60)数据集打乱
dataset = dataset.shuffle() >>> ENZYMES(600)例子2:初始化Cora数据集(一个图,2708个节点,7类节点标签)
from torch_geometric.datasets import Planetoid dataset = Planetoid(root='/tmp/Cora', name='Cora') >>> Cora() len(dataset) >>> 1 > dataset.num_classes >>> 7 data = dataset[0] >>> Data(edge_index=[2, 10556], test_mask=[2708], train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708]) # 训练节点数 data.train_mask.sum().item() >>> 140 # 验证节点数 data.val_mask.sum().item() >>> 500 # 测试节点数 data.test_mask.sum().item() >>> 1000Cora类Data对象每个节点都有类别,额外多了test_mask、train_mask、val_mask属性,表示哪些是测试节点、训练节点、验证节点。
神经网络通常按batch训练,PyGeometric通过创建稀疏块对角线邻接矩阵(由edge_index定义),在节点维度上拼接batch内的所有节点特征矩阵和目标矩阵,从而实现了batch内并行化。这种拼接允许一个batch中的图实例具有不同数量的节点和边: 这里Ai,Xi,Yi为batch内第i个图的邻接矩阵属性、节点属性、目标属性。 PyGeometric有自己的 torch_geometric.data.DataLoader类,该类内已将batch内的拼接过程进行了处理。
from torch_geometric.datasets import TUDataset from torch_geometric.data import DataLoader dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES', use_node_attr=True) loader = DataLoader(dataset, batch_size=32, shuffle=True) for batch in loader: batch >>> Batch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32]) batch.num_graphs >>> 32torch_geometric.data.Batch类继承自torch_geometric.data.Data包含一个额外属性batch。batch为一个列向量,第i个元素用于表示batch内的第i个节点属于哪个图。 可以使用batch属性对batch内每个图的所有节点特征求平均。
from torch_scatter import scatter_mean from torch_geometric.datasets import TUDataset from torch_geometric.data import DataLoader dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES', use_node_attr=True) loader = DataLoader(dataset, batch_size=32, shuffle=True) for data in loader: data >>> Batch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32]) data.num_graphs >>> 32 x = scatter_mean(data.x, data.batch, dim=0) x.size() # 共32个图,每个图的平均特征为21维 >>> torch.Size([32, 21])与torchvision类似,torch_geometric.transfoems输入一个Data对象,输出一个变换后的Data对象。torch_geometric.transfoems.Compose对一列tansform操作进行组合。transform操作应在处理后的数据集保存在磁盘之前(pre_transform)或访问数据集中的图之前(transform)使用。 例子1:ShaneNet点云数据集(17000个3D点云,点的标签有16类)为例。 (1)通过变换transforms生成最近邻图将点云数据集处理为图数据集
import torch_geometric.transforms as T from torch_geometric.datasets import ShapeNet dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'], pre_transform=T.KNNGraph(k=6)) dataset[0] >>> Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])Note: 在将数据保存到磁盘之前,使用pre_transform进行了转换,可缩短加载时间。下次数据集初始化时,不用使用transform就已经包含了图的边。 (2)通过变换扩增Data对象,如随机平移每个借点的位置一点点。
import torch_geometric.transforms as T from torch_geometric.datasets import ShapeNet dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'], pre_transform=T.KNNGraph(k=6), transform=T.RandomTranslate(0.01)) dataset[0] >>> Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])所有变换列表可在torch_geometirc.transforms找到。
以Cora数据集为例,构建一个简单的两个GCN。 (1)加载数据集,由于Cora只有一个图,不需要Dataloader
from torch_geometric.datasets import Planetoid dataset = Planetoid(root='/tmp/Cora', name='Cora')(2)GCN实现
import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv class Net(torch.nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = GCNConv(dataset.num_node_features, 16) self.conv2 = GCNConv(16, dataset.num_classes) def forward(self, data): x, edge_index = data.x, data.edge_index x = self.conv1(x, edge_index) x = F.relu(x) x = F.dropout(x, training=self.training) x = self.conv2(x, edge_index) return F.log_softmax(x, dim=1)(3)在训练节点(data.test_mask)上训练网络
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = Net().to(device) data = dataset[0].to(device) optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) model.train() for epoch in range(200): optimizer.zero_grad() out = model(data) loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step()(4)在测试节点上评估模型性能
model.eval() _, pred = model(data).max(dim=1) correct = int(pred[data.test_mask].eq(data.y[data.test_mask]).sum().item()) acc = correct / int(data.test_mask.sum()) print('Accuracy: {:.4f}'.format(acc)) >>> Accuracy: 0.8150