Custom models & datasets¶
Custom models and datasets go through the same statistical pipeline as the built-ins — multi-seed, Holm-corrected, CD-diagrammed.
Plug in your own model¶
Once a model declares which task it supports, three integration paths cover the common cases:
Decorator — permanent registration at import time. Best for libraries or shared modules:
import torch
from torch_geometric.nn import GCNConv
from graphnetz import register_model
@register_model(task_type={"node_cls"})
class MyGNN(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, data):
x, ei = data.x, data.edge_index
return self.conv2(torch.relu(self.conv1(x, ei)), ei)
run_benchmark("social", {"MyGNN": MyGNN}, task_type="node_cls", seeds=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
Class attribute — same effect, no decorator dependency:
Inline tuple — one-shot variants for hyperparameter sweeps. The third
slot is a factory (in_channels, hidden_channels, out_channels) -> Module:
run_benchmark(
"social",
{
"MyGNN-d0.3": (MyGNN, "node_cls", lambda i, h, o: MyGNN(i, h, o, dropout=0.3)),
"MyGNN-d0.5": (MyGNN, "node_cls", lambda i, h, o: MyGNN(i, h, o, dropout=0.5)),
},
)
For node-level encoders that should run on all four task types without writing the adapter glue, see the multi-task factory.
Plug in your own dataset¶
Custom datasets get the same statistical pipeline as the built-ins.
The minimal contract is the standard PyG one — your dataset object exposes
ds[0] returning a Data, plus the relevant attributes for the task
(num_features, num_classes, or num_relations).
Quickest path — wrap an already-loaded dataset and pass it via
tasks=:
from graphnetz import GCN, run_benchmark, task_from_dataset
# Your dataset (any PyG-shaped object).
ds = my_loader("data/my_dataset")
task = task_from_dataset("my_dataset", "node_cls", ds, epochs=100)
report = run_benchmark(
models={"GCN": GCN},
tasks=[task],
seeds=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
)
No BENCHMARK_TASKS mutation, no global state — tasks= bypasses the
registry entirely. category defaults to "custom" for cache-path
namespacing.
Permanent registration — make your dataset visible to
run_benchmark(category, ...) and iter_benchmark_tasks:
from graphnetz import register_task, task_from_dataset, unregister_task
register_task("biology", task_from_dataset("my_assay", "graph_cls", ds, epochs=50))
# ... later, if you want to remove it:
unregister_task("biology", "my_assay")
Seed-aware loaders — for synthetic datasets where each seed should
produce a fresh sample, write a loader that takes a seed keyword. The
dispatcher detects it via inspect.signature and passes the benchmark
seed in:
from graphnetz.benchmark import Task
def my_loader(root: str, *, seed: int):
return MySyntheticDataset(root, num_graphs=100, seed=seed)
task = Task("synthetic_g100", "graph_cls", my_loader, epochs=20)
report = run_benchmark(models={"GCN": GCN}, tasks=[task], seeds=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
Tip
The conventions for each task (which attributes the dataset must
expose, how splits are encoded) live in the trainer docstrings:
graphnetz.train_node_classification,
graphnetz.train_graph_classification,
graphnetz.train_graph_regression,
graphnetz.train_link_prediction, and
graphnetz.train_relational_link_prediction.