ML Detection Iteration Log — 2026-03-17

Status: Experiment Audience: Team, AI agents Purpose: Historical detailed iteration log for the feat/ml-detection-improvement branch

English summary

This is the longest historical log in the folder. It documents:

  • the first niamoto-score baseline

  • the diagnosis of weak holdout families such as forest_inventory

  • early changes to value features and fusion features

  • the move toward a more product-oriented benchmark

  • the first surrogate/autoresearch decisions

  • the first acquisition lots and their effect on the benchmark

The detailed body below is preserved as historical working material and still contains French wording from the original session notes.

Objet

Ce journal capture l’état des expérimentations réalisées sur la branche feat/ml-detection-improvement, avec focus sur :

  • le nouveau protocole niamoto-score

  • les points faibles révélés par les holdouts

  • les corrections tentées

  • les décisions prises pour les itérations suivantes

Ce document est daté et doit pouvoir être complété par de nouvelles entrées au fil de l’expérimentation.

Contexte de départ

La branche a migré d’une logique historique plus monolithique vers un pipeline hybride :

  • alias exacts

  • modèle header

  • modèle values

  • modèle de fusion

  • projection vers semantic_profile et affordances

En parallèle, le protocole d’évaluation a été renforcé :

  • suppression de la fuite de données dans l’évaluation fusion

  • réalignement progressif train/runtime

  • ajout du NiamotoOfflineScore

  • ajout de holdouts langue, famille et anonymous

  • ajout d’un logging de progression pour les runs longues

Mesure de référence observée

Run complet niamoto-score observé avant les itérations ciblées de ce journal :

NiamotoOfflineScore = 78.8351

Sous-scores et holdouts marquants :

  • primary : 78.835

  • holdout_lang[fr] : 67.491

  • holdout_lang[es] : 88.599

  • holdout_lang[de] : 88.949

  • holdout_lang[zh] : 100.000

  • holdout_family[dwc_gbif] : 71.407

  • holdout_family[forest_inventory] : 47.918

  • holdout_family[research_traits] : 66.891

  • holdout_family[tropical_field] : 54.146

  • holdout_anonymous : 94.583

  • diagnostic_synthetic : 67.388

Lecture initiale

Le signal principal n’est pas un problème global de colonne anonyme. Au contraire, le holdout anonymous est fort.

Les faiblesses se concentrent sur :

  • certaines familles de datasets réels ;

  • en particulier forest_inventory ;

  • puis tropical_field ;

  • et, dans une moindre mesure, fr.

Conclusion provisoire :

  • la stack sait bien exploiter les patterns forts et les headers lisibles ;

  • elle généralise encore mal sur certains domaines métier ;

  • le problème semble plus domain-specific que purement linguistique.

Diagnostic ciblé sur forest_inventory

Un diagnostic dédié a été exécuté sur le holdout forest_inventory.

Mesure ciblée de référence :

  • forest_role_f1 = 0.2769

  • forest_concept_f1 = 0.2008

Constat principal

La fusion suivait trop souvent la branche values sur des erreurs systématiques.

Distribution des sources d’erreur observées :

  • 89 erreurs followed_value_wrong

  • 32 erreurs followed_header_wrong

  • 27 erreurs header_right_value_wrong

  • 12 erreurs both_wrong_same

Lecture :

  • la branche values dominait trop sur ce domaine ;

  • elle poussait des concepts inadaptés sur des colonnes codées métier ;

  • la fusion ne corrigeait pas suffisamment ce biais.

Confusions saillantes

Confusions de rôle :

  • category -> statistic

  • identifier -> statistic

  • category -> location

  • measurement -> statistic

Confusions de concepts :

  • location.admin_area -> event.date

  • identifier.plot -> statistic.count

  • category.tree_condition -> statistic.count

  • measurement.cover -> statistic.count

  • identifier.record -> statistic.count

Exemples d’erreurs à haute confiance

