gcn-python 1.0.0__tar.gz → 1.0.2__tar.gz

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 (56) hide show
  1. gcn_python-1.0.2/PKG-INFO +1107 -0
  2. gcn_python-1.0.2/README.md +1093 -0
  3. {gcn_python-1.0.0 → gcn_python-1.0.2}/pyproject.toml +2 -1
  4. gcn_python-1.0.0/PKG-INFO +0 -12
  5. {gcn_python-1.0.0 → gcn_python-1.0.2}/.gitignore +0 -0
  6. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/__init__.py +0 -0
  7. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/constants.py +0 -0
  8. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/__init__.py +0 -0
  9. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/json_reader.py +0 -0
  10. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/loader.py +0 -0
  11. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/schema.py +0 -0
  12. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/verbalize_loader.py +0 -0
  13. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/__init__.py +0 -0
  14. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/eval_runner.py +0 -0
  15. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/metrics.py +0 -0
  16. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/recorder.py +0 -0
  17. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer1/__init__.py +0 -0
  18. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer1/features.py +0 -0
  19. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer1/representation.py +0 -0
  20. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer2/__init__.py +0 -0
  21. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer2/interface.py +0 -0
  22. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer2/reference.py +0 -0
  23. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/__init__.py +0 -0
  24. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/interface.py +0 -0
  25. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/pytorch_rgcn.py +0 -0
  26. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/reference.py +0 -0
  27. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/__init__.py +0 -0
  28. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/cgnp.py +0 -0
  29. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/cli.py +0 -0
  30. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/ir_emitter.py +0 -0
  31. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/label_builder.py +0 -0
  32. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/taxonomy/__init__.py +0 -0
  33. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/taxonomy/loader.py +0 -0
  34. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/__init__.py +0 -0
  35. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/bootstrap.py +0 -0
  36. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/checkpoint.py +0 -0
  37. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/train.py +0 -0
  38. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/__init__.py +0 -0
  39. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/cli.py +0 -0
  40. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/decoder.py +0 -0
  41. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/interface.py +0 -0
  42. {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/trainable.py +0 -0
  43. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/conftest.py +0 -0
  44. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_evaluation.py +0 -0
  45. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_ir_emitter.py +0 -0
  46. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_json_reader.py +0 -0
  47. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_layer1.py +0 -0
  48. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_layer2.py +0 -0
  49. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_layer3.py +0 -0
  50. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_pipeline.py +0 -0
  51. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_pytorch_rgcn.py +0 -0
  52. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_taxonomy.py +0 -0
  53. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_trainable_decoder.py +0 -0
  54. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_training.py +0 -0
  55. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_verbalize_loader.py +0 -0
  56. {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_verbalizer.py +0 -0
@@ -0,0 +1,1107 @@
1
+ Metadata-Version: 2.5
2
+ Name: gcn-python
3
+ Version: 1.0.2
4
+ Summary: CGNP ML layers — Causal Graph Neural Parser architecture
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click>=8.1
7
+ Requires-Dist: numpy<3.0,>=1.24
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: spacy<4.0,>=3.7
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7.4; extra == 'dev'
12
+ Requires-Dist: ruff>=0.1; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # gcn-python — Couches ML du moteur GCN-Core
16
+
17
+ [![PyPI version](https://img.shields.io/pypi/v/gcn-python)](https://pypi.org/project/gcn-python/)
18
+ [![Python](https://img.shields.io/pypi/pyversions/gcn-python)](https://pypi.org/project/gcn-python/)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
20
+
21
+ **Moteur de raisonnement causal — extraire, modéliser et inférer la causalité dans le texte naturel et le code source.**
22
+
23
+ ---
24
+
25
+ ## Vision
26
+
27
+ GCN-Core est un **moteur**, pas un modèle pré-entraîné. Comme le Transformer est une architecture que l'on entraîne sur ses propres données, GCN-Core est une architecture de raisonnement causal que chaque utilisateur entraîne sur son corpus.
28
+
29
+ ### Quel problème résout-il ?
30
+
31
+ Extraire la structure causale d'un texte ou d'un programme — *qui fait quoi, pourquoi, avec quelle conséquence* — est un problème mal résolu par les LLM génériques : ils produisent du texte vraisemblable, pas une structure vérifiable. GCN-Core produit une **Représentation Intermédiaire Causale** (`CausalIR`) : un graphe orienté typé, sérialisable en JSON, interrogeable par GCN-QL, et raisonnable au sens de Pearl (niveaux 1-2-3).
32
+
33
+ ### Pour qui ?
34
+
35
+ - **Chercheurs en NLP causal** : annotation et évaluation de relations de cause-effet
36
+ - **Ingénieurs** : extraction de dépendances causales depuis de la documentation ou du code
37
+ - **Data scientists** : construction de systèmes d'explication (XAI) basés sur des graphes causaux vérifiables
38
+ - **Quiconque** veut comprendre *pourquoi* quelque chose se produit, pas seulement *quoi*
39
+
40
+ ### Quand l'utiliser ?
41
+
42
+ Utilise `gcn-python` quand tu as besoin de :
43
+ 1. Entraîner les couches ML (MLP + R-GCN) sur ton propre corpus annoté
44
+ 2. Intégrer le pipeline de vectorisation et d'inférence dans ton code Python
45
+ 3. Évaluer les performances de classification causale
46
+ 4. Construire un décodeur texte depuis un graphe causal
47
+
48
+ ---
49
+
50
+ ## Architecture
51
+
52
+ ```
53
+ Texte naturel (fr/en) ou Code source (Python/Rust/JS)
54
+
55
+ [gcn-cli — Rust]
56
+
57
+ CausalIR (JSON)
58
+
59
+ ┌──────────────┴──────────────┐
60
+ │ │
61
+ Layer 1 — Features Tokens annotés
62
+ FeatureVocabulary │
63
+ vectorize_clause() reps_from_sentence()
64
+ │ │
65
+ └──────────────┬──────────────┘
66
+
67
+ Layer 2 — Encodeur
68
+ MLPEncoder
69
+ NodeType classifier (7 classes)
70
+ RelationType classifier (11 classes)
71
+
72
+ Layer 3 — Graphe causal
73
+ RGCNLayer / RGCNLayerPT
74
+ Message passing R-GCN
75
+
76
+ CGNPipeline.forward()
77
+
78
+ CausalIR enrichi
79
+
80
+ [Optionnel] TrainableDecoder
81
+
82
+ Surface texte
83
+ ```
84
+
85
+ Les **7 types de nœuds** : `etat`, `action`, `transition`, `processus`, `condition`, `entite`, `etat_systemique`
86
+
87
+ Les **11 types de relations** : `cause`, `enable`, `prevent`, `condition`, `concession`, `sequence`, `motivation`, `filter`, `opposition`, `data_dependency`, `control_dependency`
88
+
89
+ ---
90
+
91
+ ## Installation
92
+
93
+ ```bash
94
+ # Package Python seul
95
+ pip install gcn-python
96
+
97
+ # Avec support GPU/MPS (R-GCN PyTorch)
98
+ pip install gcn-python torch
99
+
100
+ # Avec le moteur Rust (CLI gcn-analyze, gcn-query, gcn-export)
101
+ git clone https://github.com/devmail0561-web/gcn_engine.git
102
+ cd gcn_engine && make install
103
+ ```
104
+
105
+ **Prérequis :** Python ≥ 3.10, NumPy ≥ 1.24
106
+
107
+ ---
108
+
109
+ ## Démarrage rapide
110
+
111
+ ### Inférence depuis un corpus annoté
112
+
113
+ ```python
114
+ from pathlib import Path
115
+ from gcn_python.data.loader import GCNDataLoader, reps_from_sentence
116
+ from gcn_python.layer1.features import FeatureVocabulary
117
+ from gcn_python.layer2.reference import MLPEncoder
118
+ from gcn_python.layer3.reference import RGCNLayer
119
+ from gcn_python.pipeline.cgnp import CGNPipeline
120
+ from gcn_python.training.checkpoint import load_checkpoint
121
+
122
+ # Construire le pipeline
123
+ vocab = FeatureVocabulary()
124
+ encoder = MLPEncoder(vocab.d_clause, vocab.d_edge)
125
+ graph = RGCNLayer(vocab.d_clause, vocab.d_clause) # d_out == d_clause (contrainte)
126
+ pipeline = CGNPipeline(encoder, graph, lang="fr", vocabulary=vocab)
127
+
128
+ # Charger un checkpoint entraîné
129
+ load_checkpoint(pipeline, Path("model.npz"))
130
+
131
+ # Inférence
132
+ loader = GCNDataLoader(Path("mon_corpus/"), lang="fr")
133
+ for sample in loader:
134
+ reps, valid_idxs, connector_reps = reps_from_sentence(sample.sentence)
135
+ cir = pipeline.forward(
136
+ reps,
137
+ text=sample.sentence.text,
138
+ connector_reps=connector_reps,
139
+ )
140
+ # cir est un dict JSON-serializable conforme au schéma CausalIR
141
+ print(cir["nodes"]) # [{id, node_type, label, ...}, ...]
142
+ print(cir["edges"]) # [[src_idx, dst_idx, {relation, confidence, ...}], ...]
143
+ ```
144
+
145
+ ### Entraînement
146
+
147
+ ```bash
148
+ gcn-train \
149
+ --data-dir mon_corpus/ \
150
+ --epochs 100 \
151
+ --lr 0.001 \
152
+ --output model.npz \
153
+ --log-csv courbe.csv
154
+ ```
155
+
156
+ ```python
157
+ # Accès programmatique aux métriques après entraînement
158
+ import json
159
+ with open("courbe.json") as f:
160
+ curve = json.load(f)
161
+ # [{"epoch": 1, "loss": 2.3, "node_accuracy": 0.41, "edge_macro_f1": 0.28}, ...]
162
+ ```
163
+
164
+ ### Évaluation
165
+
166
+ ```bash
167
+ gcn-eval --data-dir mon_corpus/ --model-path model.npz
168
+ ```
169
+
170
+ Sortie :
171
+ ```json
172
+ {
173
+ "n_samples": 120,
174
+ "n_skipped": 2,
175
+ "node_accuracy": 0.87,
176
+ "node_macro_f1": 0.83,
177
+ "edge_accuracy": 0.79,
178
+ "edge_macro_f1": 0.74
179
+ }
180
+ ```
181
+
182
+ ### Bootstrap — générer un corpus depuis du texte brut
183
+
184
+ ```bash
185
+ # 1. Préparer phrases_fr.txt (une phrase par ligne)
186
+ # 2. Lancer gcn-bootstrap (requiert gcn-cli Rust installé)
187
+ gcn-bootstrap \
188
+ --input phrases_fr.txt \
189
+ --out-dir corpus/ \
190
+ --taxonomy-dir gcn-references/taxonomies/ \
191
+ --lang fr
192
+
193
+ # 3. Réviser manuellement les JSON générés
194
+ # 4. Entraîner
195
+ gcn-train --data-dir corpus/ --epochs 50 --output model.npz
196
+ ```
197
+
198
+ ### R-GCN PyTorch (GPU/MPS)
199
+
200
+ ```python
201
+ from gcn_python.layer3.pytorch_rgcn import RGCNLayerPT
202
+
203
+ # Détection automatique cuda / mps / cpu
204
+ graph_pt = RGCNLayerPT(vocab.d_clause, vocab.d_clause)
205
+ pipeline = CGNPipeline(encoder, graph_pt, lang="fr", vocabulary=vocab)
206
+
207
+ # Pour un entraînement natif PyTorch (avec autograd)
208
+ import torch
209
+ H = torch.from_numpy(clause_vecs).float().to(graph_pt._device)
210
+ enriched = graph_pt.forward_torch(H, edge_index, edge_types)
211
+ # utiliser optimizer.step() — NE PAS appeler pipeline.backward() avec PyTorch
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Format de données
217
+
218
+ Les données d'entraînement sont des fichiers JSON conformes au schéma `gcn-nl` :
219
+
220
+ ```json
221
+ {
222
+ "document": {
223
+ "id": "doc-001",
224
+ "lang": "fr",
225
+ "sentences": [
226
+ {
227
+ "id": "s001",
228
+ "text": "Si les ventes baissent, on réduit les coûts.",
229
+ "tokens": [
230
+ {
231
+ "id": 1, "form": "Si", "lemma": "si", "pos": "SCONJ",
232
+ "dep_rel": "mark", "dep_head": 4,
233
+ "morph": {},
234
+ "gcn": {"causal_type": "conjonction", "causal_class": "condition"}
235
+ }
236
+ ],
237
+ "cir": {
238
+ "nodes": [
239
+ {
240
+ "id": "n001", "type": "processus",
241
+ "label": "décroissance(ventes)",
242
+ "token_span": [3, 4], "origin": "explicit",
243
+ "scope": "universal", "temporal_index": 0,
244
+ "attributes": {"entity": "ventes"}
245
+ },
246
+ {
247
+ "id": "n002", "type": "action",
248
+ "label": "réduire(coûts)",
249
+ "token_span": [6, 8], "origin": "explicit",
250
+ "scope": "universal", "temporal_index": 1,
251
+ "attributes": {}
252
+ }
253
+ ],
254
+ "edges": [
255
+ {
256
+ "source": "n001", "target": "n002",
257
+ "relation": "condition",
258
+ "attributes": {
259
+ "confidence": 1.0, "explicit": true,
260
+ "negated": false, "marker_token": 1
261
+ }
262
+ }
263
+ ]
264
+ }
265
+ }
266
+ ]
267
+ }
268
+ }
269
+ ```
270
+
271
+ Trois schémas disponibles dans `gcn-datasets/schemas/` :
272
+ - `gcn-nl.schema.yaml` — texte naturel (fr/en)
273
+ - `gcn-pl.schema.yaml` — code source (Python/Rust/JS)
274
+ - `gcn-verbalize.schema.yaml` — paires CausalIR ↔ surface texte (entraînement décodeur)
275
+
276
+ ---
277
+
278
+ ## Référence API
279
+
280
+ ### `gcn_python.constants`
281
+
282
+ Constantes synchronisées avec les types Rust de `gcn-ir`.
283
+
284
+ ```python
285
+ from gcn_python.constants import NODE_TYPES, RELATION_TYPES
286
+
287
+ NODE_TYPES # ['etat', 'action', 'transition', 'processus',
288
+ # 'condition', 'entite', 'etat_systemique'] — 7 valeurs
289
+
290
+ RELATION_TYPES # ['cause', 'enable', 'prevent', 'condition', 'concession',
291
+ # 'sequence', 'motivation', 'filter', 'opposition',
292
+ # 'data_dependency', 'control_dependency'] — 11 valeurs
293
+
294
+ # Également disponibles :
295
+ # SCOPE_VALUES, NODE_ORIGIN_VALUES, AGENT_TYPE_VALUES
296
+ # UPOS_TAGS (19), UD_DEP_RELS (38)
297
+ # UD_TENSE_VALUES (5), UD_ASPECT_VALUES (4), UD_MOOD_VALUES (5)
298
+ # SUBJECT_POS_CATS (5)
299
+ ```
300
+
301
+ ---
302
+
303
+ ### `gcn_python.data.schema` — Structures de données
304
+
305
+ ```python
306
+ from gcn_python.data.schema import TokenRecord, ClauseRecord, EdgeRecord, SentenceRecord
307
+ ```
308
+
309
+ #### `TokenRecord`
310
+ ```python
311
+ @dataclass
312
+ class TokenRecord:
313
+ id: int # position 1-based dans la phrase
314
+ form: str # forme de surface
315
+ lemma: str
316
+ pos: str # tag UPOS (ex. "VERB", "SCONJ")
317
+ dep_rel: str # relation Universal Dependencies (ex. "nsubj", "mark")
318
+ dep_head: int # 0 = racine
319
+ morph: dict[str, str] # {"Tense": "Past", "Mood": "Ind", ...}
320
+ gcn_causal_type: str | None # "verbe", "conjonction", ...
321
+ gcn_causal_class: str | None # "processus", "condition", ...
322
+ ```
323
+
324
+ #### `ClauseRecord`
325
+ ```python
326
+ @dataclass
327
+ class ClauseRecord:
328
+ node_id: str # "n001"
329
+ node_type: str # valeur de NODE_TYPES
330
+ label: str # étiquette humaine du nœud
331
+ token_span: tuple[int, int] # indices de tokens (1-based, inclus)
332
+ scope: str # valeur de SCOPE_VALUES
333
+ temporal_index: int # ordre temporel dans la phrase
334
+ origin: str # "explicit" | "inferred" | "hypothetical"
335
+ attributes: dict # entité, qualité, agent, patient, ...
336
+ modifiers: list[dict] # modificateurs aspectuels, modaux, ...
337
+ ```
338
+
339
+ #### `EdgeRecord`
340
+ ```python
341
+ @dataclass
342
+ class EdgeRecord:
343
+ source: str # "n001"
344
+ target: str # "n002"
345
+ relation: str # valeur de RELATION_TYPES
346
+ confidence: float # [0.0, 1.0]
347
+ explicit: bool # marqueur lexical présent
348
+ negated: bool
349
+ marker_token: int | None # id du token marqueur
350
+ ```
351
+
352
+ #### `SentenceRecord`
353
+ ```python
354
+ @dataclass
355
+ class SentenceRecord:
356
+ id: str
357
+ text: str
358
+ lang: str
359
+ tokens: list[TokenRecord]
360
+ clauses: list[ClauseRecord]
361
+ edges: list[EdgeRecord]
362
+ ```
363
+
364
+ ---
365
+
366
+ ### `gcn_python.data.json_reader`
367
+
368
+ ```python
369
+ from gcn_python.data.json_reader import load_sentences, load_all_sentences
370
+ from pathlib import Path
371
+
372
+ # Lire un fichier JSON unique
373
+ sentences = load_sentences(Path("corpus/doc001.json"), lang="fr")
374
+ # -> list[SentenceRecord]
375
+
376
+ # Lire un répertoire entier
377
+ sentences = load_all_sentences(Path("corpus/"), lang="fr")
378
+ # Parcourt *.json (trié), concatène tous les SentenceRecord
379
+ ```
380
+
381
+ Supporte deux formats JSON :
382
+ - **Format dataset** : `document.sentences` avec tokens + CIR
383
+ - **Format exemples** : `examples[].expected_cir` (CIR sans tokens)
384
+
385
+ ---
386
+
387
+ ### `gcn_python.data.loader`
388
+
389
+ ```python
390
+ from gcn_python.data.loader import GCNDataLoader, TrainingSample, reps_from_sentence
391
+ ```
392
+
393
+ #### `TrainingSample`
394
+ ```python
395
+ @dataclass
396
+ class TrainingSample:
397
+ sentence: SentenceRecord
398
+ gold_node_labels: np.ndarray # shape (N,) int64 — indices dans NODE_TYPES
399
+ edge_map: dict # {(src_clause_idx, tgt_clause_idx): rel_idx}
400
+ # edge_map : arêtes consécutives forward uniquement (gap=1, src < tgt)
401
+ ```
402
+
403
+ #### `GCNDataLoader`
404
+ ```python
405
+ loader = GCNDataLoader(
406
+ data_dir=Path("corpus/"),
407
+ lang="fr", # code langue
408
+ repeat=False, # True = itérateur infini
409
+ )
410
+
411
+ len(loader) # nombre de SentenceRecord chargés
412
+ for sample in loader: # yield TrainingSample
413
+ ...
414
+ ```
415
+
416
+ #### `reps_from_sentence`
417
+ ```python
418
+ reps, valid_idxs, connector_reps = reps_from_sentence(sentence_record)
419
+ # reps : list[UDRepresentation] — une par clause valide
420
+ # valid_idxs : list[int] — indices originaux dans sentence_record.clauses
421
+ # connector_reps : list[UDRepresentation | None] — len = len(reps) - 1
422
+ # token connecteur (SCONJ/CCONJ/ADP) entre chaque paire de clauses
423
+ # Retourne ([], [], []) si pas de tokens ou pas de clauses
424
+ ```
425
+
426
+ ---
427
+
428
+ ### `gcn_python.data.verbalize_loader`
429
+
430
+ ```python
431
+ from gcn_python.data.verbalize_loader import VerbalizerDataLoader, VerbalizeSample
432
+ ```
433
+
434
+ #### `VerbalizeSample`
435
+ ```python
436
+ @dataclass
437
+ class VerbalizeSample:
438
+ ir_json: str # CausalIR JSON (string)
439
+ node_type_embeddings: np.ndarray # shape (N, 7) — one-hot NODE_TYPES
440
+ gold_tokens: np.ndarray # shape (T,) int64 — indices SurfaceVocabulary
441
+ source_text: str
442
+ ```
443
+
444
+ #### `VerbalizerDataLoader`
445
+ ```python
446
+ loader = VerbalizerDataLoader(
447
+ data_dir=Path("corpus/"),
448
+ vocab=None, # None = construit le vocab depuis les surfaces gold/silver
449
+ )
450
+
451
+ loader.vocab # SurfaceVocabulary construit
452
+ loader.source_text_map() # dict[str, list[np.ndarray]] pour joint training
453
+ len(loader)
454
+ for sample in loader: # yield VerbalizeSample
455
+ ...
456
+ ```
457
+
458
+ ---
459
+
460
+ ### `gcn_python.layer1.representation`
461
+
462
+ ```python
463
+ from gcn_python.layer1.representation import UDRepresentation
464
+ ```
465
+
466
+ #### `UDRepresentation`
467
+ ```python
468
+ @dataclass
469
+ class UDRepresentation:
470
+ tokens: list[dict] # [{lemma, pos, dep_rel, morph}, ...]
471
+ root_lemma: str
472
+ root_pos: str # UPOS du token racine
473
+ root_dep_rel: str
474
+ root_morph: dict[str, str] # {"Tense": ..., "Aspect": ..., "Mood": ...}
475
+ subject_pos: str | None # UPOS du sujet (nsubj), ou None
476
+ has_object: bool # obj/iobj/nobj dans le span
477
+ has_advcl: bool # advcl dans le span
478
+ has_temporal_obl: bool # obl / obl:tmod dans le span
479
+ token_span: tuple[int, int]
480
+ lang: str
481
+
482
+ # Propriétés calculées
483
+ rep.tense # root_morph.get("Tense", "_absent")
484
+ rep.aspect # root_morph.get("Aspect", "_absent")
485
+ rep.mood # root_morph.get("Mood", "_absent")
486
+ rep.is_negative # root_morph.get("Polarity", "") == "Neg"
487
+ ```
488
+
489
+ ---
490
+
491
+ ### `gcn_python.layer1.features`
492
+
493
+ ```python
494
+ from gcn_python.layer1.features import (
495
+ FeatureVocabulary,
496
+ vectorize_clause,
497
+ vectorize_connector,
498
+ vectorize_edge,
499
+ )
500
+ ```
501
+
502
+ #### `FeatureVocabulary`
503
+ ```python
504
+ vocab = FeatureVocabulary() # utilise les constantes par défaut
505
+
506
+ vocab.d_clause # 80 (19 UPOS + 38 DEP_REL + 5 subj_pos + 5 tense
507
+ # + 4 aspect + 5 mood + 1 polarity + 3 flags)
508
+ vocab.d_conn # 21 (19 UPOS + 2 flags directionnels)
509
+ vocab.d_edge # 181 (2 * d_clause + d_conn)
510
+
511
+ # Sérialisation pour checkpoint
512
+ json_str = vocab.to_json()
513
+ vocab2 = FeatureVocabulary.from_json(json_str)
514
+ ```
515
+
516
+ #### `vectorize_clause`
517
+ ```python
518
+ vec = vectorize_clause(rep, vocab)
519
+ # rep : UDRepresentation
520
+ # vocab: FeatureVocabulary
521
+ # -> np.ndarray shape (d_clause,) float32
522
+ # Concaténation : one_hot(root_pos) + one_hot(root_dep_rel) + one_hot(subject_pos)
523
+ # + one_hot(tense) + one_hot(aspect) + one_hot(mood)
524
+ # + [is_negative] + [has_object, has_advcl, has_temporal_obl]
525
+ ```
526
+
527
+ #### `vectorize_connector`
528
+ ```python
529
+ vec = vectorize_connector(marker_rep, src_idx, dst_idx, n_clauses, vocab)
530
+ # marker_rep : UDRepresentation du token connecteur, ou None
531
+ # -> np.ndarray shape (d_conn,) float32
532
+ ```
533
+
534
+ #### `vectorize_edge`
535
+ ```python
536
+ vec = vectorize_edge(src_rep, dst_rep, connector_rep, src_idx, dst_idx, n_clauses, vocab)
537
+ # -> np.ndarray shape (d_edge,) float32
538
+ # = concat(vectorize_clause(src), vectorize_clause(dst), vectorize_connector(...))
539
+ ```
540
+
541
+ ---
542
+
543
+ ### `gcn_python.layer2.interface`
544
+
545
+ ```python
546
+ from gcn_python.layer2.interface import CausalEncoder
547
+ ```
548
+
549
+ Protocol `@runtime_checkable`. Implémenter pour substituer le MLP de référence.
550
+
551
+ ```python
552
+ class MonEncoder:
553
+ def forward_node(self, x: np.ndarray) -> np.ndarray:
554
+ # x: (d_clause,) -> (7,) logits NODE_TYPES
555
+ ...
556
+
557
+ def forward_edge(self, x: np.ndarray) -> np.ndarray:
558
+ # x: (d_edge,) -> (11,) logits RELATION_TYPES
559
+ ...
560
+
561
+ def parameters(self) -> list[np.ndarray]: ...
562
+ def update_node(self, grads, lr: float) -> None: ...
563
+ def update_edge(self, grads, lr: float) -> None: ...
564
+ def update(self, grads, lr: float) -> None: ...
565
+
566
+ assert isinstance(MonEncoder(), CausalEncoder) # True
567
+ ```
568
+
569
+ ---
570
+
571
+ ### `gcn_python.layer2.reference`
572
+
573
+ ```python
574
+ from gcn_python.layer2.reference import MLPEncoder
575
+ ```
576
+
577
+ #### `MLPEncoder`
578
+ Implémentation NumPy de référence de `CausalEncoder`.
579
+
580
+ **Architecture :**
581
+ - Node MLP : `d_clause → 128 → 64 → 7` (initialisation He, ReLU)
582
+ - Edge MLP : `d_edge → 256 → 128 → 11` (initialisation He, ReLU)
583
+
584
+ ```python
585
+ encoder = MLPEncoder(d_clause=vocab.d_clause, d_edge=vocab.d_edge, seed=42)
586
+
587
+ # Inférence
588
+ node_logits = encoder.forward_node(clause_vec) # (7,)
589
+ edge_logits = encoder.forward_edge(edge_vec) # (11,)
590
+
591
+ # Backward (utilisé par CGNPipeline.backward())
592
+ grads_node = encoder.backward_node(d_logits) # list[(dW, db)] — 3 couches
593
+ grads_node, d_input = encoder.backward_node_dx(d_logits) # + gradient en entrée
594
+
595
+ # Snapshots (pour backward multi-nœuds sans re-forward)
596
+ snap = encoder.snapshot_node_cache()
597
+ encoder.restore_node_cache(snap)
598
+
599
+ # SGD manuel
600
+ encoder.update_node(grads_node, lr=0.001)
601
+ encoder.update_edge(grads_edge, lr=0.001)
602
+
603
+ # Tous les paramètres (pour checkpoint)
604
+ params = encoder.parameters() # [W1,b1,W2,b2,W3,b3] nœud + idem arête = 12 arrays
605
+ ```
606
+
607
+ ---
608
+
609
+ ### `gcn_python.layer3.interface`
610
+
611
+ ```python
612
+ from gcn_python.layer3.interface import CausalGraph
613
+ ```
614
+
615
+ Protocol `@runtime_checkable`. Formule R-GCN :
616
+
617
+ ```
618
+ h_i^(l+1) = σ( Σ_r Σ_{j∈N_r(i)} (1/c_{i,r}) W_r h_j + W_0 h_i )
619
+ ```
620
+
621
+ ```python
622
+ class MonGraph:
623
+ d_out: int # dimension de sortie (doit == vocab.d_clause)
624
+
625
+ def message_pass(
626
+ self,
627
+ node_features: np.ndarray, # (N, D_in)
628
+ edge_index: np.ndarray, # (2, E) — [sources, targets]
629
+ edge_types: np.ndarray, # (E,) int — indices RELATION_TYPES
630
+ ) -> np.ndarray: ... # (N, D_out)
631
+
632
+ def parameters(self) -> list[np.ndarray]: ...
633
+ def update(self, grads, lr: float) -> None: ...
634
+ ```
635
+
636
+ ---
637
+
638
+ ### `gcn_python.layer3.reference`
639
+
640
+ ```python
641
+ from gcn_python.layer3.reference import RGCNLayer
642
+ ```
643
+
644
+ #### `RGCNLayer`
645
+ Implémentation NumPy de référence de `CausalGraph`. Supporte les cycles.
646
+
647
+ ```python
648
+ graph = RGCNLayer(
649
+ d_in=vocab.d_clause,
650
+ d_out=vocab.d_clause, # contrainte : d_out == d_clause
651
+ n_relations=11, # défaut : len(RELATION_TYPES)
652
+ seed=42,
653
+ )
654
+
655
+ # Forward
656
+ enriched = graph.message_pass(node_features, edge_index, edge_types)
657
+ # node_features : (N, d_in) — vecteurs de clauses
658
+ # edge_index : (2, E)
659
+ # edge_types : (E,)
660
+ # -> (N, d_out)
661
+
662
+ # Backward
663
+ d_input, [dW_r, dW_0] = graph.backward_message_pass(d_output)
664
+
665
+ # SGD
666
+ graph.update([dW_r, dW_0], lr=0.001)
667
+
668
+ # Paramètres : [W_r (n_rel, d_out, d_in), W_0 (d_out, d_in)]
669
+ graph.parameters()
670
+ ```
671
+
672
+ ---
673
+
674
+ ### `gcn_python.layer3.pytorch_rgcn`
675
+
676
+ ```python
677
+ from gcn_python.layer3.pytorch_rgcn import RGCNLayerPT
678
+ ```
679
+
680
+ #### `RGCNLayerPT`
681
+ Implémentation PyTorch avec support GPU/MPS.
682
+
683
+ ```python
684
+ graph_pt = RGCNLayerPT(
685
+ d_in=vocab.d_clause,
686
+ d_out=vocab.d_clause,
687
+ n_relations=11,
688
+ device=None, # auto-détecte cuda > mps > cpu
689
+ seed=42,
690
+ )
691
+
692
+ # Inférence (retourne numpy, sans grad)
693
+ enriched = graph_pt.message_pass(node_features, edge_index, edge_types)
694
+
695
+ # Entraînement natif PyTorch (garde le graphe de calcul)
696
+ import torch
697
+ H = torch.from_numpy(node_features).float().to(graph_pt._device)
698
+ enriched_t = graph_pt.forward_torch(H, edge_index, edge_types) # Tensor (N, d_out)
699
+ # Utiliser loss.backward() + optimizer.step()
700
+
701
+ # Paramètres PyTorch pour optimizer
702
+ params = graph_pt.torch_parameters() # list[nn.Parameter]
703
+ optimizer = torch.optim.Adam(params, lr=0.001)
704
+
705
+ # Déplacer sur un autre device
706
+ graph_pt.to_device("cuda")
707
+ ```
708
+
709
+ ---
710
+
711
+ ### `gcn_python.pipeline.cgnp`
712
+
713
+ ```python
714
+ from gcn_python.pipeline.cgnp import CGNPipeline
715
+ ```
716
+
717
+ #### `CGNPipeline`
718
+ Compose les couches 1-3 en un pipeline complet.
719
+
720
+ ```python
721
+ pipeline = CGNPipeline(
722
+ encoder=encoder, # CausalEncoder
723
+ graph=graph, # CausalGraph
724
+ lang="fr",
725
+ vocabulary=vocab,
726
+ decoder=None, # TrainableDecoder optionnel
727
+ )
728
+ # Précondition : graph.d_out == vocab.d_clause (ValueError sinon)
729
+ ```
730
+
731
+ #### `forward`
732
+ ```python
733
+ cir = pipeline.forward(
734
+ reps, # list[UDRepresentation]
735
+ text="", # texte source
736
+ clause_positions=None, # list[int] — indices des clauses dans la phrase complète
737
+ n_total_clauses=None, # nombre total de clauses (pour feature distance)
738
+ connector_reps=None, # list[UDRepresentation | None], len == len(reps)-1
739
+ )
740
+ # -> dict CausalIR JSON-sérialisable
741
+ # Remplit tous les attributs _cached_*
742
+ ```
743
+
744
+ #### `loss`
745
+ ```python
746
+ total_loss, d_node, d_edge = pipeline.loss(
747
+ node_logits=pipeline._cached_node_logits, # (N, 7)
748
+ edge_logits=pipeline._cached_edge_logits, # (E, 11) ou None
749
+ gold_node=gold_node_labels, # (N,) int
750
+ gold_edge=gold_edge_labels, # (E,) int ou None
751
+ edge_loss_weight=1.0,
752
+ gold_surface=None, # (T,) int pour décodeur
753
+ )
754
+ # total_loss : float
755
+ # d_node : (N, 7) gradient logits nœuds
756
+ # d_edge : (E, 11) gradient logits arêtes
757
+ ```
758
+
759
+ #### `backward`
760
+ ```python
761
+ pipeline.backward(
762
+ d_node_logits=d_node, # (N, 7)
763
+ d_edge_logits=d_edge, # (E, 11)
764
+ lr=0.001,
765
+ )
766
+ # Étapes : backward MLP nœuds (par snapshot) → backward MLP arêtes
767
+ # → backward décodeur (si présent) → backward R-GCN → update poids
768
+ # No-op si l'encodeur n'implémente pas backward_node_dx (ex. implémentation custom)
769
+ ```
770
+
771
+ #### `filter_edge_cache`
772
+ ```python
773
+ pipeline.filter_edge_cache(valid_edge_idxs)
774
+ # Filtre les caches arêtes APRÈS forward() pour aligner edge_logits avec gold_edge.
775
+ # valid_edge_idxs : np.ndarray d'indices (produit par GCNDataLoader)
776
+ ```
777
+
778
+ ---
779
+
780
+ ### `gcn_python.pipeline.ir_emitter`
781
+
782
+ ```python
783
+ from gcn_python.pipeline.ir_emitter import emit
784
+
785
+ cir = emit(
786
+ text="Si les ventes baissent, on réduit les coûts.",
787
+ lang="fr",
788
+ node_types=["processus", "action"],
789
+ node_labels=["décroissance(ventes)", "réduire(coûts)"],
790
+ token_spans=[(3, 4), (6, 8)],
791
+ scopes=["universal", "universal"],
792
+ edge_triples=[
793
+ # (src_idx, dst_idx, relation, confidence, negated, marker_token)
794
+ (0, 1, "condition", 1.0, False, 1),
795
+ ],
796
+ node_origins=["explicit", "explicit"], # optionnel
797
+ )
798
+ # -> dict CausalIR JSON-sérialisable
799
+ ```
800
+
801
+ ---
802
+
803
+ ### `gcn_python.pipeline.label_builder`
804
+
805
+ ```python
806
+ from gcn_python.pipeline.label_builder import build_label
807
+
808
+ label = build_label(
809
+ rep=ud_rep, # UDRepresentation
810
+ node_type="action", # type prédit
811
+ taxonomies_dir=None, # Path vers taxonomies (nominalizations.yaml)
812
+ )
813
+ # -> str ex. "réduire(coûts)", "décroissance(ventes)", "hidden_cause(?)"
814
+ ```
815
+
816
+ ---
817
+
818
+ ### `gcn_python.taxonomy.loader`
819
+
820
+ ```python
821
+ from gcn_python.taxonomy.loader import TaxonomyIndex
822
+
823
+ tax = TaxonomyIndex.load(
824
+ taxonomies_dir=Path("gcn-references/taxonomies/"),
825
+ lang_code="fr",
826
+ )
827
+
828
+ tax.membership("provoquer")
829
+ # -> {"verbes.cause": True, "verbes.etat": False, ...}
830
+
831
+ tax.keys()
832
+ # -> ["verbes.cause", "verbes.condition", "verbes.enable", ...]
833
+
834
+ len(tax) # nombre de classes chargées
835
+ ```
836
+
837
+ ---
838
+
839
+ ### `gcn_python.training.checkpoint`
840
+
841
+ ```python
842
+ from gcn_python.training.checkpoint import save_checkpoint, load_checkpoint
843
+ from pathlib import Path
844
+
845
+ # Sauvegarder
846
+ save_checkpoint(pipeline, Path("model.npz"))
847
+ # Contenu .npz : encoder_0..N, graph_0..1, _vocab_json
848
+ # + decoder_0..N et _decoder_meta_json si décodeur présent
849
+
850
+ # Restaurer (atomique — lève ValueError si shapes incompatibles)
851
+ load_checkpoint(pipeline, Path("model.npz"))
852
+ # Restaure aussi FeatureVocabulary et TrainableDecoder depuis le checkpoint
853
+ ```
854
+
855
+ ---
856
+
857
+ ### `gcn_python.training.train`
858
+
859
+ ```
860
+ gcn-train [OPTIONS]
861
+
862
+ Options :
863
+ --data-dir PATH Répertoire des données d'entraînement [requis]
864
+ --lang TEXT Code langue (défaut: fr)
865
+ --epochs INT Nombre d'époques (défaut: 50)
866
+ --lr FLOAT Taux d'apprentissage (défaut: 0.001)
867
+ --output PATH Fichier checkpoint .npz (défaut: model.npz)
868
+ --log-csv PATH Log CSV par époque (optionnel)
869
+ --verbalize-dir PATH Répertoire verbalize pour entraînement conjoint (optionnel)
870
+ ```
871
+
872
+ Avec `--log-csv loss.csv`, le fichier `loss.json` est aussi généré avec `node_accuracy` et `edge_macro_f1` par époque.
873
+
874
+ ---
875
+
876
+ ### `gcn_python.training.bootstrap`
877
+
878
+ ```
879
+ gcn-bootstrap [OPTIONS]
880
+
881
+ Options :
882
+ --input PATH Fichier .txt (une phrase par ligne) [requis]
883
+ --lang TEXT Code langue (défaut: fr)
884
+ --out-dir PATH Répertoire de sortie JSON [requis]
885
+ --taxonomy-dir PATH Répertoire taxonomies (ou env GCN_TAXONOMY_DIR)
886
+ --gcn-bin TEXT Chemin vers le binaire gcn (défaut: gcn)
887
+ ```
888
+
889
+ Génère un `generated_NNNN.json` par phrase. Les JSON produits sont à réviser manuellement avant entraînement.
890
+
891
+ ---
892
+
893
+ ### `gcn_python.evaluation.metrics`
894
+
895
+ Toutes les fonctions sont pures NumPy, sans dépendances externes.
896
+
897
+ ```python
898
+ from gcn_python.evaluation.metrics import (
899
+ node_accuracy, node_f1_per_class, node_macro_f1,
900
+ edge_accuracy, edge_f1_per_class, edge_macro_f1,
901
+ causal_graph_similarity,
902
+ decoder_causal_fidelity,
903
+ cross_modal_consistency,
904
+ roundtrip_similarity,
905
+ generation_bleu,
906
+ )
907
+
908
+ # Métriques nœuds
909
+ pred = ["action", "processus", "action"]
910
+ gold = ["action", "action", "condition"]
911
+ node_accuracy(pred, gold) # -> 0.333...
912
+ node_macro_f1(pred, gold) # -> float
913
+ node_f1_per_class(pred, gold)
914
+ # -> {"action": {"precision": 0.5, "recall": 1.0, "f1": 0.67, "support": 2}, ...}
915
+
916
+ # Métriques arêtes (mêmes signatures)
917
+ edge_accuracy(pred_rels, gold_rels)
918
+ edge_macro_f1(pred_rels, gold_rels)
919
+
920
+ # Similarité de graphes causaux
921
+ sim = causal_graph_similarity(pred_cir_dict, gold_cir_dict)
922
+ # -> {"node_count_ratio": 1.0, "node_type_accuracy": 0.8,
923
+ # "edge_count_ratio": 1.0, "edge_relation_accuracy": 0.75, "overall": 0.89}
924
+
925
+ # Fidélité du décodeur (re-parser la sortie du décodeur)
926
+ decoder_causal_fidelity(decoded_cir, gold_cir)
927
+ # -> même structure + "causal_fidelity" == "overall"
928
+
929
+ # Consistance cross-modale (fr vs python sur le même CIR)
930
+ cross_modal_consistency(ir_fr, ir_python)
931
+ # -> même structure + "consistency" == "overall"
932
+
933
+ # Fidélité roundtrip (texte → CIR → texte → CIR)
934
+ roundtrip_similarity(source_cir, decoded_cir)
935
+ # -> même structure + "roundtrip" == "overall"
936
+
937
+ # BLEU simplifié (NumPy pur)
938
+ generation_bleu("on réduit les coûts", ["on réduit les coûts de production"])
939
+ # -> float [0.0, 1.0]
940
+ ```
941
+
942
+ ---
943
+
944
+ ### `gcn_python.evaluation.recorder`
945
+
946
+ ```python
947
+ from gcn_python.evaluation.recorder import TrainingRecorder, EpochRecord
948
+
949
+ recorder = TrainingRecorder()
950
+ recorder.record(epoch=1, loss=2.31, metrics={"node_accuracy": 0.41, "edge_macro_f1": 0.28})
951
+ recorder.record(epoch=2, loss=1.87, metrics={"node_accuracy": 0.58, "edge_macro_f1": 0.45})
952
+
953
+ recorder.learning_curve()
954
+ # -> {"epoch": [1, 2], "loss": [2.31, 1.87], "node_accuracy": [0.41, 0.58], ...}
955
+
956
+ recorder.best_epoch(metric="loss", mode="min")
957
+ # -> EpochRecord(epoch=2, loss=1.87, metrics={...})
958
+
959
+ recorder.summary()
960
+ # -> {"n_epochs": 2, "first_loss": 2.31, "last_loss": 1.87, "best_loss": 1.87, ...}
961
+
962
+ recorder.to_csv(Path("curve.csv"))
963
+ recorder.to_json(Path("curve.json"))
964
+
965
+ len(recorder) # 2
966
+ ```
967
+
968
+ ---
969
+
970
+ ### `gcn_python.evaluation.eval_runner`
971
+
972
+ ```python
973
+ from gcn_python.evaluation.eval_runner import run_eval
974
+ from pathlib import Path
975
+
976
+ report = run_eval(
977
+ data_dir=Path("corpus/"),
978
+ model_path=Path("model.npz"),
979
+ lang="fr",
980
+ )
981
+ # -> {"n_samples": 120, "n_skipped": 2,
982
+ # "node_accuracy": 0.87, "node_macro_f1": 0.83,
983
+ # "edge_accuracy": 0.79, "edge_macro_f1": 0.74}
984
+ ```
985
+
986
+ ```
987
+ gcn-eval --data-dir corpus/ --model-path model.npz [--lang fr] [--output rapport.json]
988
+ ```
989
+
990
+ ---
991
+
992
+ ### `gcn_python.verbalizer.interface`
993
+
994
+ ```python
995
+ from gcn_python.verbalizer.interface import VerbalizerDecoder
996
+ ```
997
+
998
+ Protocol `@runtime_checkable`. Une seule méthode :
999
+
1000
+ ```python
1001
+ class MonDecoder:
1002
+ def decode(self, ir_json: str) -> str:
1003
+ # CausalIR JSON string -> surface texte
1004
+ # Le format de sortie dépend entièrement des données d'entraînement
1005
+ ...
1006
+ ```
1007
+
1008
+ ---
1009
+
1010
+ ### `gcn_python.verbalizer.decoder`
1011
+
1012
+ ```python
1013
+ from gcn_python.verbalizer.decoder import ReferenceDecoder
1014
+
1015
+ decoder = ReferenceDecoder()
1016
+ surface = decoder.decode(json.dumps(cir_dict))
1017
+ # -> "décroissance(ventes) -[condition]-> réduire(coûts)"
1018
+ # Linéarisation structurelle — ne nécessite pas d'entraînement
1019
+ ```
1020
+
1021
+ ---
1022
+
1023
+ ### `gcn_python.verbalizer.trainable`
1024
+
1025
+ ```python
1026
+ from gcn_python.verbalizer.trainable import SurfaceVocabulary, TrainableDecoder
1027
+ ```
1028
+
1029
+ #### `SurfaceVocabulary`
1030
+ ```python
1031
+ vocab = SurfaceVocabulary()
1032
+ vocab.build(["on réduit les coûts", "si les ventes baissent"])
1033
+
1034
+ vocab.encode("on réduit les coûts") # -> [2, 3, 4, 5]
1035
+ vocab.decode([2, 3, 4, 5]) # -> "on réduit les coûts"
1036
+ len(vocab) # nombre de tokens
1037
+
1038
+ json_str = vocab.to_json()
1039
+ vocab2 = SurfaceVocabulary.from_json(json_str)
1040
+ ```
1041
+
1042
+ #### `TrainableDecoder`
1043
+ Décodeur NumPy entraînable. Architecture : mean-pool(node_embeddings) → MLP 2 couches → logits vocabulaire.
1044
+
1045
+ ```python
1046
+ decoder = TrainableDecoder(vocab=surface_vocab, d_hidden=64, seed=0)
1047
+
1048
+ # Entraînement
1049
+ logits = decoder.forward_decode(node_embeddings) # (N, D_in) -> (|V|,)
1050
+ loss, d_logits = decoder.loss_decode(logits, gold_tokens)
1051
+ d_mean, layer_grads = decoder.backward_decode(d_logits)
1052
+ decoder.update(layer_grads, lr=0.001)
1053
+
1054
+ # Inférence
1055
+ surface = decoder.decode(ir_json_str) # -> str
1056
+
1057
+ # Checkpoint
1058
+ json_str = decoder.to_json()
1059
+ decoder2 = TrainableDecoder.from_json(json_str)
1060
+ ```
1061
+
1062
+ **Entraînement conjoint avec CGNPipeline :**
1063
+
1064
+ ```python
1065
+ pipeline = CGNPipeline(encoder, graph, lang="fr", vocabulary=vocab, decoder=decoder)
1066
+ cir = pipeline.forward(reps, text=text)
1067
+ loss, d_node, d_edge = pipeline.loss(
1068
+ pipeline._cached_node_logits,
1069
+ pipeline._cached_edge_logits,
1070
+ gold_node,
1071
+ gold_edge,
1072
+ gold_surface=gold_surface_tokens, # active la loss décodeur
1073
+ )
1074
+ pipeline.backward(d_node, d_edge, lr=0.001)
1075
+ # Le gradient du décodeur se propage vers le R-GCN (couplage encodeur-décodeur)
1076
+ ```
1077
+
1078
+ ---
1079
+
1080
+ ### CLI `gcn-verbalize`
1081
+
1082
+ ```bash
1083
+ # Depuis un fichier CausalIR JSON
1084
+ gcn-verbalize cir.json
1085
+
1086
+ # Depuis stdin
1087
+ gcn analyze "Si les ventes baissent, on réduit les coûts." | gcn-verbalize -
1088
+ # -> "décroissance(ventes) -[condition]-> réduire(coûts)"
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ ## Contraintes de conception
1094
+
1095
+ | Contrainte | Raison |
1096
+ |---|---|
1097
+ | `d_out == d_clause` obligatoire | Les sorties R-GCN sont réinjectées dans le MLP nœud, qui attend `d_clause` dimensions. `CGNPipeline.__init__` lève `ValueError` si non respecté. |
1098
+ | Pas de spaCy à l'inférence | `reps_from_sentence` lit les annotations directement depuis le JSON. spaCy est déclaré comme dépendance mais aucune ligne de code du moteur ne l'appelle. |
1099
+ | Supervision arêtes consécutives uniquement | Le pipeline prédit les arêtes entre clauses adjacentes (gap=1, direction croissante). Les arêtes longue-distance ou inverses déclenchent un `UserWarning` et sont exclues du calcul de la loss. |
1100
+ | Backward par snapshot | `MLPEncoder` sauvegarde les activations (`snapshot_node_cache`) pour permettre le backward par nœud sans re-exécuter le forward. Cela garantit des gradients corrects lors de l'accumulation sur N nœuds. |
1101
+ | Protocols extensibles | `CausalEncoder` et `CausalGraph` sont des `@runtime_checkable` Protocols. Toute implémentation PyTorch, JAX ou custom peut être branchée dans `CGNPipeline` sans modification. |
1102
+
1103
+ ---
1104
+
1105
+ ## Licence
1106
+
1107
+ MIT — voir [LICENSE](https://github.com/devmail0561-web/gcn_engine/blob/master/LICENSE)