graphssl 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. graphssl/__init__.py +63 -0
  2. graphssl/augmentation/__init__.py +11 -0
  3. graphssl/augmentation/compose.py +60 -0
  4. graphssl/augmentation/functional.py +127 -0
  5. graphssl/augmentation/transforms.py +84 -0
  6. graphssl/config/__init__.py +14 -0
  7. graphssl/config/load.py +114 -0
  8. graphssl/config/schema.py +413 -0
  9. graphssl/core/__init__.py +14 -0
  10. graphssl/core/augmentation.py +16 -0
  11. graphssl/core/callback.py +44 -0
  12. graphssl/core/encoder.py +18 -0
  13. graphssl/core/model.py +58 -0
  14. graphssl/core/registry.py +47 -0
  15. graphssl/data/__init__.py +1 -0
  16. graphssl/data/datamodule.py +100 -0
  17. graphssl/encoders/__init__.py +3 -0
  18. graphssl/encoders/gcn.py +104 -0
  19. graphssl/encoders/gin.py +181 -0
  20. graphssl/encoders/transformer.py +206 -0
  21. graphssl/evaluation/__init__.py +3 -0
  22. graphssl/evaluation/knn.py +52 -0
  23. graphssl/evaluation/linear_probe.py +116 -0
  24. graphssl/evaluation/visualization.py +206 -0
  25. graphssl/losses/__init__.py +6 -0
  26. graphssl/losses/barlow.py +35 -0
  27. graphssl/losses/combined.py +67 -0
  28. graphssl/losses/dino.py +38 -0
  29. graphssl/losses/nt_xent.py +40 -0
  30. graphssl/losses/regression.py +44 -0
  31. graphssl/losses/vicreg.py +56 -0
  32. graphssl/models/__init__.py +14 -0
  33. graphssl/models/afgrl.py +133 -0
  34. graphssl/models/barlow_twins.py +68 -0
  35. graphssl/models/bgrl.py +110 -0
  36. graphssl/models/dgi.py +85 -0
  37. graphssl/models/graphcl.py +68 -0
  38. graphssl/models/graphdino.py +182 -0
  39. graphssl/models/supervised.py +48 -0
  40. graphssl/models/vicreg.py +72 -0
  41. graphssl/nn/__init__.py +4 -0
  42. graphssl/nn/dino_head.py +99 -0
  43. graphssl/nn/mlp.py +52 -0
  44. graphssl/nn/norm.py +24 -0
  45. graphssl/nn/pooling.py +18 -0
  46. graphssl/registry/__init__.py +1 -0
  47. graphssl/registry/registry.py +9 -0
  48. graphssl/training/__init__.py +2 -0
  49. graphssl/training/callbacks.py +138 -0
  50. graphssl/training/trainer.py +108 -0
  51. graphssl/utils/__init__.py +3 -0
  52. graphssl/utils/ema.py +14 -0
  53. graphssl/utils/positive_miner.py +119 -0
  54. graphssl/utils/schedulers.py +55 -0
  55. graphssl-0.1.0.dist-info/METADATA +273 -0
  56. graphssl-0.1.0.dist-info/RECORD +59 -0
  57. graphssl-0.1.0.dist-info/WHEEL +5 -0
  58. graphssl-0.1.0.dist-info/licenses/LICENSE +21 -0
  59. graphssl-0.1.0.dist-info/top_level.txt +1 -0
