第1章:PyTorch Geometric入門とグラフデータの基礎

グラフ構造データとGNNの第一歩

📖 読了時間: 30-35分 📊 難易度: 中級 💻 コード例: 12個 📝 演習問題: 5問

この章では、グラフニューラルネットワーク(GNN)の基礎となるグラフデータの概念と、PyTorch Geometric(PyG)ライブラリの使い方を学びます。グラフの基本構造、PyGのインストール、Dataオブジェクトの扱い方、組み込みデータセット、そして簡単なGCNレイヤーの実装を通じて、GNN開発の基盤を固めていきましょう。

学習目標

1. グラフデータの基礎概念

グラフ(Graph) は、ノード(頂点)とエッジ(辺)から構成されるデータ構造です。実世界の多くの複雑な関係性をグラフで表現できます。

グラフの基本要素

グラフの種類

分類種類説明
方向性有向グラフエッジに方向があるTwitterのフォロー関係、引用ネットワーク
無向グラフエッジに方向がないFacebookの友人関係、分子構造
ノード種類同種グラフ1種類のノードソーシャルネットワーク(人のみ)
異種グラフ複数種類のノードユーザーと商品を含むレコメンデーショングラフ
重み重み付きグラフエッジに重み(強度)がある道路ネットワーク(距離)、類似度グラフ

グラフの表現方法

グラフをコンピュータで扱うための主な表現方法:

1. 隣接行列(Adjacency Matrix)

ノード数を \(N\) とすると、\(N \times N\) の行列 \(A\) で表現:

$$A_{ij} = \begin{cases} 1 & \text{if ノード } i \text{ から } j \text{ へエッジがある} \\ 0 & \text{otherwise} \end{cases}$$

import numpy as np

# 4ノードのグラフの隣接行列
# エッジ: 0→1, 1→2, 0→2, 2→3, 1→3
adjacency_matrix = np.array([
    [0, 1, 1, 0],  # ノード0からの接続
    [0, 0, 1, 1],  # ノード1からの接続
    [0, 0, 0, 1],  # ノード2からの接続
    [0, 0, 0, 0]   # ノード3からの接続
])

print("隣接行列:\n", adjacency_matrix)

2. エッジインデックス(Edge Index)

PyTorch Geometricで採用されている効率的な表現方法。スパース(疎)なグラフに適しています。

import torch

# 同じグラフをエッジインデックスで表現
# 形状: [2, num_edges]
# 1行目: 始点ノード、2行目: 終点ノード
edge_index = torch.tensor([
    [0, 1, 0, 2, 1],  # 始点ノード
    [1, 2, 2, 3, 3]   # 終点ノード
], dtype=torch.long)

print("エッジインデックス:\n", edge_index)

💡 なぜエッジインデックス?

隣接行列は \(O(N^2)\) のメモリが必要ですが、実世界のグラフは疎(スパース)なことが多く、エッジインデックスは \(O(E)\)(\(E\)はエッジ数)で済みます。例えば、1万ノードで平均次数10のグラフでは、隣接行列は100MB必要ですが、エッジインデックスは約800KBで済みます。

2. PyTorch Geometricのインストールと環境構築

PyTorch Geometric は、PyTorchをベースにしたグラフニューラルネットワーク専用ライブラリです。

インストール方法

PyTorch Geometricは、PyTorchとCUDAのバージョンに依存します。まず、使用環境を確認しましょう。

import torch
print(f"PyTorch version: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
if torch.cuda.is_available():
    print(f"CUDA version: {torch.version.cuda}")

方法1: pip経由のインストール(推奨)

# PyTorch 2.0以降の場合(CPU版)
pip install torch-geometric

# 追加の依存パッケージ
pip install pyg-lib torch-scatter torch-sparse torch-cluster torch-spline-conv -f https://data.pyg.org/whl/torch-2.1.0+cpu.html

# GPU版(CUDA 11.8の場合)
pip install torch-geometric
pip install pyg-lib torch-scatter torch-sparse torch-cluster torch-spline-conv -f https://data.pyg.org/whl/torch-2.1.0+cu118.html

方法2: conda経由のインストール

# condaの場合
conda install pyg -c pyg

方法3: Google Colab(環境構築不要)

Google Colabでは以下のコマンドで簡単にインストールできます:

!pip install torch-geometric
!pip install pyg-lib torch-scatter torch-sparse -f https://data.pyg.org/whl/torch-2.1.0+cu118.html

インストール確認

import torch
import torch_geometric

print(f"PyTorch version: {torch.__version__}")
print(f"PyTorch Geometric version: {torch_geometric.__version__}")

# サンプルデータで動作確認
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)