Exemples observés :

  • TCL prédit statistic.count au lieu de measurement.cover

  • TCA prédit statistic.count au lieu de measurement.cover

  • PEUPNR prédit statistic.count au lieu de category.management

  • AGENTCD prédit statistic.count au lieu de category.damage

  • COUNTYCD prédit statistic.count au lieu de location.admin_area

Lecture :

  • les colonnes codées d’inventaire forestier étaient mal reconnues ;

  • le système les interprétait trop souvent comme des comptes ou des mesures simples.

Hypothèse de travail

Hypothèse centrale de cette itération :

Le principal défaut sur forest_inventory n’est pas l’absence totale de signal, mais une sur-dominance de la branche values sur des colonnes codées métier, combinée à une fusion trop permissive.

Changements tentés

1. Factorisation des features values

Objectif :

  • supprimer le mismatch train/runtime sur les features values ;

  • disposer d’un point unique pour enrichir les signaux utiles aux colonnes codées.

Changements :

  • création de src/niamoto/core/imports/ml/value_features.py

  • réutilisation depuis :

    • ml/scripts/train/train_value_model.py

    • src/niamoto/core/imports/ml/classifier.py

Nouveaux signaux ajoutés :

  • n_unique_values

  • dominant_ratio

  • fixed_length_ratio

  • short_code_ratio

  • dense_integer_domain

  • tiny_integer_domain

2. Enrichissement des meta-features de fusion

Objectif :

  • rendre la fusion plus sensible au désaccord header/values ;

  • détecter les cas où values pousse statistic.count sur un header codé.

Changements :

  • création de src/niamoto/core/imports/ml/fusion_features.py

  • ajout dans la fusion de :

    • max proba

    • marge top1-top2

    • entropie

    • accord/désaccord

    • flags statistic.count

    • détection de header code-like

3. Heuristique ciblée de damping

Une première tentative d’injecter l’information “coded header” dans le modèle header lui-même a été testée puis retirée, car elle dégradait forest_role_f1.

La correction conservée est plus ciblée :

  • si un header est manifestement code-like ;

  • et si une branche pousse fortement statistic.count ;

  • alors la confiance statistic.count est amortie avant la fusion.

But :

  • corriger un faux positif fréquent ;

  • sans reconfigurer agressivement le modèle header.

Résultats ciblés après itération

Baseline ciblée

  • forest_role_f1 = 0.2769

  • forest_concept_f1 = 0.2008

Après factorisation values + meta-features de fusion + damping

  • forest_role_f1 = 0.2960

  • forest_concept_f1 = 0.1938

Interprétation

Le signal obtenu est cohérent avec l’objectif produit :

  • amélioration du rôle sur le domaine difficile ;

  • pas encore d’amélioration sur le concept fin.

À ce stade, l’itération semble surtout améliorer le comportement produit pertinent, pas la précision académique du concept.

Validation globale encore requise :

  • rerun complet niamoto-score

  • vérification qu’on n’améliore pas forest_inventory en cassant d’autres familles

Validation globale de l’itération

Le rerun complet niamoto-score a été exécuté après cette itération.

Résultat final :

NiamotoOfflineScore = 79.1135

Comparaison avec la baseline observée avant l’itération :

  • baseline : 78.8351

  • itération : 79.1135

  • delta global : +0.2784

Sous-scores et holdouts après itération

  • primary : 79.113

  • holdout_lang[fr] : 66.465

  • holdout_lang[es] : 91.329

  • holdout_lang[de] : 92.380

  • holdout_lang[zh] : 99.444

  • holdout_family[dwc_gbif] : 68.267

  • holdout_family[forest_inventory] : 41.654

  • holdout_family[research_traits] : 65.081

  • holdout_family[tropical_field] : 57.131

  • holdout_anonymous : 100.000

  • diagnostic_synthetic : 71.785

Ce qui s’améliore

  • primary monte légèrement

  • es et de montent nettement

  • tropical_field monte

  • anonymous devient excellent

  • forest_inventory améliore bien son role_f1

Ce qui se dégrade

  • fr baisse légèrement

  • dwc_gbif baisse nettement

  • research_traits baisse

  • forest_inventory baisse fortement en score global

