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.
- gcn_python-1.0.2/PKG-INFO +1107 -0
- gcn_python-1.0.2/README.md +1093 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/pyproject.toml +2 -1
- gcn_python-1.0.0/PKG-INFO +0 -12
- {gcn_python-1.0.0 → gcn_python-1.0.2}/.gitignore +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/constants.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/json_reader.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/loader.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/schema.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/data/verbalize_loader.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/eval_runner.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/metrics.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/evaluation/recorder.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer1/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer1/features.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer1/representation.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer2/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer2/interface.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer2/reference.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/interface.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/pytorch_rgcn.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/layer3/reference.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/cgnp.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/cli.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/ir_emitter.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/pipeline/label_builder.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/taxonomy/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/taxonomy/loader.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/bootstrap.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/checkpoint.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/training/train.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/__init__.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/cli.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/decoder.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/interface.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/src/gcn_python/verbalizer/trainable.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/conftest.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_evaluation.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_ir_emitter.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_json_reader.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_layer1.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_layer2.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_layer3.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_pipeline.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_pytorch_rgcn.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_taxonomy.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_trainable_decoder.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_training.py +0 -0
- {gcn_python-1.0.0 → gcn_python-1.0.2}/tests/test_verbalize_loader.py +0 -0
- {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
|
+
[](https://pypi.org/project/gcn-python/)
|
|
18
|
+
[](https://pypi.org/project/gcn-python/)
|
|
19
|
+
[](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)
|