print(f"\nSample Data object created successfully!")
print(f"Number of nodes: {data.num_nodes}")
print(f"Number of edges: {data.num_edges}")

出力例:

PyTorch version: 2.1.0
PyTorch Geometric version: 2.4.0

Sample Data object created successfully!
Number of nodes: 3
Number of edges: 4

3. PyGのDataオブジェクト

PyTorch Geometricの中心的なデータ構造がDataオブジェクト です。グラフの構造と特徴量を効率的に格納します。

Dataオブジェクトの構造

属性形状説明
x[num_nodes, num_features]ノード特徴量行列
edge_index[2, num_edges]エッジの接続情報(COO形式)
edge_attr[num_edges, num_edge_features]エッジ特徴量行列(オプション)
y任意ターゲットラベル(ノードまたはグラフ)
pos[num_nodes, num_dimensions]ノードの位置座標(オプション)

Dataオブジェクトの作成

import torch
from torch_geometric.data import Data

# ノード特徴量(3ノード、各ノード2次元特徴)
x = torch.tensor([[1.0, 2.0],
                  [3.0, 4.0],
                  [5.0, 6.0]], dtype=torch.float)

# エッジインデックス(4つのエッジ)
# 0→1, 1→0, 1→2, 2→1
edge_index = torch.tensor([[0, 1, 1, 2],
                           [1, 0, 2, 1]], dtype=torch.long)

# エッジ特徴量(各エッジ1次元特徴)
edge_attr = torch.tensor([[1.0], [1.0], [2.0], [2.0]], dtype=torch.float)

# ノードラベル(ノード分類タスクの場合)
y = torch.tensor([0, 1, 0], dtype=torch.long)

# Dataオブジェクト作成
data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y)

print(data)
print(f"\nNumber of nodes: {data.num_nodes}")
print(f"Number of edges: {data.num_edges}")
print(f"Number of features: {data.num_node_features}")
print(f"Has isolated nodes: {data.has_isolated_nodes()}")
print(f"Has self-loops: {data.has_self_loops()}")
print(f"Is undirected: {data.is_undirected()}")

出力:

Data(x=[3, 2], edge_index=[2, 4], edge_attr=[4, 1], y=[3])

Number of nodes: 3
Number of edges: 4
Number of features: 2
Has isolated nodes: False
Has self-loops: False
Is undirected: True

Dataオブジェクトの操作

import torch
from torch_geometric.data import Data

data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y)

# 特定ノードの特徴量取得
print("Node 0 features:", data.x[0])

# 特定エッジの情報取得
print("Edge 0:", data.edge_index[:, 0])
print("Edge 0 attribute:", data.edge_attr[0])

# データをGPUに転送
if torch.cuda.is_available():
    data = data.to('cuda')
    print(f"Data moved to: {data.x.device}")

# CPUに戻す
data = data.to('cpu')

# データの検証
print(f"\nIs valid: {data.validate()}")

4. 基本的なデータ操作と組み込みデータセット

PyTorch Geometricには、研究・学習用の組み込みデータセットが多数用意されています。

主要な組み込みデータセット

データセット種類ノード数説明
Cora引用ネットワーク2,708論文の引用関係、7クラス分類
Citeseer引用ネットワーク3,327論文の引用関係、6クラス分類
PubMed引用ネットワーク19,717医学論文の引用関係、3クラス分類
PPI生物ネットワーク14,755タンパク質相互作用、マルチラベル分類
QM9分子グラフ約13万分子分子特性予測、回帰タスク

Coraデータセットの読み込み