Point critique sur forest_inventory

Le résultat forest_inventory après itération est particulièrement important :

  • NiamotoOfflineScore = 41.654

  • role = 0.296

  • critical = 0.533

  • pairs = 0.390

  • confQ = 0.500

  • hc_err = 0.500

Lecture :

  • le rôle s’améliore bien par rapport à la baseline ciblée ;

  • mais la confiance, la cohérence dataset-level et les erreurs à haute confiance se dégradent trop fortement ;

  • le mécanisme actuel améliore un aspect local mais abîme trop le comportement global sur ce garde-fou critique.

Verdict sur l’itération

Verdict retenu :

itération non validée comme nouveau baseline de la stack

Raison :

  • le gain global est trop faible ;

  • il est obtenu au prix d’une dégradation trop forte sur forest_inventory ;

  • la baisse sur dwc_gbif et research_traits rend l’amélioration globale peu défendable ;

  • l’hypothèse était bonne, mais l’implémentation du correctif reste trop agressive ou mal calibrée.

Ce qui est conservé de cette itération :

  • le diagnostic détaillé ;

  • la compréhension du biais values -> statistic.count ;

  • la nécessité de traiter forest_inventory comme garde-fou explicite.

Décisions prises

  • conserver la factorisation shared des features values ;

  • conserver les meta-features de fusion ;

  • ne pas promouvoir automatiquement le damping ciblé actuel comme nouveau baseline sans nouvelle itération plus prudente ;

  • ne pas conserver l’injection directe de coded dans le texte header ;

  • utiliser forest_inventory comme garde-fou explicite dans les prochaines décisions d’autoresearch.

Ce que cette itération nous apprend

  1. Le vrai point faible n’est pas d’abord la langue française.

  2. Le vrai point faible est le comportement sur certains domaines métier, notamment l’inventaire forestier.

  3. Les gains les plus utiles passent par :

    • une meilleure fusion ;

    • une meilleure prudence ;

    • des signaux plus explicites sur les colonnes codées ;

    • des garde-fous orientés domaine.

Prochaines itérations recommandées

Court terme

  • finir la validation complète niamoto-score

  • comparer avant/après sur tous les holdouts

  • décider si l’itération est gardée au niveau stack complet

Si le gain global tient

  • ajouter un reporting dédié sur les prédictions -> statistic.count

  • intégrer forest_inventory comme garde-fou autoresearch

  • attaquer ensuite tropical_field

Si le gain global ne tient pas

  • conserver l’analyse ;

  • réduire la portée de l’heuristique ;

  • ou déplacer la logique vers une fusion plus structurée au lieu d’un damping simple

Évolution décidée du benchmark

Après cette run, il a été décidé de faire évoluer le benchmark avant une nouvelle itération ML.

Objectif :

  • rendre les diagnostics plus honnêtes ;

  • mieux séparer anglais standard et anglais métier ;

  • éviter de surinterpréter des splits faciles comme zh.

Changements ajoutés au reporting de ml/scripts/eval/evaluate.py :

  • diagnostic_lang[zh] comme diagnostic secondaire et non plus holdout stratégique

  • diagnostic_subset[en_standard]

  • diagnostic_subset[en_field]

  • diagnostic_subset[coded_headers]

  • diagnostic_subset[gbif_core_standard]

  • diagnostic_subset[gbif_extended]

  • split interne de forest_inventory :

    • forest_inventory/ifn_fr

    • forest_inventory/fia_en

    • forest_inventory/nordic_inventory

  • split interne de dwc_gbif :

    • dwc_gbif/core_standard

    • dwc_gbif/extended

Décision associée :

  • ne pas changer immédiatement la formule du NiamotoOfflineScore

  • changer d’abord le reporting et les garde-fous

  • relancer ensuite une run complète pour identifier le vrai point faible : IFN, FIA, GBIF standard, GBIF étendu, en_field, ou headers codés

Commandes utiles

Validation complète avec progression :

OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 VECLIB_MAXIMUM_THREADS=1 NUMEXPR_NUM_THREADS=1 LOKY_MAX_CPU_COUNT=1 .venv/bin/python -m ml.scripts.eval.evaluate --model all --metric niamoto-score --splits 3 --verbose-progress

Boucle silencieuse compatible autoresearch :

.venv/bin/python -m ml.scripts.eval.evaluate --model all --metric niamoto-score --splits 3 --quiet

Run recommandée après évolution du benchmark :

OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 VECLIB_MAXIMUM_THREADS=1 NUMEXPR_NUM_THREADS=1 LOKY_MAX_CPU_COUNT=1 .venv/bin/python -m ml.scripts.eval.evaluate --model all --metric niamoto-score --splits 3 --verbose-progress

Lot Guyane 1 — acquisition et intégration

Un premier lot Guyane a été traité pour rapprocher le benchmark des jeux réellement visés côté produit.

Vérification d’accès Dataverse

Jeux inspectés :

  • ForestScan / Paracou

  • Trinite Forest Censuses

  • Tresor Forest Censuses

  • Tibourou Forest Censuses

  • Montagne Tortue Forest Censuses

Résultat vérifié :

  • le CSV principal ForestScan / Paracou est publiquement téléchargeable ;

  • les CSV principaux Trinite, Tresor, Tibourou et Montagne Tortue sont restreints et exposés via fileAccessRequest ;

  • leurs fichiers de description restent publics et ont été récupérés localement comme support de préparation.

Fichiers récupérés

Sous ml/data/silver/guyane/ :

  • paracou/FGPlotsCensusData2023.csv

  • paracou/FGPlotsDescription.csv

  • trinite/TriniteDescription.csv

  • tresor/TresorDescription.csv

  • tibourou/TibourouDescription.csv

  • montagne_tortue/Montagne_TortueDescription.csv

Lecture du schéma

Constats utiles :

  • le census Paracou contient 38 colonnes ;

  • les fichiers de description Guyafor ont un schéma homogène de 53 colonnes ;

  • le census Paracou est immédiatement utile pour le benchmark tropical_field / plot_inventory ;

  • les fichiers de description sont utiles pour préparer les futurs labels, mais pas encore assez sûrs pour être injectés dans le gold set sans risque de bruit et de redondance.

Intégration retenue

Choix fait :

  • intégrer seulement le census Paracou / ForestScan ;

  • ne pas intégrer les fichiers de description ;

  • garder les autres datasets Guyane en attente d’accès à leurs CSV principaux.

Changements réalisés :

  • ajout d’un bloc FORESTSCAN_PARACOU_LABELS dans ml/scripts/data/build_gold_set.py

  • ajout de la source forestscan_paracou_census dans SOURCES

  • régénération du gold set

Résultat :

  • forestscan_paracou_census apporte 34 colonnes gold ;

  • le gold set passe à 2265 colonnes au total, dont 1669 gold et 596 synthetic.

Correction d’outillage connexe

Défaut corrigé dans ml/scripts/data/build_gold_set.py :

  • uv run python -m ml.scripts.data.build_gold_set cassait sur ModuleNotFoundError: scripts

  • le script ajoute maintenant à la fois src/ et la racine du repo au sys.path

  • les deux modes fonctionnent désormais :

    • uv run python -m ml.scripts.data.build_gold_set

    • uv run python -m ml.scripts.data.build_gold_set

Décision

État du lot Guyane 1 :

  • Paracou / ForestScan : intégré

  • Trinite / Tresor / Tibourou / Montagne Tortue : documentés localement mais non intégrés faute d’accès aux CSV census

Suite logique :

  • ajouter ensuite un premier lot GBIF ciblé

  • ou intégrer des datasets d’instances réelles dès qu’ils sont disponibles

Lot GBIF ciblé 1 — régional général

Un premier lot GBIF régional a été récupéré via l’API publique occurrence/search avec les contraintes suivantes :

  • régions : NC, GF, GA, CM

  • règne : Plantae

  • volume cible : 5000 occurrences par région

