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,257 @@
1
+ """
2
+ Métriques d'évaluation CGNP — NumPy pur, framework-agnostique.
3
+
4
+ Le data scientist appelle ces fonctions après chaque epoch pour
5
+ mesurer la qualité des prédictions. Aucune dépendance à PyTorch/sklearn.
6
+ """
7
+ from __future__ import annotations
8
+ import numpy as np
9
+ from ..constants import NODE_TYPES, RELATION_TYPES
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Types de base
14
+ # ---------------------------------------------------------------------------
15
+
16
+ Predictions = list[str] # valeurs snake_case ex: ["action", "processus"]
17
+ GoldLabels = list[str]
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Métriques nœuds
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def node_accuracy(pred: Predictions, gold: GoldLabels) -> float:
25
+ """Exactitude globale sur la prédiction des NodeType."""
26
+ if not gold:
27
+ return 0.0
28
+ correct = sum(p == g for p, g in zip(pred, gold))
29
+ return correct / len(gold)
30
+
31
+
32
+ def node_f1_per_class(
33
+ pred: Predictions, gold: GoldLabels
34
+ ) -> dict[str, dict[str, float]]:
35
+ """
36
+ F1, précision, rappel par NodeType.
37
+
38
+ Retourne :
39
+ {"action": {"precision": 0.9, "recall": 0.8, "f1": 0.85, "support": 12}, …}
40
+ """
41
+ return _f1_per_class(pred, gold, NODE_TYPES)
42
+
43
+
44
+ def node_macro_f1(pred: Predictions, gold: GoldLabels) -> float:
45
+ """F1 macro-moyenné sur tous les NodeType présents dans gold."""
46
+ per_class = node_f1_per_class(pred, gold)
47
+ scores = [v["f1"] for v in per_class.values() if v["support"] > 0]
48
+ return float(np.mean(scores)) if scores else 0.0
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Métriques arêtes
53
+ # ---------------------------------------------------------------------------
54
+
55
+ def edge_accuracy(pred: Predictions, gold: GoldLabels) -> float:
56
+ """Exactitude globale sur la prédiction des RelationType."""
57
+ if not gold:
58
+ return 0.0
59
+ correct = sum(p == g for p, g in zip(pred, gold))
60
+ return correct / len(gold)
61
+
62
+
63
+ def edge_f1_per_class(
64
+ pred: Predictions, gold: GoldLabels
65
+ ) -> dict[str, dict[str, float]]:
66
+ """F1, précision, rappel par RelationType."""
67
+ return _f1_per_class(pred, gold, RELATION_TYPES)
68
+
69
+
70
+ def edge_macro_f1(pred: Predictions, gold: GoldLabels) -> float:
71
+ per_class = edge_f1_per_class(pred, gold)
72
+ scores = [v["f1"] for v in per_class.values() if v["support"] > 0]
73
+ return float(np.mean(scores)) if scores else 0.0
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Similarité de graphe causal
78
+ # ---------------------------------------------------------------------------
79
+
80
+ def causal_graph_similarity(pred_ir: dict, gold_ir: dict) -> dict[str, float]:
81
+ """
82
+ Mesure la similarité entre deux CausalIR (dicts JSON).
83
+
84
+ Retourne :
85
+ {
86
+ "node_count_ratio": float, # |pred_nodes| / |gold_nodes|
87
+ "node_type_accuracy": float, # % de nœuds avec le bon type (par position)
88
+ "edge_count_ratio": float,
89
+ "edge_relation_accuracy": float,
90
+ "overall": float, # moyenne des 4 mesures
91
+ }
92
+ """
93
+ pred_nodes = pred_ir.get("nodes", [])
94
+ gold_nodes = gold_ir.get("nodes", [])
95
+ pred_edges = pred_ir.get("edges", [])
96
+ gold_edges = gold_ir.get("edges", [])
97
+
98
+ # Node count ratio (capped at 1.0)
99
+ node_count_ratio = (
100
+ min(len(pred_nodes), len(gold_nodes)) / max(len(gold_nodes), 1)
101
+ )
102
+
103
+ # Node type accuracy (align by position)
104
+ n = min(len(pred_nodes), len(gold_nodes))
105
+ node_type_acc = (
106
+ sum(pred_nodes[i]["node_type"] == gold_nodes[i]["node_type"] for i in range(n)) / max(n, 1)
107
+ )
108
+
109
+ # Edge count ratio
110
+ edge_count_ratio = (
111
+ min(len(pred_edges), len(gold_edges)) / max(len(gold_edges), 1)
112
+ )
113
+
114
+ # Edge relation accuracy (align by position)
115
+ e = min(len(pred_edges), len(gold_edges))
116
+ edge_rel_acc = (
117
+ sum(pred_edges[i][2]["relation"] == gold_edges[i][2]["relation"] for i in range(e))
118
+ / max(e, 1)
119
+ )
120
+
121
+ overall = float(np.mean([node_count_ratio, node_type_acc,
122
+ edge_count_ratio, edge_rel_acc]))
123
+
124
+ return {
125
+ "node_count_ratio": round(node_count_ratio, 4),
126
+ "node_type_accuracy": round(node_type_acc, 4),
127
+ "edge_count_ratio": round(edge_count_ratio, 4),
128
+ "edge_relation_accuracy": round(edge_rel_acc, 4),
129
+ "overall": round(overall, 4),
130
+ }
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Métriques décodeur (CausalIR → texte)
135
+ # ---------------------------------------------------------------------------
136
+
137
+ def decoder_causal_fidelity(decoded_ir: dict, gold_ir: dict) -> dict[str, float]:
138
+ """
139
+ Fidélité causale du décodeur : compare le CausalIR obtenu en re-parsant
140
+ la sortie du décodeur avec le CausalIR gold d'origine.
141
+
142
+ Le data scientist appelle cette fonction après avoir re-parsé la surface
143
+ générée par le décodeur. Utilise causal_graph_similarity en interne.
144
+
145
+ Retourne les mêmes clés que causal_graph_similarity + "causal_fidelity"
146
+ (alias de "overall" pour clarté sémantique).
147
+ """
148
+ sim = causal_graph_similarity(decoded_ir, gold_ir)
149
+ sim["causal_fidelity"] = sim["overall"]
150
+ return sim
151
+
152
+
153
+ def cross_modal_consistency(ir_a: dict, ir_b: dict) -> dict[str, float]:
154
+ """
155
+ Cohérence cross-modale : mesure si deux CausalIR issus de surfaces
156
+ différentes (ex: fr + python) encodent la même structure causale.
157
+
158
+ Les deux IR doivent avoir été produits depuis le même graphe causal.
159
+ Un score "consistency" proche de 1.0 indique que les deux surfaces
160
+ encodent fidèlement la même structure.
161
+
162
+ Retourne les mêmes clés que causal_graph_similarity + "consistency".
163
+ """
164
+ sim = causal_graph_similarity(ir_a, ir_b)
165
+ sim["consistency"] = sim["overall"]
166
+ return sim
167
+
168
+
169
+ def roundtrip_similarity(source_ir: dict, decoded_ir: dict) -> dict[str, float]:
170
+ """
171
+ Similarité round-trip : mesure la fidélité du cycle complet
172
+ texte → CausalIR → texte → CausalIR.
173
+
174
+ source_ir : CausalIR produit par l'encodeur depuis le texte original
175
+ decoded_ir : CausalIR produit par l'encodeur depuis le texte généré
176
+ par le décodeur
177
+
178
+ Un score "roundtrip" proche de 1.0 indique que l'architecture
179
+ encodeur-décodeur préserve la structure causale.
180
+ """
181
+ sim = causal_graph_similarity(decoded_ir, source_ir)
182
+ sim["roundtrip"] = sim["overall"]
183
+ return sim
184
+
185
+
186
+ def generation_bleu(hypothesis: str, references: list[str], max_n: int = 4) -> float:
187
+ """
188
+ BLEU score simplifié — NumPy pur, sans dépendance externe.
189
+
190
+ Mesure la qualité de surface de la sortie du décodeur par rapport
191
+ aux surfaces gold du dataset. Utile pour les surfaces en langage naturel.
192
+ Pour le code source, préférer decoder_causal_fidelity.
193
+ """
194
+ import math
195
+ from collections import Counter
196
+
197
+ hyp = hypothesis.split()
198
+ refs = [r.split() for r in references]
199
+
200
+ if not hyp or not refs:
201
+ return 0.0
202
+
203
+ hyp_len = len(hyp)
204
+ ref_len = min((len(r) for r in refs), key=lambda rl: (abs(rl - hyp_len), rl))
205
+ bp = 1.0 if hyp_len >= ref_len else math.exp(1 - ref_len / hyp_len)
206
+
207
+ # Note : on itère jusqu'à min(max_n, len(hyp)) — pas jusqu'à max_n.
208
+ # Comportement intentionnel : BLEU tronqué aux n-grammes disponibles pour
209
+ # les courtes hypothèses NLP (labels causaux, clauses). Standard BLEU-4
210
+ # retournerait 0 pour toute hypothèse < 4 tokens ; ce n'est pas utile ici.
211
+ precisions: list[float] = []
212
+ for n in range(1, min(max_n, len(hyp)) + 1):
213
+ hyp_ng = Counter(_ngrams(hyp, n))
214
+ clipped_total = 0
215
+ for ng, cnt in hyp_ng.items():
216
+ max_ref = max(Counter(_ngrams(r, n))[ng] for r in refs)
217
+ clipped_total += min(cnt, max_ref)
218
+ total = sum(hyp_ng.values())
219
+ precisions.append(clipped_total / total if total > 0 else 0.0)
220
+
221
+ if not precisions or min(precisions) == 0.0:
222
+ return 0.0
223
+
224
+ log_avg = sum(math.log(p) for p in precisions) / len(precisions)
225
+ return round(bp * math.exp(log_avg), 4)
226
+
227
+
228
+ def _ngrams(tokens: list[str], n: int) -> list[tuple]:
229
+ return [tuple(tokens[i: i + n]) for i in range(len(tokens) - n + 1)]
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Helpers internes
234
+ # ---------------------------------------------------------------------------
235
+
236
+ def _f1_per_class(
237
+ pred: Predictions, gold: GoldLabels, classes: list[str]
238
+ ) -> dict[str, dict[str, float]]:
239
+ result = {}
240
+ for cls in classes:
241
+ tp = sum(p == cls and g == cls for p, g in zip(pred, gold))
242
+ fp = sum(p == cls and g != cls for p, g in zip(pred, gold))
243
+ fn = sum(p != cls and g == cls for p, g in zip(pred, gold))
244
+ support = sum(g == cls for g in gold)
245
+
246
+ precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
247
+ recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
248
+ f1 = (2 * precision * recall / (precision + recall)
249
+ if (precision + recall) > 0 else 0.0)
250
+
251
+ result[cls] = {
252
+ "precision": round(precision, 4),
253
+ "recall": round(recall, 4),
254
+ "f1": round(f1, 4),
255
+ "support": support,
256
+ }
257
+ return result
@@ -0,0 +1,158 @@
1
+ """
2
+ TrainingRecorder — suivi des métriques et courbes d'apprentissage par epoch.
3
+
4
+ Framework-agnostique (NumPy pur). Le data scientist appelle .record() dans
5
+ sa boucle d'entraînement. Le recorder stocke l'historique et permet
6
+ l'export CSV pour visualisation externe (matplotlib, Excel, etc.).
7
+ """
8
+ from __future__ import annotations
9
+ import csv
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+
14
+
15
+ @dataclass
16
+ class EpochRecord:
17
+ epoch: int
18
+ loss: float
19
+ metrics: dict[str, float] # {"node_accuracy": 0.87, "edge_macro_f1": 0.72, …}
20
+
21
+
22
+ class TrainingRecorder:
23
+ """
24
+ Enregistre loss et métriques CGNP par epoch.
25
+
26
+ Usage typique dans la boucle du DS :
27
+
28
+ recorder = TrainingRecorder()
29
+ for epoch in range(n_epochs):
30
+ pipeline.forward(reps, text)
31
+ node_logits = pipeline._cached_node_logits
32
+ edge_logits = pipeline._cached_edge_logits
33
+ loss_val, d_node, d_edge = pipeline.loss(
34
+ node_logits, edge_logits, gold_node, gold_edge
35
+ )
36
+ pipeline.backward(d_node, d_edge, lr=0.001)
37
+ metrics = {
38
+ "node_accuracy": node_accuracy(pred_types, gold_types),
39
+ "edge_macro_f1": edge_macro_f1(pred_rels, gold_rels),
40
+ }
41
+ recorder.record(epoch, loss_val, metrics)
42
+
43
+ recorder.to_csv("training_history.csv")
44
+ curve = recorder.learning_curve()
45
+ """
46
+
47
+ def __init__(self) -> None:
48
+ self._history: list[EpochRecord] = []
49
+
50
+ def record(self, epoch: int, loss: float, metrics: dict[str, float]) -> None:
51
+ """Enregistre une epoch."""
52
+ self._history.append(EpochRecord(epoch=epoch, loss=loss, metrics=dict(metrics)))
53
+
54
+ def learning_curve(self) -> dict[str, list]:
55
+ """
56
+ Retourne les séries temporelles pour tracer les courbes d'apprentissage.
57
+
58
+ Exemple de retour :
59
+ {
60
+ "epoch": [0, 1, 2, …],
61
+ "loss": [2.1, 1.8, 1.5, …],
62
+ "node_accuracy": [0.3, 0.55, 0.72, …],
63
+ "node_macro_f1": […],
64
+ "edge_accuracy": […],
65
+ "edge_macro_f1": […],
66
+ }
67
+ """
68
+ if not self._history:
69
+ return {}
70
+
71
+ curve: dict[str, list] = {
72
+ "epoch": [r.epoch for r in self._history],
73
+ "loss": [r.loss for r in self._history],
74
+ }
75
+ # Collect all metric keys that appear across epochs
76
+ all_keys: set[str] = set()
77
+ for r in self._history:
78
+ all_keys.update(r.metrics.keys())
79
+
80
+ for key in sorted(all_keys):
81
+ curve[key] = [r.metrics.get(key, float("nan")) for r in self._history]
82
+
83
+ return curve
84
+
85
+ def best_epoch(self, metric: str = "loss", mode: str = "min") -> EpochRecord | None:
86
+ """
87
+ Retourne l'epoch avec la meilleure valeur d'une métrique.
88
+
89
+ mode="min" → minimize (pour loss)
90
+ mode="max" → maximize (pour accuracy, f1)
91
+ """
92
+ if not self._history:
93
+ return None
94
+
95
+ def get_val(r: EpochRecord) -> float:
96
+ if metric == "loss":
97
+ return r.loss
98
+ return r.metrics.get(metric, float("nan"))
99
+
100
+ valid = [r for r in self._history if not _is_nan(get_val(r))]
101
+ if not valid:
102
+ return None
103
+
104
+ if mode == "min":
105
+ return min(valid, key=get_val)
106
+ return max(valid, key=get_val)
107
+
108
+ def summary(self) -> dict:
109
+ """Résumé : première epoch, dernière epoch, meilleure loss."""
110
+ if not self._history:
111
+ return {}
112
+ first = self._history[0]
113
+ last = self._history[-1]
114
+ best = self.best_epoch("loss", "min")
115
+ return {
116
+ "n_epochs": len(self._history),
117
+ "first_loss": first.loss,
118
+ "last_loss": last.loss,
119
+ "best_loss": best.loss if best else None,
120
+ "best_epoch": best.epoch if best else None,
121
+ "last_metrics": last.metrics,
122
+ }
123
+
124
+ def to_csv(self, path: Path | str) -> None:
125
+ """Export CSV — une ligne par epoch, toutes les métriques en colonnes."""
126
+ curve = self.learning_curve()
127
+ if not curve:
128
+ return
129
+ path = Path(path)
130
+ path.parent.mkdir(parents=True, exist_ok=True)
131
+ keys = list(curve.keys())
132
+ with path.open("w", newline="", encoding="utf-8") as f:
133
+ writer = csv.writer(f)
134
+ writer.writerow(keys)
135
+ n = len(curve["epoch"])
136
+ for i in range(n):
137
+ writer.writerow([curve[k][i] for k in keys])
138
+
139
+ def to_json(self, path: Path | str) -> None:
140
+ """Export JSON — liste d'objets {epoch, loss, …metrics}."""
141
+ path = Path(path)
142
+ path.parent.mkdir(parents=True, exist_ok=True)
143
+ records = []
144
+ for r in self._history:
145
+ row = {"epoch": r.epoch, "loss": r.loss}
146
+ row.update(r.metrics)
147
+ records.append(row)
148
+ path.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8")
149
+
150
+ def __len__(self) -> int:
151
+ return len(self._history)
152
+
153
+ def __repr__(self) -> str:
154
+ return f"TrainingRecorder(epochs={len(self._history)})"
155
+
156
+
157
+ def _is_nan(v: float) -> bool:
158
+ return v != v # NaN check without math import
File without changes
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ import json
4
+ import numpy as np
5
+
6
+ from ..constants import (
7
+ UPOS_TAGS, UD_DEP_RELS, UD_TENSE_VALUES, UD_ASPECT_VALUES,
8
+ UD_MOOD_VALUES, SUBJECT_POS_CATS,
9
+ )
10
+ from .representation import UDRepresentation
11
+
12
+
13
+ @dataclass
14
+ class FeatureVocabulary:
15
+ """
16
+ Schéma ordonné des features de la Couche 1.
17
+ Sérialisable JSON — partagé entre entraînement et inférence.
18
+ Le data scientist sérialise cette instance avec son checkpoint.
19
+ Features purement syntaxiques : UPOS, DEP_REL, tense/aspect/mood, polarity, flags structurels.
20
+ """
21
+ upos_tags: list[str] = field(default_factory=lambda: list(UPOS_TAGS))
22
+ dep_rels: list[str] = field(default_factory=lambda: list(UD_DEP_RELS))
23
+ tense_values: list[str] = field(default_factory=lambda: list(UD_TENSE_VALUES))
24
+ aspect_values: list[str] = field(default_factory=lambda: list(UD_ASPECT_VALUES))
25
+ mood_values: list[str] = field(default_factory=lambda: list(UD_MOOD_VALUES))
26
+ subject_pos_cats: list[str] = field(default_factory=lambda: list(SUBJECT_POS_CATS))
27
+
28
+ @property
29
+ def d_clause(self) -> int:
30
+ return (
31
+ len(self.upos_tags)
32
+ + len(self.dep_rels)
33
+ + len(self.subject_pos_cats)
34
+ + len(self.tense_values)
35
+ + len(self.aspect_values)
36
+ + len(self.mood_values)
37
+ + 1 # Polarity
38
+ + 3 # structural flags
39
+ )
40
+
41
+ @property
42
+ def d_conn(self) -> int:
43
+ return len(self.upos_tags) + 2
44
+
45
+ @property
46
+ def d_edge(self) -> int:
47
+ return 2 * self.d_clause + self.d_conn
48
+
49
+ def to_json(self) -> str:
50
+ return json.dumps({
51
+ "upos_tags": self.upos_tags,
52
+ "dep_rels": self.dep_rels,
53
+ "tense_values": self.tense_values,
54
+ "aspect_values": self.aspect_values,
55
+ "mood_values": self.mood_values,
56
+ "subject_pos_cats": self.subject_pos_cats,
57
+ }, ensure_ascii=False)
58
+
59
+ @classmethod
60
+ def from_json(cls, s: str) -> "FeatureVocabulary":
61
+ return cls(**json.loads(s))
62
+
63
+
64
+ def _one_hot(value: str, vocab: list[str]) -> np.ndarray:
65
+ v = np.zeros(len(vocab), dtype=np.float32)
66
+ if value in vocab:
67
+ v[vocab.index(value)] = 1.0
68
+ elif "_unk" in vocab:
69
+ v[vocab.index("_unk")] = 1.0
70
+ return v
71
+
72
+
73
+ def vectorize_clause(
74
+ rep: UDRepresentation,
75
+ vocab: FeatureVocabulary,
76
+ ) -> np.ndarray:
77
+ """UDRepresentation → np.ndarray[d_clause]"""
78
+ parts = [
79
+ _one_hot(rep.root_pos, vocab.upos_tags),
80
+ _one_hot(rep.root_dep_rel, vocab.dep_rels),
81
+ _one_hot(rep.subject_pos or "_absent", vocab.subject_pos_cats),
82
+ _one_hot(rep.tense, vocab.tense_values),
83
+ _one_hot(rep.aspect, vocab.aspect_values),
84
+ _one_hot(rep.mood, vocab.mood_values),
85
+ np.array([1.0 if rep.is_negative else 0.0], dtype=np.float32),
86
+ np.array([float(rep.has_object), float(rep.has_advcl), float(rep.has_temporal_obl)],
87
+ dtype=np.float32),
88
+ ]
89
+ return np.concatenate(parts)
90
+
91
+
92
+ def vectorize_connector(
93
+ marker_rep: UDRepresentation | None,
94
+ src_idx: int,
95
+ dst_idx: int,
96
+ n_clauses: int,
97
+ vocab: FeatureVocabulary,
98
+ ) -> np.ndarray:
99
+ """Connector features between two clauses → np.ndarray[d_conn]"""
100
+ if marker_rep is not None:
101
+ upos_vec = _one_hot(marker_rep.root_pos, vocab.upos_tags)
102
+ else:
103
+ upos_vec = np.zeros(len(vocab.upos_tags), dtype=np.float32)
104
+
105
+ pos_vec = np.array(
106
+ [float(src_idx < dst_idx), abs(dst_idx - src_idx) / max(n_clauses, 1)],
107
+ dtype=np.float32,
108
+ )
109
+ return np.concatenate([upos_vec, pos_vec])
110
+
111
+
112
+ def vectorize_edge(
113
+ src: UDRepresentation,
114
+ dst: UDRepresentation,
115
+ connector: UDRepresentation | None,
116
+ src_idx: int,
117
+ dst_idx: int,
118
+ n_clauses: int,
119
+ vocab: FeatureVocabulary,
120
+ ) -> np.ndarray:
121
+ """Two clauses + connector → np.ndarray[d_edge]"""
122
+ return np.concatenate([
123
+ vectorize_clause(src, vocab),
124
+ vectorize_clause(dst, vocab),
125
+ vectorize_connector(connector, src_idx, dst_idx, n_clauses, vocab),
126
+ ])
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+
4
+
5
+ @dataclass
6
+ class UDRepresentation:
7
+ """
8
+ Abstraction UD-agnostique d'une clause construite depuis des tokens YAML annotés.
9
+ Aucune heuristique linguistique — uniquement des features UD universelles.
10
+ """
11
+ tokens: list[dict] # [{lemma, pos(UPOS), dep_rel(UD), morph: dict}]
12
+ root_lemma: str
13
+ root_pos: str # UPOS
14
+ root_dep_rel: str # UD dep_rel
15
+ root_morph: dict[str, str] # {Tense: "Past", Aspect: "Imp", Mood: "Ind", …}
16
+ subject_pos: str | None # UPOS du nsubj, ou None
17
+ has_object: bool # dep obj/iobj existe
18
+ has_advcl: bool # dep advcl existe
19
+ has_temporal_obl: bool # dep obl avec morph temporel
20
+ token_span: tuple[int, int]
21
+ lang: str
22
+
23
+ @property
24
+ def tense(self) -> str:
25
+ return self.root_morph.get("Tense", "_absent")
26
+
27
+ @property
28
+ def aspect(self) -> str:
29
+ return self.root_morph.get("Aspect", "_absent")
30
+
31
+ @property
32
+ def mood(self) -> str:
33
+ return self.root_morph.get("Mood", "_absent")
34
+
35
+ @property
36
+ def is_negative(self) -> bool:
37
+ return self.root_morph.get("Polarity", "") == "Neg"
File without changes
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+ from typing import Protocol, runtime_checkable
3
+ import numpy as np
4
+
5
+
6
+ @runtime_checkable
7
+ class CausalEncoder(Protocol):
8
+ """
9
+ Contrat d'interface de la Couche 2 — Encodage causal.
10
+
11
+ Le data scientist implémente ce protocol avec le framework de son choix
12
+ (NumPy, PyTorch, JAX, sklearn, …).
13
+
14
+ Entrée : vecteur de features Couche 1 (UDRepresentation vectorisé)
15
+ Sortie : logits sur les types causaux GCN
16
+ """
17
+
18
+ def forward_node(self, x: np.ndarray) -> np.ndarray:
19
+ """
20
+ x : shape (D_clause,) — features d'une clause
21
+ retour : shape (7,) — logits sur NODE_TYPES (non normalisés)
22
+ """
23
+ ...
24
+
25
+ def forward_edge(self, x: np.ndarray) -> np.ndarray:
26
+ """
27
+ x : shape (D_edge,) — features d'une paire de clauses
28
+ retour : shape (11,) — logits sur RELATION_TYPES (non normalisés)
29
+ """
30
+ ...
31
+
32
+ def parameters(self) -> list[np.ndarray]:
33
+ """Retourne tous les paramètres apprenables (utilisé par le checkpoint)."""
34
+ ...
35
+
36
+ def update_node(self, grads: list[tuple[np.ndarray, np.ndarray]], lr: float) -> None:
37
+ """
38
+ Applique les gradients du MLP nœud.
39
+ grads : liste de (dW, db) par couche, dans l'ordre des couches du node MLP.
40
+ """
41
+ ...
42
+
43
+ def update_edge(self, grads: list[tuple[np.ndarray, np.ndarray]], lr: float) -> None:
44
+ """
45
+ Applique les gradients du MLP arête.
46
+ grads : liste de (dW, db) par couche, dans l'ordre des couches du edge MLP.
47
+ """
48
+ ...
49
+
50
+ def update(self, grads: list[np.ndarray], lr: float) -> None:
51
+ """Applique une liste plate de gradients (conservé pour compatibilité externe)."""
52
+ ...