feat(phase2): Gazetteers FINESS 102K établissements + fine-tuning CamemBERT-bio F1=89%

Gazetteers FINESS (data.gouv.fr open data):
- 102K numéros FINESS → détection par lookup exact dans _mask_admin_label + selective_rescan
- 122K noms d'établissements, 113K téléphones, 76K adresses (disponibles)
- Un nombre 9 chiffres matchant un vrai FINESS est masqué même sans label "FINESS"

Fine-tuning CamemBERT-bio (almanach/camembert-bio-base):
- Export silver annotations réécrit : alignement original↔pseudonymisé (difflib)
  → 6862 entités B- (vs 3344 avec l'ancien audit-only) sur 222K tokens
- Sliding windows (200 tokens, stride 100) pour documents longs
- WeightedNERTrainer avec class weights cappés (max 10x) + label smoothing
- Résultat: Precision=88.1%, Recall=89.8%, F1=88.9% (20 epochs, lr=1e-5)
- Modèle sauvegardé dans models/camembert-bio-deid/best (non commité)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 13:27:37 +01:00
parent 6e0e8c7312
commit 26b210607c
36 changed files with 447533 additions and 62915 deletions

View File

@@ -150,6 +150,55 @@ def _load_insee_gazetteers():
_load_insee_gazetteers()
# ----------------- Gazetteer FINESS (établissements de santé) -----------------
_FINESS_NUMBERS: set = set() # numéros FINESS 9 chiffres
_FINESS_ETAB_NAMES: set = set() # noms d'établissements (lowercase)
_FINESS_TELEPHONES: set = set() # téléphones 10 chiffres
def _load_finess_gazetteers():
"""Charge les gazetteers FINESS (établissements, numéros, téléphones)."""
global _FINESS_NUMBERS, _FINESS_ETAB_NAMES, _FINESS_TELEPHONES
data_dir = Path(__file__).parent / "data" / "finess"
# Numéros FINESS
finess_path = data_dir / "finess_numbers.txt"
if finess_path.exists():
try:
_FINESS_NUMBERS = {
line.strip() for line in finess_path.read_text(encoding="utf-8").splitlines()
if line.strip()
}
log.info(f"Gazetteer FINESS numéros: {len(_FINESS_NUMBERS)} entrées")
except Exception as e:
log.warning(f"Erreur chargement FINESS numéros: {e}")
# Noms d'établissements (pour détection HOPITAL)
noms_path = data_dir / "etablissements_noms.txt"
if noms_path.exists():
try:
_FINESS_ETAB_NAMES = {
line.strip().lower() for line in noms_path.read_text(encoding="utf-8").splitlines()
if line.strip() and len(line.strip()) >= 6
}
log.info(f"Gazetteer FINESS noms: {len(_FINESS_ETAB_NAMES)} entrées")
except Exception as e:
log.warning(f"Erreur chargement FINESS noms: {e}")
# Téléphones (pour validation)
tel_path = data_dir / "telephones.txt"
if tel_path.exists():
try:
_FINESS_TELEPHONES = {
line.strip() for line in tel_path.read_text(encoding="utf-8").splitlines()
if line.strip()
}
log.info(f"Gazetteer FINESS téléphones: {len(_FINESS_TELEPHONES)} entrées")
except Exception as e:
log.warning(f"Erreur chargement FINESS téléphones: {e}")
_load_finess_gazetteers()
# ----------------- Whitelists Médicales -----------------
_MEDICAL_STRUCTURAL_TERMS = set()
_MEDICATION_WHITELIST = set()
@@ -1030,11 +1079,23 @@ def _apply_overrides(line: str, audit: List[PiiHit], page_idx: int, cfg: Dict[st
return line
RE_BARE_9DIGITS = re.compile(r"\b(\d{9})\b")
def _mask_admin_label(line: str, audit: List[PiiHit], page_idx: int) -> str:
m = RE_FINESS.search(line)
if m:
val = m.group(1); audit.append(PiiHit(page_idx, "FINESS", val, PLACEHOLDERS["FINESS"]))
return RE_FINESS.sub(lambda _: f"FINESS : {PLACEHOLDERS['FINESS']}", line)
# Détection FINESS par gazetteer : nombre 9 chiffres qui matche un vrai numéro FINESS
if _FINESS_NUMBERS:
for m9 in RE_BARE_9DIGITS.finditer(line):
if m9.group(1) in _FINESS_NUMBERS:
val = m9.group(1)
audit.append(PiiHit(page_idx, "FINESS", val, PLACEHOLDERS["FINESS"]))
line = line.replace(val, PLACEHOLDERS["FINESS"], 1)
return line
m = RE_OGC.search(line)
if m:
val = m.group(1); audit.append(PiiHit(page_idx, "OGC", val, PLACEHOLDERS["OGC"]))
@@ -2012,6 +2073,11 @@ def selective_rescan(text: str, cfg: Dict[str, Any] | None = None) -> str:
protected = RE_EPISODE.sub(PLACEHOLDERS["EPISODE"], protected)
# N° RPPS
protected = RE_RPPS.sub(PLACEHOLDERS["RPPS"], protected)
# FINESS par gazetteer (nombres 9 chiffres matchant un vrai numéro FINESS)
if _FINESS_NUMBERS:
def _rescan_finess(m: re.Match) -> str:
return PLACEHOLDERS["FINESS"] if m.group(1) in _FINESS_NUMBERS else m.group(0)
protected = RE_BARE_9DIGITS.sub(_rescan_finess, protected)
# Établissements
protected = RE_ETABLISSEMENT.sub(PLACEHOLDERS["ETAB"], protected)
protected = RE_HOPITAL_VILLE.sub(PLACEHOLDERS["ETAB"], protected)

76414
data/finess/adresses.txt Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

101941
data/finess/finess_numbers.txt Normal file

File diff suppressed because it is too large Load Diff

113247
data/finess/telephones.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,62 @@
[MASK] O
Centre B-HOPITAL
Hospitalier I-HOPITAL
de I-HOPITAL
la I-HOPITAL
Côte I-HOPITAL
Basque I-HOPITAL
LABORATOIRE O
de O
BIOLOGIE O
MEDICALE O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
13 B-ADRESSE
avenue I-ADRESSE
de I-ADRESSE
l'interne B-ZIP
Jacques I-ZIP
Loëb I-ZIP
64109 I-ZIP
BAYONNE O
- O
Tel O
: O
[TEL] B-TEL
0559443674 B-TEL
Microbiologie O
Dr O
[NOM] B-PER
JAOUEN B-PER
Anne-Christine I-PER
Hématologie O
Dr O
[NOM] B-PER
MENARD-DEROURE B-PER
Fanny I-PER
(chef O
de O
service) O
Dr O
[NOM] B-PER
BENARD B-PER
Yohan I-PER
Dr O
[NOM] B-PER
GUILLEMAUD B-PER
Julien I-PER
Dr O
[NOM] B-PER
MONIER B-PER
Laurie I-PER
Dr O
[NOM] B-PER
DECOEUR B-PER
Lucie I-PER
Dr O
[NOM] B-PER
LEYSSENE B-PER
David I-PER
Biochimie O
Dr O
[NOM] B-PER
CURUTCHET-BURTIN B-PER
Marie-Laure I-PER
Dr O
[NOM] B-PER
SEGUES B-PER
Rémi I-PER
Assistante O
Dr O
[NOM] B-PER
BEVIERE B-PER
Marion I-PER
Diffusé O
le O
: O
@@ -46,14 +66,14 @@ rendu O
Complet O
05/12/2023 O
10.40 O
[NOM] B-PER
[NOM] B-PER
[NOM] B-PER
SIMONET B-PER
Marie I-PER
lise I-PER
Nom O
usuel O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
[NOM] B-PER
13/09/1948 B-DATE_NAISSANCE
OYARCABAL B-PER
URGENCES O
75 O
a O
@@ -65,42 +85,36 @@ F O
23232115 O
IPP O
: O
[IPP] B-IPP
BA125020 B-IPP
N° O
venue O
: O
DEMANDE O
N° O
[DOSSIER] B-NDA
2300261164 B-NDA
Prescrit O
le O
: O
03/12/2023 O
11 O
: O
44 O
11:44 O
Par O
: O
[NOM] B-PER
[NOM] B-PER
TEILLARD B-PER
Lucie I-PER
Prélevé O
le O
: O
03/12/2023 O
11 O
: O
47 O
11:47 O
Par O
: O
[NOM] B-PER
[NOM] B-PER
VIGNES B-PER
Sophie I-PER
Reçu O
le O
: O
03/12/2023 O
12 O
: O
10 O
12:10 O
Résultat O
Borne O
BACTERIOLOGIE O
@@ -278,29 +292,31 @@ biologiste O
Page O
1/2 O
Dr. O
[NOM] B-PER
Anne B-PER
Christine I-PER
JAOUEN I-PER
N° O
8-3188 O
Portée O
disponible O
sur O
[ETABLISSEMENT] O
[NOM] B-PER
[NOM] B-PER
[NOM] B-PER
www.cofrac.fr B-PER
SIMONET I-PER
Marie I-PER
lise I-PER
Nom O
usuel O
: O
[NOM] B-PER
OYARCABAL B-PER
DDN O
: O
SEXE O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
13/09/1948 B-DATE_NAISSANCE
F O
DEMANDE O
N° O
[DOSSIER] B-NDA
2300261164 B-NDA
Résultat O
Antibiogramme O
. O
@@ -500,7 +516,7 @@ N° O
Portée O
disponible O
sur O
[ETABLISSEMENT] O
www.cofrac.fr O
Validé O
et O
diffusé O
@@ -513,4 +529,6 @@ biologiste O
Page O
2/2 O
Dr. O
[NOM] B-PER
Anne B-PER
Christine I-PER
JAOUEN I-PER

View File

@@ -1,8 +1,14 @@
[MASK] O
Centre B-HOPITAL
Hospitalier I-HOPITAL
de I-HOPITAL
la I-HOPITAL
Côte I-HOPITAL
Basque I-HOPITAL
Anesthésiste O
: O
Dr O
[NOM] B-PER
DUFOUR B-PER
Eric I-PER
DOSSIER O
DE O
CONSULTATION O
@@ -15,24 +21,26 @@ Date O
Nom O
: O
M. O
[NOM] B-PER
URRUTY B-PER
Joseph I-PER
Né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
08/05/1950 B-DATE_NAISSANCE
72 O
ans O
N°Ipp O
: O
[IPP] B-IPP
S1032021 B-IPP
N° O
Csult O
: O
[DOSSIER] B-NDA
23056022 B-NDA
/ O
Nom O
naiss. O
: O
[DOSSIER] B-NDA
23056022 B-NDA
Poids O
: O
85 O
@@ -48,14 +56,18 @@ Profession O
: O
Adresse O
: O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
65 B-ADRESSE
LOTISSEMENT I-ADRESSE
HITTA I-ADRESSE
GOTEIN-LI B-ZIP
64130 I-ZIP
GOTEIN-LIBARRENX I-ZIP
N° O
Tél O
: O
[TEL] B-TEL
0681460115 B-TEL
à O
[TEL] B-TEL
09:17 B-TEL
Spécialiste O
: O
Date O
@@ -69,11 +81,13 @@ Motif O
d'admission O
: O
NÉPHRO O
[NOM] B-PER
URÉTÉRECTOMIE B-PER
COELIO I-PER
Opérateur O
: O
Dr O
[NOM] B-PER
MASCLE B-PER
Laurent I-PER
Prévenir O
: O
Mémo O
@@ -105,15 +119,14 @@ le O
: O
Service O
: O
__ O
: O
__ O
__:__ O
__/__/__ O
Antécédents O
/ O
Traitements O
Examen O
[ETABLISSEMENT] O
clinique O
Décisions O
/ O
Prescriptions O
ATCD O
@@ -132,8 +145,7 @@ cardio-vasculaires O
Derniers O
examens/Epreuve O
d'effort O
2020 O
: O
2020: O
normale O
ATCD O
pulmonaires O
@@ -181,8 +193,7 @@ Histoire O
de O
la O
maladie O
HDM O
: O
HDM: O
lésion O
de O
l'uretère O
@@ -379,15 +390,13 @@ Récent(s) O
Autre O
[le O
17/04 O
Na O
: O
Na: O
144 O
K: O
4.6 O
Créat: O
80 O
DFG O
: O
DFG: O
84 O
Hb: O
14.4 O
@@ -425,9 +434,7 @@ jeun O
le O
25/04/2023 O
à O
00 O
: O
00 O
00:00 O
Merci O
de O
proposer O
@@ -489,16 +496,15 @@ Le O
24 O
Avril O
2023 O
17 O
: O
07 O
17:07 O
Page O
: O
1/2 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
DUFOUR B-PER
Eric I-PER
DOSSIER O
DE O
CONSULTATION O
@@ -511,24 +517,26 @@ Date O
Nom O
: O
M. O
[NOM] B-PER
URRUTY B-PER
Joseph I-PER
Né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
08/05/1950 B-DATE_NAISSANCE
72 O
ans O
N°Ipp O
: O
[IPP] B-IPP
S1032021 B-IPP
N° O
Csult O
: O
[DOSSIER] B-NDA
23056022 B-NDA
/ O
Nom O
naiss. O
: O
[DOSSIER] B-NDA
23056022 B-NDA
Poids O
: O
85 O
@@ -544,12 +552,16 @@ Profession O
: O
Adresse O
: O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
65 B-ADRESSE
LOTISSEMENT I-ADRESSE
HITTA I-ADRESSE
GOTEIN-LI B-ZIP
64130 I-ZIP
GOTEIN-LIBARRENX I-ZIP
N° O
Tél O
: O
[TEL] B-TEL
0681460115 B-TEL
(Bienvenu) O
per-opératoire O
: O
@@ -589,40 +601,41 @@ Le O
24 O
Avril O
2023 O
17 O
: O
07 O
17:07 O
Page O
: O
2/2 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
DUFOUR B-PER
Eric I-PER
Prémédication O
IPP O
I.P.P. O
: O
[IPP] B-IPP
S1032021 B-IPP
Patient O
: O
[NOM] B-PER
[NOM] B-PER
[DATE_NAISSANCE] B-DATE_NAISSANCE
N° O
: O
[DOSSIER] B-NDA
Né(e) O
URRUTY B-PER
JOSEPH I-PER
né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
08/05/1950 B-DATE_NAISSANCE
N° I-DATE_NAISSANCE
Interv I-DATE_NAISSANCE
: I-DATE_NAISSANCE
23056022 I-DATE_NAISSANCE
Né(e) I-DATE_NAISSANCE
le I-DATE_NAISSANCE
: I-DATE_NAISSANCE
08/05/1950 I-DATE_NAISSANCE
72 O
ans O
Date O
: O
24/04/2023 O
16 O
: O
41 O
16:41 O
Consigne(s) O
IDE O
PREPARATIONS O
@@ -631,9 +644,7 @@ jeun O
le O
25/04/2023 O
à O
00 O
: O
00 O
00:00 O
Merci O
de O
proposer O
@@ -744,9 +755,7 @@ Le O
24 O
Avril O
2023 O
17 O
: O
07 O
17:07 O
Page O
: O
1/1 O

View File

@@ -1,8 +1,14 @@
[MASK] O
Centre B-HOPITAL
Hospitalier I-HOPITAL
de I-HOPITAL
la I-HOPITAL
Côte I-HOPITAL
Basque I-HOPITAL
Anesthésiste O
: O
Dr O
[NOM] B-PER
KARAM B-PER
Lydia I-PER
DOSSIER O
DE O
CONSULTATION O
@@ -15,24 +21,26 @@ Date O
Nom O
: O
M. O
[NOM] B-PER
GASTESI B-PER
Michel I-PER
Né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
01/02/1952 B-DATE_NAISSANCE
71 O
ans O
N°Ipp O
: O
[IPP] B-IPP
20023294 B-IPP
N° O
Csult O
: O
[DOSSIER] B-NDA
23605230 B-NDA
/ O
Nom O
naiss. O
: O
[DOSSIER] B-NDA
23060661 B-NDA
Poids O
: O
87 O
@@ -48,16 +56,19 @@ Profession O
: O
Adresse O
: O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
137 B-ADRESSE
HAMEAU I-ADRESSE
DE I-ADRESSE
GARLATZETXE B-ZIP
ALAIA I-ZIP
64700 I-ZIP
BIRIATOU I-ZIP
N° O
Tél O
: O
[TEL] B-TEL
0559203820 B-TEL
à O
12 O
: O
14 O
12:14 O
Spécialiste O
: O
Date O
@@ -74,7 +85,8 @@ HOLEP O
Opérateur O
: O
Dr O
[NOM] B-PER
LAMMERTYN B-PER
Yann I-PER
Prévenir O
: O
Mémo O
@@ -104,9 +116,7 @@ le O
: O
Service O
: O
__ O
: O
__ O
__:__ O
__/__/__ O
Thrombo-embolique O
: O
@@ -126,7 +136,8 @@ Antécédents O
/ O
Traitements O
Examen O
[ETABLISSEMENT] O
clinique O
Décisions O
/ O
Prescriptions O
ATCD O
@@ -179,8 +190,7 @@ antiagrégant O
Derniers O
examens/Echo O
01/2021 O
: O
01/2021: O
VG O
non O
dilaté O
@@ -215,11 +225,10 @@ depuis O
> O
1 O
an O
presque O
: O
presque; O
suivi O
Dr O
[NOM] B-PER
Mathieu B-PER
. O
Tabac/Sevré O
@@ -237,8 +246,7 @@ ans O
Examens O
paracliniques O
récents/EFR O
11/2022 O
: O
11/2022: O
VEMS O
à O
124% O
@@ -286,8 +294,7 @@ Cs O
neuro O
11/2022 O
Dr O
[NOM] B-PER
: O
Tollet: B-PER
élocution O
plus O
harmonieuse. O
@@ -367,8 +374,7 @@ d'effort/ O
4 O
à O
7 O
actif O
: O
actif; O
a O
repris O
ses O
@@ -558,16 +564,15 @@ Le O
02 O
Avril O
2023 O
17 O
: O
30 O
17:30 O
Page O
: O
1/3 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
KARAM B-PER
Lydia I-PER
DOSSIER O
DE O
CONSULTATION O
@@ -580,24 +585,26 @@ Date O
Nom O
: O
M. O
[NOM] B-PER
GASTESI B-PER
Michel I-PER
Né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
01/02/1952 B-DATE_NAISSANCE
71 O
ans O
N°Ipp O
: O
[IPP] B-IPP
20023294 B-IPP
N° O
Csult O
: O
[DOSSIER] B-NDA
23605230 B-NDA
/ O
Nom O
naiss. O
: O
[DOSSIER] B-NDA
23060661 B-NDA
Poids O
: O
87 O
@@ -613,12 +620,17 @@ Profession O
: O
Adresse O
: O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
137 B-ADRESSE
HAMEAU I-ADRESSE
DE I-ADRESSE
GARLATZETXE B-ZIP
ALAIA I-ZIP
64700 I-ZIP
BIRIATOU I-ZIP
N° O
Tél O
: O
[TEL] B-TEL
0559203820 B-TEL
Interrogatoire O
/ O
Autorisation O
@@ -963,12 +975,8 @@ biologique O
: O
Résultat(s) O
récent(s) O
(N O
: O
Normal, O
A O
: O
Anormal) O
(N:Normal, O
A:Anormal) O
: O
- O
Créat O
@@ -1064,9 +1072,7 @@ jeun O
le O
06/04/2023 O
à O
00 O
: O
00 O
00:00 O
Merci O
de O
proposer O
@@ -1121,16 +1127,15 @@ Le O
02 O
Avril O
2023 O
17 O
: O
30 O
17:30 O
Page O
: O
2/3 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
KARAM B-PER
Lydia I-PER
DOSSIER O
DE O
CONSULTATION O
@@ -1143,24 +1148,26 @@ Date O
Nom O
: O
M. O
[NOM] B-PER
GASTESI B-PER
Michel I-PER
Né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
01/02/1952 B-DATE_NAISSANCE
71 O
ans O
N°Ipp O
: O
[IPP] B-IPP
20023294 B-IPP
N° O
Csult O
: O
[DOSSIER] B-NDA
23605230 B-NDA
/ O
Nom O
naiss. O
: O
[DOSSIER] B-NDA
23060661 B-NDA
Poids O
: O
87 O
@@ -1176,12 +1183,17 @@ Profession O
: O
Adresse O
: O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
137 B-ADRESSE
HAMEAU I-ADRESSE
DE I-ADRESSE
GARLATZETXE B-ZIP
ALAIA I-ZIP
64700 I-ZIP
BIRIATOU I-ZIP
N° O
Tél O
: O
[TEL] B-TEL
0559203820 B-TEL
. O
Baby-Noradrénaline O
@@ -1194,13 +1206,12 @@ PRE-ANESTHESIQUE O
Date O
: O
02/04/2023 O
15 O
: O
02 O
15:02 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
LEONARD B-PER
Grégoire I-PER
VPA O
/ O
Eléments O
@@ -1242,40 +1253,41 @@ Le O
02 O
Avril O
2023 O
17 O
: O
30 O
17:30 O
Page O
: O
3/3 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
KARAM B-PER
Lydia I-PER
Prémédication O
IPP O
I.P.P. O
: O
[IPP] B-IPP
20023294 B-IPP
Patient O
: O
[NOM] B-PER
[NOM] B-PER
[DATE_NAISSANCE] B-DATE_NAISSANCE
N° O
: O
[DOSSIER] B-NDA
Né(e) O
GASTESI B-PER
MICHEL I-PER
né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
01/02/1952 B-DATE_NAISSANCE
N° I-DATE_NAISSANCE
Interv I-DATE_NAISSANCE
: I-DATE_NAISSANCE
23060661 I-DATE_NAISSANCE
Né(e) I-DATE_NAISSANCE
le I-DATE_NAISSANCE
: I-DATE_NAISSANCE
01/02/1952 I-DATE_NAISSANCE
71 O
ans O
Date O
: O
29/03/2023 O
11 O
: O
40 O
11:40 O
Consigne(s) O
IDE O
PREPARATIONS O
@@ -1284,9 +1296,7 @@ jeun O
le O
06/04/2023 O
à O
00 O
: O
00 O
00:00 O
Merci O
de O
proposer O
@@ -1433,9 +1443,7 @@ CP O
- O
Articulaire O
-, O
Matin O
: O
1, O
Matin:1, O
A O
continuer O
le O
@@ -1446,9 +1454,7 @@ azarga O
10/5 O
// O
goutte, O
Matin O
: O
1, O
Matin:1, O
A O
continuer O
le O
@@ -1461,9 +1467,7 @@ mg O
- O
PO O
-, O
Soir O
: O
10 O
Soir:10 O
10 O
mg O
ezetimibbe O
@@ -1473,9 +1477,7 @@ CP O
- O
PO O
-, O
Soir O
: O
1, O
Soir:1, O
A O
continuer O
jusqu'à O
@@ -1488,17 +1490,13 @@ CP O
kardegic O
75mg O
//, O
Matin O
: O
1 O
Matin:1 O
monoprost O
// O
goutte O
[Oeil O
G], O
Soir O
: O
1, O
Soir:1, O
A O
continuer O
jusqu'à O
@@ -1515,9 +1513,7 @@ CP O
- O
PO O
-, O
Matin O
: O
1, O
Matin:1, O
Soir:1, O
A O
continuer O
@@ -1541,9 +1537,7 @@ CP O
- O
PO O
-, O
Soir O
: O
1, O
Soir:1, O
A O
continuer O
jusqu'à O
@@ -1558,40 +1552,41 @@ Le O
02 O
Avril O
2023 O
17 O
: O
30 O
17:30 O
Page O
: O
1/2 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
KARAM B-PER
Lydia I-PER
Prémédication O
IPP O
I.P.P. O
: O
[IPP] B-IPP
20023294 B-IPP
Patient O
: O
[NOM] B-PER
[NOM] B-PER
[DATE_NAISSANCE] B-DATE_NAISSANCE
N° O
: O
[DOSSIER] B-NDA
Né(e) O
GASTESI B-PER
MICHEL I-PER
né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
01/02/1952 B-DATE_NAISSANCE
N° I-DATE_NAISSANCE
Interv I-DATE_NAISSANCE
: I-DATE_NAISSANCE
23060661 I-DATE_NAISSANCE
Né(e) I-DATE_NAISSANCE
le I-DATE_NAISSANCE
: I-DATE_NAISSANCE
01/02/1952 I-DATE_NAISSANCE
71 O
ans O
Date O
: O
29/03/2023 O
11 O
: O
40 O
11:40 O
Date O
/ O
Heure O
@@ -1602,9 +1597,7 @@ Le O
02 O
Avril O
2023 O
17 O
: O
30 O
17:30 O
Page O
: O
2/2 O

View File

@@ -1,8 +1,14 @@
[MASK] O
Centre B-HOPITAL
Hospitalier I-HOPITAL
de I-HOPITAL
la I-HOPITAL
Côte I-HOPITAL
Basque I-HOPITAL
Anesthésiste O
: O
Dr O
[NOM] B-PER
LEGRAS B-PER
Claire I-PER
DOSSIER O
DE O
CONSULTATION O
@@ -15,24 +21,26 @@ Date O
Nom O
: O
M. O
[NOM] B-PER
PONCABARE B-PER
Jean I-PER
Né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
17/04/1963 B-DATE_NAISSANCE
60 O
ans O
N°Ipp O
: O
[IPP] B-IPP
S1024244 B-IPP
N° O
Csult O
: O
[DOSSIER] B-NDA
23694563 B-NDA
/ O
Nom O
naiss. O
: O
[DOSSIER] B-NDA
23139653 B-NDA
Poids O
: O
88 O
@@ -52,9 +60,7 @@ N° O
Tél O
: O
à O
12 O
: O
11 O
12:11 O
Spécialiste O
: O
Date O
@@ -102,9 +108,7 @@ le O
: O
Service O
: O
__ O
: O
__ O
__:__ O
__/__/__ O
Classe O
ASA O
@@ -114,7 +118,8 @@ Antécédents O
/ O
Traitements O
Examen O
[ETABLISSEMENT] O
clinique O
Décisions O
/ O
Prescriptions O
ATCD O
@@ -192,7 +197,7 @@ gauche O
Consultation O
cardio O
Dr O
[NOM] B-PER
Minviole B-PER
04/23 O
: O
FEVG O
@@ -499,16 +504,15 @@ Le O
03 O
Septembre O
2023 O
17 O
: O
32 O
17:32 O
Page O
: O
1/2 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
LEGRAS B-PER
Claire I-PER
DOSSIER O
DE O
CONSULTATION O
@@ -521,24 +525,26 @@ Date O
Nom O
: O
M. O
[NOM] B-PER
PONCABARE B-PER
Jean I-PER
Né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
17/04/1963 B-DATE_NAISSANCE
60 O
ans O
N°Ipp O
: O
[IPP] B-IPP
S1024244 B-IPP
N° O
Csult O
: O
[DOSSIER] B-NDA
23694563 B-NDA
/ O
Nom O
naiss. O
: O
[DOSSIER] B-NDA
23139653 B-NDA
Poids O
: O
88 O
@@ -608,9 +614,7 @@ Prescription O
biologique O
: O
Résultat(s) O
(N O
: O
Normal, O
(N:Normal, O
A:Anormal) O
: O
- O
@@ -631,12 +635,8 @@ N O
) O
Résultat(s) O
récent(s) O
(N O
: O
Normal, O
A O
: O
Anormal) O
(N:Normal, O
A:Anormal) O
: O
- O
Groupe O
@@ -689,9 +689,7 @@ jeun O
le O
04/09/2023 O
à O
00 O
: O
00 O
00:00 O
Jeune O
pré-opératoire O
: O
@@ -715,19 +713,18 @@ pré-opératoire O
: O
. O
[NOM] B-PER
GOXOAN B-PER
VISITE O
PRE-ANESTHESIQUE O
Date O
: O
03/09/2023 O
17 O
: O
29 O
17:29 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
HANNEQUIN B-PER
Charlène I-PER
VPA O
/ O
Eléments O
@@ -755,40 +752,41 @@ Le O
03 O
Septembre O
2023 O
17 O
: O
32 O
17:32 O
Page O
: O
2/2 O
Anesthésiste O
: O
Dr O
[NOM] B-PER
LEGRAS B-PER
Claire I-PER
Prémédication O
IPP O
I.P.P. O
: O
[IPP] B-IPP
S1024244 B-IPP
Patient O
: O
[NOM] B-PER
[NOM] B-PER
[DATE_NAISSANCE] B-DATE_NAISSANCE
N° O
: O
[DOSSIER] B-NDA
Né(e) O
PONCABARE B-PER
JEAN I-PER
né(e) O
le O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
17/04/1963 B-DATE_NAISSANCE
N° I-DATE_NAISSANCE
Interv I-DATE_NAISSANCE
: I-DATE_NAISSANCE
23139653 I-DATE_NAISSANCE
Né(e) I-DATE_NAISSANCE
le I-DATE_NAISSANCE
: I-DATE_NAISSANCE
17/04/1963 I-DATE_NAISSANCE
60 O
ans O
Date O
: O
24/08/2023 O
10 O
: O
41 O
10:41 O
Consigne(s) O
IDE O
PREPARATIONS O
@@ -797,9 +795,7 @@ jeun O
le O
04/09/2023 O
à O
00 O
: O
00 O
00:00 O
Jeune O
pré-opératoire O
: O
@@ -882,15 +878,11 @@ J-1 O
indapamide O
2.5mg O
CP, O
Matin O
: O
1 O
Matin:1 O
perindopril O
10mg O
CP, O
Matin O
: O
1 O
Matin:1 O
Date O
/ O
Heure O
@@ -901,9 +893,7 @@ Le O
03 O
Septembre O
2023 O
17 O
: O
32 O
17:32 O
Page O
: O
1/1 O

View File

@@ -28,55 +28,52 @@ S O
Q O
U O
E O
[MASK] O
* O
[MASK] O
* O
640780417 B-HOPITAL
*640780417* I-HOPITAL
Praticiens O
Hospitaliers O
: O
Dr O
T. O
[NOM] B-PER
GRELLETY B-PER
Oncologie O
médicale O
Dr O
F. O
[NOM] B-PER
MINNE B-PER
Oncologie O
médicale O
Dr O
S. O
[NOM] B-PER
GHECK B-PER
Chirurgie O
Sénologie- O
Gynécologie O
Dr O
L. O
[NOM] B-PER
BOURDARIAS B-PER
Chirurgie O
Sénologie- O
Gynécologie O
Dr O
B. O
[NOM] B-PER
GOLHEN B-PER
Radiologie O
Dr O
A. O
[NOM] B-PER
CHARRIE B-PER
Radiologie O
Dr O
C. O
[NOM] B-PER
MAUREL B-PER
Radiologie O
Dr O
S. O
[NOM] B-PER
S.GIRAUD O
Médecin O
généticien O
Mme O
A. O
[NOM] B-PER
DENISE B-PER
Conseillère O
en O
génétique O
@@ -86,14 +83,14 @@ Service O
: O
Mme O
C. O
[NOM] B-PER
MONNIER B-PER
 O
[TEL] B-TEL
05.59.44.33.02 B-TEL
Mr O
L. O
[NOM] B-PER
DAVID B-PER
 O
[TEL] B-TEL
05.59.44.37.73 B-TEL
Secrétariat O
Médical O
: O
@@ -101,39 +98,43 @@ Accueil, O
Rendez-vous O
Mme O
C. O
[NOM] B-PER
SARRATIA B-PER
Mme O
A. O
[NOM] B-PER
BOUNEY B-PER
 O
[TEL] B-TEL
[EMAIL] B-EMAIL
05.33.78.81.90 B-TEL
secr.hms@ch-cotebasque.fr B-EMAIL
 O
[ADRESSE] B-ADRESSE
Interne O
[NOM] B-PER
[NOM] B-PER
13 B-PER
avenue I-PER
de I-PER
lInterne I-PER
Jacques I-PER
Loëb I-PER
- O
B.P. O
8 O
O
[CODE_POSTAL] B-ZIP
[MASK] O
64109 B-ZIP
BAYONNE I-ZIP
Cedex I-ZIP
Pôle B-HOPITAL
Spécialités I-HOPITAL
Médicales I-HOPITAL
(Chef O
de O
[MASK] O
. O
[NOM] B-PER
) O
Pôle O
Dr O
E.ELLIE) O
 O
Secrétariat O
: O
[TEL] B-TEL
05.33.78.81.90 B-TEL
Télécopie O
: O
[TEL] B-TEL
[EMAIL] B-EMAIL
05.59.44.33.62 B-TEL
secr.hms@ch-cotebasque.fr B-EMAIL
CL O
Bayonne, O
le O
@@ -141,28 +142,60 @@ le O
Juin O
2023 O
Docteur O
[NOM] B-PER
[MASK] O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
[CODE_POSTAL] B-ZIP
Marie-Alix B-PER
GREGOIRE I-PER
POLYCLINIQUE I-PER
CÔTE I-PER
BASQUE I-PER
SUD I-PER
7, B-HOPITAL
RUE I-HOPITAL
LÉONCE I-HOPITAL
GOYETCHE I-HOPITAL
- I-HOPITAL
CS I-HOPITAL
30149 B-ZIP
64501 I-ZIP
ST I-ZIP
JEAN I-ZIP
DE I-ZIP
LUZ I-ZIP
CEDEX I-ZIP
Doubles O
aux O
: O
Docteur O
[NOM] B-PER
Jean-Jacques B-PER
BENICHOU I-PER
Cabinet O
Médical O
[NOM] B-PER
Aice B-PER
Egoa O
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
Place B-ADRESSE
du I-ADRESSE
Fronton B-ZIP
64500 I-ZIP
CIBOURE I-ZIP
Docteur O
[NOM] B-PER
[NOM] B-PER
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
[CODE_POSTAL] B-ZIP
Oliver B-PER
JENKINS I-PER
POLYCLINIQUE I-PER
CÔTE I-PER
BASQUE I-PER
SUD I-PER
7, I-PER
RUE I-PER
LÉONCE I-PER
GOYETCHE I-PER
- I-PER
CS I-PER
30149 B-ZIP
64501 I-ZIP
ST I-ZIP
JEAN I-ZIP
DE I-ZIP
LUZ I-ZIP
CEDEX I-ZIP
Chers O
Confrères, O
Nous O
@@ -179,20 +212,22 @@ Sénologie-gynécologie O
le O
02/06/2023, O
Madame O
[NOM] B-PER
, O
[DATE_NAISSANCE] B-DATE_NAISSANCE
. O
Nicole O
CLAVEL, O
née O
le O
25/08/1942. O
Veuillez O
trouver O
ci-joint O
le O
compte-rendu. O
Docteur O
[NOM] B-PER
Sophie B-PER
GHECK I-PER
Docteur O
[NOM] B-PER
Floriane B-PER
MINNE I-PER
Courrier O
relu O
et O
@@ -200,11 +235,15 @@ validé O
par O
les O
médecins O
[ETABLISSEMENT] O
[NOM] B-PER
[NOM] B-PER
, O
[DATE_NAISSANCE] B-DATE_NAISSANCE
Hôpital B-PER
de I-PER
Jour I-PER
Multidisciplinaire I-PER
de B-DATE_NAISSANCE
Sénologie-Gynécologie I-DATE_NAISSANCE
CLAVEL I-DATE_NAISSANCE
NICOLE, I-DATE_NAISSANCE
25/08/1942 I-DATE_NAISSANCE
COMPTE-RENDU O
DE O
VISITE O
@@ -218,27 +257,30 @@ Vue O
par O
: O
Docteurs O
[NOM] B-PER
, O
[NOM] B-PER
GHECK, B-PER
MINNE I-PER
ainsi O
que O
Madame O
[NOM] B-PER
, O
ITHURBIDE, O
IPA O
Adressée O
par O
: O
Docteur O
[NOM] B-PER
GREGOIRE B-PER
du O
court O
séjour O
gériatrie O
à O
la O
[ETABLISSEMENT] O
Polyclinique O
de O
Saint O
Jean O
de O
Luz O
où O
est O
actuellement O
@@ -462,7 +504,10 @@ DE O
PRISE O
EN O
CHARGE O
[AGE] B-AGE
Patiente B-AGE
de I-AGE
80 I-AGE
ans I-AGE
vue O
pour O
probable O
@@ -540,10 +585,9 @@ symptomatique O
est O
à O
envisager. O
[NOM] B-PER
[NOM] B-PER
, O
[DATE_NAISSANCE] B-DATE_NAISSANCE
CLAVEL B-PER
NICOLE, B-DATE_NAISSANCE
25/08/1942 I-DATE_NAISSANCE
Questionnaire O
Qualité O
de O
@@ -589,9 +633,11 @@ disponible O
si O
nécessité. O
Docteur O
[NOM] B-PER
Sophie B-PER
GHECK I-PER
Docteur O
[NOM] B-PER
Floriane B-PER
MINNE I-PER
Courrier O
relu O
et O

View File

@@ -1,9 +1,8 @@
CROp O
[NOM] B-PER
Epi B-PER
- O
[NOM] B-PER
, O
[NOM] B-PER
COSSU, B-PER
REMI I-PER
_______________________________________________________________________________________________________________ O
Compte O
rendu O
@@ -12,16 +11,14 @@ opératoire O
neurochirurgie O
type O
22/08/23 O
14 O
: O
31 O
14:31 O
(mod. O
le O
22/08/23 O
14:58 O
par O
[NOM] B-PER
[NOM] B-PER
ARTIGUEBIEILLE B-PER
Veronique I-PER
, O
statut O
Réf O
@@ -31,16 +28,22 @@ Bayonne, O
le O
22/08/2023 O
Dr O
[NOM] B-PER
Maria B-PER
BISCAY-SALLABERRY I-PER
CABINET O
[NOM] B-PER
ETXEBARNONDOA B-PER
Le O
BOURG O
[CODE_POSTAL] B-ZIP
64780 B-ZIP
IRISSARRY I-ZIP
Mr O
[NOM] B-PER
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
REMI B-PER
COSSU I-PER
200 B-ADRESSE
CHEMIN I-ADRESSE
SORHABIETA B-ZIP
64640 I-ZIP
IHOLDY I-ZIP
Madame O
et O
cher O
@@ -62,9 +65,7 @@ patient, O
Mr O
R O
le O
[DATE_NAISSANCE] B-DATE_NAISSANCE
. O
07/08/1999. O
En O
vous O
remerciant O
@@ -88,7 +89,8 @@ sentiments O
confraternellement O
dé O
Docteur O
[NOM] B-PER
Joe B-PER
FADDOUL I-PER
Courrier O
lu O
et O
@@ -102,34 +104,38 @@ OPÉRATOIRE O
Date O
: O
22/08/2023 O
[DOSSIER] B-NDA
Dossier O
: O
23159905 O
Nom O
: O
[NOM] B-PER
COSSU B-PER
Prénom O
: O
[NOM] B-PER
REMI B-PER
Date O
de O
naissance O
: O
[DATE_NAISSANCE] B-DATE_NAISSANCE
07/08/1999 B-DATE_NAISSANCE
Service O
: O
Neurochirurgie O
CHIRURGIEN O
: O
Dr O
[NOM] B-PER
FADDOUL B-PER
Joe I-PER
AIDE O
OPERATOIRE O
: O
[NOM] B-PER
[NOM] B-PER
STEFANINI B-PER
Andréa I-PER
ANESTHÉSISTE O
: O
Dr O
[NOM] B-PER
CUCUPHAT B-PER
Pierre-Lou I-PER
INTERVENTION O
PRATIQUÉE O
: O
@@ -149,10 +155,11 @@ LA O
MALADIE O
: O
Mr O
[NOM] B-PER
, O
[DATE_NAISSANCE] B-DATE_NAISSANCE
, O
COSSU O
Rémi, O
né O
le O
07/08/1999, O
qui O
est O
connu O
@@ -171,15 +178,12 @@ patient O
Page O
1 O
18/04/2025 O
11 O
: O
54:37 O
11:54:37 O
CROp O
[NOM] B-PER
Epi B-PER
- O
[NOM] B-PER
, O
[NOM] B-PER
COSSU, B-PER
REMI I-PER
_______________________________________________________________________________________________________________ O
Compte O
rendu O
@@ -206,11 +210,11 @@ complication O
particulière O
et O
Mr O
[NOM] B-PER
COSSU B-PER
était O
hospitalisé O
à O
[NOM] B-PER
MARIENIA B-PER
depui O
pour O
réaliser O
@@ -243,12 +247,11 @@ nos O
collègues O
MPR, O
Dr O
[NOM] B-PER
BEGUE B-PER
à O
[NOM] B-PER
, O
MARIENIA, O
Mr O
[NOM] B-PER
COSSU B-PER
a O
été O
transféré O
@@ -293,7 +296,7 @@ bien O
expliqués O
à O
Mr O
[NOM] B-PER
COSSU B-PER
qu O
geste O
chirurgical. O
@@ -482,7 +485,8 @@ négligeable, O
non O
compensée O
Docteur O
[NOM] B-PER
Joe B-PER
FADDOUL I-PER
Courrier O
lu O
et O
@@ -496,6 +500,4 @@ patient O
Page O
2 O
18/04/2025 O
11 O
: O
54:37 O
11:54:37 O

View File

@@ -1,9 +1,8 @@
CROp O
[NOM] B-PER
Epi B-PER
- O
[NOM] B-PER
, O
[NOM] B-PER
DEAUX, B-PER
JEAN I-PER
_______________________________________________________________________________________________________________ O
Compte O
rendu O
@@ -14,16 +13,14 @@ type O
chirurgie O
viscérale O
13/09/23 O
14 O
: O
55 O
14:55 O
(mod. O
le O
13/09/23 O
15:05 O
par O
[NOM] B-PER
[NOM] B-PER
LEVERGE B-PER
Jessica I-PER
, O
statut O
: O
@@ -34,22 +31,36 @@ Bayonne, O
le O
13/09/2023 O
Docteur O
[NOM] B-PER
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
Martine B-PER
GOMEZ I-PER
10 B-ADRESSE
rue I-ADRESSE
des B-ZIP
augustins I-ZIP
64 I-ZIP
100 I-ZIP
BAYONNE O
Monsieur O
[NOM] B-PER
[ADRESSE] B-ADRESSE
[CODE_POSTAL] B-ZIP
JEAN B-PER
DEAUX I-PER
36 B-ADRESSE
RUE I-ADRESSE
VICTOR B-ZIP
HUGO I-ZIP
64100 I-ZIP
BAYONNE I-ZIP
Docteur O
[NOM] B-PER
Tam B-PER
KHUONG I-PER
Gastro O
entérologie O
[MASK] O
CHCB B-HOPITAL
Monsieur O
[NOM] B-PER
[DATE_NAISSANCE] B-DATE_NAISSANCE
JEAN B-PER
DEAUX I-PER
Né B-DATE_NAISSANCE
le I-DATE_NAISSANCE
14/04/1953 I-DATE_NAISSANCE
RESECTION O
SEGMENTAIRE O
DE O
@@ -69,11 +80,11 @@ Opérateur O
: O
Docteur O
R. O
[NOM] B-PER
GONTIER B-PER
Anesthésiste(s) O
Docteur O
E. O
[NOM] B-PER
DUFOUR B-PER
Aide(s) O
: O
L'interne O
@@ -285,15 +296,12 @@ patient O
Page O
1 O
22/04/2025 O
10 O
: O
07:08 O
10:07:08 O
CROp O
[NOM] B-PER
Epi B-PER
- O
[NOM] B-PER
, O
[NOM] B-PER
DEAUX, B-PER
JEAN I-PER
_______________________________________________________________________________________________________________ O
Compte O
rendu O
@@ -333,6 +341,4 @@ patient O
Page O
2 O
22/04/2025 O
10 O
: O
07:08 O
10:07:08 O

View File

@@ -2,7 +2,7 @@ Courrier O
Epi O
- O
RICHARD, O
[NOM] B-PER
CLAUDE B-PER
___________________________________________________________________________________________________________________________ O
Courriers O
médicaux O
@@ -11,16 +11,14 @@ Lettre O
de O
sortie O
05/07/23 O
14 O
: O
17 O
14:17 O
(mod. O
le O
07/07/23 O
12:19 O
par O
[NOM] B-PER
[NOM] B-PER
PENOUILH B-PER
Emilie I-PER
, O
statut O
: O
@@ -34,7 +32,8 @@ le O
juillet O
2023 O
Docteur O
[NOM] B-PER
Philippe B-PER
ETCHETO I-PER
Centre O
Médical O
de O
@@ -43,14 +42,16 @@ Zup O
Quartier O
Ste O
Croix O
[CODE_POSTAL] B-ZIP
64100 B-ZIP
BAYONNE I-ZIP
Cher O
Confrère, O
Monsieur O
[NOM] B-PER
, O
[DATE_NAISSANCE] B-DATE_NAISSANCE
, O
Claude O
RICHARD, O
né O
le O
27/08/1954, O
a O
été O
hospitalisé O
@@ -409,21 +410,20 @@ patient O
Page O
1 O
17/04/2025 O
09 O
: O
17:42 O
09:17:42 O
Courrier O
Epi O
- O
RICHARD, O
[NOM] B-PER
CLAUDE B-PER
___________________________________________________________________________________________________________________________ O
Courriers O
médicaux O
Bien O
confraternellement. O
Docteur O
[NOM] B-PER
Antoine B-PER
DOUARD I-PER
Courrier O
lu O
et O
@@ -437,6 +437,4 @@ patient O
Page O
2 O
17/04/2025 O
09 O
: O
17:42 O
09:17:42 O

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""
Export silver annotations — Génère des données d'entraînement BIO à partir du pipeline existant.
================================================================================================
Utilise le pipeline regex+NER+VLM actuel pour produire des annotations "silver standard"
sur les 706 OGC. Ces annotations servent de base pour fine-tuner CamemBERT-bio.
Export silver annotations — BIO via alignement texte original ↔ pseudonymisé.
=============================================================================
Aligne le texte extrait du PDF original avec le texte pseudonymisé (.pseudonymise.txt)
pour créer des annotations BIO fiables. Les placeholders [NOM], [TEL], etc. dans le
texte pseudonymisé indiquent exactement quels tokens ont été masqués.
Usage:
python scripts/export_silver_annotations.py [--limit N] [--out-dir DIR]
@@ -13,21 +14,15 @@ Format BIO: TOKEN\tLABEL (un token par ligne, lignes vides entre phrases)
"""
import sys
import re
import json
import difflib
import argparse
from pathlib import Path
from typing import List, Tuple
from typing import Dict, List, Tuple
sys.path.insert(0, str(Path(__file__).parent.parent))
# Regex pour détecter les placeholders et reconstruire l'alignement
PLACEHOLDER_RE = re.compile(
r"\[(NOM|TEL|EMAIL|NIR|IPP|DOSSIER|NDA|EPISODE|RPPS|DATE_NAISSANCE|"
r"ADRESSE|CODE_POSTAL|VILLE|MASK|FINESS|OGC|AGE|ETAB|IBAN)\]"
)
# Mapping placeholder → label BIO
PH_TO_BIO = {
PLACEHOLDER_TO_BIO: Dict[str, str] = {
"NOM": "PER",
"TEL": "TEL",
"EMAIL": "EMAIL",
@@ -41,78 +36,178 @@ PH_TO_BIO = {
"ADRESSE": "ADRESSE",
"CODE_POSTAL": "ZIP",
"VILLE": "VILLE",
"ETAB": "HOPITAL",
"FINESS": "HOPITAL",
"HOPITAL": "HOPITAL",
"MASK": "HOPITAL", # [MASK] = hôpital masqué par force_regex
"IBAN": "IBAN",
"AGE": "AGE",
"OGC": "NDA",
"MASK": "O", # MASK générique = pas d'annotation spécifique
}
RE_PLACEHOLDER = re.compile(r"^\[([A-Z_]+)\]$")
def text_to_bio(pseudonymised_text: str) -> List[Tuple[str, str]]:
"""Convertit un texte pseudonymisé en séquence BIO.
SRC = Path("/home/dom/Téléchargements/II-1 Ctrl_T2A_2025_CHCB_DocJustificatifs (1)")
AUDIT_DIR = SRC / "anonymise_audit_30"
Les tokens [PLACEHOLDER] deviennent B-TYPE / I-TYPE.
Les tokens normaux deviennent O.
def extract_original_text(pdf_path: Path) -> str:
"""Extrait le texte brut d'un PDF (même méthode que le pipeline)."""
import anonymizer_core_refactored_onnx as core
pages_text, _, _, _ = core.extract_text_with_fallback_ocr(pdf_path)
return "\f".join(pages_text)
def tokenize_text(text: str) -> List[str]:
"""Split en tokens whitespace, en nettoyant les caractères de contrôle."""
# Remplacer \f et \r par \n pour l'alignement
text = text.replace("\f", "\n").replace("\r", "")
tokens = []
for line in text.split("\n"):
line_toks = line.split()
if line_toks:
tokens.extend(line_toks)
return tokens
def align_and_annotate(original_text: str, pseudo_text: str) -> List[Tuple[str, str]]:
"""Aligne texte original et pseudonymisé pour créer les annotations BIO.
Utilise SequenceMatcher pour trouver les différences.
Quand le pseudo contient [PLACEHOLDER], les tokens originaux correspondants
reçoivent le label BIO approprié.
"""
orig_tokens = tokenize_text(original_text)
pseudo_tokens = tokenize_text(pseudo_text)
# Normaliser pour l'alignement (lowercase, sans accents pour meilleur matching)
def normalize(tok):
return tok.lower().strip(".,;:!?()[]{}\"'")
orig_norm = [normalize(t) for t in orig_tokens]
pseudo_norm = [normalize(t) for t in pseudo_tokens]
sm = difflib.SequenceMatcher(None, orig_norm, pseudo_norm, autojunk=False)
opcodes = sm.get_opcodes()
bio_tokens: List[Tuple[str, str]] = []
# Split le texte en segments : alternance texte normal / placeholder
parts = PLACEHOLDER_RE.split(pseudonymised_text)
# parts = [texte, label, texte, label, texte, ...]
for tag, i1, i2, j1, j2 in opcodes:
if tag == "equal":
# Tokens identiques → O
for t in orig_tokens[i1:i2]:
bio_tokens.append((t, "O"))
i = 0
while i < len(parts):
if i % 2 == 0:
# Texte normal
text_part = parts[i]
for word in text_part.split():
word = word.strip()
if word:
bio_tokens.append((word, "O"))
elif tag == "replace":
# Analyser le côté pseudo : quels tokens sont des placeholders ?
pseudo_chunk = pseudo_tokens[j1:j2]
placeholder_labels = [] # (index_in_pseudo, bio_label) pour chaque placeholder
non_placeholder_norms = set()
for pi, pt in enumerate(pseudo_chunk):
m = RE_PLACEHOLDER.match(pt)
if m:
bio_label = PLACEHOLDER_TO_BIO.get(m.group(1))
if bio_label:
placeholder_labels.append((pi, bio_label))
else:
# Label de placeholder
label = parts[i]
bio_label = PH_TO_BIO.get(label, "O")
if bio_label != "O":
# Le placeholder remplace un ou plusieurs tokens
bio_tokens.append((f"[{label}]", f"B-{bio_label}"))
non_placeholder_norms.add(normalize(pt))
if not placeholder_labels:
# Pas de placeholder → O
for t in orig_tokens[i1:i2]:
bio_tokens.append((t, "O"))
elif len(placeholder_labels) == 1:
# Un seul placeholder : tous les tokens originaux (sauf ceux
# qui matchent un token non-placeholder du pseudo) prennent ce label
label = placeholder_labels[0][1]
first = True
for t in orig_tokens[i1:i2]:
if normalize(t) in non_placeholder_norms:
bio_tokens.append((t, "O"))
first = True
else:
bio_tokens.append((f"[{label}]", "O"))
i += 1
prefix = "B-" if first else "I-"
bio_tokens.append((t, f"{prefix}{label}"))
first = False
else:
# Plusieurs placeholders : distribuer les tokens originaux
# Stratégie : répartir proportionnellement, chaque groupe commence par B-
n_orig = i2 - i1
n_placeholders = len(placeholder_labels)
# Exclure d'abord les tokens qui matchent des non-placeholders
orig_assignments = []
for t in orig_tokens[i1:i2]:
if normalize(t) in non_placeholder_norms:
orig_assignments.append(("O", None))
else:
orig_assignments.append(("PII", None))
# Distribuer les tokens PII entre les placeholders
pii_indices = [k for k, (tp, _) in enumerate(orig_assignments) if tp == "PII"]
n_pii = len(pii_indices)
if n_pii > 0 and n_placeholders > 0:
chunk_size = max(1, n_pii // n_placeholders)
for pi_idx, (_, label) in enumerate(placeholder_labels):
start_pii = pi_idx * chunk_size
end_pii = (pi_idx + 1) * chunk_size if pi_idx < n_placeholders - 1 else n_pii
for k in range(start_pii, min(end_pii, n_pii)):
orig_assignments[pii_indices[k]] = ("PII", label)
# Générer les BIO tokens
prev_label = None
for k, (t, (tp, label)) in enumerate(zip(orig_tokens[i1:i2], orig_assignments)):
if tp == "O" or label is None:
bio_tokens.append((t, "O"))
prev_label = None
else:
prefix = "B-" if label != prev_label else "I-"
bio_tokens.append((t, f"{prefix}{label}"))
prev_label = label
elif tag == "delete":
# Tokens présents uniquement dans l'original → O
for t in orig_tokens[i1:i2]:
bio_tokens.append((t, "O"))
elif tag == "insert":
# Tokens ajoutés dans le pseudo (rare) → ignorer
pass
return bio_tokens
def export_document(pseudo_path: Path, out_dir: Path) -> int:
"""Exporte un fichier pseudonymisé en format BIO. Retourne le nombre de tokens."""
text = pseudo_path.read_text(encoding="utf-8", errors="replace")
def export_document(pdf_path: Path, pseudo_path: Path, out_dir: Path) -> Tuple[int, int]:
"""Exporte un document en format BIO. Retourne (nb_tokens, nb_entités)."""
# Extraire le texte original
original_text = extract_original_text(pdf_path)
if not original_text.strip():
return 0, 0
bio_tokens = text_to_bio(text)
if not bio_tokens:
return 0
# Lire le texte pseudonymisé
pseudo_text = pseudo_path.read_text(encoding="utf-8")
if not pseudo_text.strip():
return 0, 0
# Écrire en format CoNLL (TOKEN\tLABEL)
out_path = out_dir / pseudo_path.name.replace(".pseudonymise.txt", ".bio")
# Aligner et annoter
bio_tokens = align_and_annotate(original_text, pseudo_text)
# Écrire en format CoNLL
out_name = pdf_path.stem + ".bio"
out_path = out_dir / out_name
lines = []
for token, label in bio_tokens:
# Séparer les "phrases" par des lignes vides (heuristique: point final ou retour ligne)
# Séparer les phrases par des lignes vides (ponctuation finale)
if token in (".", "!", "?") and label == "O":
lines.append(f"{token}\t{label}")
lines.append("") # séparateur de phrase
lines.append("")
else:
lines.append(f"{token}\t{label}")
out_path.write_text("\n".join(lines), encoding="utf-8")
return len(bio_tokens)
n_ents = sum(1 for _, l in bio_tokens if l.startswith("B-"))
return len(bio_tokens), n_ents
def main():
parser = argparse.ArgumentParser(description="Export silver annotations BIO")
parser.add_argument("--input-dir", type=Path,
default=Path("/home/dom/Téléchargements/II-1 Ctrl_T2A_2025_CHCB_DocJustificatifs (1)/anonymise_audit_30"),
help="Répertoire contenant les .pseudonymise.txt")
parser = argparse.ArgumentParser(description="Export silver annotations BIO (alignement original ↔ pseudo)")
parser.add_argument("--out-dir", type=Path,
default=Path(__file__).parent.parent / "data" / "silver_annotations",
help="Répertoire de sortie")
@@ -121,23 +216,34 @@ def main():
args.out_dir.mkdir(parents=True, exist_ok=True)
pseudo_files = sorted(args.input_dir.glob("*.pseudonymise.txt"))
if args.limit > 0:
pseudo_files = pseudo_files[:args.limit]
# Trouver les paires PDF + pseudo
pseudo_files = sorted(AUDIT_DIR.glob("*.pseudonymise.txt"))
pairs = []
for pseudo_path in pseudo_files:
# Retrouver le PDF source
base_name = pseudo_path.name.replace(".pseudonymise.txt", ".pdf")
# Chercher dans les sous-dossiers OGC
found = list(SRC.glob(f"*/{base_name}"))
if found:
pairs.append((found[0], pseudo_path))
print(f"Export silver annotations: {len(pseudo_files)} fichiers → {args.out_dir}")
if args.limit > 0:
pairs = pairs[:args.limit]
print(f"Export silver annotations: {len(pairs)} documents → {args.out_dir}")
total_tokens = 0
total_entities = 0
for f in pseudo_files:
n = export_document(f, args.out_dir)
ent_count = sum(1 for line in (args.out_dir / f.name.replace(".pseudonymise.txt", ".bio")).read_text().splitlines()
if line and not line.endswith("\tO"))
total_tokens += n
total_entities += ent_count
print(f" {f.name}: {n} tokens, {ent_count} entités")
for pdf_path, pseudo_path in pairs:
try:
n_tok, n_ent = export_document(pdf_path, pseudo_path, args.out_dir)
total_tokens += n_tok
total_entities += n_ent
print(f" {pdf_path.name}: {n_tok} tokens, {n_ent} entités")
except Exception as e:
print(f" {pdf_path.name}: ERREUR {e}")
print(f"\nTotal: {total_tokens} tokens, {total_entities} entités annotées")
print(f"\nTotal: {total_tokens} tokens, {total_entities} entités B-")
print(f"Sortie: {args.out_dir}")

View File

@@ -15,8 +15,11 @@ import sys
import argparse
from pathlib import Path
from typing import Dict, List
from collections import Counter
import numpy as np
import torch
from torch import nn
# Vérifier les dépendances
try:
@@ -59,38 +62,60 @@ ID2LABEL = {i: l for l, i in LABEL2ID.items()}
MODEL_NAME = "almanach/camembert-bio-base"
def load_bio_files(data_dir: Path) -> Dict[str, List]:
"""Charge les fichiers .bio en format HuggingFace datasets."""
def load_bio_files(data_dir: Path, window_size: int = 200, stride: int = 100) -> Dict[str, List]:
"""Charge les fichiers .bio et découpe en fenêtres glissantes.
Les documents cliniques sont très longs. On les découpe en fenêtres de
~window_size tokens avec un chevauchement de stride. On ne garde que les
fenêtres contenant au moins une entité (pour l'équilibre des classes).
"""
tokens_list: List[List[str]] = []
labels_list: List[List[int]] = []
for bio_file in sorted(data_dir.glob("*.bio")):
text = bio_file.read_text(encoding="utf-8")
current_tokens: List[str] = []
current_labels: List[int] = []
# Charger tous les tokens du document
all_tokens: List[str] = []
all_labels: List[int] = []
for line in text.splitlines():
line = line.strip()
if not line:
# Fin de phrase
if current_tokens:
tokens_list.append(current_tokens)
labels_list.append(current_labels)
current_tokens = []
current_labels = []
continue
parts = line.split("\t")
if len(parts) != 2:
continue
token, label = parts
label_id = LABEL2ID.get(label, LABEL2ID["O"])
current_tokens.append(token)
current_labels.append(label_id)
all_tokens.append(token)
all_labels.append(label_id)
if current_tokens:
tokens_list.append(current_tokens)
labels_list.append(current_labels)
if not all_tokens:
continue
# Découper en fenêtres glissantes
n = len(all_tokens)
for start in range(0, n, stride):
end = min(start + window_size, n)
chunk_tokens = all_tokens[start:end]
chunk_labels = all_labels[start:end]
# Corriger les I- en début de fenêtre → B-
if chunk_labels and chunk_labels[0] > 0:
lbl_name = LABEL_LIST[chunk_labels[0]]
if lbl_name.startswith("I-"):
b_name = "B-" + lbl_name[2:]
if b_name in LABEL2ID:
chunk_labels[0] = LABEL2ID[b_name]
# Garder les fenêtres avec entités + quelques fenêtres "O" (10%)
has_entities = any(l != 0 for l in chunk_labels)
if has_entities or (start % (stride * 10) == 0):
tokens_list.append(chunk_tokens)
labels_list.append(chunk_labels)
if end >= n:
break
return {"tokens": tokens_list, "ner_tags": labels_list}
@@ -131,6 +156,59 @@ def tokenize_and_align(examples, tokenizer):
return tokenized
class WeightedNERTrainer(Trainer):
"""Trainer avec poids de classe pour contrer le déséquilibre O vs entités."""
def __init__(self, class_weights=None, **kwargs):
super().__init__(**kwargs)
if class_weights is not None:
self.class_weights = class_weights.to(self.args.device)
else:
self.class_weights = None
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
labels = inputs.pop("labels")
outputs = model(**inputs)
logits = outputs.logits
if self.class_weights is not None:
loss_fct = nn.CrossEntropyLoss(
weight=self.class_weights,
ignore_index=-100,
label_smoothing=0.1,
)
else:
loss_fct = nn.CrossEntropyLoss(ignore_index=-100, label_smoothing=0.1)
loss = loss_fct(logits.view(-1, self.model.config.num_labels), labels.view(-1))
return (loss, outputs) if return_outputs else loss
def compute_class_weights(raw_data: Dict, num_labels: int, max_weight: float = 10.0) -> torch.FloatTensor:
"""Calcule les poids inversement proportionnels à la fréquence, cappés après normalisation."""
counts = Counter()
for labels in raw_data["ner_tags"]:
for l in labels:
counts[l] += 1
total = sum(counts.values())
weights = torch.ones(num_labels)
for label_id, count in counts.items():
if count > 0:
weights[label_id] = total / (num_labels * count)
# Normaliser : O=1.0
if weights[0] > 0:
scale = 1.0 / weights[0]
weights *= scale
# Capper APRÈS normalisation pour limiter le déséquilibre
weights = torch.clamp(weights, max=max_weight)
print(f" Class weights (O={weights[0]:.1f}, non-O moyen={weights[1:].mean():.1f}, max={weights[1:].max():.1f})")
return weights
def main():
parser = argparse.ArgumentParser(description="Fine-tune CamemBERT-bio pour désidentification")
parser.add_argument("--data-dir", type=Path,
@@ -203,6 +281,10 @@ def main():
"f1": results["overall_f1"],
}
# Class weights pour contrer le déséquilibre 97% O
print("\nCalcul des poids de classe...")
weights = compute_class_weights(raw_data, len(LABEL_LIST))
# Training
args.output_dir.mkdir(parents=True, exist_ok=True)
training_args = TrainingArguments(
@@ -218,13 +300,14 @@ def main():
load_best_model_at_end=True,
metric_for_best_model="f1",
logging_steps=50,
fp16=False, # CPU training
fp16=True, # GPU training avec mixed precision
report_to="none",
save_total_limit=2,
)
data_collator = DataCollatorForTokenClassification(tokenizer)
trainer = Trainer(
trainer = WeightedNERTrainer(
class_weights=weights,
model=model,
args=training_args,
train_dataset=tokenized["train"],