PyTorch Geometric : 例題によるイントロダクション

PyTorch Geometric : 例題によるイントロダクション (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 06/07/2019

* 本ページは、PyTorch Geometric のドキュメント Introduction by example を翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

PyTorch Geometric : 例題によるイントロダクション

自己充足的なサンプルを通して PyTorch Geometric の基本概念を簡単に紹介します。そのコアで、PyTorch Geometric は次の主要な特徴を提供します :

  • グラフのデータ処理
  • 一般的なベンチマーク・データセット
  • ミニバッチ
  • データ変換
  • グラフ上の学習メソッド

 

グラフのデータ処理

グラフはオブジェクト (ノード) 間の pairwise な関係 (エッジ) をモデル化するために使用されます。PyTorch Geometric の単一グラフは torch_geometric.data.Data のインスタンスにより記述され、これはデフォルトで次の属性を保持します :

  • data.x: shape [num_nodes, num_node_features] を持つ特徴行列
  • data.edge_index: shape [2, num_edges] と型 torch.long を持つ COO フォーマットによるグラフ連結度
  • data.edge_attr: shape [num_edges, num_edge_features] によるエッジ特徴行列
  • data.y: 訓練するためのターゲット (任意の shape を持つでしょう)
  • data.pos: [num_nodes, num_dimensions] を持つノード位置行列

これらの属性はどれも必須ではありません。実際に、Data オブジェクトはこれらの属性に制限さえされません。例えば、shape [3, num_faces] と型 torch.long を持つ tensor で 3D メッシュから三角形の連結をセーブするための data.face でそれを拡張できます。

Note : PyTorch と torchvision は画像とターゲットのタプルとしてサンプルを定義します。PyTorch Geometric ではクリーンで理解可能な方法で様々なデータ構造を可能にするためにこの記法を omit します。

3 つのノードと 4 つのエッジを持つ非重み付き (= unweighted) そして無向グラフの単純な例を示します。各ノードは正確に一つの特徴を含みます :

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(x=[3, 1], edge_index=[2, 4])

edge_index, i.e. 総てのエッジのソースとターゲットノードを定義する tensor はインデックスのタプルのリストでは ない ことに注意してください。インデックスをこのように書くことを望むのであれば、それらを data コンストラクタに渡す前に transpose してその上で 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(x=[3, 1], edge_index=[2, 4])

グラフは 2 つのエッジだけを持ちますが、エッジの両方の方向を説明するために 4 つのインデックス・タプルを定義する必要があります。

Note : データ・オブジェクトをいつでも出力表示できてその属性と shape について短い情報を受け取ることができます。

平易な古い python オブジェクトである他に、torch_geometric.data.Data は幾つかのユティリティ関数を提供します、e.g. :

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

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

for key, item in data:
    print(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_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)

torch_geometric.data.Data で総てのメソッドの完全なリストを見つけることができます。

 

共通ベンチマーク・データセット

PyTorch Geometric は巨大な数の共通のベンチマーク・データセットを含みます、e.g. 総ての Planetoid データセット (Cora, Citeseer, Pubmed)、http://graphkernels.cs.tu-dortmund.de/ からの総てのグラフ分類データセット、QM7 と QM9 データセットそして FAUST, ModelNet10/40 と ShapeNet のようなひと握りの 3D メッシュ/ポイントクラウド・データセットです。

データセットの初期化は率直です。データセットの初期化は自動的にその生ファイルをダウンロードしてそれらを前に説明したデータフォーマットへと処理します。E.g., (6 クラスの 600 グラフから成る) ENZYMES データセットをロードするためには、以下をタイプします :

from torch_geometric.datasets import TUDataset

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

len(dataset)
>>> 600

dataset.num_classes
>>> 6

dataset.num_features
>>> 21

今ではデータセットの総ての 600 グラフへのアクセスを持ちます :

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

data.is_undirected()
>>> True

データセットの最初のグラフは 37 ノードを含み、各一つは 21 特徴を持つことが見れます。168/2 = 84 無向エッジがありグラフは正確に一つのクラスに割り当てられます。

データセットを分割するためにスライス, long or byte tensor を使用することさえできます。E.g., 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_features
>>> 1433

ここで、データセットは単一の、無向引用 (= citation) グラフを含みます :

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

data.is_undirected()
>>> True

data.train_mask.sum()
>>> 140

data.val_mask.sum()
>>> 500

data.test_mask.sum()
>>> 1000

今度は、Data オブジェクトは追加の属性を保持します : train_mask, val_mask と test_mask です :

  • train_mask はどのノードに対して訓練するかを表します (140 ノード)
  • val_mask はどのノードを検証のために使用するかを表します、e.g. early stopping を遂行するために (500 ノード)
  • test_mask はどのノードに対してテストするかを表します (1000 ノード)

 

ミニバッチ

ニューラルネットワークは通常はバッチ的な流儀で訓練されます。PyTorch Geometric はミニバッチに渡る並列処理を (edge_index と edge_attr で定義される) スパースブロック対角隣接行列を作成して特徴とターゲット行列をノード次元で結合することにより達成します。この構成は一つのバッチのサンプルに渡るノードとエッジの異なる数を可能にします :

\[
\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}
\]