from torch_geometric.datasets import Planetoid

# Coraデータセットをダウンロード・読み込み
dataset = Planetoid(root='/tmp/Cora', name='Cora')

print(f"Dataset: {dataset}")
print(f"Number of graphs: {len(dataset)}")
print(f"Number of features: {dataset.num_features}")
print(f"Number of classes: {dataset.num_classes}")

# 最初のグラフ(Coraは単一グラフ)
data = dataset[0]

print(f"\nGraph structure:")
print(f"Number of nodes: {data.num_nodes}")
print(f"Number of edges: {data.num_edges}")
print(f"Average node degree: {data.num_edges / data.num_nodes:.2f}")
print(f"Training nodes: {data.train_mask.sum().item()}")
print(f"Validation nodes: {data.val_mask.sum().item()}")
print(f"Test nodes: {data.test_mask.sum().item()}")

# ノード特徴量とラベルの確認
print(f"\nNode features shape: {data.x.shape}")
print(f"Node labels shape: {data.y.shape}")
print(f"First node features: {data.x[0][:10]}...")
print(f"First node label: {data.y[0].item()}")

出力例:

Dataset: Cora()
Number of graphs: 1
Number of features: 1433
Number of classes: 7

Graph structure:
Number of nodes: 2708
Number of edges: 10556
Average node degree: 3.90
Training nodes: 140
Validation nodes: 500
Test nodes: 1000

Node features shape: torch.Size([2708, 1433])
Node labels shape: torch.Size([2708])
First node features: tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])...
First node label: 3

DataLoaderの使い方

複数のグラフを含むデータセットでは、DataLoaderを使ってバッチ処理します。

from torch_geometric.datasets import TUDataset
from torch_geometric.loader import DataLoader

# ENZYMES データセット(タンパク質のグラフ分類)
dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')

print(f"Dataset: {dataset}")
print(f"Number of graphs: {len(dataset)}")
print(f"Number of classes: {dataset.num_classes}")
print(f"Number of features: {dataset.num_features}")

# DataLoader作成
loader = DataLoader(dataset, batch_size=32, shuffle=True)

# バッチの確認
for batch in loader:
    print(f"\nBatch:")
    print(f"Number of graphs in batch: {batch.num_graphs}")
    print(f"Total nodes in batch: {batch.num_nodes}")
    print(f"Total edges in batch: {batch.num_edges}")
    print(f"Batch shape: {batch.batch.shape}")
    break  # 最初のバッチのみ表示

💡 バッチ処理の仕組み

PyGのDataLoaderは、複数のグラフを1つの大きなグラフとして結合します。各ノードがどのグラフに属するかはbatch属性で管理されます。これにより、異なるサイズのグラフを効率的にバッチ処理できます。

5. 簡単なGNNの実装例

最も基本的なグラフニューラルネットワーク層であるGCNConv(Graph Convolutional Network) を使って、ノード分類モデルを実装します。

GCNの基本原理

GCNは各ノードの特徴量を、隣接ノードの特徴量を集約して更新します:

$$\mathbf{x}i^{(k+1)} = \sigma\left(\sum{j \in \mathcal{N}(i) \cup \{i\}} \frac{1}{\sqrt{d_i d_j}} \mathbf{W}^{(k)} \mathbf{x}_j^{(k)}\right)$$

ここで:

GCNモデルの実装

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

class GCN(torch.nn.Module):
    def __init__(self, num_features, num_classes):
        super(GCN, self).__init__()
        # 2層のGCN
        self.conv1 = GCNConv(num_features, 16)
        self.conv2 = GCNConv(16, num_classes)

    def forward(self, data):
        x, edge_index = data.x, data.edge_index

        # 第1層: 入力 → 16次元
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, p=0.5, training=self.training)

        # 第2層: 16次元 → クラス数
        x = self.conv2(x, edge_index)

        return F.log_softmax(x, dim=1)

# モデル作成
from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/tmp/Cora', name='Cora')
data = dataset[0]

model = GCN(num_features=dataset.num_features,
            num_classes=dataset.num_classes)