Sorties créées sous ml/data/silver/gbif_targeted/ :

  • new_caledonia/occurrences.csv

  • guyane/occurrences.csv

  • gabon/occurrences.csv

  • cameroon/occurrences.csv

Volumes récupérés :

  • new_caledonia : 5000 / 442442

  • guyane : 5000 / 299072

  • gabon : 5000 / 583825

  • cameroon : 5000 / 621574

Lecture

Ce lot est utile pour construire un sous-benchmark GBIF régional, mais il est fortement tiré par les jeux observationnels grand public, en particulier des datasets type iNaturalist.

Conclusion :

  • bon lot GBIF régional général

  • mauvais candidat comme unique approximation du GBIF institutionnel

Intégration gold set

Les sources suivantes ont été ajoutées dans ml/scripts/data/build_gold_set.py :

  • gbif_targeted_new_caledonia

  • gbif_targeted_guyane

  • gbif_targeted_gabon

  • gbif_targeted_cameroon

Apport observé :

  • 41 colonnes pour new_caledonia

  • 38 colonnes pour guyane

  • 37 colonnes pour gabon

  • 39 colonnes pour cameroon

Lot GBIF ciblé 2 — institutionnel

Un second flux a ensuite été ajouté pour isoler un sous-corpus plus institutionnel.

Filtre retenu :

  • basisOfRecord in {PRESERVED_SPECIMEN, MATERIAL_SAMPLE, OCCURRENCE}

  • présence d’au moins un champ institutionnel :

    • institutionCode

    • collectionCode

    • institutionID

    • collectionKey

  • exclusion explicite des gros jeux observationnels :

    • iNaturalist

    • observation.org

    • Pl@ntNet

    • eBird

    • quelques autres catalogues analogues

Test de rendement

Un sample institutional a été exécuté sur 5000 enregistrements scannés par région.

Résultat :

  • new_caledonia : 6

  • guyane : 16

  • gabon : 200 (cap atteint)

  • cameroon : 200 (cap atteint)

Lecture :

  • NC et GF ont très peu d’institutionnel dans les premiers résultats régionaux ;

  • GA et CM ont un rendement institutionnel suffisant ;

  • il est pertinent de conserver NC/GF dans le lot général et GA/CM dans un lot institutionnel dédié.

Lot institutionnel retenu

Le lot complet retenu à ce stade est :

  • gabon

  • cameroon

Sorties créées sous ml/data/silver/gbif_targeted_institutional/ :

  • gabon/occurrences.csv

  • cameroon/occurrences.csv

Volumes retenus :

  • gabon : 2000

  • cameroon : 2000

Qualité observée :

  • forte présence de PRESERVED_SPECIMEN

  • jeux type Tropicos

  • signal nettement plus institutionnel que le lot GBIF régional général

Intégration gold set

Les sources suivantes ont été ajoutées dans ml/scripts/data/build_gold_set.py :

  • gbif_targeted_institutional_gabon

  • gbif_targeted_institutional_cameroon

Apport observé :

  • 36 colonnes pour gabon

  • 36 colonnes pour cameroon

Effet cumulé sur le gold set

Après intégration du lot GBIF général puis du lot institutionnel GA/CM :

  • ml/data/gold_set.json passe à 2492 colonnes

  • dont 1896 gold et 596 synthetic

Décision retenue :

  • conserver gbif_targeted/ comme sous-corpus régional général

  • conserver gbif_targeted_institutional/gabon et cameroon comme sous-corpus institutionnel

  • ne pas forcer pour l’instant un scan institutionnel profond sur new_caledonia et guyane

Entrée du 2026-03-18 — run sur gold set enrichi

Avant cette run, les copies de référence locales ont été internalisées dans ml/data/silver/ et ml/scripts/data/build_gold_set.py a été réaligné pour ne plus dépendre de copies hors repo pour :

  • guyadiv_trees

  • guyadiv_plots

  • afrique_occ

  • afrique_plots

  • nc_occ

  • nc_plots

Le gold set reste à ce stade à :

  • 2492 colonnes au total

  • 1896 gold

  • 596 synthetic

Résultat global

Run complète observée :

