gcn-python 1.0.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.
- gcn_python/__init__.py +2 -0
- gcn_python/constants.py +28 -0
- gcn_python/data/__init__.py +0 -0
- gcn_python/data/json_reader.py +124 -0
- gcn_python/data/loader.py +212 -0
- gcn_python/data/schema.py +50 -0
- gcn_python/data/verbalize_loader.py +96 -0
- gcn_python/evaluation/__init__.py +0 -0
- gcn_python/evaluation/eval_runner.py +123 -0
- gcn_python/evaluation/metrics.py +257 -0
- gcn_python/evaluation/recorder.py +158 -0
- gcn_python/layer1/__init__.py +0 -0
- gcn_python/layer1/features.py +126 -0
- gcn_python/layer1/representation.py +37 -0
- gcn_python/layer2/__init__.py +0 -0
- gcn_python/layer2/interface.py +52 -0
- gcn_python/layer2/reference.py +159 -0
- gcn_python/layer3/__init__.py +0 -0
- gcn_python/layer3/interface.py +32 -0
- gcn_python/layer3/pytorch_rgcn.py +190 -0
- gcn_python/layer3/reference.py +102 -0
- gcn_python/pipeline/__init__.py +0 -0
- gcn_python/pipeline/cgnp.py +419 -0
- gcn_python/pipeline/cli.py +77 -0
- gcn_python/pipeline/ir_emitter.py +71 -0
- gcn_python/pipeline/label_builder.py +82 -0
- gcn_python/taxonomy/__init__.py +0 -0
- gcn_python/taxonomy/loader.py +74 -0
- gcn_python/training/__init__.py +0 -0
- gcn_python/training/bootstrap.py +117 -0
- gcn_python/training/checkpoint.py +93 -0
- gcn_python/training/train.py +238 -0
- gcn_python/verbalizer/__init__.py +4 -0
- gcn_python/verbalizer/cli.py +25 -0
- gcn_python/verbalizer/decoder.py +38 -0
- gcn_python/verbalizer/interface.py +21 -0
- gcn_python/verbalizer/trainable.py +204 -0
- gcn_python-1.0.0.dist-info/METADATA +12 -0
- gcn_python-1.0.0.dist-info/RECORD +41 -0
- gcn_python-1.0.0.dist-info/WHEEL +4 -0
- gcn_python-1.0.0.dist-info/entry_points.txt +6 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _relu(x: np.ndarray) -> np.ndarray:
|
|
6
|
+
return np.maximum(0.0, x)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _relu_grad(x: np.ndarray) -> np.ndarray:
|
|
10
|
+
return (x > 0).astype(np.float32)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _LinearLayer:
|
|
14
|
+
def __init__(self, in_dim: int, out_dim: int, rng: np.random.Generator):
|
|
15
|
+
scale = np.sqrt(2.0 / in_dim) # He init
|
|
16
|
+
self.W = rng.normal(0, scale, (out_dim, in_dim)).astype(np.float32)
|
|
17
|
+
self.b = np.zeros(out_dim, dtype=np.float32)
|
|
18
|
+
self._cache: dict = {}
|
|
19
|
+
|
|
20
|
+
def forward(self, x: np.ndarray) -> np.ndarray:
|
|
21
|
+
out = x @ self.W.T + self.b
|
|
22
|
+
self._cache["x"] = x
|
|
23
|
+
return out
|
|
24
|
+
|
|
25
|
+
def backward(self, d_out: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
26
|
+
x = self._cache["x"]
|
|
27
|
+
dW = np.outer(d_out, x)
|
|
28
|
+
db = d_out.copy()
|
|
29
|
+
dx = d_out @ self.W
|
|
30
|
+
return dx, dW, db
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MLPEncoder:
|
|
34
|
+
"""
|
|
35
|
+
Implémentation de référence de la Couche 2 en NumPy pur.
|
|
36
|
+
Architecture : fc(D_in→128)+ReLU → fc(128→64)+ReLU → fc(64→out)
|
|
37
|
+
|
|
38
|
+
Le data scientist substitue par son propre CausalEncoder
|
|
39
|
+
(PyTorch, JAX, etc.) sans modifier le pipeline.
|
|
40
|
+
update_node / update_edge délèguent à _apply_grads — toute modification
|
|
41
|
+
de la règle de mise à jour (clipping, weight decay…) se fait une seule fois.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, d_clause: int, d_edge: int, seed: int = 42):
|
|
45
|
+
rng = np.random.default_rng(seed)
|
|
46
|
+
|
|
47
|
+
# Node MLP : d_clause → 128 → 64 → 7
|
|
48
|
+
self._node_layers = [
|
|
49
|
+
_LinearLayer(d_clause, 128, rng),
|
|
50
|
+
_LinearLayer(128, 64, rng),
|
|
51
|
+
_LinearLayer(64, 7, rng),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Edge MLP : d_edge → 256 → 128 → 11
|
|
55
|
+
self._edge_layers = [
|
|
56
|
+
_LinearLayer(d_edge, 256, rng),
|
|
57
|
+
_LinearLayer(256, 128, rng),
|
|
58
|
+
_LinearLayer(128, 11, rng),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
self._node_cache: list = []
|
|
62
|
+
self._edge_cache: list = []
|
|
63
|
+
|
|
64
|
+
def _forward_mlp(
|
|
65
|
+
self, x: np.ndarray, layers: list[_LinearLayer], cache_out: list
|
|
66
|
+
) -> np.ndarray:
|
|
67
|
+
cache_out.clear()
|
|
68
|
+
h = x
|
|
69
|
+
for i, layer in enumerate(layers):
|
|
70
|
+
z = layer.forward(h)
|
|
71
|
+
if i < len(layers) - 1:
|
|
72
|
+
h = _relu(z)
|
|
73
|
+
cache_out.append((z, h))
|
|
74
|
+
else:
|
|
75
|
+
h = z
|
|
76
|
+
cache_out.append((z, z))
|
|
77
|
+
return h
|
|
78
|
+
|
|
79
|
+
def forward_node(self, x: np.ndarray) -> np.ndarray:
|
|
80
|
+
self._node_cache = []
|
|
81
|
+
return self._forward_mlp(x, self._node_layers, self._node_cache)
|
|
82
|
+
|
|
83
|
+
def forward_edge(self, x: np.ndarray) -> np.ndarray:
|
|
84
|
+
self._edge_cache = []
|
|
85
|
+
return self._forward_mlp(x, self._edge_layers, self._edge_cache)
|
|
86
|
+
|
|
87
|
+
def _backward_mlp(
|
|
88
|
+
self, d_logits: np.ndarray, layers: list[_LinearLayer], cache: list
|
|
89
|
+
) -> tuple[list[tuple[np.ndarray, np.ndarray]], np.ndarray]:
|
|
90
|
+
"""Retourne (grads, d_input) où d_input est le gradient vers l'entrée."""
|
|
91
|
+
grads = []
|
|
92
|
+
d = d_logits
|
|
93
|
+
for i in reversed(range(len(layers))):
|
|
94
|
+
z, _ = cache[i]
|
|
95
|
+
if i < len(layers) - 1:
|
|
96
|
+
d = d * _relu_grad(z)
|
|
97
|
+
dx, dW, db = layers[i].backward(d)
|
|
98
|
+
grads.insert(0, (dW, db))
|
|
99
|
+
d = dx
|
|
100
|
+
return grads, d
|
|
101
|
+
|
|
102
|
+
def backward_node(self, d_logits: np.ndarray) -> list[tuple[np.ndarray, np.ndarray]]:
|
|
103
|
+
grads, _ = self._backward_mlp(d_logits, self._node_layers, self._node_cache)
|
|
104
|
+
return grads
|
|
105
|
+
|
|
106
|
+
def backward_node_dx(self, d_logits: np.ndarray) -> tuple[list[tuple[np.ndarray, np.ndarray]], np.ndarray]:
|
|
107
|
+
"""Comme backward_node mais retourne aussi le gradient vers l'entrée (pour R-GCN)."""
|
|
108
|
+
return self._backward_mlp(d_logits, self._node_layers, self._node_cache)
|
|
109
|
+
|
|
110
|
+
def backward_edge(self, d_logits: np.ndarray) -> list[tuple[np.ndarray, np.ndarray]]:
|
|
111
|
+
grads, _ = self._backward_mlp(d_logits, self._edge_layers, self._edge_cache)
|
|
112
|
+
return grads
|
|
113
|
+
|
|
114
|
+
def snapshot_node_cache(self) -> list:
|
|
115
|
+
"""Snapshot du cache node + inputs des couches (pour backward par nœud)."""
|
|
116
|
+
return [
|
|
117
|
+
((z.copy(), h.copy()), l._cache.get("x", np.zeros(0)).copy())
|
|
118
|
+
for (z, h), l in zip(self._node_cache, self._node_layers)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def restore_node_cache(self, snapshot: list) -> None:
|
|
122
|
+
self._node_cache = [(z.copy(), h.copy()) for (z, h), _ in snapshot]
|
|
123
|
+
for ((_, _), x), layer in zip(snapshot, self._node_layers):
|
|
124
|
+
layer._cache["x"] = x.copy()
|
|
125
|
+
|
|
126
|
+
def snapshot_edge_cache(self) -> list:
|
|
127
|
+
return [
|
|
128
|
+
((z.copy(), h.copy()), l._cache.get("x", np.zeros(0)).copy())
|
|
129
|
+
for (z, h), l in zip(self._edge_cache, self._edge_layers)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
def restore_edge_cache(self, snapshot: list) -> None:
|
|
133
|
+
self._edge_cache = [(z.copy(), h.copy()) for (z, h), _ in snapshot]
|
|
134
|
+
for ((_, _), x), layer in zip(snapshot, self._edge_layers):
|
|
135
|
+
layer._cache["x"] = x.copy()
|
|
136
|
+
|
|
137
|
+
def parameters(self) -> list[np.ndarray]:
|
|
138
|
+
params = []
|
|
139
|
+
for layer in self._node_layers + self._edge_layers:
|
|
140
|
+
params.extend([layer.W, layer.b])
|
|
141
|
+
return params
|
|
142
|
+
|
|
143
|
+
def _apply_grads(
|
|
144
|
+
self, layers: list[_LinearLayer],
|
|
145
|
+
grads: list[tuple[np.ndarray, np.ndarray]], lr: float,
|
|
146
|
+
) -> None:
|
|
147
|
+
for layer, (dW, db) in zip(layers, grads):
|
|
148
|
+
layer.W -= lr * dW
|
|
149
|
+
layer.b -= lr * db
|
|
150
|
+
|
|
151
|
+
def update_node(self, grads: list[tuple[np.ndarray, np.ndarray]], lr: float) -> None:
|
|
152
|
+
self._apply_grads(self._node_layers, grads, lr)
|
|
153
|
+
|
|
154
|
+
def update_edge(self, grads: list[tuple[np.ndarray, np.ndarray]], lr: float) -> None:
|
|
155
|
+
self._apply_grads(self._edge_layers, grads, lr)
|
|
156
|
+
|
|
157
|
+
def update(self, grads: list[np.ndarray], lr: float) -> None:
|
|
158
|
+
for p, g in zip(self.parameters(), grads):
|
|
159
|
+
p -= lr * g
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Protocol, runtime_checkable
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class CausalGraph(Protocol):
|
|
8
|
+
"""
|
|
9
|
+
Contrat d'interface de la Couche 3 — Graphe causal R-GCN.
|
|
10
|
+
|
|
11
|
+
Implémente le message passing relationnel sur le graphe causal.
|
|
12
|
+
Supporte les cycles (pas de restriction DAG).
|
|
13
|
+
|
|
14
|
+
Formule R-GCN :
|
|
15
|
+
h_i^(l+1) = σ( Σ_r Σ_{j∈N_r(i)} (1/c_{i,r}) W_r^(l) h_j^(l) + W_0^(l) h_i^(l) )
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
d_out: int
|
|
19
|
+
|
|
20
|
+
def message_pass(
|
|
21
|
+
self,
|
|
22
|
+
node_features: np.ndarray, # (N, D_node)
|
|
23
|
+
edge_index: np.ndarray, # (2, E) — [sources, targets]
|
|
24
|
+
edge_types: np.ndarray, # (E,) int — index dans RELATION_TYPES
|
|
25
|
+
) -> np.ndarray: # (N, D_out) — représentations enrichies
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def parameters(self) -> list[np.ndarray]:
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def update(self, grads: list[np.ndarray], lr: float) -> None:
|
|
32
|
+
...
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
R-GCN PyTorch — implémentation haute performance de la couche 3.
|
|
3
|
+
|
|
4
|
+
Remplace RGCNLayer (NumPy référence) par une implémentation PyTorch optimisée :
|
|
5
|
+
- GPU / MPS support via device placement
|
|
6
|
+
- Gradient automatique (autograd) pour la rétropropagation
|
|
7
|
+
- Scatter-add vectorisé (pas de boucle Python par relation)
|
|
8
|
+
- Compatible avec le Protocol CausalGraph de layer3/interface.py
|
|
9
|
+
|
|
10
|
+
Usage (à la place de RGCNLayer) :
|
|
11
|
+
from gcn_python.layer3.pytorch_rgcn import RGCNLayerPT
|
|
12
|
+
layer = RGCNLayerPT(d_in=64, d_out=128)
|
|
13
|
+
out = layer.message_pass(node_feat, edge_index, edge_types)
|
|
14
|
+
|
|
15
|
+
Intégration dans une boucle d'entraînement PyTorch standard :
|
|
16
|
+
optimizer = torch.optim.Adam(layer.torch_parameters(), lr=1e-3)
|
|
17
|
+
loss = criterion(out, targets)
|
|
18
|
+
loss.backward()
|
|
19
|
+
optimizer.step()
|
|
20
|
+
|
|
21
|
+
Note : torch est une dépendance optionnelle. Si PyTorch n'est pas installé,
|
|
22
|
+
l'import de ce module lève ImportError avec un message explicite.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import torch
|
|
28
|
+
import torch.nn as nn
|
|
29
|
+
import torch.nn.functional as F
|
|
30
|
+
except ImportError as e: # pragma: no cover
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"PyTorch est requis pour RGCNLayerPT. "
|
|
33
|
+
"Installez-le avec : pip install torch "
|
|
34
|
+
"(voir https://pytorch.org/get-started/locally/ pour les options GPU)"
|
|
35
|
+
) from e
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
from typing import Any
|
|
39
|
+
from ..constants import RELATION_TYPES
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RGCNLayerPT(nn.Module):
|
|
43
|
+
"""
|
|
44
|
+
Couche R-GCN PyTorch.
|
|
45
|
+
|
|
46
|
+
Formule :
|
|
47
|
+
h_i^(out) = σ( Σ_r (1/c_{i,r}) Σ_{j∈N_r(i)} W_r h_j + W_0 h_i )
|
|
48
|
+
|
|
49
|
+
Implémentée avec scatter_add_ vectorisé pour éviter les boucles Python
|
|
50
|
+
par relation. Supportant nativement les cycles.
|
|
51
|
+
|
|
52
|
+
Compatible avec le Protocol CausalGraph : message_pass / parameters / update.
|
|
53
|
+
Les utilisateurs PyTorch utiliseront de préférence torch_parameters() et
|
|
54
|
+
un optimizer PyTorch standard plutôt que update() (qui reste disponible
|
|
55
|
+
pour la compatibilité avec CGNPipeline).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
d_in: int,
|
|
61
|
+
d_out: int,
|
|
62
|
+
n_relations: int | None = None,
|
|
63
|
+
device: str | torch.device | None = None,
|
|
64
|
+
seed: int = 42,
|
|
65
|
+
) -> None:
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.d_in = d_in
|
|
68
|
+
self.d_out = d_out
|
|
69
|
+
self.n_relations = n_relations or len(RELATION_TYPES)
|
|
70
|
+
|
|
71
|
+
if device is None:
|
|
72
|
+
device = (
|
|
73
|
+
"cuda" if torch.cuda.is_available()
|
|
74
|
+
else "mps" if torch.backends.mps.is_available()
|
|
75
|
+
else "cpu"
|
|
76
|
+
)
|
|
77
|
+
self._device = torch.device(device)
|
|
78
|
+
|
|
79
|
+
torch.manual_seed(seed)
|
|
80
|
+
scale = (2.0 / d_in) ** 0.5
|
|
81
|
+
|
|
82
|
+
# Relation-specific weights: (R, D_out, D_in)
|
|
83
|
+
self.W_r = nn.Parameter(
|
|
84
|
+
torch.empty(self.n_relations, d_out, d_in, device=self._device).normal_(0, scale)
|
|
85
|
+
)
|
|
86
|
+
# Self-loop weight: (D_out, D_in)
|
|
87
|
+
self.W_0 = nn.Parameter(
|
|
88
|
+
torch.empty(d_out, d_in, device=self._device).normal_(0, scale)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
# CausalGraph Protocol — message_pass
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def message_pass(
|
|
96
|
+
self,
|
|
97
|
+
node_features: np.ndarray, # (N, D_in)
|
|
98
|
+
edge_index: np.ndarray, # (2, E)
|
|
99
|
+
edge_types: np.ndarray, # (E,) int
|
|
100
|
+
) -> np.ndarray: # (N, D_out)
|
|
101
|
+
"""Passe les messages sur le graphe causal et retourne les représentations enrichies."""
|
|
102
|
+
H = torch.as_tensor(node_features, dtype=torch.float32, device=self._device)
|
|
103
|
+
out = self._forward_pt(H, edge_index, edge_types)
|
|
104
|
+
return out.detach().cpu().numpy()
|
|
105
|
+
|
|
106
|
+
def _forward_pt(
|
|
107
|
+
self,
|
|
108
|
+
H: torch.Tensor, # (N, D_in)
|
|
109
|
+
edge_index: np.ndarray,
|
|
110
|
+
edge_types: np.ndarray,
|
|
111
|
+
) -> torch.Tensor: # (N, D_out)
|
|
112
|
+
"""Version PyTorch native (pour l'entraînement avec autograd)."""
|
|
113
|
+
N = H.shape[0]
|
|
114
|
+
|
|
115
|
+
# Self-loop : H @ W_0^T
|
|
116
|
+
out = H @ self.W_0.t() # (N, D_out)
|
|
117
|
+
|
|
118
|
+
if edge_index.shape[1] > 0:
|
|
119
|
+
src = torch.as_tensor(edge_index[0], dtype=torch.long, device=self._device)
|
|
120
|
+
dst = torch.as_tensor(edge_index[1], dtype=torch.long, device=self._device)
|
|
121
|
+
rtypes = torch.as_tensor(edge_types, dtype=torch.long, device=self._device)
|
|
122
|
+
|
|
123
|
+
# Pour chaque type de relation, scatter_add vectorisé
|
|
124
|
+
for r in range(self.n_relations):
|
|
125
|
+
mask = rtypes == r
|
|
126
|
+
if not mask.any():
|
|
127
|
+
continue
|
|
128
|
+
src_r = src[mask] # arêtes de type r
|
|
129
|
+
dst_r = dst[mask]
|
|
130
|
+
|
|
131
|
+
# Comptage des voisins par destination (normalisation)
|
|
132
|
+
counts = torch.zeros(N, device=self._device, dtype=torch.float32)
|
|
133
|
+
counts.scatter_add_(0, dst_r, torch.ones_like(dst_r, dtype=torch.float32))
|
|
134
|
+
counts = counts.clamp(min=1.0)
|
|
135
|
+
|
|
136
|
+
# Messages : (|E_r|, D_out) = H[src_r] @ W_r[r]^T
|
|
137
|
+
msgs = H[src_r] @ self.W_r[r].t()
|
|
138
|
+
|
|
139
|
+
# Aggrégation dans les nœuds destination
|
|
140
|
+
agg = torch.zeros(N, self.d_out, device=self._device)
|
|
141
|
+
agg.scatter_add_(0, dst_r.unsqueeze(1).expand_as(msgs), msgs)
|
|
142
|
+
out = out + agg / counts.unsqueeze(1)
|
|
143
|
+
|
|
144
|
+
return torch.sigmoid(out)
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
# CausalGraph Protocol — parameters / update (compatibilité NumPy)
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def parameters(self) -> list[np.ndarray]: # type: ignore[override]
|
|
151
|
+
"""Retourne les poids sous forme NumPy (compatibilité Protocol)."""
|
|
152
|
+
return [
|
|
153
|
+
self.W_r.detach().cpu().numpy(),
|
|
154
|
+
self.W_0.detach().cpu().numpy(),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
def update(self, grads: list[np.ndarray], lr: float) -> None:
|
|
158
|
+
"""Mise à jour manuelle des poids (gradient descent numpy).
|
|
159
|
+
Les utilisateurs PyTorch préféreront optimizer.step() via torch_parameters().
|
|
160
|
+
"""
|
|
161
|
+
with torch.no_grad():
|
|
162
|
+
self.W_r -= lr * torch.as_tensor(grads[0], dtype=torch.float32, device=self._device)
|
|
163
|
+
self.W_0 -= lr * torch.as_tensor(grads[1], dtype=torch.float32, device=self._device)
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# API PyTorch native
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def torch_parameters(self) -> list[nn.Parameter]:
|
|
170
|
+
"""Retourne les paramètres PyTorch pour un optimizer standard."""
|
|
171
|
+
return list(super().parameters())
|
|
172
|
+
|
|
173
|
+
def forward_torch(
|
|
174
|
+
self,
|
|
175
|
+
H: torch.Tensor,
|
|
176
|
+
edge_index: np.ndarray,
|
|
177
|
+
edge_types: np.ndarray,
|
|
178
|
+
) -> torch.Tensor:
|
|
179
|
+
"""Forward pass PyTorch natif, conserve le graphe de calcul pour backward()."""
|
|
180
|
+
return self._forward_pt(H, edge_index, edge_types)
|
|
181
|
+
|
|
182
|
+
def to_device(self, device: str | torch.device) -> "RGCNLayerPT":
|
|
183
|
+
self._device = torch.device(device)
|
|
184
|
+
return self.to(self._device)
|
|
185
|
+
|
|
186
|
+
def __repr__(self) -> str:
|
|
187
|
+
return (
|
|
188
|
+
f"RGCNLayerPT(d_in={self.d_in}, d_out={self.d_out}, "
|
|
189
|
+
f"n_relations={self.n_relations}, device={self._device})"
|
|
190
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ..constants import RELATION_TYPES
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _sigmoid(x: np.ndarray) -> np.ndarray:
|
|
7
|
+
return 1.0 / (1.0 + np.exp(-np.clip(x, -30, 30)))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RGCNLayer:
|
|
11
|
+
"""
|
|
12
|
+
Implémentation NumPy de référence d'une couche R-GCN.
|
|
13
|
+
|
|
14
|
+
h_i^(out) = σ( Σ_r (1/c_{i,r}) Σ_{j∈N_r(i)} W_r h_j + W_0 h_i )
|
|
15
|
+
|
|
16
|
+
Supporte les cycles — aucune restriction DAG.
|
|
17
|
+
Le DS empile plusieurs couches ou substitue par PyTorch Geometric.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, d_in: int, d_out: int, n_relations: int | None = None, seed: int = 42):
|
|
21
|
+
self.d_in = d_in
|
|
22
|
+
self.d_out = d_out
|
|
23
|
+
self.n_relations = n_relations or len(RELATION_TYPES)
|
|
24
|
+
rng = np.random.default_rng(seed)
|
|
25
|
+
scale = np.sqrt(2.0 / d_in)
|
|
26
|
+
self.W_r = rng.normal(0, scale, (self.n_relations, d_out, d_in)).astype(np.float32)
|
|
27
|
+
self.W_0 = rng.normal(0, scale, (d_out, d_in)).astype(np.float32)
|
|
28
|
+
self._fwd_inputs: tuple | None = None
|
|
29
|
+
self._fwd_output: np.ndarray | None = None
|
|
30
|
+
|
|
31
|
+
def message_pass(
|
|
32
|
+
self,
|
|
33
|
+
node_features: np.ndarray, # (N, D_in)
|
|
34
|
+
edge_index: np.ndarray, # (2, E)
|
|
35
|
+
edge_types: np.ndarray, # (E,) int
|
|
36
|
+
) -> np.ndarray: # (N, D_out)
|
|
37
|
+
N = node_features.shape[0]
|
|
38
|
+
out = node_features @ self.W_0.T # self-loop
|
|
39
|
+
|
|
40
|
+
if edge_index.shape[1] > 0:
|
|
41
|
+
src, dst = edge_index[0], edge_index[1]
|
|
42
|
+
for r in range(self.n_relations):
|
|
43
|
+
mask = edge_types == r
|
|
44
|
+
if not np.any(mask):
|
|
45
|
+
continue
|
|
46
|
+
src_r, dst_r = src[mask], dst[mask]
|
|
47
|
+
counts = np.maximum(np.bincount(dst_r, minlength=N).astype(np.float32), 1.0)
|
|
48
|
+
msgs = node_features[src_r] @ self.W_r[r].T # (|E_r|, D_out)
|
|
49
|
+
for e_idx, d in enumerate(dst_r):
|
|
50
|
+
out[d] += msgs[e_idx] / counts[d]
|
|
51
|
+
|
|
52
|
+
h = _sigmoid(out)
|
|
53
|
+
# Copier les tableaux : évite que des mutations externes corrompent le backward
|
|
54
|
+
self._fwd_inputs = (node_features.copy(), edge_index.copy(), edge_types.copy())
|
|
55
|
+
self._fwd_output = h
|
|
56
|
+
return h
|
|
57
|
+
|
|
58
|
+
def backward_message_pass(
|
|
59
|
+
self,
|
|
60
|
+
d_output: np.ndarray, # (N, D_out)
|
|
61
|
+
) -> tuple[np.ndarray, list[np.ndarray]]:
|
|
62
|
+
"""Rétropropagation à travers le message passing R-GCN.
|
|
63
|
+
|
|
64
|
+
Retourne (d_input, [dW_r, dW_0]) où d_input est le gradient vers
|
|
65
|
+
les features d'entrée (ignoré — pas de paramètres apprenables en amont).
|
|
66
|
+
"""
|
|
67
|
+
assert self._fwd_inputs is not None, "backward_message_pass appelé avant message_pass"
|
|
68
|
+
node_features, edge_index, edge_types = self._fwd_inputs
|
|
69
|
+
h = self._fwd_output
|
|
70
|
+
N = node_features.shape[0]
|
|
71
|
+
|
|
72
|
+
# Gradient à travers sigmoid : d_pre_act = d_output * h * (1 - h)
|
|
73
|
+
d_pre_act = d_output * h * (1.0 - h) # (N, D_out)
|
|
74
|
+
|
|
75
|
+
# Self-loop : out_self = node_features @ W_0.T
|
|
76
|
+
dW_0 = d_pre_act.T @ node_features # (D_out, D_in)
|
|
77
|
+
d_input = d_pre_act @ self.W_0 # (N, D_in)
|
|
78
|
+
|
|
79
|
+
dW_r = np.zeros_like(self.W_r) # (n_relations, D_out, D_in)
|
|
80
|
+
|
|
81
|
+
if edge_index.shape[1] > 0:
|
|
82
|
+
src, dst = edge_index[0], edge_index[1]
|
|
83
|
+
for r in range(self.n_relations):
|
|
84
|
+
mask = edge_types == r
|
|
85
|
+
if not np.any(mask):
|
|
86
|
+
continue
|
|
87
|
+
src_r, dst_r = src[mask], dst[mask]
|
|
88
|
+
counts = np.maximum(np.bincount(dst_r, minlength=N).astype(np.float32), 1.0)
|
|
89
|
+
|
|
90
|
+
# Gradient des messages normalisés vers W_r et noeuds sources
|
|
91
|
+
d_msgs = d_pre_act[dst_r] / counts[dst_r, np.newaxis] # (|E_r|, D_out)
|
|
92
|
+
dW_r[r] = d_msgs.T @ node_features[src_r] # (D_out, D_in)
|
|
93
|
+
np.add.at(d_input, src_r, d_msgs @ self.W_r[r]) # (|E_r|, D_in)
|
|
94
|
+
|
|
95
|
+
return d_input, [dW_r, dW_0]
|
|
96
|
+
|
|
97
|
+
def parameters(self) -> list[np.ndarray]:
|
|
98
|
+
return [self.W_r, self.W_0]
|
|
99
|
+
|
|
100
|
+
def update(self, grads: list[np.ndarray], lr: float) -> None:
|
|
101
|
+
self.W_r -= lr * grads[0]
|
|
102
|
+
self.W_0 -= lr * grads[1]
|
|
File without changes
|