示例介绍

我们通过自包含的示例简要介绍的基本概念。

有关图机器学习的介绍,我们建议感兴趣的读者参考 斯坦福CS224W:图机器学习讲座。 对于的互动介绍,我们推荐我们精心策划的 Google Colab笔记本。

其核心是, 提供了以下主要功能:

图的数据处理

图用于建模对象(节点)之间的成对关系(边)。在中,单个图由torch_geometric.data.Data的实例描述,默认情况下包含以下属性:

  • data.x: 节点特征矩阵,形状为 [num_nodes, num_node_features]

  • data.edge_index: 图形连接性在COO格式中,形状为[2, num_edges],类型为torch.long

  • data.edge_attr: 边特征矩阵,形状为 [num_edges, num_edge_features]

  • data.y: 训练目标(可能具有任意形状),例如,形状为 [num_nodes, *] 的节点级目标或形状为 [1, *] 的图级目标

  • data.pos: 节点位置矩阵,形状为 [num_nodes, num_dimensions]

这些属性都不是必需的。 事实上,Data 对象甚至不限于这些属性。 我们可以,例如,通过 data.face 来扩展它,以保存来自3D网格的三角形连接性,存储在一个形状为 [3, num_faces] 且类型为 torch.long 的张量中。

注意

torchvision 将一个示例定义为一个图像和一个目标的元组。 我们在 中省略了这种表示法,以便以清晰易懂的方式允许各种数据结构。

我们展示了一个简单的无权无向图的例子,该图包含三个节点和四条边。 每个节点恰好包含一个特征:

import torch
from torch_geometric.data import Data

edge_index = torch.tensor([[0, 1, 1, 2],
                           [1, 0, 2, 1]], dtype=torch.long)
x = torch.tensor([[-1], [0], [1]], dtype=torch.float)

data = Data(x=x, edge_index=edge_index)
>>> Data(edge_index=[2, 4], x=[3, 1])
../_images/graph.svg

请注意,edge_index,即定义所有边的源节点和目标节点的张量,不是索引元组的列表。 如果你想以这种方式编写索引,你应该在将它们传递给数据构造函数之前对其进行转置并调用contiguous

import torch
from torch_geometric.data import Data

edge_index = torch.tensor([[0, 1],
                           [1, 0],
                           [1, 2],
                           [2, 1]], dtype=torch.long)
x = torch.tensor([[-1], [0], [1]], dtype=torch.float)

data = Data(x=x, edge_index=edge_index.t().contiguous())
>>> Data(edge_index=[2, 4], x=[3, 1])

尽管图中只有两条边,我们需要定义四个索引元组来考虑边的两个方向。

注意

您可以随时打印出您的数据对象,并接收关于其属性及其形状的简短信息。

请注意,edge_index中的元素必须仅包含在{ 0, ..., num_nodes - 1}范围内的索引。 这是必要的,因为我们希望最终的数据表示尽可能紧凑,例如,我们希望通过x[0]x[1]分别索引第一条边(0, 1)的源节点和目标节点特征。 您可以通过运行validate()来检查您的最终Data对象是否满足这些要求:

data.validate(raise_on_error=True)

除了包含许多节点级别、边级别或图级别的属性外,Data 还提供了许多有用的实用函数,例如

print(data.keys())
>>> ['x', 'edge_index']

print(data['x'])
>>> tensor([[-1.0],
            [0.0],
            [1.0]])

for key, item in data:
    print(f'{key} found in data')
>>> 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.has_isolated_nodes()
>>> False

data.has_self_loops()
>>> False

data.is_directed()
>>> False

# Transfer data object to GPU.
device = torch.device('cuda')
data = data.to(device)

你可以在torch_geometric.data.Data找到所有方法的完整列表。

常见的基准数据集

包含了大量常见的基准数据集,例如,所有的Planetoid数据集(Cora、Citeseer、Pubmed),来自TUDatasets的所有图分类数据集及其清理版本,QM7和QM9数据集,以及一些3D网格/点云数据集,如FAUST、ModelNet10/40和ShapeNet。

初始化数据集非常简单。 数据集的初始化将自动下载其原始文件并将其处理为之前描述的Data格式。 例如,要加载ENZYMES数据集(包含6个类别中的600个图),请输入:

from torch_geometric.datasets import TUDataset

dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')
>>> ENZYMES(600)

len(dataset)
>>> 600

dataset.num_classes
>>> 6

dataset.num_node_features
>>> 3

我们现在可以访问数据集中的所有600个图表:

data = dataset[0]
>>> Data(edge_index=[2, 168], x=[37, 3], y=[1])

data.is_undirected()
>>> True

我们可以看到,数据集中的第一个图包含37个节点,每个节点有3个特征。 有168/2 = 84条无向边,并且该图被分配到一个类别。 此外,数据对象正好持有一个图级目标。

我们甚至可以使用切片、长整型或布尔张量来分割数据集。 例如,要创建一个90/10的训练/测试分割,输入:

train_dataset = dataset[:540]
>>> ENZYMES(540)

test_dataset = dataset[540:]
>>> ENZYMES(60)

如果您不确定数据集在拆分之前是否已经打乱,您可以通过运行以下命令来随机排列它:

dataset = dataset.shuffle()
>>> ENZYMES(600)

这相当于做:

perm = torch.randperm(len(dataset))
dataset = dataset[perm]
>> ENZYMES(600)

让我们再试一个!让我们下载Cora,这是半监督图节点分类的标准基准数据集:

from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/tmp/Cora', name='Cora')
>>> Cora()

len(dataset)
>>> 1

dataset.num_classes
>>> 7

dataset.num_node_features
>>> 1433

