Masks, Splits, and Leakage: Trustworthy Supervision in Tensor Graphs
Node classification on a single graph has a trap that supervised learning on independent samples does not. The model sees the whole graph structure during training — including the nodes it will be tested on — and the only thing separating train from test is which node labels it is allowed to use. Get the masks wrong, let an edge or a feature carry the answer across the split, and you report a number that evaporates on real data. TGraphX takes this seriously with explicit mask storage and a small leakage-checking toolkit. This guide shows how to use them.
It extends the validation philosophy of Shape-Aware Validation into the data-hygiene domain.
Masks travel with the graph
The first design decision is where splits live. In TGraphX, when you pass train_mask=, val_mask=, or test_mask= to a Graph, they are stored inside metadata['masks'] rather than as detached globals (see batching internals). The benefit is structural: the split moves with the graph through device transfers, serialization, and batching, so you cannot accidentally pair a graph with someone else's split. A mask whose length does not match the node count is rejected at construction.
import tgraphx as tgx
g = tgx.Graph(x=x, edge_index=edge_index, labels=y,
train_mask=train_mask, val_mask=val_mask, test_mask=test_mask)
# masks now live in g.metadata['masks'] and travel with g
The leakage toolkit
tgraphx/ux/leakage.py provides three functions and a dedicated error type:
| Function | Checks |
|---|---|
check_leakage(...) |
masks do not overlap (a node in two splits) |
leakage_report(...) |
a structured summary of split sizes and overlaps |
validate_split_policy(...) |
the split obeys a stated policy |
LeakageError |
raised (a ValueError subclass) when a check fails |
The most common, most embarrassing leak is a node that appears in both the train and test masks. check_leakage catches exactly that — overlapping index sets — and raises LeakageError rather than letting the contaminated split run. leakage_report gives you the numbers (sizes, intersections) for a logbook, and validate_split_policy lets you assert a rule about how the split was constructed.
report = tgx.leakage_report(train_mask=train_mask, val_mask=val_mask, test_mask=test_mask)
tgx.check_leakage(train_mask=train_mask, test_mask=test_mask) # raises on overlap
The one-call tgx.classify_nodes(...) helper, added in v1.4.1, runs with a built-in leakage guard, so the safe path is also the default path for newcomers.
What leakage detection does — and does not — catch
This is where honesty matters most, because a leakage checker that oversells itself is worse than none. check_leakage detects mask overlap: the same node assigned to two splits. That is the most frequent error, and catching it automatically is genuinely valuable. But leakage has subtler forms it cannot detect for you:
- Feature leakage — an input feature that encodes the label (the
graph_featuresvsgraph_labelseparation discussed in the pooling article exists partly to prevent this). - Structural leakage — in transductive settings, message passing legitimately uses test-node structure; whether that is acceptable depends on your evaluation protocol, not a function call.
- Preprocessing leakage — normalisation statistics computed over the whole graph including test nodes.
TGraphX gives you a reliable guard for the overlap case and a reporting tool for the rest; it does not claim to certify a pipeline leak-free. Treat check_leakage as a smoke alarm, not a fire marshal.
A trustworthy-supervision checklist
- Build the graph with
train/val/testmasks so they live inmetadata['masks']. - Run
tgx.check_leakage(...)to assert non-overlapping splits. - Keep graph-level inputs in
graph_features, never in a field aliased to the label. - Compute any normalisation statistics on the training mask only.
- Log
tgx.leakage_report(...)alongside results for reproducibility (see GNN reproducibility).
Steps 1–2 and 5 are one function call each; steps 3–4 are discipline the tooling supports but cannot enforce.
Transductive vs inductive — the split means different things
Whether mask overlap is even the right question depends on your setting. In a transductive task, training and test nodes live in the same graph and the model sees all node structure during training — only the labels are masked. Here the masks are the entire definition of the split, and check_leakage guarding their disjointness is the core safeguard. In an inductive task, test nodes (or whole test graphs) are unseen at training time; the split is at the graph or subgraph level, and leakage can hide in shared preprocessing rather than in node masks.
TGraphX stores masks in metadata['masks'] either way, but the meaning differs: transductive masks select which of a shared graph's nodes are supervised, while inductive splits separate graphs entirely. Naming your setting explicitly — and stating it in a paper — prevents the most common reviewer confusion, because a number that is strong transductively can be far weaker inductively. The leakage tools check the mechanics; matching the split to the claim is your responsibility.
In short, the masks encode your experimental contract; storing them with the graph and checking them with check_leakage keeps that contract intact from construction through to the reported number, which is exactly where a quiet leak would otherwise creep in.
Related guides
- Shape-Aware Validation in TGraphX
- Research Reproducibility in GNN Projects
- The TGraphX Graph and GraphBatch
Conclusion
Trustworthy supervision on graphs starts with splits that cannot drift from their data and overlaps that cannot pass silently. TGraphX stores masks in metadata['masks'], rejects malformed ones at construction, and provides check_leakage / leakage_report / validate_split_policy to catch the overlap leak automatically. Used with discipline about feature and preprocessing leakage, these tools keep your reported numbers honest — which is the whole point of a validation step.