AnonyNER v3.15 : transformer, diagnostic FN et 7 points de F1 gagnés
Suite directe du bilan v3.11. Ce cycle a trois étapes : migration de l’architecture tok2vec vers un transformer, diagnostic systématique des faux négatifs par label, puis correction ciblée des causes racines identifiées. Résultat : F1 global 88% → 95,9%.
Contexte v3.12→v3.14 — Entre v3.11 et v3.14, trois cycles d’amélioration ont produit des résultats décevants : ajout de 500 exemples synthétiques EC2 pour HOSTNAME (+2 points de recall), tentatives de rééquilibrage des labels PID et MAC_ADDRESS. La migration vers l’architecture transformer (v3.14) a apporté un gain net de +4 points de F1 global, mais le recall HOSTNAME restait bloqué à 59%. C’est ce plafond persistant qui a déclenché le diagnostic décrit ici.
Étape 1 — Migration vers spaCy Transformers
L’article v3.11 concluait : “tok2vec atteint son plafond naturel.” À F1=88%, le modèle ne progressait plus malgré les ajouts de corpus. Le passage à RoBERTa s’est fait en changeant deux paramètres dans la config spaCy :
# Avant (tok2vec, efficiency)
python training/scripts/train_anonyner.py
# Après (RoBERTa, accuracy)
python training/scripts/train_anonyner.py --transformer
Le script train_anonyner.py génère la config appropriée selon le mode. En mode transformer, spaCy utilise roberta-base via spacy-transformers, avec un learning rate warmup et un scheduler cosine — les hyperparamètres par défaut de spaCy pour ce mode sont bien calibrés, aucun réglage manuel nécessaire.
Résultats de la migration (corpus v3.14, ~20 800 exemples) :
F1 : 91.9% Precision : 92.8% Recall : 91.1%
Soit +3.9 points de F1 par rapport au tok2vec. La migration paie, mais le recall HOSTNAME reste anormalement bas.
Étape 2 — Diagnostic systématique des faux négatifs
Un script générique d’analyse des faux négatifs par label a été développé. Pour chaque entité gold manquée par le modèle, il affiche ce que le modèle a prédit à la place et le contexte environnant :
python training/scripts/analyze_fn.py --label HOSTNAME
Total FN HOSTNAME : 208
Labels prédits à la place de HOSTNAME :
IP_ADDRESS 175 (84.1%)
— 33 (15.9%)
── Prédit comme IP_ADDRESS (175 cas) ──
175× 'ip-10-77-20-248'
── Exemples de contexte ──────────────────────────────────────
gold='ip-10-77-20-248' prédit=IP_ADDRESS
Mar 5 12:01:32 «ip-10-77-20-248»[→IP_ADDRESS] sshd[1234]: ...
Lecture immédiate : 84% des HOSTNAME manqués sont une seule valeur — ip-10-77-20-248 — systématiquement prédit comme IP_ADDRESS. Le modèle n’a pas un problème de recall HOSTNAME en général, il a un problème avec cette chaîne spécifique.
Étape 3 — Diagnostic de corpus : le mislabel à 2 350 entrées
Un audit du corpus a révélé la cause racine :
ip-10-77-20-248 : 2 350× IP_ADDRESS + 1 318× HOSTNAME
La même chaîne est annotée IP_ADDRESS dans 64% des cas et HOSTNAME dans 36%. Ce n’est pas une ambiguïté métier — c’est un bug d’annotation. Le script d’auto-annotation utilisait la forme lexicale (ip-X-X-X-X ressemble à une IP) sans tenir compte de la position dans le log.
Le format syslog est non ambigu :
<Mois> <Jour> <HH:MM:SS> <hostname> <service>[<pid>]: <message>
Dans ce contexte, ip-10-77-20-248 est un hostname EC2 AWS en position syslog prefix — pas une adresse IP. Ce sont des noms de machines AWS en DNS inverse (PTR records) : ip-10-77-20-248.eu-west-1.compute.internal.
Effet sur l’apprentissage : le modèle reçoit des gradients contradictoires pour la même chaîne dans le même contexte. Avec 64% de signal IP_ADDRESS, il apprend à ignorer la position et à se fier à la forme — exactement l’inverse de ce qu’on veut.
Correction : diversify_hostname_corpus.py
Le script de correction fait deux choses :
- Relabellise les
IP_ADDRESS→HOSTNAMEquand la chaîne est en position syslog prefix (détection par regex) - Substitue toutes les occurrences
HOSTNAMEdeip-10-77-20-248par des valeurs variées d’un pool EC2 (18 variantes), forçant le modèle à apprendre sur la position plutôt que sur la valeur
Résultats dry-run :
2 350 mislabels corrigés (IP_ADDRESS → HOSTNAME)
3 534 valeurs substituées (ip-10-77-20-248 → pool EC2)
168 valeurs conservées (tirage aléatoire identique)
Le corpus résultant (v3.15) contient 21 844 enregistrements.
Résultats après correction corpus
L’entraînement sur corpus v3.15 — avec les 2 350 mislabels corrigés et les valeurs EC2 diversifiées — donne des résultats immédiats :
F1 : 95.3% Precision : 94.8% Recall : 95.4%
Le score HOSTNAME seul :
| Métrique | v3.14 (tok2vec) | v3 transformer | v3.15 (fix) |
|---|---|---|---|
| Precision | 61.2% | ~85% | 99.1% |
| Recall | 52.5% | 59.5% | 98.2% |
| F1 | 56.5% | 71.7% | 98.6% |
| FN | ~180 | 208 | 19 |
Le recall HOSTNAME passe de 52.5% à 98.2% en corrigeant une seule source d’erreur dans le corpus. Le transformer ne pouvait pas apprendre correctement contre 2 350 mislabels contradictoires — les 500 exemples synthétiques ajoutés entre-temps ne représentaient que 15% du signal, insuffisant pour contre-balancer.
Étape 4 — COMMAND_LINE : données synthétiques ciblées
L’analyse FN sur COMMAND_LINE donnait :
Total FN COMMAND_LINE : 21 (100% non détectés)
14/21 = chpasswd[pid] format syslog service
2/21 = User-Agent Mozilla/5.0
1/21 = C:\windows\...\powershell.exe
1/21 = /ausearch
Cause : seulement 200 exemples COMMAND_LINE dans le corpus, sur 4 patterns très différents. Le pattern dominant des FN — chpasswd[31100] en position syslog service — est structurellement identique au label PROCESS_NAME (sshd[1234]). Le modèle ne différencie pas les deux sans exemples explicites.
Un générateur synthétique ciblé (generate_command_line_contexts.py) produit 500 exemples répartis sur les 4 familles manquantes :
| Générateur | Exemples | Pattern |
|---|---|---|
gen_syslog_cmd_service |
250 | chpasswd[pid]:, passwd[pid]:, useradd[pid]: en position service |
gen_unix_commands |
120 | Commandes Unix dans logs audit/cron/sudo |
gen_windows_cmd |
80 | C:\windows\...\powershell.exe dans EventLog |
gen_useragent |
50 | Mozilla/5.0 ... dans logs web/proxy |
Résultats sur corpus v3.16 (v3.15 + 500 exemples COMMAND_LINE) :
F1 : 95.9% Precision : 94.9% Recall : 96.9%
COMMAND_LINE : F1 25.9% → 86.4%, Recall 16.7% → 83.1%.
Effet secondaire positif : les templates Windows incluaient des annotations EVENT_ID et PID, ce qui a dopé ces labels sans ciblage explicite (EVENT_ID 0% → 78.6%, PID 55% → 94.3%).
Tableau de bord final — v3.16
| Label | P | R | F1 | vs v3.11 |
|---|---|---|---|---|
IP_ADDRESS |
99.0% | 99.9% | 99.4% | +11.9 |
HOSTNAME |
99.0% | 98.8% | 98.9% | +42.4 |
REGISTRY_KEY |
99.3% | 99.7% | 99.5% | +0.1 |
WIN_USER |
98.2% | 100.0% | 99.1% | +1.0 |
UNIX_USER |
98.8% | 99.2% | 99.0% | +2.1 |
DOMAIN |
98.2% | 96.8% | 97.5% | +1.8 |
CVE |
100.0% | 100.0% | 100.0% | +4.2 |
PROCESS_NAME |
98.6% | 96.9% | 97.7% | +4.8 |
FILE_PATH |
90.1% | 96.8% | 93.3% | +2.3 |
COMMAND_LINE |
90.1% | 83.1% | 86.4% | +60.5 |
PORT_NUMBER |
88.8% | 94.1% | 91.4% | +23.9 |
EVENT_ID |
68.8% | 91.7% | 78.6% | +78.6 |
PID |
89.2% | 100.0% | 94.3% | +39.3 |
URL_URI |
80.0% | 81.8% | 80.9% | +4.4 |
MAC_ADDRESS |
71.9% | 82.1% | 76.7% | +5.9 |
FILE_HASH |
33.3% | 66.7% | 44.4% | +15.8 |
SCHEDULED_TASK |
50.0% | 33.3% | 40.0% | +40.0 |
| GLOBAL | 94.9% | 96.9% | 95.9% | +7.9 |
Ce que ce cycle enseigne
Le diagnostic FN est plus utile que l’ajout aveugle de données. Trois cycles d’ajout de données synthétiques sur HOSTNAME (v3.13, v3.14) ont produit des gains marginaux (+1-2%). Un seul audit de corpus a produit +42 points de F1. La question à poser avant d’ajouter des données n’est pas “combien en manque-t-il ?” mais “pourquoi le modèle se trompe-t-il ?”
Les mislabels contradictoires sont des poisons lents. 2 350 entrées mal étiquetées dans un corpus de 20 000 (11.7%) ont suffi à plafonner le recall HOSTNAME à 60% malgré un transformer. Le signal contradictoire force le modèle à moyenner les gradients — il apprend à hésiter plutôt qu’à décider.
Les données synthétiques ciblées fonctionnent pour les labels structurels. Pour COMMAND_LINE, 500 exemples générés par template ont réparé un F1 à 25.9%. La condition : cibler le pattern exact qui échoue (position syslog service pour chpasswd[pid]), pas le label en général.
Le transformer ne compense pas un corpus corrompu. Passer de tok2vec à RoBERTa a apporté +4 points de F1. Corriger les mislabels HOSTNAME a apporté +42 points sur ce label seul. L’architecture compte, mais les données comptent davantage.
Prochaines priorités
| Label | F1 actuel | FN | Action |
|---|---|---|---|
URL_URI |
80.9% | 66 | Données synthétiques Apache/nginx avec chemins diversifiés |
COMMAND_LINE |
86.4% | 24 | Analyse FN résiduelle — patterns encore manqués |
MAC_ADDRESS |
76.7% | 5 | Logs ARP/DHCP supplémentaires |
FILE_HASH |
44.4% | 1 | Trop peu d’exemples dev — vérifier le corpus |
SCHEDULED_TASK |
40.0% | 2 | Logs schtasks.exe / Event 7045 |
L’entraînement v3.17 — corpus v3.16 + 500 exemples URL_URI synthétiques — est en cours.
Scripts disponibles dans Victor/training/scripts/ : eval_anonyner.py, analyze_fn.py, diversify_hostname_corpus.py, generate_hostname_contexts.py, generate_command_line_contexts.py, generate_url_uri_contexts.py.