这里,数据集仅包含一个无向引用图:

data = dataset[0]
>>> Data(edge_index=[2, 10556], test_mask=[2708],
         train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])

data.is_undirected()
>>> True

data.train_mask.sum().item()
>>> 140

data.val_mask.sum().item()
>>> 500

data.test_mask.sum().item()
>>> 1000

这次,Data 对象为每个节点保存了一个标签,以及额外的节点级属性:train_maskval_masktest_mask,其中

  • train_mask 表示针对哪些节点进行训练(140个节点),

  • val_mask 表示哪些节点用于验证,例如,用于执行早期停止(500个节点),

  • test_mask 表示要测试哪些节点(1000个节点)。

小批量

神经网络通常以批处理的方式进行训练。 通过创建稀疏块对角邻接矩阵(由 edge_index 定义)并在节点维度上连接特征和目标矩阵来实现小批量并行化。 这种组合允许在一个批次中的不同示例具有不同数量的节点和边:

\[\begin{split}\mathbf{A} = \begin{bmatrix} \mathbf{A}_1 & & \\ & \ddots & \\ & & \mathbf{A}_n \end{bmatrix}, \qquad \mathbf{X} = \begin{bmatrix} \mathbf{X}_1 \\ \vdots \\ \mathbf{X}_n \end{bmatrix}, \qquad \mathbf{Y} = \begin{bmatrix} \mathbf{Y}_1 \\ \vdots \\ \mathbf{Y}_n \end{bmatrix}\end{split}\]

包含其自己的 torch_geometric.loader.DataLoader,它已经处理了这个连接过程。 让我们通过一个例子来了解它:

from torch_geometric.datasets import TUDataset
from torch_geometric.loader 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
    >>> DataBatch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32])

    batch.num_graphs
    >>> 32

torch_geometric.data.Batch 继承自 torch_geometric.data.Data 并且包含一个额外的属性叫做 batch

batch 是一个列向量,它将每个节点映射到批次中各自的图:

\[\mathrm{batch} = {\begin{bmatrix} 0 & \cdots & 0 & 1 & \cdots & n - 2 & n -1 & \cdots & n - 1 \end{bmatrix}}^{\top}\]

你可以使用它来,例如,在节点维度上为每个图单独平均节点特征:

from torch_geometric.utils import scatter
from torch_geometric.datasets import TUDataset
from torch_geometric.loader 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
    >>> DataBatch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32])

    data.num_graphs
    >>> 32

    x = scatter(data.x, data.batch, dim=0, reduce='mean')
    x.size()
    >>> torch.Size([32, 21])

您可以了解更多关于内部批处理程序的信息,例如,如何修改其行为,这里。 有关分散操作的文档,我们建议感兴趣的读者查阅torch_scatter文档

数据转换

torchvision中,变换是处理图像和执行增强的常见方式。 自带其自己的变换,这些变换期望一个Data对象作为输入,并返回一个新的变换后的Data对象。 变换可以使用torch_geometric.transforms.Compose链接在一起,并在将处理后的数据集保存到磁盘之前(pre_transform)或在访问数据集中的图之前(transform)应用。

让我们来看一个例子,我们在ShapeNet数据集上应用变换(包含17,000个3D形状点云和来自16个形状类别的每个点的标签)。

from torch_geometric.datasets import ShapeNet

dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'])

dataset[0]
>>> Data(pos=[2518, 3], y=[2518])

我们可以通过从点云生成最近邻图来将点云数据集转换为图数据集:

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])

注意

我们使用pre_transform在将数据保存到磁盘之前进行转换(从而加快加载时间)。 请注意,下次初始化数据集时,即使您不传递任何转换,它也将包含图边。 如果pre_transform与已处理数据集中的不匹配,您将收到警告。

此外,我们可以使用transform参数来随机增强一个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.RandomJitter(0.01))

dataset[0]
>>> Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])

你可以在 torch_geometric.transforms 找到所有已实现变换的完整列表。

图上的学习方法

在学习了中的数据操作、数据集、加载器和转换之后,是时候实现我们的第一个图神经网络了!

我们将使用一个简单的GCN层并在Cora引用数据集上重复实验。 有关GCN的高级解释,请查看其博客文章

我们首先需要加载Cora数据集:

from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/tmp/Cora', name='Cora')
>>> Cora()

注意,我们不需要使用转换或数据加载器。 现在让我们实现一个两层的GCN:

import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self):
        super().__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)

构造函数定义了两个GCNConv层,这些层在我们网络的前向传播中被调用。 请注意,非线性并未集成在conv调用中,因此需要在之后应用(这在中的所有操作符中是一致的)。 在这里,我们选择使用ReLU作为中间非线性,并最终输出类别的softmax分布。 让我们在训练节点上训练这个模型200个周期:

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)

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()

最后,我们可以在测试节点上评估我们的模型:

model.eval()
pred = model(data).argmax(dim=1)
correct = (pred[data.test_mask] == data.y[data.test_mask]).sum()
acc = int(correct) / int(data.test_mask.sum())
print(f'Accuracy: {acc:.4f}')
>>> Accuracy: 0.8150

这就是实现你的第一个图神经网络所需的全部内容。 了解更多关于图神经网络的最简单方法是研究examples/目录中的示例,并浏览torch_geometric.nn。 祝你编程愉快!

Exercises

  1. edge_index.t().contiguous() 是做什么的?

  2. TUDataset基准套件中加载"IMDB-BINARY"数据集,并随机将其分割为80%/10%/10%的训练、验证和测试图。

  3. 以下输出的每个数字代表什么意思?

    print(batch)
    >>> DataBatch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32])