PyTorch Geometric (PyG) — Graph Neural Networks
Overview
PyTorch Geometric is a library built on PyTorch for developing and training Graph Neural Networks (GNNs). It provides 40+ convolutional layers, mini-batch processing via block-diagonal adjacency matrices, neighbor sampling for large-scale graphs, and heterogeneous graph support for multi-type node/edge networks.
When to Use
- Node classification on citation, social, or biological networks
- Graph-level classification (molecular activity, protein function)
- Link prediction (knowledge graphs, recommendation systems)
- Molecular property prediction (drug discovery, quantum chemistry)
- 3D point cloud processing and mesh analysis
- Large-scale graph learning with neighbor sampling (>100K nodes)
- Heterogeneous graphs with multiple node/edge types
- For non-graph deep learning → use PyTorch directly
- For traditional graph algorithms (shortest path, centrality) → use NetworkX
Prerequisites
pip install torch torch_geometric
# Optional sparse operations (recommended):
# pip install pyg_lib torch_scatter torch_sparse torch_cluster
import torch
import torch.nn.functional as F
from torch_geometric.data import Data
from torch_geometric.nn import GCNConv
Quick Start
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GCNConv
import torch, torch.nn.functional as F
dataset = Planetoid(root='/tmp/Cora', name='Cora')
data = dataset[0]
class GCN(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv1 = GCNConv(dataset.num_features, 16)
self.conv2 = GCNConv(16, dataset.num_classes)
def forward(self, data):
x = F.relu(self.conv1(data.x, data.edge_index))
return self.conv2(x, data.edge_index)
model = GCN()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
for epoch in range(200):
model.train(); optimizer.zero_grad()
F.cross_entropy(model(data)[data.train_mask], data.y[data.train_mask]).backward()
optimizer.step()
model.eval()
pred = model(data).argmax(dim=1)
acc = (pred[data.test_mask] == data.y[data.test_mask]).float().mean()
print(f'Test Accuracy: {acc:.4f}') # ~0.81
Core API
1. Data Representation
import torch
from torch_geometric.data import Data
# Create a graph: 3 nodes, 4 edges (undirected)
edge_index = torch.tensor([[0, 1, 1, 2],
[1, 0, 2, 1]], dtype=torch.long)
x = torch.randn(3, 16) # Node features [num_nodes, features]
y = torch.tensor([0, 1, 0]) # Node labels
data = Data(x=x, edge_index=edge_index, y=y)
print(f'Nodes: {data.num_nodes}, Edges: {data.num_edges}')
print(f'Features: {data.num_node_features}')
print(f'Has self-loops: {data.has_self_loops()}')
print(f'Is undirected: {data.is_undirected()}')
# Optional attributes
data.edge_attr = torch.randn(4, 8) # Edge features [num_edges, features]
data.pos = torch.randn(3, 3) # Node positions (3D)
data.train_mask = torch.tensor([True, True, False]) # Custom masks
# Mini-batch processing — graphs concatenated as block-diagonal
from torch_geometric.loader import DataLoader
loader = DataLoader(dataset, batch_size=32, shuffle=True)
for batch in loader:
print(f'Graphs: {batch.num_graphs}, Nodes: {batch.num_nodes}')
# batch.batch maps each node → its source graph index
# No padding needed — computationally efficient
2. Convolutional Layers
from torch_geometric.nn import GCNConv, GATConv, SAGEConv, GINConv
import torch.nn as nn
# GCNConv — spectral graph convolution (baseline)
conv = GCNConv(in_channels=16, out_channels=32)
# Supports: edge_weight, SparseTensor, Bipartite, Lazy init
# GATConv — attention-based neighbor weighting
conv = GATConv(16, 32, heads=8, dropout=0.6)
# Output: [N, heads * out_channels] (concat) or [N, out_channels] (concat=False)
# SAGEConv — inductive learning via sampling
conv = SAGEConv(16, 32, aggr='mean') # 'mean', 'max', 'lstm'
# GINConv — maximally powerful for graph isomorphism
nn_module = nn.Sequential(nn.Linear(16, 32), nn.ReLU(), nn.Linear(32, 32))
conv = GINConv(nn_module)
# TransformerConv — graph transformer
from torch_geometric.nn import TransformerConv
conv = TransformerConv(16, 32, heads=8, beta=True)
# All layers: x_out = conv(x, edge_index)
x_out = conv(x, edge_index)
print(f'Output shape: {x_out.shape}') # [num_nodes, out_channels]
3. Custom Message Passing
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import add_self_loops, degree
class CustomConv(MessagePassing):
def __init__(self, in_channels, out_channels):
super().__init__(aggr='add') # 'add', 'mean', 'max'
self.lin = torch.nn.Linear(in_channels, out_channels)
def forward(self, x, edge_index):
edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
x = self.lin(x)
# Degree-based normalization
row, col = edge_index
deg = degree(col, x.size(0), dtype=x.dtype)
norm = deg.pow(-0.5)
norm = norm[row] * norm[col]
return self.propagate(edge_index, x=x, norm=norm)
def message(self, x_j, norm):
# x_j: source node features (automatic via _j suffix)
return norm.view(-1, 1) * x_j
# Key methods: forward(), message(), aggregate(), update()
# _i suffix → target node, _j suffix → source node
4. Pooling & Graph-Level Readout
from torch_geometric.nn import (
global_mean_pool, global_max_pool, global_add_pool,
TopKPooling, SAGPooling
)
# Global pooling: node features → graph-level representation
x_graph = global_mean_pool(x, batch) # [num_graphs, features]
# Hierarchical pooling: coarsen graph
pool = TopKPooling(64, ratio=0.8) # Keep top 80% nodes
x, edge_index, _, batch, _, _ = pool(x, edge_index, None, batch)
# Graph classification model
class GraphClassifier(torch.nn.Module):
def __init__(self, num_features, num_classes):
super().__init__()
self.conv1 = GCNConv(num_features, 64)
self.conv2 = GCNConv(64, 64)
self.pool = TopKPooling(64, ratio=0.8)
self.lin = torch.nn.Linear(64, num_classes)
def forward(self, data):
x, edge_index, batch = data.x, data.edge_index, data.batch
x = F.relu(self.conv1(x, edge_index))
x, edge_index, _, batch, _, _ = self.pool(x, edge_index, None, batch)
x = F.relu(self.conv2(x, edge_index))
x = global_mean_pool(x, batch)
return self.lin(x)
5. Heterogeneous Graphs
from torch_geometric.data import HeteroData
from torch_geometric.nn import HeteroConv, GCNConv, SAGEConv, to_hetero
# Create heterogeneous graph
data = HeteroData()
data['paper'].x = torch.randn(100, 128)
data['author'].x = torch.randn(200, 64)
data['author', 'writes', 'paper'].edge_index = torch.randint(0, 200, (2, 500))
data['paper', 'cites', 'paper'].edge_index = torch.randint(0, 100, (2, 300))
print(data) # Shows all node/edge types
# Method 1: Auto-convert homogeneous model
model = GCN(...)
model = to_hetero(model, data.metadata(), aggr='sum')
out = model(data.x_dict, data.edge_index_dict)
# Method 2: Custom per-edge-type convolutions
class HeteroGNN(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv1 = HeteroConv({
('paper', 'cites', 'paper'): GCNConv(-1, 64),
('author', 'writes', 'paper'): SAGEConv((-1, -1), 64),
}, aggr='sum')
def forward(self, x_dict, edge_index_dict):
x_dict = self.conv1(x_dict, edge_index_dict)
return {k: F.relu(v) for k, v in x_dict.items()}
6. Transforms & Preprocessing
from torch_geometric.transforms import (
NormalizeFeatures, AddSelfLoops, ToUndirected,
RandomNodeSplit, RandomLinkSplit, Compose,
KNNGraph, RadiusGraph, AddLaplacianEigenvectorPE
)
# Single transform
dataset = Planetoid(root='/tmp/Cor