NiamotoOfflineScore = 78.6199

Comparaison avec la run documentée précédente :

  • run précédente : 79.1135

  • nouvelle run : 78.6199

  • delta global : -0.4936

Diagnostics marquants

  • primary : 78.620

  • holdout_lang[fr] : 66.176

  • holdout_lang[es] : 89.457

  • holdout_lang[de] : 92.742

  • holdout_family[dwc_gbif] : 70.210

  • holdout_family[tropical_field] : 63.952

  • holdout_family[research_traits] : 71.370

  • holdout_anonymous : 100.000

  • diagnostic_synthetic : 70.892

Diagnostics structurels :

  • diagnostic_subset[en_standard] : 95.656

  • diagnostic_subset[en_field] : 75.917

  • diagnostic_subset[coded_headers] : 72.606

  • diagnostic_subset[gbif_core_standard] : 96.322

  • diagnostic_subset[gbif_extended] : 87.025

Lecture des nouveaux sous-buckets

Le split forest_inventory confirme très nettement que le problème principal de ce bloc n’est pas homogène :

  • forest_inventory : 41.841

  • forest_inventory/ifn_fr : 17.950

  • forest_inventory/fia_en : 50.306

  • forest_inventory/nordic_inventory : 65.225

Lecture :

  • ifn_fr est de loin le sous-bucket le plus faible ;

  • fia_en est difficile mais moins catastrophique ;

  • nordic_inventory reste le sous-cas le plus tolérable.

Interprétation

Cette run ne change pas la conclusion de fond :

  • les points forts restent GBIF standard, anglais standard, anonymous ;

  • les points faibles restent en_field, les coded_headers, et certains domaines métier compacts ;

  • forest_inventory, et surtout ifn_fr, est un bon révélateur de fragilité, mais ne doit pas piloter seul la roadmap produit.

Pour la cible produit visée à ce stade, les signaux les plus utiles restent :

  • tropical_field

  • research_traits

  • gbif_core_standard

  • gbif_extended

  • en_field

Décision sur le protocole

Décision prise après cette run :

  • retirer zh du protocole courant ;

  • ne plus l’utiliser comme diagnostic reporté dans la boucle standard ;

  • garder l’accent sur fr, es, de, tropical_field, research_traits, gbif_*, en_field et coded_headers.

Conséquence :

  • la prochaine run sera un peu plus propre côté reporting ;

  • zh est désormais considéré comme non stratégique pour la décision produit.

Entrée du 2026-03-18 — baseline ProductScore

Le protocole de décision a ensuite été enrichi avec un ProductScore spécifique à la cible produit courante.

Poids retenus :

  • tropical_field : 30%

  • research_traits : 15%

  • gbif_core_standard : 20%

  • gbif_extended : 10%

  • en_field : 15%

  • anonymous : 10%

Baseline observée :

ProductScore = 79.2454

Détail de la baseline :

  • tropical_field : 63.952

  • research_traits : 71.370

  • gbif_core_standard : 96.322

  • gbif_extended : 87.025

  • en_field : 75.917

  • anonymous : 100.000

Décision associée :

  • utiliser product-score comme cible principale d’autoresearch

  • conserver niamoto-score comme garde-fou global secondaire

  • conserver forest_inventory comme stress test secondaire, sans le laisser piloter la décision finale

Entrée du 2026-03-18 — pivot autoresearch

Après plusieurs essais, le constat est devenu clair :

  • les boucles stack complètes trouvent parfois des candidats plausibles ;

  • mais la validation complète est trop coûteuse pour une itération autonome dense ;

  • même les variantes fast restent trop lentes pour atteindre un vrai rythme autoresearch.

Conclusion

Le problème principal n’est plus la seule qualité de la métrique.

Le vrai problème est la granularité de la recherche :

  • on réentraîne trop de choses pour des changements locaux ;

  • on dépense le budget en certification au lieu d’explorer ;

  • la boucle autonome reste trop lente pour délivrer sa valeur.

Décision

