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:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user