Skill Tracer
Skills › Scientific Agent Skills › torch-geometric

torch-geometric

skill · 487 lines · MIT license · skills/torch-geometric/SKILL.md

The facts

Its description

Supports PyTorch Geometric (PyG) graph neural networks — node/link/graph classification, message passing (GCN, GAT, GraphSAGE, GIN), heterogeneous graphs, neighbor sampling, and custom datasets. Use when working with torch_geometric, not for general NetworkX analytics or non-graph PyTorch models.

The description the agent reads to decide when to use it.

From its settingsline 3

Files it mentions

references/review.md, references/message_passing.md, references/link_prediction.md, references/scaling.md (first 5 of 8 lines)

Files the text names, such as ones to read or write. Found by matching words, so treat them as pointers.

Found by matching wordsline 34 line 90 line 217 line 305 line 337

Licence

MIT license, from the skill file

The terms its author shares it under, which let it be shown here.

From its settingsline 4 licence text

Other settings

compatibility: Requires Python 3.10+, PyTorch 2.9+, and torch-geometric 2.8.0.post1. Optional pyg-lib, torch-scatter and torch-sparse wheels must match Python, OS, PyTorch and CUDA/CPU. Network access is needed only for installation and dataset/model downloads. metadata: version: "1.4" last-reviewed: "2026-10-01" skill-author: K-Dense Inc.

More settings at the top of the file, shown as written.

From its settingsline 5 line 6
Other files in this skill (7) Close

The text

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1---
2name: torch-geometric
3description: Supports PyTorch Geometric (PyG) graph neural networks — node/link/graph classification, message passing (GCN, GAT, GraphSAGE, GIN), heterogeneous graphs, neighbor sampling, and custom datasets. Use when working with torch_geometric, not for general NetworkX analytics or non-graph PyTorch models.
4license: MIT license
5compatibility: Requires Python 3.10+, PyTorch 2.9+, and torch-geometric 2.8.0.post1. Optional pyg-lib, torch-scatter and torch-sparse wheels must match Python, OS, PyTorch and CUDA/CPU. Network access is needed only for installation and dataset/model downloads.
6metadata:
7 version: "1.4"
8 last-reviewed: "2026-10-01"
9 skill-author: K-Dense Inc.
10---
12# PyTorch Geometric (PyG)
14PyG is the standard library for Graph Neural Networks built on PyTorch. It provides data structures for graphs, 60+ GNN layer implementations, scalable mini-batch training, and support for heterogeneous graphs.

PyTorch Geometric (PyG)

PyG is the standard library for Graph Neural Networks built on PyTorch. It provides data structures for graphs, 60+ GNN layer implementations, scalable mini-batch training, and support for heterogeneous graphs.

