使用 DGL 进行节点分类

GNN 是用于图上许多机器学习任务的强大工具。在本入门教程中,你将学习使用 GNN 进行节点分类的基本工作流程,即预测图中的节点类别。

完成本教程后,你将能够:

  • 加载 DGL 提供的数据集。

  • 使用 DGL 提供的神经网络模块构建 GNN 模型。

  • 在 CPU 或 GPU 上训练和评估用于节点分类的 GNN 模型。

本教程假设你具有使用 PyTorch 构建神经网络的经验。

(预估时间:13 分钟)

import os

os.environ["DGLBACKEND"] = "pytorch"
import dgl
import dgl.data
import torch
import torch.nn as nn
import torch.nn.functional as F

GNN 节点分类概述

图数据上最流行且广泛采用的任务之一是节点分类,其中模型需要预测每个节点的真实类别。在图神经网络出现之前,许多提出的方法要么只使用连接性(如 DeepWalk 或 node2vec),要么简单地组合连接性和节点自身的特征。相比之下,GNN 提供了一个机会,可以通过组合连接性和 局部邻居 的特征来获得节点表示。

Kipf 等人的工作就是一个例子,它将节点分类问题表述为半监督节点分类任务。借助少量标记节点,图神经网络 (GNN) 可以准确预测其他节点的类别。

本教程将展示如何在 Cora 数据集上构建这样的 GNN 进行半监督节点分类,该数据集是一个引文网络,其中论文是节点,引文是边,并且只使用少量标记节点。任务是预测给定论文的类别。正如论文第 5.2 节所述,每篇论文节点都包含一个词频向量作为其特征,并经过归一化,使其总和为一。

加载 Cora 数据集

dataset = dgl.data.CoraGraphDataset()
print(f"Number of categories: {dataset.num_classes}")
Downloading /root/.dgl/cora_v2.zip from https://data.dgl.ai/dataset/cora_v2.zip...

/root/.dgl/cora_v2.zip:   0%|          | 0.00/132k [00:00<?, ?B/s]
/root/.dgl/cora_v2.zip: 100%|██████████| 132k/132k [00:00<00:00, 7.74MB/s]
Extracting file to /root/.dgl/cora_v2_d697a464
Finished data loading and preprocessing.
  NumNodes: 2708
  NumEdges: 10556
  NumFeats: 1433
  NumClasses: 7
  NumTrainingSamples: 140
  NumValidationSamples: 500
  NumTestSamples: 1000
Done saving data into cached files.
Number of categories: 7

一个 DGL Dataset 对象可以包含一个或多个图。本教程中使用的 Cora 数据集只包含一个图。

g = dataset[0]

DGL 图可以在两个类似字典的属性 ndataedata 中存储节点特征和边特征。在 DGL Cora 数据集中,图包含以下节点特征:

  • train_mask: 一个布尔张量,指示节点是否在训练集中。

  • val_mask: 一个布尔张量,指示节点是否在验证集中。

  • test_mask: 一个布尔张量,指示节点是否在测试集中。

  • label: 节点的真实类别。

  • feat: 节点特征。

print("Node features")
print(g.ndata)
print("Edge features")
print(g.edata)
Node features
{'train_mask': tensor([ True,  True,  True,  ..., False, False, False]), 'val_mask': tensor([False, False, False,  ..., False, False, False]), 'test_mask': tensor([False, False, False,  ...,  True,  True,  True]), 'label': tensor([3, 4, 4,  ..., 3, 3, 3]), 'feat': tensor([[0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        ...,
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.]])}
Edge features
{}

定义图卷积网络 (GCN)

本教程将构建一个两层图卷积网络 (GCN)。每一层通过聚合邻居信息来计算新的节点表示。

要构建多层 GCN,你可以简单地堆叠 dgl.nn.GraphConv 模块,这些模块继承自 torch.nn.Module

from dgl.nn import GraphConv


class GCN(nn.Module):
    def __init__(self, in_feats, h_feats, num_classes):
        super(GCN, self).__init__()
        self.conv1 = GraphConv(in_feats, h_feats)
        self.conv2 = GraphConv(h_feats, num_classes)

    def forward(self, g, in_feat):
        h = self.conv1(g, in_feat)
        h = F.relu(h)
        h = self.conv2(g, h)
        return h


# Create the model with given dimensions
model = GCN(g.ndata["feat"].shape[1], 16, dataset.num_classes)

DGL 提供了许多流行邻居聚合模块的实现。你只需一行代码即可轻松调用它们。

