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.
- graphssl/__init__.py +63 -0
- graphssl/augmentation/__init__.py +11 -0
- graphssl/augmentation/compose.py +60 -0
- graphssl/augmentation/functional.py +127 -0
- graphssl/augmentation/transforms.py +84 -0
- graphssl/config/__init__.py +14 -0
- graphssl/config/load.py +114 -0
- graphssl/config/schema.py +413 -0
- graphssl/core/__init__.py +14 -0
- graphssl/core/augmentation.py +16 -0
- graphssl/core/callback.py +44 -0
- graphssl/core/encoder.py +18 -0
- graphssl/core/model.py +58 -0
- graphssl/core/registry.py +47 -0
- graphssl/data/__init__.py +1 -0
- graphssl/data/datamodule.py +100 -0
- graphssl/encoders/__init__.py +3 -0
- graphssl/encoders/gcn.py +104 -0
- graphssl/encoders/gin.py +181 -0
- graphssl/encoders/transformer.py +206 -0
- graphssl/evaluation/__init__.py +3 -0
- graphssl/evaluation/knn.py +52 -0
- graphssl/evaluation/linear_probe.py +116 -0
- graphssl/evaluation/visualization.py +206 -0
- graphssl/losses/__init__.py +6 -0
- graphssl/losses/barlow.py +35 -0
- graphssl/losses/combined.py +67 -0
- graphssl/losses/dino.py +38 -0
- graphssl/losses/nt_xent.py +40 -0
- graphssl/losses/regression.py +44 -0
- graphssl/losses/vicreg.py +56 -0
- graphssl/models/__init__.py +14 -0
- graphssl/models/afgrl.py +133 -0
- graphssl/models/barlow_twins.py +68 -0
- graphssl/models/bgrl.py +110 -0
- graphssl/models/dgi.py +85 -0
- graphssl/models/graphcl.py +68 -0
- graphssl/models/graphdino.py +182 -0
- graphssl/models/supervised.py +48 -0
- graphssl/models/vicreg.py +72 -0
- graphssl/nn/__init__.py +4 -0
- graphssl/nn/dino_head.py +99 -0
- graphssl/nn/mlp.py +52 -0
- graphssl/nn/norm.py +24 -0
- graphssl/nn/pooling.py +18 -0
- graphssl/registry/__init__.py +1 -0
- graphssl/registry/registry.py +9 -0
- graphssl/training/__init__.py +2 -0
- graphssl/training/callbacks.py +138 -0
- graphssl/training/trainer.py +108 -0
- graphssl/utils/__init__.py +3 -0
- graphssl/utils/ema.py +14 -0
- graphssl/utils/positive_miner.py +119 -0
- graphssl/utils/schedulers.py +55 -0
- graphssl-0.1.0.dist-info/METADATA +273 -0
- graphssl-0.1.0.dist-info/RECORD +59 -0
- graphssl-0.1.0.dist-info/WHEEL +5 -0
- graphssl-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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,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
|
+
)
|
graphssl/config/load.py
ADDED
|
@@ -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)
|