print(model)
print(f"\nTotal parameters: {sum(p.numel() for p in model.parameters())}")

学習ループの実装

import torch
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid

# データとモデルの準備
dataset = Planetoid(root='/tmp/Cora', name='Cora')
data = dataset[0]

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = GCN(num_features=dataset.num_features,
            num_classes=dataset.num_classes).to(device)
data = data.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()

    # 10エポックごとに結果表示
    if (epoch + 1) % 10 == 0:
        model.eval()
        _, pred = model(data).max(dim=1)
        correct = pred[data.train_mask].eq(data.y[data.train_mask]).sum().item()
        accuracy = correct / data.train_mask.sum().item()
        print(f'Epoch {epoch+1:03d}, Loss: {loss:.4f}, Train Acc: {accuracy:.4f}')
        model.train()

モデルの評価

def test(model, data):
    model.eval()
    with torch.no_grad():
        out = model(data)
        _, pred = out.max(dim=1)

        # 訓練データ精度
        correct = pred[data.train_mask].eq(data.y[data.train_mask]).sum().item()
        train_acc = correct / data.train_mask.sum().item()

        # 検証データ精度
        correct = pred[data.val_mask].eq(data.y[data.val_mask]).sum().item()
        val_acc = correct / data.val_mask.sum().item()

        # テストデータ精度
        correct = pred[data.test_mask].eq(data.y[data.test_mask]).sum().item()
        test_acc = correct / data.test_mask.sum().item()

    return train_acc, val_acc, test_acc

train_acc, val_acc, test_acc = test(model, data)
print(f'\nFinal Results:')
print(f'Train Accuracy: {train_acc:.4f}')
print(f'Validation Accuracy: {val_acc:.4f}')
print(f'Test Accuracy: {test_acc:.4f}')

出力例:

Epoch 010, Loss: 1.9234, Train Acc: 0.3143
Epoch 020, Loss: 1.7845, Train Acc: 0.4357
Epoch 030, Loss: 1.5234, Train Acc: 0.6000
...
Epoch 200, Loss: 0.5123, Train Acc: 0.9714

Final Results:
Train Accuracy: 0.9714
Validation Accuracy: 0.7540
Test Accuracy: 0.8130

🎉 初めてのGNN実装完了!

Coraデータセットでテスト精度81%を達成しました。これはグラフ構造を考慮しないMLPモデル(約60%)と比べて大幅に改善されています。GNNはノード間の関係性を学習することで、より高い精度を実現しています。

演習問題

演習1:カスタムグラフの作成

以下の条件でグラフを作成してください:

  1. 5つのノード(各ノード3次元特徴量)
  2. 無向グラフ(双方向エッジ)
  3. エッジ: 0-1, 1-2, 2-3, 3-4, 4-0
  4. 各ノードにランダムなラベル(0,1,2のいずれか)を付与
# ここにコードを書く

演習2:データセット探索

Citeseerデータセットを読み込み、以下の情報を出力してください:

演習3:3層GCNの実装

2層GCNを拡張して、3層のGCNモデルを実装してください。中間層の次元数は32と16にしてください。Coraデータセットで学習し、精度を比較してください。

ヒント: 層を増やすと過学習しやすくなるため、Dropoutの調整が必要かもしれません。

演習4:エッジ特徴量の活用

エッジに重み(特徴量)を持つグラフを作成し、edge_attr属性を設定してください。エッジの重みはランダムな値(0.1〜1.0の範囲)としてください。

演習5:グラフの可視化

NetworkXとMatplotlibを使って、作成したグラフを可視化してください。ノードの色をラベルで分けて表示してください。

import networkx as nx
import matplotlib.pyplot as plt
from torch_geometric.utils import to_networkx

# PyGのDataオブジェクトをNetworkXグラフに変換
# ここにコードを書く

まとめ

この章では、グラフニューラルネットワークの基礎を学びました:

🎉 次のステップ

次章では、グラフ畳み込みネットワーク(GCN)のメッセージパッシングの仕組みを詳しく学び、ノード分類タスクを完全に理解します。過学習対策やハイパーパラメータ調整についても実践的に学びます。


参考リソース