Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for building Graph Neural Networks with PyTorch Geometric (PyG). Use this skill whenever the user asks about graph neural networks, GNNs, node classification, link prediction, graph classification, message passing networks, heterogeneous graphs, neighbor sampling, or any task involving torch_geometric / PyG. Also trigger when you see imports from torch_geometric, or the user mentions graph convolutions (GCN, GAT, GraphSAGE, GIN), graph data structures, or working with relational/network da
.claude/skills/mkurman-torch-geometric/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 163% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 142% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 294% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 86% | 0% |
-|----------|----------| | GCNConv | Homogeneous, semi-supervised node classification | Spectral-inspired, degree-normalized aggregation | | GATConv / GATv2Conv | When neighbor importance varies | Attention-weighted messages | | SAGEConv | Large graphs, inductive settings | Sampling-friendly, learnable aggregation | | GINConv | Graph classification, maximizing expressiveness | As powerful as WL test | | TransformerConv | Rich edge features, complex interactions | Multi-head attention with edge features | | EdgeConv | Point clouds, dynamic graphs | MLP on edge features (x_i, x_j - x_i) | | RGCNConv | Heterogeneous with many relation types | Relation-specific weight matrices | | HGTConv | Heterogeneous graphs | Type-specific attention |
All conv layers accept (x, edge_index) at minimum. Many also accept edge_attr for edge features.
Use -1 for input channels to let PyG infer dimensions automatically — especially useful for heterogeneous models:
pythonconv = SAGEConv((-1, -1), 64) # Input dims inferred on first forward pass # Initialize lazy modules: with torch.no_grad(): out = model(data.x, data.edge_index)
For common architectures, PyG provides ready-made model classes:
pythonfrom torch_geometric.nn import GraphSAGE, GCN, GAT, GIN model = GraphSAGE( in_channels=dataset.num_features, hidden_channels=64, out_channels=dataset.num_classes, num_layers=2, )
To implement a novel GNN layer, subclass MessagePassing. The framework is:
propagate() orchestrates the message passingmessage() defines what info flows along each edge (the phi function)aggregate() combines messages at each node (sum/mean/max)update() transforms the aggregated result (the gamma function)pythonfrom torch_geometric.nn import MessagePassing from torch_geometric.utils import add_self_loops, degree class MyConv(MessagePassing): def __init__(self, in_channels, out_channels): super().__init__(aggr='add') # "add", "mean", or "max" self.lin = torch.nn.Linear(in_channels, out_channels) def forward(self, x, edge_index): # Pre-processing before message passing x = self.lin(x) # Start message passing return self.propagate(edge_index, x=x) def message(self, x_j): # x_j: features of source nodes for each edge [num_edges, features] # The _j suffix auto-indexes source nodes, _i indexes target nodes return x_j
The _i / _j convention: any tensor passed to propagate() can be auto-indexed by appending _i (target/central node) or _j (source/neighbor node) in the message() signature. So if you pass x=... to propagate, you can access x_i and x_j in message().
Read references/message_passing.md for the full GCN and EdgeConv implementation examples.
python# Full-batch training on a single graph (e.g., Cora) model.train() for epoch in range(200): optimizer.zero_grad() out = model(data.x, data.edge_index) loss = F.cross_entropy(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step() # Evaluation model.eval() pred = model(data.x, data.edge_index).argmax(dim=1) acc = (pred[data.test_mask] == data.y[data.test_mask]).float().mean()
Multiple graphs — use DataLoader for mini-batching and global pooling to get graph-level representations:
pythonfrom torch_geometric.loader import DataLoader from torch_geometric.nn import GCNConv, global_mean_pool loader = DataLoader(dataset, batch_size=32, shuffle=True) class GraphClassifier(torch.nn.Module): def __init__(self, in_ch, hidden_ch, out_ch): super().__init__() self.conv1 = GCNConv(in_ch, hidden_ch) self.conv2 = GCNConv(hidden_ch, hidden_ch) self.lin = torch.nn.Linear(hidden_ch, out_ch) def forward(self, x, edge_index, batch): x = self.conv1(x, edge_index).relu() x = self.conv2(x, edge_index).relu() x = global_mean_pool(x, batch) # [num_graphs_in_batch, hidden_ch] return self.lin(x) # Training loop for data in loader: out = model(data.x, data.edge_index, data.batch) loss = F.cross_entropy(out, data.y)
PyG's DataLoader batches multiple graphs by creating block-diagonal adjacency matrices. The batch tensor maps each node to its graph index. Pooling ops (global_mean_pool, global_max_pool, global_add_pool) use this to aggregate per-graph.
Split edges into train/val/test, use negative sampling:
pythonfrom torch_geometric.transforms import RandomLinkSplit transform = RandomLinkSplit( num_val=0.1, num_test=0.1, is_undirected=True, add_negative_train_samples=False, ) train_data, val_data, test_data = transform(data) # Encode nodes, then score edges z = model.encode(train_data.x, train_data.edge_index) # Positive edges pos_score = (z[train_data.edge_label_index[0]] * z[train_data.edge_label_index[1]]).sum(dim=1)
Read references/link_prediction.md for the complete link prediction guide: GAE/VGAE autoencoders, full training loops, LinkNeighborLoader for large graphs, heterogeneous link prediction, and evaluation metrics.
For graphs that don't fit in GPU memory, use neighbor sampling via NeighborLoader:
pythonfrom torch_geometric.loader import NeighborLoader train_loader = NeighborLoader( data, num_neighbors=[15, 10], # Sample 15 neighbors in hop 1, 10 in hop 2 batch_size=128, # Number of seed nodes per batch input_nodes=data.train_mask, # Which nodes to sample from shuffle=True, ) for batch in train_loader: batch = batch.to(device) out = model(batch.x, batch.edge_index) # Only use first batch_size nodes for loss (these are the seed nodes) loss = F.cross_entropy(out[:batch.batch_size], batch.y[:batch.batch_size])
Key points about NeighborLoader:
num_neighbors list length should match GNN depth (number of message passing layers)batch.batch_size nodes in the outputbatch.n_id maps relabeled indices back to original node IDsData and HeteroDataLinkNeighborLoader insteadOther scalability options: ClusterLoader (ClusterGCN), GraphSAINTSampler, ShaDowKHopSampler. For multi-GPU training, DDP, PyTorch Lightning integration, and torch.compile support, read references/scaling.md.
For graphs with multiple node and edge types (social networks, knowledge graphs, recommendation):
pythonfrom torch_geometric.data import HeteroData data = HeteroData() # Node features — indexed by node type string data['user'].x = torch.randn(1000, 64) data['movie'].x = torch.randn(500, 128) # Edge indices — indexed by (src_type, edge_type, dst_type) triplet data['user', 'rates', 'movie'].edge_index = torch.randint(0, 500, (2, 3000)) data['user', 'follows', 'user'].edge_index = torch.randint(0, 1000, (2, 5000)) # Access convenience dicts data.x_dict # {'user': tensor, 'movie': tensor} data.edge_index_dict # {('user','rates','movie'): tensor, ...} data.metadata() # ([node_types], [edge_types])
1. Auto-convert with to_hetero() — write a homogeneous model, convert automatically:
pythonfrom torch_geometric.nn import SAGEConv, to_hetero class GNN(torch.nn.Module): def __init__(self, hidden_channels, out_channels): super().__init__() self.conv1 = SAGEConv((-1, -1), hidden_channels) self.conv2 = SAGEConv((-1, -1), out_channels) def forward(self, x, edge_index): x = self.conv1(x, edge_index).relu() x = self.conv2(x, edge_index) return x model = GNN(64, dataset.num_classes) model = to_hetero(model, data.metadata(), aggr='sum') # Now accepts dicts: out = model(data.x_dict, data.edge_index_dict)
Use (-1, -1) for bipartite input channels (source, target may differ). Lazy init handles the rest.
2. HeteroConv wrapper — different conv per edge type:
pythonfrom torch_geometric.nn import HeteroConv, GCNConv, SAGEConv, GATConv conv = HeteroConv({ ('paper', 'cites', 'paper'): GCNConv(-1, 64), ('author', 'writes', 'paper'): SAGEConv((-1, -1), 64), ('paper', 'rev_writes', 'author'): GATConv((-1, -1), 64, add_self_loops=False), }, aggr='sum')
3. Native heterogeneous operators like HGTConv:
pythonfrom torch_geometric.nn import HGTConv conv = HGTConv(hidden_channels, hidden_channels, data.metadata(), num_heads=4)
Important for heterogeneous graphs:
T.ToUndirected() to add reverse edge types for bidirectional message flowadd_self_loops in bipartite conv layers (different source/dest types) — use skip connections instead: conv(x, edge_index) + lin(x)input_nodes as ('node_type', mask) tuplenum_neighbors can be a dict keyed by edge type for fine-grained controlRead references/heterogeneous.md for complete examples including training loops and NeighborLoader usage with heterogeneous graphs.
For loading your own data into PyG:
Data objects directly and pass a list to DataLoaderInMemoryDataset — override raw_file_names, processed_file_names, download(), process()Dataset — also override len() and get()Data or HeteroDatafrom_networkx(G) converts a NetworkX graph directlyfrom_scipy_sparse_matrix(adj) extracts edge_indexRead references/custom_datasets.md for complete examples with all patterns, CSV loading with encoders, and the MovieLens walkthrough.
PyG provides torch_geometric.explain for interpreting GNN predictions:
pythonfrom torch_geometric.explain import Explainer, GNNExplainer explainer = Explainer( model=model, algorithm=GNNExplainer(epochs=200), explanation_type='model', node_mask_type='attributes', edge_mask_type='object', model_config=dict( mode='multiclass_classification', task_level='node', return_type='log_probs', ), ) explanation = explainer(data.x, data.edge_index, index=10) explanation.visualize_graph() # Important subgraph explanation.visualize_feature_importance(top_k=10) # Feature importance
Available algorithms: GNNExplainer (optimization-based), PGExplainer (parametric, trained), CaptumExplainer (gradient-based via Captum), AttentionExplainer (attention weights). Works for both homogeneous and heterogeneous graphs.
Read references/explainability.md for all algorithms, heterogeneous explanations, evaluation metrics, and PGExplainer training.
[2, num_edges], not [num_edges, 2]. Transpose if needed.add_self_loops=True when source and dest node types differ. Use skip connections instead.batch.batch_size nodes are your seed nodes. Slice predictions and labels accordingly.edge_index, or use T.ToUndirected().-1 input channels need one forward pass with torch.no_grad() before training to initialize parameters.global_mean_pool(x, batch) (not manual reshape) to aggregate node features to graph-level.len(num_neighbors) equal to the number of GNN layers. More hops than layers wastes compute; fewer means wasted model capacity.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 12,934 | 11,328 | -12% | 1 | 1 | 0% | 2,403 | 5,826 | +142% | 0 | 0 | — |
case-02 | fail→pass | 11,753 | 11,862 | +1% | 1 | 1 | 0% | 2,157 | 5,682 | +163% | 0 | 0 | — |
case-03 | pass→pass | 5,835 | 5,465 | -6% | 1 | 1 | 0% | 1,158 | 4,568 | +294% | 0 | 0 | — |
case-04 | pass→pass | 18,418 | 14,836 | -19% | 1 | 1 | 0% | 3,247 | 6,047 | +86% | 0 | 0 | — |
case-05 | pass→pass | 7,682 | 6,462 | -16% | 1 | 1 | 0% | 1,398 | 4,711 | +237% | 0 | 0 | — |
case-06 | pass→pass | 10,336 | 12,194 | +18% | 1 | 1 | 0% | 1,789 | 5,476 | +206% | 0 | 0 | — |
case-07 | pass→pass | 6,067 | 7,389 | +22% | 1 | 1 | 0% | 1,148 | 5,030 | +338% | 0 | 0 | — |
case-08 | pass→pass | 15,059 | 11,745 | -22% | 1 | 1 | 0% | 3,065 | 5,868 | +91% | 0 | 0 | — |
case-09 | pass→pass | 12,185 | 12,727 | +4% | 1 | 1 | 0% | 2,377 | 5,958 | +151% | 0 | 0 | — |
case-10 | pass→pass | 10,805 | 8,956 | -17% | 1 | 1 | 0% | 1,997 | 5,267 | +164% | 0 | 0 | — |
case-11 | pass→pass | 9,047 | 5,850 | -35% | 1 | 1 | 0% | 1,783 | 4,622 | +159% | 0 | 0 | — |
case-12 | fail→pass | 14,446 | 8,909 | -38% | 1 | 1 | 0% | 2,883 | 5,293 | +84% | 0 | 0 | — |
case-13 | pass→pass | 14,365 | 10,087 | -30% | 1 | 1 | 0% | 2,812 | 5,652 | +101% | 0 | 0 | — |
case-14 | pass→pass | 13,848 | 9,651 | -30% | 1 | 1 | 0% | 2,915 | 5,370 | +84% | 0 | 0 | — |
case-15 | pass→pass | 9,356 | 7,297 | -22% | 1 | 1 | 0% | 1,762 | 4,931 | +180% | 0 | 0 | — |
case-16 | pass→pass | 12,268 | 11,515 | -6% | 1 | 1 | 0% | 2,223 | 5,540 | +149% | 0 | 0 | — |
case-17 | pass→pass | 13,205 | 11,033 | -16% | 1 | 1 | 0% | 2,574 | 5,583 | +117% | 0 | 0 | — |
case-18 | pass→pass | 13,004 | 6,568 | -49% | 1 | 1 | 0% | 2,399 | 4,658 | +94% | 0 | 0 | — |
case-19 | pass→pass | 11,198 | 10,210 | -9% | 1 | 1 | 0% | 2,012 | 5,415 | +169% | 0 | 0 | — |
case-20 | fail→fail | 6,660 | 5,999 | -10% | 1 | 1 | 0% | 1,322 | 4,846 | +267% | 0 | 0 | — |
case-21 | pass→pass | 5,319 | 7,668 | +44% | 1 | 1 | 0% | 1,093 | 4,900 | +348% | 0 | 0 | — |
case-22 | pass→pass | 5,805 | 5,871 | +1% | 1 | 1 | 0% | 1,137 | 4,711 | +314% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.