PyTorch Geometric はそれ自身の torch_geometric.data.DataLoader を含み、これはこの結合プロセスを既に処理します。サンプルでそれについて学習しましょう :

from torch_geometric.datasets import TUDataset
from torch_geometric.data import DataLoader

dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')
loader = DataLoader(dataset, batch_size=32, shuffle=True)

for batch in loader:
    batch
    >>> Batch(x=[1082, 21], edge_index=[2, 4066], y=[32], batch=[1082])

    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}
\]

それを e.g., 個々の各グラフのためのノード次元のノード特徴を平均するために使用できます :

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')
loader = DataLoader(dataset, batch_size=32, shuffle=True)

for data in loader:
    data
    >>> Batch(x=[1082, 21], edge_index=[2, 4066], y=[32], batch=[1082])

    data.num_graphs
    >>> 32

    x = scatter_mean(data.x, data.batch, dim=0)
    x.size()
    >>> torch.Size([32, 21])

scatter 演算については torch_scatter の ドキュメント で更に学習できます。

 

データ変換

transforms (変換) は画像を変換して増強を遂行するための torchvision の一般的な方法です。PyTorch Geometric はそれ自身の transforms を持ちます、これは入力として Data オブジェクトを想定して新しい変換された Data オブジェクトを返します。torch_geometric.transforms.Compose を使用して transforms は一緒に連鎖できて処理されたデータセットをディスクにセーブする前 (pre_transform) やデータセットのグラフにアクセスする前 (transform) に適用されます。

サンプルを見てみましょう、そこでは ShapeNet データセット (17,000 3D shape ポイントクラウドと 16 shape カテゴリからポイント毎のラベルを含みます) 上に transforms を適用します 。

from torch_geometric.datasets import ShapeNet

dataset = ShapeNet(root='/tmp/ShapeNet', category='Airplane')

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

transforms を通してポイントクラウドから最近傍グラフを生成することによりポイントクラウド・データセットをグラフデータセットに変換できます :

import torch_geometric.transforms as T
from torch_geometric.datasets import ShapeNet

dataset = ShapeNet(root='/tmp/ShapeNet', category='Airplane',
                    pre_transform=T.KNNGraph(k=6))

data[0]
>>> Data(edge_index=[2, 17768], pos=[2518, 3], y=[2518])

Note : データを変換するためにそれをディスクにセーブする前に pre_transform を使用します (より高速なロード時間に繋がります)。次にデータセットが初期化されるときどのような変換を渡さない場合でもそれは既にグラフエッジを含むことに注意してください。

更に、Data オブジェクトをランダムに増強するために transform 引数を使用できます、e.g. 各ノード位置を小さい数で移動する :

import torch_geometric.transforms as T
from torch_geometric.datasets import ShapeNet

dataset = ShapeNet(root='/tmp/ShapeNet', category='Airplane',
                    pre_transform=T.KNNGraph(k=6),
                    transform=T.RandomTranslate(0.01))

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

総ての実装された transforms の完全なリストを torch_geometric.transforms で見つけることができます。

 

グラフ上の学習メソッド

PyTorch Geometric でデータ処理、データセット、ローダと transforms について学習した後は、最初のグラフニューラルネットワークを実装する時です!

単純な GCN 層を使用して Cora citation データセット上の実験を模写します。GCN の高位な説明については ブログ投稿 を見てください。

Cora データセットを最初にロードする必要があります :

from torch_geometric.datasets import Planetoid

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

transforms や dataloader を使用する必要がないことに注意してください。さて 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_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)

コンストラクタは 2 つの GCNConv 層を定義します、これはネットワークの forward パスで呼び出されます。非線形性は conv 呼び出しには統合されていませんので後で適用される必要があります (これは PyTorch Geometric の総ての演算子に渡り一貫したものです)。ここで、中間的な非線形性として ReLU を仕様することを選択して最後にクラス数に渡る softmax 分布を出力します。訓練ノード上でこのモデルを 200 エポック訓練しましょう :

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

最期にテストノード上でモデルを評価できます :

model.eval()
_, pred = model(data).max(dim=1)
correct = float (pred[data.test_mask].eq(data.y[data.test_mask]).sum().item())
acc = correct / data.test_mask.sum().item()
print('Accuracy: {:.4f}'.format(acc))
>>> Accuracy: 0.8150

これが貴方の最初のグラフニューラルネットワークを実装するためにかかる総てです。グラフ畳み込みとプーリングについて更に学習する最も容易な方法は examples/ ディレクトリのサンプルを学習して torch_geometric.nn をブラウズすることです。Happy hacking!

 

以上