训练 GCN

训练这个 GCN 与训练其他 PyTorch 神经网络类似。

def train(g, model):
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    best_val_acc = 0
    best_test_acc = 0

    features = g.ndata["feat"]
    labels = g.ndata["label"]
    train_mask = g.ndata["train_mask"]
    val_mask = g.ndata["val_mask"]
    test_mask = g.ndata["test_mask"]
    for e in range(100):
        # Forward
        logits = model(g, features)

        # Compute prediction
        pred = logits.argmax(1)

        # Compute loss
        # Note that you should only compute the losses of the nodes in the training set.
        loss = F.cross_entropy(logits[train_mask], labels[train_mask])

        # Compute accuracy on training/validation/test
        train_acc = (pred[train_mask] == labels[train_mask]).float().mean()
        val_acc = (pred[val_mask] == labels[val_mask]).float().mean()
        test_acc = (pred[test_mask] == labels[test_mask]).float().mean()

        # Save the best validation accuracy and the corresponding test accuracy.
        if best_val_acc < val_acc:
            best_val_acc = val_acc
            best_test_acc = test_acc

        # Backward
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if e % 5 == 0:
            print(
                f"In epoch {e}, loss: {loss:.3f}, val acc: {val_acc:.3f} (best {best_val_acc:.3f}), test acc: {test_acc:.3f} (best {best_test_acc:.3f})"
            )


model = GCN(g.ndata["feat"].shape[1], 16, dataset.num_classes)
train(g, model)
In epoch 0, loss: 1.945, val acc: 0.122 (best 0.122), test acc: 0.160 (best 0.160)
In epoch 5, loss: 1.888, val acc: 0.560 (best 0.560), test acc: 0.545 (best 0.545)
In epoch 10, loss: 1.803, val acc: 0.618 (best 0.632), test acc: 0.623 (best 0.620)
In epoch 15, loss: 1.695, val acc: 0.652 (best 0.652), test acc: 0.639 (best 0.639)
In epoch 20, loss: 1.563, val acc: 0.678 (best 0.680), test acc: 0.676 (best 0.668)
In epoch 25, loss: 1.411, val acc: 0.708 (best 0.708), test acc: 0.700 (best 0.700)
In epoch 30, loss: 1.246, val acc: 0.714 (best 0.714), test acc: 0.713 (best 0.709)
In epoch 35, loss: 1.074, val acc: 0.728 (best 0.728), test acc: 0.742 (best 0.742)
In epoch 40, loss: 0.905, val acc: 0.740 (best 0.742), test acc: 0.752 (best 0.748)
In epoch 45, loss: 0.749, val acc: 0.750 (best 0.750), test acc: 0.759 (best 0.759)
In epoch 50, loss: 0.612, val acc: 0.752 (best 0.752), test acc: 0.766 (best 0.760)
In epoch 55, loss: 0.495, val acc: 0.748 (best 0.752), test acc: 0.768 (best 0.760)
In epoch 60, loss: 0.400, val acc: 0.760 (best 0.760), test acc: 0.767 (best 0.766)
In epoch 65, loss: 0.324, val acc: 0.762 (best 0.762), test acc: 0.771 (best 0.769)
In epoch 70, loss: 0.263, val acc: 0.768 (best 0.768), test acc: 0.776 (best 0.773)
In epoch 75, loss: 0.216, val acc: 0.766 (best 0.768), test acc: 0.776 (best 0.773)
In epoch 80, loss: 0.179, val acc: 0.764 (best 0.768), test acc: 0.771 (best 0.773)
In epoch 85, loss: 0.149, val acc: 0.764 (best 0.768), test acc: 0.769 (best 0.773)
In epoch 90, loss: 0.126, val acc: 0.766 (best 0.768), test acc: 0.769 (best 0.773)
In epoch 95, loss: 0.108, val acc: 0.764 (best 0.768), test acc: 0.768 (best 0.773)

在 GPU 上训练

在 GPU 上训练需要使用 to 方法将模型和图都放到 GPU 上,这与你在 PyTorch 中的操作类似。

g = g.to('cuda')
model = GCN(g.ndata['feat'].shape[1], 16, dataset.num_classes).to('cuda')
train(g, model)

下一步是什么?

# Thumbnail credits: Stanford CS224W Notes
# sphinx_gallery_thumbnail_path = '_static/blitz_1_introduction.png'

脚本总运行时间: (0 分 2.076 秒)

Sphinx-Gallery 生成的图集