Pivot décidé :

  • documenter puis construire une surrogate loop fusion-only

  • séparer l’exploration autonome de la validation stack complète

  • réserver product-score et niamoto-score à la promotion des meilleurs candidats, pas à chaque itération

Document de référence :

  • docs/05-ml-detection/autoresearch-surrogate-loop.md

Entrée du 2026-03-18 — première mesure surrogate loop

La surrogate loop fusion-only a été implémentée puis mesurée sur le gold set courant.

Première tentative

  • builder du cache interrompu après environ 16 min

  • diagnostic :

    • coût du build trop élevé

    • principale cause identifiée : prédictions header/value faites record par record

Correctif appliqué

  • batch prediction pour header

  • batch prediction pour values

  • extraction metadata batchée

  • logging par fold dans le builder

Mesures après optimisation

Build du cache :

build_fusion_surrogate_cache --splits 3
real = 490.08s

Soit environ 8m10s de préchauffe one-shot.

Baseline surrogate-fast sur cache chaud :

FusionSurrogateFast = 55.6326
real = 1.66s

Baseline surrogate-mid sur cache chaud :

FusionSurrogateMid = 59.2746
real = 1.31s

Lecture

  • le build du cache reste un coût réel, mais il est désormais ponctuel ;

  • les évaluations surrogate-fast et surrogate-mid sont enfin dans la bonne zone pour autoresearch ;

  • la surrogate loop devient donc exploitable pour une exploration dense ;

  • le prochain enjeu n’est plus la faisabilité du pivot, mais la stratégie de promotion des candidats gagnants vers product-score-fast-fast, puis product-score, puis niamoto-score.

Entrée du 2026-03-19 — corrections runner et premier gain autoresearch

Corrections du runner surrogate

Trois problèmes identifiés et corrigés dans ml/scripts/research/run_fusion_surrogate_autoresearch.py :

  1. Validation stack trop coûteuse — Quand un candidat passait surrogate-mid, le runner lançait product-score-fast-fast (bloquant pendant des heures). Correction : --defer-stack-validation (défaut true), les candidats passants vont dans .autoresearch/fusion-surrogate-promotions.jsonl.

  2. Fichiers hors périmètre — L’iter 11 du run Codex avait touché le log d’expérimentations. Correction : statut reject_scope, rejet immédiat si des fichiers hors DEFAULT_ALLOWED_PATHS sont touchés.

  3. Pre-commit hooks bloquants — Le commit automatique échouait car ruff-format reformatait les fichiers stagés. Correction : --no-verify pour les commits automatiques du runner.

  4. restore_paths crashait sur fichiers untrackedgit restore --source=HEAD échoue sur les fichiers qui n’existent pas dans HEAD. Correction : séparer tracked (restore) et untracked (unlink).

Remplacement Codex → Claude

Après épuisement des crédits Codex, le moteur a été remplacé par claude -p --dangerously-skip-permissions.

Run Codex (2026-03-18) — 150 itérations

  • 141 codex_error (symlinks cassés ~/.codex/skills/)

  • 9 itérations utiles : 7 reject_fast, 2 reject_mid

  • meilleur fast : 55.8177 (baseline 55.6326)

  • meilleur mid : 58.9922 (baseline 59.2746)

  • 0 promotion

Premier gain Claude — cross-rank reciprocity

L’itération 2 du premier run Claude a produit un candidat gagnant (fast 55.6326 56.5524, delta +0.9198). Le candidat a aussi amélioré mid (59.2746 60.1680, delta +0.8934).

4 nouvelles features de fusion ajoutées :

  1. header_top1_value_rank — position du concept #1 header dans le classement values (0 = aussi #1, 1 = dernier)

  2. value_top1_header_rank — l’inverse

  3. top2_cross_match — le #2 d’une branche correspond au #1 de l’autre

  4. both_weak — les deux branches sont faibles (max proba < 0.3)

Fichiers modifiés : train_fusion.py et classifier.py.

Nouvelles baselines après gain

surrogate-fast = 56.5524 (était 55.6326, +0.92)
surrogate-mid  = 60.1680 (était 59.2746, +0.89)

