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.
Files changed (41) hide show
  1. gcn_python/__init__.py +2 -0
  2. gcn_python/constants.py +28 -0
  3. gcn_python/data/__init__.py +0 -0
  4. gcn_python/data/json_reader.py +124 -0
  5. gcn_python/data/loader.py +212 -0
  6. gcn_python/data/schema.py +50 -0
  7. gcn_python/data/verbalize_loader.py +96 -0
  8. gcn_python/evaluation/__init__.py +0 -0
  9. gcn_python/evaluation/eval_runner.py +123 -0
  10. gcn_python/evaluation/metrics.py +257 -0
  11. gcn_python/evaluation/recorder.py +158 -0
  12. gcn_python/layer1/__init__.py +0 -0
  13. gcn_python/layer1/features.py +126 -0
  14. gcn_python/layer1/representation.py +37 -0
  15. gcn_python/layer2/__init__.py +0 -0
  16. gcn_python/layer2/interface.py +52 -0
  17. gcn_python/layer2/reference.py +159 -0
  18. gcn_python/layer3/__init__.py +0 -0
  19. gcn_python/layer3/interface.py +32 -0
  20. gcn_python/layer3/pytorch_rgcn.py +190 -0
  21. gcn_python/layer3/reference.py +102 -0
  22. gcn_python/pipeline/__init__.py +0 -0
  23. gcn_python/pipeline/cgnp.py +419 -0
  24. gcn_python/pipeline/cli.py +77 -0
  25. gcn_python/pipeline/ir_emitter.py +71 -0
  26. gcn_python/pipeline/label_builder.py +82 -0
  27. gcn_python/taxonomy/__init__.py +0 -0
  28. gcn_python/taxonomy/loader.py +74 -0
  29. gcn_python/training/__init__.py +0 -0
  30. gcn_python/training/bootstrap.py +117 -0
  31. gcn_python/training/checkpoint.py +93 -0
  32. gcn_python/training/train.py +238 -0
  33. gcn_python/verbalizer/__init__.py +4 -0
  34. gcn_python/verbalizer/cli.py +25 -0
  35. gcn_python/verbalizer/decoder.py +38 -0
  36. gcn_python/verbalizer/interface.py +21 -0
  37. gcn_python/verbalizer/trainable.py +204 -0
  38. gcn_python-1.0.0.dist-info/METADATA +12 -0
  39. gcn_python-1.0.0.dist-info/RECORD +41 -0
  40. gcn_python-1.0.0.dist-info/WHEEL +4 -0
  41. 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