graphssl/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """GraphSSL: a modular Graph Machine Learning library in pure PyTorch."""
2
+
3
+ # ── Core abstractions ─────────────────────────────────────────────────────────
4
+ # ── Augmentation ──────────────────────────────────────────────────────────────
5
+ from graphssl.augmentation import MultiView, compose
6
+ from graphssl.augmentation.transforms import (
7
+ EdgeAdd,
8
+ EdgeDrop,
9
+ FeatMask,
10
+ FeatNoise,
11
+ FeatShuffle,
12
+ Subgraph,
13
+ )
14
+ from graphssl.core.augmentation import BaseAugmentation
15
+ from graphssl.core.callback import Callback
16
+ from graphssl.core.encoder import BaseEncoder
17
+ from graphssl.core.model import BaseModel, BaseSSLModel
18
+ from graphssl.core.registry import Registry
19
+
20
+ # ── Data ──────────────────────────────────────────────────────────────────────
21
+ from graphssl.data import DataModule
22
+
23
+ # ── Encoders ──────────────────────────────────────────────────────────────────
24
+ from graphssl.encoders import GCNEncoder, GINEncoder, TransformerEncoder
25
+
26
+ # ── Evaluation ────────────────────────────────────────────────────────────────
27
+ from graphssl.evaluation import KNNEvaluator, LogRegEvaluator, extract_embeddings
28
+
29
+ # ── Losses ────────────────────────────────────────────────────────────────────
30
+ from graphssl.losses import (
31
+ BarlowTwinsLoss,
32
+ CombinedLoss,
33
+ CosineRegressionLoss,
34
+ DINOLoss,
35
+ NTXentLoss,
36
+ VICRegLoss,
37
+ )
38
+
39
+ # ── Models ────────────────────────────────────────────────────────────────────
40
+ from graphssl.models import (
41
+ AFGRL,
42
+ BGRL,
43
+ DGI,
44
+ BarlowTwins,
45
+ GraphCL,
46
+ GraphDINO,
47
+ Supervised,
48
+ VICReg,
49
+ )
50
+
51
+ # ── Neural network building blocks ────────────────────────────────────────────
52
+ from graphssl.nn import MLP, DINOHead, Predictor, Projector, pool_graph_embeddings
53
+
54
+ # ── Training ──────────────────────────────────────────────────────────────────
55
+ from graphssl.training import (
56
+ DINOTrainer,
57
+ EmbeddingLoggerCallback,
58
+ LinearEvalCallback,
59
+ VisualizationCallback,
60
+ )
61
+
62
+ # ── Utilities ─────────────────────────────────────────────────────────────────
63
+ from graphssl.utils import CosineDecayScheduler, CosineEMAScheduler, update_ema_params
@@ -0,0 +1,11 @@
1
+ from . import functional
2
+ from .compose import MultiView, compose
3
+ from .transforms import (
4
+ EdgeAdd,
5
+ EdgeDrop,
6
+ FeatMask,
7
+ FeatNoise,
8
+ FeatShuffle,
9
+ NodeDrop,
10
+ Subgraph,
11
+ )
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional
4
+
5
+ import torch
6
+ from torch_geometric.data import Data
7
+
8
+ from graphssl.registry import AUGMENTS
9
+
10
+
11
+ def compose(data: Data, augments: list, protected_nodes: Optional[torch.Tensor] = None) -> Data:
12
+ """Apply a sequence of registry-based augmentations to a Data object.
13
+
14
+ Args:
15
+ augments: List of (name, kwargs) tuples referencing AUGMENTS registry.
16
+ protected_nodes: Optional tensor of node indices that must not be removed
17
+ or dropped. Passed to every augmentation function; functions that do
18
+ not use it accept and ignore the parameter.
19
+ """
20
+ for name, kwargs in augments:
21
+ data = AUGMENTS.build(name, data=data, protected_nodes=protected_nodes, **kwargs)
22
+ return data
23
+
24
+
25
+ class MultiView:
26
+ """Generate n independent augmented views of a Data object.
27
+
28
+ Supports both registry-style augments (list of (name, kwargs) tuples) and
29
+ class-based transforms from augmentation/transforms.py (callables Data → Data).
30
+
31
+ Args:
32
+ transforms: Either a list of (name, kwargs) tuples OR a list of callables.
33
+ n_views: Number of independent views to generate.
34
+ """
35
+
36
+ def __init__(self, transforms: list, n_views: int = 2):
37
+ self.transforms = transforms
38
+ self.n_views = n_views
39
+ # Detect which API is being used
40
+ self._registry_style = (
41
+ transforms and isinstance(transforms[0], tuple) and isinstance(transforms[0][0], str)
42
+ )
43
+
44
+ def __call__(
45
+ self,
46
+ data: Data,
47
+ protected_nodes: Optional[torch.Tensor] = None,
48
+ ) -> List[Data]:
49
+ if self._registry_style:
50
+ return [
51
+ compose(data, self.transforms, protected_nodes=protected_nodes)
52
+ for _ in range(self.n_views)
53
+ ]
54
+ views = []
55
+ for _ in range(self.n_views):
56
+ view = data
57
+ for t in self.transforms:
58
+ view = t(view)
59
+ views.append(view)
60
+ return views
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import torch
6
+ from torch import Tensor
7
+ from torch_geometric.data import Data
8
+ from torch_geometric.utils import k_hop_subgraph
9
+
10
+ from graphssl.registry import AUGMENTS
11
+
12
+
13
+ @AUGMENTS.register("edge_drop")
14
+ def edge_drop(data: Data, p=0.2, protected_nodes: Optional[Tensor] = None):
15
+ assert data.edge_index is not None
16
+ mask = torch.rand(data.edge_index.size(1), device=data.edge_index.device) > p
17
+ out = data.clone()
18
+ out.edge_index = data.edge_index[:, mask]
19
+ if data.edge_attr is not None:
20
+ out.edge_attr = data.edge_attr[mask]
21
+ return out
22
+
23
+
24
+ @AUGMENTS.register("edge_add")
25
+ def edge_add(data: Data, p=0.1, protected_nodes: Optional[Tensor] = None):
26
+ assert data.edge_index is not None
27
+ assert data.num_nodes is not None
28
+ n = data.num_nodes
29
+ n_add = max(1, int(data.edge_index.size(1) * p))
30
+ src = torch.randint(0, n, (n_add,), device=data.edge_index.device)
31
+ dst = torch.randint(0, n, (n_add,), device=data.edge_index.device)
32
+ out = data.clone()
33
+ out.edge_index = torch.cat([data.edge_index, torch.stack([src, dst])], dim=1)
34
+ if data.edge_attr is not None:
35
+ # Synthetic edges have no known features — pad with zeros.
36
+ pad = torch.zeros(
37
+ n_add, data.edge_attr.size(1), dtype=data.edge_attr.dtype, device=data.edge_attr.device
38
+ )
39
+ out.edge_attr = torch.cat([data.edge_attr, pad], dim=0)
40
+ return out
41
+
42
+
43
+ @AUGMENTS.register("subgraph")
44
+ def subgraph(data: Data, num_hops=2, protected_nodes: Optional[Tensor] = None):
45
+ assert data.edge_index is not None
46
+ assert data.num_nodes is not None
47
+ n = data.num_nodes
48
+ seed = int(torch.randint(0, n, (1,), device=data.edge_index.device).item())
49
+ node_idx, edge_index_sub, _, edge_mask = k_hop_subgraph(
50
+ node_idx=seed,
51
+ num_hops=num_hops,
52
+ edge_index=data.edge_index,
53
+ num_nodes=n,
54
+ relabel_nodes=True,
55
+ )
56
+ out = data.clone()
57
+ out.x = data.x[node_idx] if data.x is not None else None
58
+ out.edge_index = edge_index_sub
59
+ out.batch = data.batch[node_idx] if data.batch is not None else None
60
+ if data.edge_attr is not None:
61
+ out.edge_attr = data.edge_attr[edge_mask]
62
+ return out
63
+
64
+
65
+ @AUGMENTS.register("feat_mask")
66
+ def feat_mask(data: Data, p=0.2, protected_nodes: Optional[Tensor] = None):
67
+ assert data.x is not None
68
+ mask = (torch.rand(data.x.size(1), device=data.x.device) > p).float()
69
+ out = data.clone()
70
+ out.x = data.x * mask
71
+ return out
72
+
73
+
74
+ @AUGMENTS.register("feat_noise")
75
+ def feat_noise(data: Data, std=0.1, protected_nodes: Optional[Tensor] = None):
76
+ assert data.x is not None
77
+ out = data.clone()
78
+ out.x = data.x + torch.randn_like(data.x) * std
79
+ return out
80
+
81
+
82
+ @AUGMENTS.register("feat_shuffle")
83
+ def feat_shuffle(data: Data, p=0.1, protected_nodes: Optional[Tensor] = None):
84
+ assert data.x is not None
85
+ n = data.x.size(0)
86
+ perm = torch.randperm(n, device=data.x.device)
87
+ node_mask = torch.rand(n, device=data.x.device) < p
88
+ x = data.x.clone()
89
+ x[node_mask] = data.x[perm[node_mask]]
90
+ out = data.clone()
91
+ out.x = x
92
+ return out
93
+
94
+
95
+ @AUGMENTS.register("node_drop")
96
+ def node_drop(data: Data, p=0.1, protected_nodes: Optional[Tensor] = None):
97
+ """Drop nodes with probability p, remapping edge_index.
98
+
99
+ Nodes in protected_nodes are never dropped (used to preserve mini-batch
100
+ seed nodes in NeighborLoader training).
101
+ """
102
+ assert data.x is not None
103
+ assert data.edge_index is not None
104
+ n = data.num_nodes
105
+ assert n is not None
106
+
107
+ keep_mask = torch.rand(n, device=data.x.device) > p
108
+ if protected_nodes is not None:
109
+ keep_mask[protected_nodes] = True
110
+
111
+ keep_idx = keep_mask.nonzero(as_tuple=True)[0]
112
+
113
+ # Build a remapping table: old node id → new node id (-1 = dropped)
114
+ new_idx = torch.full((n,), -1, dtype=torch.long, device=data.x.device)
115
+ new_idx[keep_idx] = torch.arange(keep_idx.size(0), device=data.x.device)
116
+
117
+ # Keep only edges where both endpoints survive
118
+ src, dst = data.edge_index
119
+ edge_mask = keep_mask[src] & keep_mask[dst]
120
+
121
+ out = data.clone()
122
+ out.x = data.x[keep_idx]
123
+ out.edge_index = new_idx[data.edge_index[:, edge_mask]]
124
+ out.batch = data.batch[keep_idx] if data.batch is not None else None
125
+ if data.edge_attr is not None:
126
+ out.edge_attr = data.edge_attr[edge_mask]
127
+ return out
@@ -0,0 +1,84 @@
1
+ """Class-based graph augmentation transforms (torchvision style).
2
+
3
+ Each transform is a callable Data → Data that wraps the functional API.
4
+ They satisfy the BaseAugmentation Protocol and can be used with Compose/MultiView.
5
+ """
6
+
7
+ from torch_geometric.data import Data
8
+
9
+ from graphssl.augmentation import functional as F
10
+
11
+
12
+ class EdgeDrop:
13
+ """Randomly drop edges with probability p."""
14
+
15
+ def __init__(self, p: float = 0.2):
16
+ self.p = p
17
+
18
+ def __call__(self, data: Data) -> Data:
19
+ return F.edge_drop(data, p=self.p)
20
+
21
+
22
+ class EdgeAdd:
23
+ """Randomly add edges (fraction p of existing edges)."""
24
+
25
+ def __init__(self, p: float = 0.1):
26
+ self.p = p
27
+
28
+ def __call__(self, data: Data) -> Data:
29
+ return F.edge_add(data, p=self.p)
30
+
31
+
32
+ class Subgraph:
33
+ """Keep a k-hop subgraph around a random seed node."""
34
+
35
+ def __init__(self, num_hops: int = 2):
36
+ self.num_hops = num_hops
37
+
38
+ def __call__(self, data: Data) -> Data:
39
+ return F.subgraph(data, num_hops=self.num_hops)
40
+
41
+
42
+ class FeatMask:
43
+ """Zero-out a fraction p of feature dimensions."""
44
+
45
+ def __init__(self, p: float = 0.2):
46
+ self.p = p
47
+
48
+ def __call__(self, data: Data) -> Data:
49
+ return F.feat_mask(data, p=self.p)
50
+
51
+
52
+ class FeatNoise:
53
+ """Add Gaussian noise with standard deviation std to node features."""
54
+
55
+ def __init__(self, std: float = 0.1):
56
+ self.std = std
57
+
58
+ def __call__(self, data: Data) -> Data:
59
+ return F.feat_noise(data, std=self.std)
60
+
61
+
62
+ class FeatShuffle:
63
+ """Randomly swap features between a fraction p of nodes."""
64
+
65
+ def __init__(self, p: float = 0.1):
66
+ self.p = p
67
+
68
+ def __call__(self, data: Data) -> Data:
69
+ return F.feat_shuffle(data, p=self.p)
70
+
71
+
72
+ class NodeDrop:
73
+ """Drop nodes with probability p, remapping edge_index.
74
+
75
+ Note: when used with protected_nodes (seed nodes in mini-batch training),
76
+ use the registry-style compose() API which propagates protected_nodes
77
+ automatically. The class-based API does not support protected_nodes.
78
+ """
79
+
80
+ def __init__(self, p: float = 0.1):
81
+ self.p = p
82
+
83
+ def __call__(self, data: Data) -> Data:
84
+ return F.node_drop(data, p=self.p)
@@ -0,0 +1,14 @@
1
+ from .load import LOADERS, build_model, load_config
2
+ from .schema import (
3
+ AFGRLConfig,
4
+ AugmentConfig,
5
+ BarlowTwinsConfig,
6
+ BGRLConfig,
7
+ DGIConfig,
8
+ EncoderConfig,
9
+ GraphCLConfig,
10
+ GraphDINOConfig,
11
+ HeadConfig,
12
+ SupervisedConfig,
13
+ VICRegConfig,
14
+ )
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Union
5
+
6
+ import torch.nn as nn
7
+ from torch_geometric.loader import DataLoader, NeighborLoader
8
+
9
+ from graphssl.registry import LOADERS
10
+
11
+
12
+ @LOADERS.register("graph")
13
+ def build_graph_loader(*, dataset, params):
14
+ # Standard graph-level loader, batches whole graphs together.
15
+ return DataLoader(
16
+ dataset,
17
+ batch_size=params.get("batch_size", 32),
18
+ shuffle=params.get("shuffle", True),
19
+ num_workers=params.get("num_workers", 0),
20
+ )
21
+
22
+
23
+ @LOADERS.register("neighbor")
24
+ def build_neighbor_loader(*, dataset, params):
25
+ # Samples neighbor subgraphs per node; useful for large single-graph datasets.
26
+ return NeighborLoader(
27
+ dataset,
28
+ num_neighbors=params.get("num_neighbors", [10, 10]),
29
+ batch_size=params.get("batch_size", 1024),
30
+ shuffle=True,
31
+ )
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Config loading and model construction
36
+ # ---------------------------------------------------------------------------
37
+
38
+
39
+ def load_config(path: Union[str, Path]) -> dict:
40
+ """Load a GraphSSL YAML config file and return it as a plain dict.
41
+
42
+ The returned dict can be passed directly to ``build_model()`` or to any
43
+ model constructor (``ModelClass(config, in_channels)``).
44
+
45
+ Requires PyYAML: ``pip install pyyaml``
46
+ """
47
+ try:
48
+ import yaml
49
+ except ImportError as e:
50
+ raise ImportError("PyYAML is required to load config files: pip install pyyaml") from e
51
+ with open(path) as f:
52
+ return yaml.safe_load(f)
53
+
54
+
55
+ def build_model(
56
+ config: dict,
57
+ in_channels: int,
58
+ num_classes: Optional[int] = None,
59
+ ) -> nn.Module:
60
+ """Instantiate a GraphSSL model from a config dict.
61
+
62
+ ``config`` must contain a top-level ``name`` key identifying the model.
63
+ For ``supervised``, ``num_classes`` is required. All other fields are
64
+ forwarded to the model constructor and validated by its config dataclass.
65
+
66
+ Example::
67
+
68
+ cfg = load_config("configs/bgrl.yaml")
69
+ model = build_model(cfg, in_channels=dataset.num_features)
70
+
71
+ cfg = load_config("configs/supervised.yaml")
72
+ model = build_model(cfg, in_channels=dataset.num_features,
73
+ num_classes=dataset.num_classes)
74
+
75
+ Args:
76
+ config: Config dict with a ``name`` key and model-specific fields.
77
+ in_channels: Node feature dimensionality from the dataset.
78
+ num_classes: Required only for the ``supervised`` model.
79
+
80
+ Returns:
81
+ Instantiated model as an ``nn.Module``.
82
+ """
83
+ # Deferred import to avoid circular dependency (models import from config).
84
+ from graphssl.models import (
85
+ AFGRL,
86
+ BGRL,
87
+ DGI,
88
+ BarlowTwins,
89
+ GraphCL,
90
+ GraphDINO,
91
+ Supervised,
92
+ VICReg,
93
+ )
94
+
95
+ _MODELS = {
96
+ "dgi": DGI,
97
+ "graphcl": GraphCL,
98
+ "vicreg": VICReg,
99
+ "barlow_twins": BarlowTwins,
100
+ "bgrl": BGRL,
101
+ "afgrl": AFGRL,
102
+ "graphdino": GraphDINO,
103
+ "supervised": Supervised,
104
+ }
105
+
106
+ name = config.get("name", "").lower().replace("-", "_")
107
+ if name not in _MODELS:
108
+ raise ValueError(f"Unknown model '{name}'. Available: {sorted(_MODELS)}")
109
+ cls = _MODELS[name]
110
+ if name == "supervised":
111
+ if num_classes is None:
112
+ raise ValueError("num_classes is required for the Supervised model")
113
+ return cls(config, in_channels, num_classes)
114
+ return cls(config, in_channels)