Run Claude post-gain — 50 itérations (2026-03-19)

  • 50 itérations complétées

  • 50 reject_fast

  • meilleur fast : 56.1821 (baseline 56.5524)

  • 0 promotion

Lecture

Le gain cross-rank reciprocity semble être un plateau pour le périmètre actuel de recherche. 50 itérations supplémentaires n’ont pas réussi à battre la nouvelle baseline.

Hypothèses pour débloquer :

  • élargir DEFAULT_ALLOWED_PATHS (ajouter fusion_features.py)

  • enrichir le prompt avec des hypothèses plus ciblées

  • relancer un run stack complète sur header ou values

  • reconstruire le cache surrogate après un gain header/values

Décision

  • conserver le gain cross-rank reciprocity comme nouvelle baseline fusion

  • ne pas relancer autoresearch fusion immédiatement

  • passer à l’évaluation end-to-end par instance (niamoto-subset) pour mesurer l’impact réel du ML sur les suggestions de widgets

Entrée du 2026-03-20 — réentraînement, validation ProductScore, optimisation batch

Réentraînement des 3 modèles

Les modèles .joblib dans ml/models/ étaient toujours les anciens — l’autoresearch modifiait le code des features de fusion mais n’entraînait pas les modèles. Les scores surrogate étaient un proxy local, pas le vrai score produit.

Réentraînement complet exécuté :

  • header : macro-F1 = 0.7614

  • values : macro-F1 = 0.3783

  • fusion : mean macro-F1 = 0.6899 (leak-free CV, 5 folds)

Durée totale : ~5h (goulot = extraction record par record dans le leak-free CV).

Validation ProductScore — gain confirmé

ProductScore = 80.0372 (baseline 79.2454, delta +0.79)

Détail par bucket :

Bucket

Avant

Après

Delta

tropical_field

63.95

64.88

+0.93

research_traits

71.37

70.98

-0.39

gbif_core_standard

96.32

95.87

-0.46

gbif_extended

87.03

89.75

+2.72

en_field

75.92

78.53

+2.61

anonymous

100.0

100.0

=

Gains notables : en_field +2.61, gbif_extended +2.72, tropical_field +0.93.

Régression : fr -4.07 (holdout, pas dans le ProductScore).

Optimisation batch de l’entraînement fusion

Le goulot de l’entraînement était extract_fusion_features() appelée record par record (~15 000 appels séquentiels pour le leak-free CV).

La version batch extract_fusion_branch_probabilities_batch() existait déjà pour le cache surrogate. Elle a été câblée dans train_fusion.py pour remplacer les 3 boucles record par record.

Résultat :

  • scores strictement identiques (même macro-F1 par fold au centième)

  • temps : 5h → 15 min (~20x plus rapide)

  • zéro perte de qualité

Évaluation par instance — niamoto-subset

Un script ml/scripts/eval/evaluate_instance.py a été créé pour comparer la détection ML avec l’import.yml validé d’une instance réelle.

Résultats sur niamoto-subset (29 colonnes, 9 évaluées via ground truth) :

Mode

Role correct

Concept correct

Alias seul

4/9 (44%)

4/9 (44%)

ML (modèles réentraînés)

6/9 (67%)

5/9 (56%)

Gains ML par rapport aux alias seuls :

  • id_plot : non détecté → identifier.plot (rôle correct)

  • geo_pt : non détecté → location.coordinate (rôle + concept corrects, 0.93)

Erreurs restantes :

  • inframeasurement.diameter (faux, confiance faible 0.32)

  • plot_namelocation.locality (alias impose à confiance 1.0)

  • location → pas trouvé (champ schema, pas colonne CSV)

Décisions de cette session

  • ProductScore 80.04 validé comme nouvelle baseline

  • modèles réentraînés commités

  • batch optimization commitée (entraînement 20x plus rapide)

  • évaluation par instance opérationnelle sur niamoto-subset

  • prochaine étape : câbler le ML dans l’auto-config (plan documenté dans docs/05-ml-detection/README.md and the active reference docs in docs/05-ml-detection/)