feat(phase3): CamemBERT v3 + détection villes + initiales + texte espacé + docs réglementaires

Intégration du modèle CamemBERT-bio-deid v3 (F1=0.96, Recall=0.97, 1112 docs)
et corrections qualité issues de l'audit approfondi sur 29 fichiers.

Détection des villes en texte libre :
- Automate Aho-Corasick sur 33K communes INSEE + 11.6K villes FINESS
- Stratégie contextuelle : exige un contexte géographique (à, de, vers,
  habite, urgences de, etc.) sauf pour les villes composées (Saint-Palais)
- Blacklist de ~80 communes homonymes de mots courants (charge, signes, plan...)
- Normalisation SAINT↔ST pour les variantes orthographiques
- De 18 fuites de villes à 2 cas résiduels atypiques

Masquage des initiales de prénom :
- Post-traitement regex : "Dr T. [NOM]" → "Dr [NOM] [NOM]"
- Références initiales : "Ref : JF/VA" → "Ref : [NOM]/[NOM]"

Détection texte espacé d'en-tête :
- "C E N T R E  H O S P I T A L I E R" → [ETABLISSEMENT]

Autres corrections :
- Fix regex RE_EXTRACT_MME_MR (Mr?.? → Mr.?, \s+ → [ \t]+, * → {0,4})
- Stop words médicaux : lever, coucher, services hospitaliers (viscérale, etc.)
- CamemBERT NER manager : version tracking, propriété version, log F1/Recall
- Script finetune : export ONNX automatique + mise à jour VERSION.json
- Évaluateur qualité : exclusion stop words médicaux des alertes INSEE

Documentation :
- Spécifications techniques CamemBERT-bio-deid v3
- Conformité RGPD + AI Act (caviardage PDF raster)
- AIPD (Analyse d'Impact Protection des Données)

Score qualité : 97.0/100 (Grade A), Leak score 100/100

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 12:16:13 +01:00
parent c9572c383a
commit eb14cd219d
8 changed files with 1957 additions and 9 deletions

View File

@@ -13,9 +13,12 @@ Prérequis: pip install transformers datasets seqeval accelerate
Export ONNX post-training: python scripts/export_onnx.py
"""
import sys
import json
import subprocess
import argparse
import random
from pathlib import Path
from datetime import date
from typing import Dict, List, Tuple
from collections import Counter
@@ -690,8 +693,115 @@ def main():
print(f" Precision: {results['eval_precision']:.4f}")
print(f" Recall: {results['eval_recall']:.4f}")
print(f" F1: {results['eval_f1']:.4f}")
print(f"\nPour exporter en ONNX:")
print(f" python -m optimum.exporters.onnx --model {args.output_dir / 'best'} {args.output_dir / 'onnx'}")
# ── Export ONNX automatique ──────────────────────────────────────────────
best_dir = args.output_dir / "best"
onnx_dir = args.output_dir / "onnx"
onnx_export_ok = False
try:
print(f"\nExport ONNX automatique...")
print(f" Source : {best_dir}")
print(f" Destination : {onnx_dir}")
result = subprocess.run(
[
sys.executable, "-m", "optimum.exporters.onnx",
"--model", str(best_dir),
"--task", "token-classification",
str(onnx_dir),
],
capture_output=True,
text=True,
timeout=600,
)
if result.returncode == 0:
onnx_export_ok = True
print(f" Export ONNX réussi → {onnx_dir}")
else:
print(f" [ERREUR] Export ONNX échoué (code {result.returncode})")
if result.stderr:
# Afficher les dernières lignes d'erreur
for line in result.stderr.strip().splitlines()[-10:]:
print(f" {line}")
print(f"\n Pour exporter manuellement :")
print(f" python -m optimum.exporters.onnx --model {best_dir} --task token-classification {onnx_dir}")
except FileNotFoundError:
print(f" [WARN] optimum non installé — export ONNX ignoré")
print(f" Pour exporter manuellement :")
print(f" pip install optimum[exporters]")
print(f" python -m optimum.exporters.onnx --model {best_dir} --task token-classification {onnx_dir}")
except subprocess.TimeoutExpired:
print(f" [ERREUR] Export ONNX timeout (>600s)")
print(f" Pour exporter manuellement :")
print(f" python -m optimum.exporters.onnx --model {best_dir} --task token-classification {onnx_dir}")
except Exception as e:
print(f" [ERREUR] Export ONNX inattendu : {e}")
print(f" Pour exporter manuellement :")
print(f" python -m optimum.exporters.onnx --model {best_dir} --task token-classification {onnx_dir}")
# ── Mise à jour VERSION.json ─────────────────────────────────────────────
version_file = args.output_dir / "VERSION.json"
try:
# Compter les documents d'entraînement (.bio files)
n_bio_files = len(list(args.data_dir.glob("*.bio")))
# Déterminer le numéro de version
if version_file.exists():
version_data = json.loads(version_file.read_text(encoding="utf-8"))
else:
version_data = {
"model": "camembert-bio-deid",
"base_model": MODEL_NAME,
"versions": {},
"directories": {},
}
# Incrémenter la version
existing_versions = [
k for k in version_data.get("versions", {}).keys()
if k.startswith("v") and k[1:].isdigit()
]
if existing_versions:
max_v = max(int(k[1:]) for k in existing_versions)
new_version = f"v{max_v + 1}"
else:
new_version = "v1"
# Trouver le best checkpoint (dernier sauvegardé par Trainer)
best_checkpoint = None
checkpoints = sorted(args.output_dir.glob("checkpoint-*"))
if checkpoints:
best_checkpoint = checkpoints[-1].name
# Construire l'entrée de version
version_entry = {
"date": date.today().isoformat(),
"training_docs": n_bio_files,
"training_examples": len(train_tokens),
"epochs": args.epochs,
"batch_size": args.batch_size,
"learning_rate": args.lr,
"f1": round(results["eval_f1"], 4),
"recall": round(results["eval_recall"], 4),
"precision": round(results["eval_precision"], 4),
"onnx_exported": onnx_export_ok,
}
if best_checkpoint:
version_entry["best_checkpoint"] = best_checkpoint
version_data["current_version"] = new_version
version_data["versions"][new_version] = version_entry
version_data["directories"] = {
"onnx": f"Modèle ONNX actif ({new_version}) — utilisé en inférence CPU",
f"best": f"Modèle PyTorch {new_version} (pour ré-export ONNX si besoin)",
}
version_file.write_text(
json.dumps(version_data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
print(f"\n VERSION.json mis à jour → {new_version} (F1={results['eval_f1']:.4f})")
except Exception as e:
print(f"\n [WARN] Impossible de mettre à jour VERSION.json : {e}")
if __name__ == "__main__":