Raw lines 1 to 15
16## Installation
18Reviewed released **torch-geometric 2.8.0.post1** (2026-10-01); CPU examples tested with **Python 3.13 / PyTorch 2.14.1**. Rolling `latest` docs identify 2.9.0; check the installed release before adopting new APIs. PyG 2.8 requires PyTorch 2.9+; its original release table covers 2.9–2.12. Our 2.14.1 core tests do not establish every extension/backend combination.
20```bash
21# Install the PyTorch build for your platform from https://pytorch.org/get-started/locally/
22uv pip install torch==2.14.1
23uv pip install torch-geometric==2.8.0.post1
24python -c "import torch, torch_geometric; print(torch.__version__, torch.version.cuda, torch_geometric.__version__)"
25```
27Basic tensor-based layers need no extensions. Neighbor sampling requires `pyg-lib` or `torch-sparse`; spatial k-NN operators require `pyg-lib` in 2.8. `torch-cluster` and `torch-spline-conv` are deprecated and ignored. Inspect the [wheel index](https://data.pyg.org/whl/) for your exact Python/OS/Torch/CUDA tuple. Never install wheels for a different Torch release merely because core imports succeed. The tested macOS ARM CPU extension is below; choose a different matching wheel for other platforms, and verify its operators:
29```bash
30uv pip install --only-binary=:all: "pyg-lib==0.9.0+pt214" \
31 -f https://data.pyg.org/whl/torch-2.14.0+cpu.html
32```
34Conda packages are no longer provided for Torch >2.5. See [installation](https://pytorch-geometric.readthedocs.io/en/latest/install/installation.html) and [2.8 release changes](https://github.com/pyg-team/pytorch_geometric/releases/tag/2.8.0). Optional sampling/GPU/distributed/download examples below are illustrative unless covered by the CPU checks in [review notes](references/review.md).

Installation

Reviewed released torch-geometric 2.8.0.post1 (2026-10-01); CPU examples tested with Python 3.13 / PyTorch 2.14.1. Rolling latest docs identify 2.9.0; check the installed release before adopting new APIs. PyG 2.8 requires PyTorch 2.9+; its original release table covers 2.9–2.12. Our 2.14.1 core tests do not establish every extension/backend combination.

# Install the PyTorch build for your platform from https://pytorch.org/get-started/locally/
uv pip install torch==2.14.1
uv pip install torch-geometric==2.8.0.post1
python -c "import torch, torch_geometric; print(torch.__version__, torch.version.cuda, torch_geometric.__version__)"

Basic tensor-based layers need no extensions. Neighbor sampling requires pyg-lib or torch-sparse; spatial k-NN operators require pyg-lib in 2.8. torch-cluster and torch-spline-conv are deprecated and ignored. Inspect the wheel index for your exact Python/OS/Torch/CUDA tuple. Never install wheels for a different Torch release merely because core imports succeed. The tested macOS ARM CPU extension is below; choose a different matching wheel for other platforms, and verify its operators:

uv pip install --only-binary=:all: "pyg-lib==0.9.0+pt214" \
  -f https://data.pyg.org/whl/torch-2.14.0+cpu.html

Conda packages are no longer provided for Torch >2.5. See installation and 2.8 release changes. Optional sampling/GPU/distributed/download examples below are illustrative unless covered by the CPU checks in review notes.

Raw lines 16 to 35
Reads a file references/review.md :34
36## Core Concepts

Core Concepts

Raw lines 36 to 37
38### Graph Data: `Data` and `HeteroData`
40A graph lives in a `Data` object. The key attributes:
42```python
43from torch_geometric.data import Data
45data = Data(
46 x=node_features, # [num_nodes, num_node_features]
47 edge_index=edge_index, # [2, num_edges] — COO format, dtype=torch.long
48 edge_attr=edge_features, # [num_edges, num_edge_features]
49 y=labels, # node-level [num_nodes, *] or graph-level [1, *]
50 pos=positions, # [num_nodes, num_dimensions] (for point clouds/spatial)
52```
54**`edge_index` format is critical**: it's a `[2, num_edges]` tensor where `edge_index[0]` = source nodes, `edge_index[1]` = target nodes. It is NOT a list of tuples. If you have edge pairs as rows, transpose and call `.contiguous()`:
56```python
57# If edges are [[src1, dst1], [src2, dst2], ...] — transpose first:
58edge_index = edge_pairs.t().contiguous()
59```
61For undirected graphs, include both directions: edge (0,1) needs both `[0,1]` and `[1,0]` in edge_index.
63If node features are absent, set `data.num_nodes` explicitly from the node table. Inferring it from `edge_index.max() + 1` misses isolated nodes, which can corrupt batching offsets and outputs. Check `data.validate(raise_on_error=True)` after construction, including an edge-free or isolated-node case.
65For heterogeneous graphs, use `HeteroData` — see the Heterogeneous Graphs section below.

Graph Data: Data and HeteroData

A graph lives in a Data object. The key attributes:

from torch_geometric.data import Data

data = Data(
    x=node_features,          # [num_nodes, num_node_features]
    edge_index=edge_index,     # [2, num_edges] — COO format, dtype=torch.long
    edge_attr=edge_features,   # [num_edges, num_edge_features]
    y=labels,                  # node-level [num_nodes, *] or graph-level [1, *]
    pos=positions,             # [num_nodes, num_dimensions] (for point clouds/spatial)
)

**edge_index format is critical**: it's a [2, num_edges] tensor where edge_index[0] = source nodes, edge_index[1] = target nodes. It is NOT a list of tuples. If you have edge pairs as rows, transpose and call .contiguous():

# If edges are [[src1, dst1], [src2, dst2], ...] — transpose first:
edge_index = edge_pairs.t().contiguous()

For undirected graphs, include both directions: edge (0,1) needs both [0,1] and [1,0] in edge_index.

If node features are absent, set data.num_nodes explicitly from the node table. Inferring it from edge_index.max() + 1 misses isolated nodes, which can corrupt batching offsets and outputs. Check data.validate(raise_on_error=True) after construction, including an edge-free or isolated-node case.

For heterogeneous graphs, use HeteroData — see the Heterogeneous Graphs section below.

Raw lines 38 to 66
67### Datasets
69PyG bundles many standard datasets that auto-download and preprocess:
71```python
72from torch_geometric.datasets import Planetoid, TUDataset
74# Single-graph node classification (Cora, Citeseer, Pubmed)
75dataset = Planetoid(root='./data/Cora', name='Cora', split='public')
76data = dataset[0] # single graph with train/val/test masks
78# Multi-graph classification (ENZYMES, MUTAG, IMDB-BINARY, etc.)
79dataset = TUDataset(root='./data/TU', name='ENZYMES')
80# dataset[0], dataset[1], ... are individual graphs
81```
83Common datasets by task:
84- **Node classification**: Planetoid (Cora/Citeseer/Pubmed), OGB (ogbn-arxiv, ogbn-products, ogbn-mag)
85- **Graph classification**: TUDataset (MUTAG, ENZYMES, PROTEINS, IMDB-BINARY), OGB (ogbg-molhiv)
86- **Link prediction**: OGB (ogbl-collab, ogbl-citation2)
87- **Molecular**: QM7b, QM9, MoleculeNet
88- **Point cloud/mesh**: ShapeNet, ModelNet(name="10" or "40"), FAUST (manual download)
90Dataset classes manage provider downloads; they are not API search endpoints. Preserve the dataset version, split and preprocessing. OGB benchmarks use the separate `ogb` package/evaluator; do not replace their official split with a random split. See [review notes](references/review.md) for verified download locations and unexecuted large datasets.

Datasets

PyG bundles many standard datasets that auto-download and preprocess:

from torch_geometric.datasets import Planetoid, TUDataset

# Single-graph node classification (Cora, Citeseer, Pubmed)
dataset = Planetoid(root='./data/Cora', name='Cora', split='public')
data = dataset[0]  # single graph with train/val/test masks

# Multi-graph classification (ENZYMES, MUTAG, IMDB-BINARY, etc.)
dataset = TUDataset(root='./data/TU', name='ENZYMES')
# dataset[0], dataset[1], ... are individual graphs

Common datasets by task:

  • Node classification: Planetoid (Cora/Citeseer/Pubmed), OGB (ogbn-arxiv, ogbn-products, ogbn-mag)
  • Graph classification: TUDataset (MUTAG, ENZYMES, PROTEINS, IMDB-BINARY), OGB (ogbg-molhiv)
  • Link prediction: OGB (ogbl-collab, ogbl-citation2)
  • Molecular: QM7b, QM9, MoleculeNet
  • Point cloud/mesh: ShapeNet, ModelNet(name="10" or "40"), FAUST (manual download)

Dataset classes manage provider downloads; they are not API search endpoints. Preserve the dataset version, split and preprocessing. OGB benchmarks use the separate ogb package/evaluator; do not replace their official split with a random split. See review notes for verified download locations and unexecuted large datasets.

Raw lines 67 to 91
Reads a file references/review.md :90
92### Transforms
94Transforms preprocess or augment graph data, analogous to torchvision transforms:
96```python
97import torch_geometric.transforms as T
98from torch_geometric.datasets import ShapeNet
100# Common transforms
101T.NormalizeFeatures() # Shift by minimum, then divide row sum (clamped >=1)
102T.ToUndirected() # Add reverse edges to make graph undirected
103T.AddSelfLoops() # Add self-loop edges
104T.KNNGraph(k=6) # Build k-NN graph from positions; requires pyg-lib
105T.RandomJitter(0.01) # Random noise augmentation on positions
106T.Compose([...]) # Chain multiple transforms
108# Apply as pre_transform (once, saved to disk) or transform (every access)
109dataset = ShapeNet(root='./data', pre_transform=T.KNNGraph(k=6),
110 transform=T.RandomJitter(0.01))
111```
113`ToUndirected` may merge/reduce duplicate edge attributes (default sum); confirm weight/label semantics before applying it. Adding self-loops can also duplicate existing loops. Do not make directed or temporal relations undirected without a scientific reason.

Transforms

Transforms preprocess or augment graph data, analogous to torchvision transforms:

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

# Common transforms
T.NormalizeFeatures()    # Shift by minimum, then divide row sum (clamped >=1)
T.ToUndirected()         # Add reverse edges to make graph undirected
T.AddSelfLoops()         # Add self-loop edges
T.KNNGraph(k=6)          # Build k-NN graph from positions; requires pyg-lib
T.RandomJitter(0.01)     # Random noise augmentation on positions
T.Compose([...])         # Chain multiple transforms

# Apply as pre_transform (once, saved to disk) or transform (every access)
dataset = ShapeNet(root='./data', pre_transform=T.KNNGraph(k=6),
                   transform=T.RandomJitter(0.01))

ToUndirected may merge/reduce duplicate edge attributes (default sum); confirm weight/label semantics before applying it. Adding self-loops can also duplicate existing loops. Do not make directed or temporal relations undirected without a scientific reason.

Raw lines 92 to 114
115## Building GNN Models

Building GNN Models

Raw lines 115 to 116
117### Quick Start: Using Built-in Layers
119The fastest way to build a GNN — stack conv layers from `torch_geometric.nn`:
121```python
122import torch
123import torch.nn.functional as F
124from torch_geometric.nn import GCNConv
126class GCN(torch.nn.Module):
127 def __init__(self, in_channels, hidden_channels, out_channels):
128 super().__init__()
129 self.conv1 = GCNConv(in_channels, hidden_channels)
130 self.conv2 = GCNConv(hidden_channels, out_channels)
132 def forward(self, x, edge_index):
133 x = self.conv1(x, edge_index).relu()
134 x = F.dropout(x, p=0.5, training=self.training)
135 x = self.conv2(x, edge_index)
136 return x
137```
139`GCNConv`, `SAGEConv` and attention layers return embeddings; add the intended nonlinearities between them. `GINConv`/`EdgeConv` use supplied networks that may already contain activations.

Quick Start: Using Built-in Layers

The fastest way to build a GNN — stack conv layers from torch_geometric.nn:

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

class GCN(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super().__init__()
        self.conv1 = GCNConv(in_channels, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, out_channels)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index).relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

GCNConv, SAGEConv and attention layers return embeddings; add the intended nonlinearities between them. GINConv/EdgeConv use supplied networks that may already contain activations.

Raw lines 117 to 140
141### Choosing a Conv Layer
143Pick based on your task and graph structure:
145| Layer | Best for | Key idea |
146|-------|----------|----------|
147| `GCNConv` | Homogeneous, semi-supervised node classification | Spectral-inspired, degree-normalized aggregation |
148| `GATConv` / `GATv2Conv` | When neighbor importance varies | Attention-weighted messages |
149| `SAGEConv` | Large graphs, inductive settings | Sampling-friendly, learnable aggregation |
150| `GINConv` | Graph classification, maximizing expressiveness | Can match 1-WL under the paper's injectivity assumptions |
151| `TransformerConv` | Rich edge features, complex interactions | Multi-head attention with edge features |
152| `EdgeConv` | Point clouds, dynamic graphs | MLP on edge features (x_i, x_j - x_i) |
153| `RGCNConv` | Heterogeneous with many relation types | Relation-specific weight matrices |
154| `HGTConv` | Heterogeneous graphs | Type-specific attention |
156Check the chosen signature: `RGCNConv` also needs relation IDs (`edge_type`), `HGTConv` takes dictionaries, and `GCNConv` accepts scalar `edge_weight`, not arbitrary `edge_attr`.

Choosing a Conv Layer

Pick based on your task and graph structure:

LayerBest forKey idea
GCNConvHomogeneous, semi-supervised node classificationSpectral-inspired, degree-normalized aggregation
GATConv / GATv2ConvWhen neighbor importance variesAttention-weighted messages
SAGEConvLarge graphs, inductive settingsSampling-friendly, learnable aggregation
GINConvGraph classification, maximizing expressivenessCan match 1-WL under the paper's injectivity assumptions
TransformerConvRich edge features, complex interactionsMulti-head attention with edge features
EdgeConvPoint clouds, dynamic graphsMLP on edge features (x_i, x_j - x_i)
RGCNConvHeterogeneous with many relation typesRelation-specific weight matrices
HGTConvHeterogeneous graphsType-specific attention

Check the chosen signature: RGCNConv also needs relation IDs (edge_type), HGTConv takes dictionaries, and GCNConv accepts scalar edge_weight, not arbitrary edge_attr.

Raw lines 141 to 157
158### Lazy Initialization
160Use `-1` for input channels to let PyG infer dimensions automatically — especially useful for heterogeneous models:
162```python
163from torch_geometric.nn import SAGEConv
164conv = SAGEConv((-1, -1), 64) # Input dims inferred on first forward pass
165# Initialize lazy modules:
166with torch.no_grad():
167 out = conv(data.x, data.edge_index)
168```

Lazy Initialization

Use -1 for input channels to let PyG infer dimensions automatically — especially useful for heterogeneous models:

from torch_geometric.nn import SAGEConv
conv = SAGEConv((-1, -1), 64)  # Input dims inferred on first forward pass
# Initialize lazy modules:
with torch.no_grad():
    out = conv(data.x, data.edge_index)
Raw lines 158 to 169
170### High-Level Model APIs
172For common architectures, PyG provides ready-made model classes:
174```python
175from torch_geometric.nn import GraphSAGE, GCN as GCNModel, GAT as GATModel, GIN as GINModel
177model = GraphSAGE(
178 in_channels=dataset.num_features,
179 hidden_channels=64,
180 out_channels=dataset.num_classes,
181 num_layers=2,
183```

High-Level Model APIs

For common architectures, PyG provides ready-made model classes:

from torch_geometric.nn import GraphSAGE, GCN as GCNModel, GAT as GATModel, GIN as GINModel

model = GraphSAGE(
    in_channels=dataset.num_features,
    hidden_channels=64,
    out_channels=dataset.num_classes,
    num_layers=2,
)
Raw lines 170 to 184
185### Custom Layers via MessagePassing
187To implement a novel GNN layer, subclass `MessagePassing`. The framework is:
1891. `propagate()` orchestrates the message passing
1902. `message()` defines what info flows along each edge (the phi function)
1913. `aggregate()` combines messages at each node (sum/mean/max)
1924. `update()` transforms the aggregated result (the gamma function)
194```python
195from torch_geometric.nn import MessagePassing
196from torch_geometric.utils import add_self_loops, degree
198class MyConv(MessagePassing):
199 def __init__(self, in_channels, out_channels):
200 super().__init__(aggr='add') # "add", "mean", or "max"
201 self.lin = torch.nn.Linear(in_channels, out_channels)
203 def forward(self, x, edge_index):
204 # Pre-processing before message passing
205 x = self.lin(x)
206 # Start message passing
207 return self.propagate(edge_index, x=x)
209 def message(self, x_j):
210 # x_j: features of source nodes for each edge [num_edges, features]
211 # The _j suffix auto-indexes source nodes, _i indexes target nodes
212 return x_j
213```
215**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().
217Read `references/message_passing.md` for the full GCN and EdgeConv implementation examples.

Custom Layers via MessagePassing

To implement a novel GNN layer, subclass MessagePassing. The framework is:

  1. propagate() orchestrates the message passing
  2. message() defines what info flows along each edge (the phi function)
  3. aggregate() combines messages at each node (sum/mean/max)
  4. update() transforms the aggregated result (the gamma function)
from 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.

Raw lines 185 to 218
219## Task-Specific Patterns
221Training loops are adaptation recipes. Regression checks use tiny synthetic inputs and short runs, not full benchmark convergence.

Task-Specific Patterns

Training loops are adaptation recipes. Regression checks use tiny synthetic inputs and short runs, not full benchmark convergence.

Raw lines 219 to 222
223### Node Classification
225```python
226# Full-batch training on a single graph (e.g., Cora)
227model = GCN(dataset.num_features, 64, dataset.num_classes)
228optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
229model.train()
230for epoch in range(200):
231 optimizer.zero_grad()
232 out = model(data.x, data.edge_index)
233 loss = F.cross_entropy(out[data.train_mask], data.y[data.train_mask])
234 loss.backward()
235 optimizer.step()
237# Select checkpoints using validation only; evaluate test once afterward.
238model.eval() # Module evaluation behavior; gradients are disabled separately.
239with torch.no_grad():
240 pred = model(data.x, data.edge_index).argmax(dim=1)
241 acc = (pred[data.test_mask] == data.y[data.test_mask]).float().mean()
242```

Node Classification

# Full-batch training on a single graph (e.g., Cora)
model = GCN(dataset.num_features, 64, dataset.num_classes)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
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()

# Select checkpoints using validation only; evaluate test once afterward.
model.eval()  # Module evaluation behavior; gradients are disabled separately.
with torch.no_grad():
    pred = model(data.x, data.edge_index).argmax(dim=1)
    acc = (pred[data.test_mask] == data.y[data.test_mask]).float().mean()
Raw lines 223 to 243
244### Graph Classification
246Multiple graphs — use `DataLoader` for mini-batching and global pooling to get graph-level representations:
248```python
249from torch_geometric.loader import DataLoader
250from torch_geometric.nn import GCNConv, global_mean_pool
252loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
254class GraphClassifier(torch.nn.Module):
255 def __init__(self, in_ch, hidden_ch, out_ch):
256 super().__init__()
257 self.conv1 = GCNConv(in_ch, hidden_ch)
258 self.conv2 = GCNConv(hidden_ch, hidden_ch)
259 self.lin = torch.nn.Linear(hidden_ch, out_ch)
261 def forward(self, x, edge_index, batch):
262 x = self.conv1(x, edge_index).relu()
263 x = self.conv2(x, edge_index).relu()
264 x = global_mean_pool(x, batch) # [num_graphs_in_batch, hidden_ch]
265 return self.lin(x)
267# train_dataset is a previously split graph-level dataset with node features.
268model = GraphClassifier(dataset.num_features, 64, dataset.num_classes)
269optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
270model.train()
271for data in loader:
272 optimizer.zero_grad()
273 out = model(data.x, data.edge_index, data.batch)
274 loss = F.cross_entropy(out, data.y.view(-1).long())
275 loss.backward()
276 optimizer.step()
277```
279PyG's `DataLoader` offsets edge indices to represent a disconnected union (block-diagonal adjacency), without allocating a dense matrix. 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.

Graph Classification

Multiple graphs — use DataLoader for mini-batching and global pooling to get graph-level representations:

from torch_geometric.loader import DataLoader
from torch_geometric.nn import GCNConv, global_mean_pool

loader = DataLoader(train_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)

# train_dataset is a previously split graph-level dataset with node features.
model = GraphClassifier(dataset.num_features, 64, dataset.num_classes)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
model.train()
for data in loader:
    optimizer.zero_grad()
    out = model(data.x, data.edge_index, data.batch)
    loss = F.cross_entropy(out, data.y.view(-1).long())
    loss.backward()
    optimizer.step()

PyG's DataLoader offsets edge indices to represent a disconnected union (block-diagonal adjacency), without allocating a dense matrix. 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.

Raw lines 244 to 280
281### Link Prediction
283Split edges into train/val/test, use negative sampling:
285```python
286from torch_geometric.transforms import RandomLinkSplit
288transform = RandomLinkSplit(
289 num_val=0.1,
290 num_test=0.1,
291 is_undirected=True,
292 add_negative_train_samples=True,
293 disjoint_train_ratio=0.2, # Keep supervision out of training message edges.
295train_data, val_data, test_data = transform(data)
297# Encode nodes, then score edges
298encoder = GCN(data.num_features, 64, 32)
299z = encoder(train_data.x, train_data.edge_index)
300src, dst = train_data.edge_label_index
301logits = (z[src] * z[dst]).sum(dim=-1)
302loss = F.binary_cross_entropy_with_logits(logits, train_data.edge_label.float())
303```
305Read `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.

Link Prediction

Split edges into train/val/test, use negative sampling:

from torch_geometric.transforms import RandomLinkSplit

transform = RandomLinkSplit(
    num_val=0.1,
    num_test=0.1,
    is_undirected=True,
    add_negative_train_samples=True,
    disjoint_train_ratio=0.2,  # Keep supervision out of training message edges.
)
train_data, val_data, test_data = transform(data)

# Encode nodes, then score edges
encoder = GCN(data.num_features, 64, 32)
z = encoder(train_data.x, train_data.edge_index)
src, dst = train_data.edge_label_index
logits = (z[src] * z[dst]).sum(dim=-1)
loss = F.binary_cross_entropy_with_logits(logits, train_data.edge_label.float())

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.

Raw lines 281 to 306
307## Scaling to Large Graphs
309For graphs that don't fit in GPU memory, use `NeighborLoader` with a compatible sampling backend. The following is illustrative; it needs `pyg-lib` or `torch-sparse`:
311```python
312from torch_geometric.loader import NeighborLoader
314train_loader = NeighborLoader(
315 data,
316 num_neighbors=[15, 10], # Sample 15 neighbors in hop 1, 10 in hop 2
317 batch_size=128, # Number of seed nodes per batch
318 input_nodes=data.train_mask, # Which nodes to sample from
319 shuffle=True,
322for batch in train_loader:
323 batch = batch.to(device)
324 out = model(batch.x, batch.edge_index)
325 # Only use first batch_size nodes for loss (these are the seed nodes)
326 loss = F.cross_entropy(out[:batch.batch_size], batch.y[:batch.batch_size])
327```
329**Key points about NeighborLoader**:
330- `num_neighbors` list length should match GNN depth (number of message passing layers)
331- Seed nodes are always the first `batch.batch_size` nodes in the output
332- `batch.n_id` maps relabeled indices back to original node IDs
333- Works for both `Data` and `HeteroData`
334- For link prediction, use `LinkNeighborLoader` instead
335- Large fan-out across many hops grows rapidly; measure the sampled sizes
337Other scalability options: `ClusterLoader` (ClusterGCN), `GraphSAINTSampler`, `ShaDowKHopSampler`. For multi-GPU training, DDP, PyTorch Lightning integration, and `torch.compile` support, read `references/scaling.md`.

Scaling to Large Graphs

For graphs that don't fit in GPU memory, use NeighborLoader with a compatible sampling backend. The following is illustrative; it needs pyg-lib or torch-sparse:

from 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)
  • Seed nodes are always the first batch.batch_size nodes in the output
  • batch.n_id maps relabeled indices back to original node IDs
  • Works for both Data and HeteroData
  • For link prediction, use LinkNeighborLoader instead
  • Large fan-out across many hops grows rapidly; measure the sampled sizes

Other scalability options: ClusterLoader (ClusterGCN), GraphSAINTSampler, ShaDowKHopSampler. For multi-GPU training, DDP, PyTorch Lightning integration, and torch.compile support, read references/scaling.md.

Raw lines 307 to 338
Reads a file references/scaling.md :337
339## Heterogeneous Graphs
341For graphs with multiple node and edge types (social networks, knowledge graphs, recommendation):
343```python
344from torch_geometric.data import HeteroData
346data = HeteroData()
348# Node features — indexed by node type string
349data['user'].x = torch.randn(1000, 64)
350data['movie'].x = torch.randn(500, 128)
352# Edge indices — indexed by (src_type, edge_type, dst_type) triplet
353data['user', 'rates', 'movie'].edge_index = torch.stack([
354 torch.randint(1000, (3000,)), torch.randint(500, (3000,))])
355data['user', 'follows', 'user'].edge_index = torch.randint(0, 1000, (2, 5000))
357# Access convenience dicts
358data.x_dict # {'user': tensor, 'movie': tensor}
359data.edge_index_dict # {('user','rates','movie'): tensor, ...}
360data.metadata() # ([node_types], [edge_types])
361```

Heterogeneous Graphs

For graphs with multiple node and edge types (social networks, knowledge graphs, recommendation):

from 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.stack([
    torch.randint(1000, (3000,)), torch.randint(500, (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])
Raw lines 339 to 362
363### Three ways to build heterogeneous GNNs
365**1. Auto-convert with `to_hetero()`** — write a homogeneous model, convert automatically:
367```python
368from torch_geometric.nn import SAGEConv, to_hetero
370class GNN(torch.nn.Module):
371 def __init__(self, hidden_channels, out_channels):
372 super().__init__()
373 self.conv1 = SAGEConv((-1, -1), hidden_channels)
374 self.conv2 = SAGEConv((-1, -1), out_channels)
376 def forward(self, x, edge_index):
377 x = self.conv1(x, edge_index).relu()
378 x = self.conv2(x, edge_index)
379 return x
381model = GNN(64, dataset.num_classes)
382model = to_hetero(model, data.metadata(), aggr='sum')
384# Now accepts dicts:
385out = model(data.x_dict, data.edge_index_dict)
386```
388Use `(-1, -1)` for bipartite input channels (source, target may differ). Lazy init handles the rest.
390**2. `HeteroConv` wrapper** — different conv per edge type:
392```python
393from torch_geometric.nn import HeteroConv, GCNConv, SAGEConv, GATConv
395conv = HeteroConv({
396 ('paper', 'cites', 'paper'): GCNConv(-1, 64),
397 ('author', 'writes', 'paper'): SAGEConv((-1, -1), 64),
398 ('paper', 'rev_writes', 'author'): GATConv((-1, -1), 64, add_self_loops=False),
399}, aggr='sum')
400```
402**3. Native heterogeneous operators** like `HGTConv`:
404```python
405from torch_geometric.nn import HGTConv
406conv = HGTConv(-1, 64, data.metadata(), heads=4) # 64 must divide by 4
407```
409**Important for heterogeneous graphs**:
410- Use `T.ToUndirected()` to add reverse edge types for bidirectional message flow
411- Disable `add_self_loops` in bipartite conv layers (different source/dest types) — use skip connections instead: `conv(x, edge_index) + lin(x)`
412- For NeighborLoader on HeteroData, specify `input_nodes` as `('node_type', mask)` tuple
413- `num_neighbors` can be a dict keyed by edge type for fine-grained control
415Read `references/heterogeneous.md` for complete examples including training loops and NeighborLoader usage with heterogeneous graphs.

Three ways to build heterogeneous GNNs

**1. Auto-convert with to_hetero()** — write a homogeneous model, convert automatically:

from 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:

from 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:

from torch_geometric.nn import HGTConv
conv = HGTConv(-1, 64, data.metadata(), heads=4)  # 64 must divide by 4

Important for heterogeneous graphs:

  • Use T.ToUndirected() to add reverse edge types for bidirectional message flow
  • Disable add_self_loops in bipartite conv layers (different source/dest types) — use skip connections instead: conv(x, edge_index) + lin(x)
  • For NeighborLoader on HeteroData, specify input_nodes as ('node_type', mask) tuple
  • num_neighbors can be a dict keyed by edge type for fine-grained control

Read references/heterogeneous.md for complete examples including training loops and NeighborLoader usage with heterogeneous graphs.

Raw lines 363 to 416
Reads a file references/heterogeneous.md :415
417## Custom Datasets
419For loading your own data into PyG:
421- **Quick (no class needed)**: Create `Data` objects directly and pass a list to `DataLoader`
422- **Reusable (fits in RAM)**: Subclass `InMemoryDataset` — override `raw_file_names`, `processed_file_names`, `download()`, `process()`
423- **Large (disk-backed)**: Subclass `Dataset` — also override `len()` and `get()`
424- **From CSV**: Load node/edge tables with pandas, build mappings to consecutive indices, assemble into `Data` or `HeteroData`
425- **From NetworkX**: `from_networkx(G)` converts a NetworkX graph directly
426- **From scipy sparse**: `from_scipy_sparse_matrix(adj)` extracts edge_index
428Read `references/custom_datasets.md` for complete examples with all patterns, CSV loading with encoders, and the MovieLens walkthrough.

Custom Datasets

For loading your own data into PyG:

  • Quick (no class needed): Create Data objects directly and pass a list to DataLoader
  • Reusable (fits in RAM): Subclass InMemoryDataset — override raw_file_names, processed_file_names, download(), process()
  • Large (disk-backed): Subclass Dataset — also override len() and get()
  • From CSV: Load node/edge tables with pandas, build mappings to consecutive indices, assemble into Data or HeteroData
  • From NetworkX: from_networkx(G) converts a NetworkX graph directly
  • From scipy sparse: from_scipy_sparse_matrix(adj) extracts edge_index

Read references/custom_datasets.md for complete examples with all patterns, CSV loading with encoders, and the MovieLens walkthrough.

Raw lines 417 to 429
430## Explainability
432PyG provides `torch_geometric.explain` for interpreting GNN predictions:
434```python
435from torch_geometric.explain import Explainer, GNNExplainer
437explainer = Explainer(
438 model=model,
439 algorithm=GNNExplainer(epochs=200),
440 explanation_type='model',
441 node_mask_type='attributes',
442 edge_mask_type='object',
443 model_config=dict(
444 mode='multiclass_classification',
445 task_level='node',
446 return_type='raw', # GCN above returns logits.
447 ),
450explanation = explainer(data.x, data.edge_index, index=10)
451explanation.visualize_graph() # Important subgraph
452explanation.visualize_feature_importance(top_k=10) # Feature importance
453```
455Available algorithms: `GNNExplainer` (optimization-based), `PGExplainer` (parametric, trained), `CaptumExplainer` (gradient-based via Captum), `AttentionExplainer` (attention weights). Heterogeneous support depends on the algorithm; wrap dict-returning models to select one output node type.
457Read `references/explainability.md` for all algorithms, heterogeneous explanations, evaluation metrics, and PGExplainer training.

Explainability

PyG provides torch_geometric.explain for interpreting GNN predictions:

from 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='raw',  # GCN above returns logits.
    ),
)

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). Heterogeneous support depends on the algorithm; wrap dict-returning models to select one output node type.

Read references/explainability.md for all algorithms, heterogeneous explanations, evaluation metrics, and PGExplainer training.

Raw lines 430 to 458
459## Common Pitfalls
4611. **edge_index shape**: Must be `[2, num_edges]`, not `[num_edges, 2]`. Transpose if needed.
4622. **Forgetting activations**: Check where the chosen layer or its supplied MLP applies nonlinearities.
4633. **Self-loops in hetero bipartite**: Don't use `add_self_loops=True` when source and dest node types differ. Use skip connections instead.
4644. **NeighborLoader slicing**: Only the first `batch.batch_size` nodes are your seed nodes. Slice predictions and labels accordingly.
4655. **Undirected graphs**: If your graph is undirected, include edges in both directions in `edge_index`, or use `T.ToUndirected()`.
4666. **Lazy init**: Models with `-1` input channels need one forward pass with `torch.no_grad()` before training to initialize parameters.
4677. **Global pooling for graph tasks**: Use `global_mean_pool(x, batch)` (not manual reshape) to aggregate node features to graph-level.
4688. **num_neighbors alignment**: Keep `len(num_neighbors)` equal to the number of GNN layers. More hops than layers wastes compute; fewer means wasted model capacity.
470Choose splits before fitting features or model selection. Graph-level random splits can leak related molecules, patients, scaffolds, times, or sites; node-label masks define a transductive task unless unseen nodes/edges are excluded. Report the split unit, negative-edge universe, class balance, multiple seeds, and a task-appropriate baseline. A successful forward/backward pass is a mechanics check, not evidence of scientific generalization.

Common Pitfalls

  1. edge_index shape: Must be [2, num_edges], not [num_edges, 2]. Transpose if needed.
  2. Forgetting activations: Check where the chosen layer or its supplied MLP applies nonlinearities.
  3. Self-loops in hetero bipartite: Don't use add_self_loops=True when source and dest node types differ. Use skip connections instead.
  4. NeighborLoader slicing: Only the first batch.batch_size nodes are your seed nodes. Slice predictions and labels accordingly.
  5. Undirected graphs: If your graph is undirected, include edges in both directions in edge_index, or use T.ToUndirected().
  6. Lazy init: Models with -1 input channels need one forward pass with torch.no_grad() before training to initialize parameters.
  7. Global pooling for graph tasks: Use global_mean_pool(x, batch) (not manual reshape) to aggregate node features to graph-level.
  8. num_neighbors alignment: Keep len(num_neighbors) equal to the number of GNN layers. More hops than layers wastes compute; fewer means wasted model capacity.

Choose splits before fitting features or model selection. Graph-level random splits can leak related molecules, patients, scaffolds, times, or sites; node-label masks define a transductive task unless unseen nodes/edges are excluded. Report the split unit, negative-edge universe, class balance, multiple seeds, and a task-appropriate baseline. A successful forward/backward pass is a mechanics check, not evidence of scientific generalization.

Raw lines 459 to 471
Boilerplate, lines 472 to 487. The same text appears in 125 other files here. Close
472## Citing Scientific Agent Skills
474This skill is part of Scientific Agent Skills by K-Dense. If it materially contributed to a
475manuscript, report, presentation, or code release, add the paper to the references or
476software section and tell the user you did so:
478> Kassis, T., Agarwal, V., He, Y., Patel, D., & Brueckner, A. M. (2026). Scientific Agent
479> Skills: A Library of Procedural Knowledge for Research Agents. arXiv:2609.00065.
480> https://doi.org/10.48550/arXiv.2609.00065
482Always cite the current version. The DOI and https://arxiv.org/abs/2609.00065 resolve to the
483latest arXiv version, so never append a version suffix such as `v1`. When network access is
484available, fetch https://arxiv.org/abs/2609.00065 (or
485http://export.arxiv.org/api/query?id_list=2609.00065) before writing the reference and take
486the author list, year, and version from that record. If the record lists a journal reference
487or publisher DOI, cite the published version instead.

From K-Dense-AI/scientific-agent-skills at 5f2bc03, 2026-10-26. Not read by a model yet. Summary and techniques by Claude Opus 5.5 on 2026-11-06.