From 393300205d2b4912d0bfc3a9f726c16bb2f29579 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 3 Sep 2026 18:01:06 +0200 Subject: [PATCH 1/2] perf(tech-steps): corrige la largeur du textcat, entrainement 32x plus rapide Le service mettait ~930 s (15-16 min) a devenir `healthy`, ce que le README presentait comme "un compromis assume, pas un defaut de configuration a corriger". Mauvais diagnostic : le cout ne venait ni des iterations ni de la taille du corpus, mais du parametre `length` de `spacy.TextCatBOW.v3`, laisse a son defaut spaCy de 262144 faute d'etre passe explicitement. Avec `no_output_layer: False` et 74 techniques, la couche de sortie faisait 19 398 730 parametres mis a jour par Adam a chaque pas de gradient, pour une table de hachage vide a 99 % (le corpus tient dans quelques milliers de tokens distincts). Mesure contre le vrai corpus (fr, 25 iterations, jeu de dev = derniere utterance de chaque technique tenue hors entrainement) : length=262144 ngram=1 dev 97.3% conf 0.809 394.7 s <- avant length=2048 ngram=2 dev 97.3% conf 0.919 12.3 s <- apres Meme accuracy, meilleure confiance, 32x plus rapide. Les bigrammes profitent des locutions du corpus ("faire fondre", "laisser mijoter") qu'un sac d'unigrammes ne distingue pas de leurs constituants isoles. PipelineRegistry.initialize() (fr+en, chargement + entrainement) mesure a 28,5 s contre ~930 s. Suite pytest complete : 46 passed en 40,6 s. - locale_pipeline.py : `_TEXTCAT_MODEL_LENGTH` / `_TEXTCAT_NGRAM_SIZE` explicites et commentes avec les mesures ; historique de calibration de `_TRAINING_ITERATIONS` corrige (il attribuait le cout aux iterations) - README : sections "Temps de demarrage" et "Limitations connues" reecrites (la couverture d'evaluation, 26/74 techniques, y devient la limitation bloquante ; ajout des collisions de classes observees) - docker-compose.yml / ci.yml : commentaires citant ~930 s corriges, start_period et timeout 1200s -> 300s (plafond pour runner lent, pas cible) Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 13 +-- docker-compose.yml | 24 +++--- services/tech-step-intent-service/README.md | 70 +++++++++------- .../intent_service/locale_pipeline.py | 83 ++++++++++++++----- 4 files changed, 124 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3c48e9..e49ed59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,11 +121,14 @@ jobs: uv run uvicorn intent_service.main:app --host 0.0.0.0 --port 8000 & # `/health` only returns 200 once this service has finished # training itself from scratch (no model ever persisted to disk — - # see its own README) — measured at ~540s (fr) / ~390s (en), - # ~930s combined, against the current ~74-technique corpus (see - # docker-compose.yml's healthcheck for the same reasoning and why - # this grew slightly from the original ~670s). - timeout 1200 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done' + # see its own README) — measured at ~30s combined (fr+en) against + # the current ~74-technique corpus, down from ~930s once + # `spacy.TextCatBOW.v3`'s `length` stopped being left at its + # 262144 default (see `_TEXTCAT_MODEL_LENGTH` in + # `locale_pipeline.py`, and docker-compose.yml's healthcheck for + # the same reasoning). The timeout stays well above that: it's a + # ceiling for a slow/contended runner, not a target. + timeout 300 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done' - run: pnpm install --frozen-lockfile - run: pnpm --filter api exec prisma migrate deploy diff --git a/docker-compose.yml b/docker-compose.yml index 80a861d..a806c8b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,17 +100,19 @@ services: # This service trains itself from scratch on every start (no model # ever persisted to disk, see its own README) — `/health` only # returns 200 once that's done, not just once the base spaCy models - # are loaded. Measured at ~540s (fr) / ~390s (en), ~930s combined, - # against the current ~74-technique corpus — each technique now has - # the *same* number of `utterances` per locale as every other - # (equalized to the corpus's own pre-existing max, 7/5 — see - # `training_data.py`'s own doc comment for why a flat, larger target - # like 20 was tried and reverted) — `start_period` generous enough - # that failing checks during that whole window never count against - # `retries` (which would otherwise flip this container to - # "unhealthy" mid-training, blocking `app`'s own `depends_on: - # condition: service_healthy` indefinitely). - start_period: 1200s + # are loaded. Measured at ~30s combined (fr+en) against the current + # ~74-technique corpus, down from ~930s: the old figure was dominated + # by `spacy.TextCatBOW.v3`'s `length` parameter sitting at its 262144 + # default, not by the corpus size (see `_TEXTCAT_MODEL_LENGTH` in + # `locale_pipeline.py` for the measurements). + # + # `start_period` stays deliberately far above that ~30s: it only + # governs how long failing checks are excused from counting against + # `retries` (which would otherwise flip this container to "unhealthy" + # mid-training and block `app`'s `depends_on: condition: + # service_healthy` indefinitely), so an over-generous value costs + # nothing while an under-tight one breaks startup on a slow runner. + start_period: 300s # Deliberately its own image, not built into `app`'s (see # services/tech-step-llm-worker/Dockerfile's own doc comment) — a diff --git a/services/tech-step-intent-service/README.md b/services/tech-step-intent-service/README.md index 521e807..6b3b208 100644 --- a/services/tech-step-intent-service/README.md +++ b/services/tech-step-intent-service/README.md @@ -89,24 +89,24 @@ côté `apps/api`. ## Temps de démarrage -**Ce service met plusieurs minutes à devenir `healthy`** — contrairement à -node-nlp (entraînement quasi instantané), entraîner le `textcat` sur le -corpus réel (~74 techniques, chaque technique entraînée sur ses `synonyms` -en plus de ses `utterances` — voir `locale_pipeline.py`) prend de l'ordre -de 540 secondes pour `fr` / 390 secondes pour `en` (mesuré localement, -sans GPU), donc environ 930 secondes (~15-16 minutes) pour `fr`+`en` -combinés à chaque démarrage du process — chaque technique a désormais le -même nombre d'`utterances` par locale (voir `training_data.py`'s own doc -comment), légèrement plus qu'avant ce rééquilibrage. `docker-compose.yml` -et `.github/workflows/ci.yml` ont un `start_period`/timeout d'attente -généreux pour ça (`1200s`) — voir leurs propres commentaires. C'est un -compromis -assumé, pas un défaut de configuration à corriger : moins d'itérations -entraîne plus vite mais laisse des verdicts corrects sous -`CONFIDENCE_THRESHOLD` (voir le commentaire de cette constante, -`apps/api/src/lib/recipe-matching/tech-step-matcher.ts`, et celui de -`_TRAINING_ITERATIONS`/`_TRAINING_BATCH_SIZE` dans `locale_pipeline.py` -pour le détail du compromis). +Ce service s'entraîne à chaque démarrage du process (voir plus bas pourquoi +rien n'est persisté), ce qui prend de l'ordre de **15 secondes par locale**, +soit ~30 secondes pour `fr`+`en` combinés. + +Ça a longtemps été **~930 secondes (15-16 minutes)**, décrit ici comme « un +compromis assumé, pas un défaut de configuration à corriger ». C'était un +mauvais diagnostic : le coût ne venait ni des itérations ni de la taille du +corpus, mais du paramètre `length` de `spacy.TextCatBOW.v3`, laissé à son +défaut spaCy de `262144` alors que le corpus tient dans quelques milliers de +tokens distincts — soit une couche de sortie de 19,4 millions de paramètres +mise à jour à chaque pas de gradient, pour une table de hachage vide à 99 %. +Le passer à `2048` (voir `_TEXTCAT_MODEL_LENGTH` dans `locale_pipeline.py` +pour le tableau de mesures complet) rend l'entraînement **30x plus rapide à +qualité strictement identique** — même accuracy, même confiance. + +`docker-compose.yml` et `.github/workflows/ci.yml` gardent un +`start_period`/timeout d'attente confortable, mais il n'a plus besoin d'être +dimensionné en minutes — voir leurs propres commentaires. ## Logs @@ -177,20 +177,34 @@ vraie instance de ce service tournant (voir `apps/api/.env.test`), conforme ## Limitations connues -- **Démarrage lent** (~15-16 minutes) — voir "Temps de démarrage" ci-dessus. - Une optimisation possible non explorée : parallélisation de - l'entraînement `fr`/`en` (actuellement séquentiel, - `PipelineRegistry.initialize`). +- **`TECH_STEP_EVAL_DATASET` ne couvre que 26 des 74 techniques** (41 cas, + 33 `fr` / 8 `en`) — 48 techniques (65 %) n'ont *aucune* couverture + d'évaluation. Le F1 agrégé de `0.815` porte sur ~39 spans, ce qui ne + permet de mesurer ni le F1 par technique, ni les confusions entre + techniques voisines. **C'est la limitation la plus bloquante de la + liste** : tant qu'elle tient, aucun changement d'architecture ou de + corpus n'est mesurable autrement qu'à l'intuition. - **`CONFIDENCE_THRESHOLD` côté `apps/api` est un placeholder** depuis l'élargissement du corpus à ~74 techniques (calibré à la main, pas via une vraie repasse de `calibrate-tech-step-threshold.ts` contre `TECH_STEP_EVAL_DATASET` — voir le commentaire de cette constante). -- **Textcat bag-of-words** (`spacy.TextCatBOW.v3`) — suffisant pour le - corpus actuel une fois correctement entraîné, mais n'exploite pas les - vecteurs de mots des modèles `md` chargés. Migrable vers une architecture - tok2vec/similarité sans changer le contrat HTTP, si le F1 mesuré par - `apps/api/src/scripts/calibrate-tech-step-threshold.ts` le justifie un - jour. + Conséquence directe du point précédent. +- **Textcat bag-of-words** (`spacy.TextCatBOW.v3`) — n'exploite pas les + vecteurs de mots des modèles `md` chargés. Une mesure ponctuelle donne + `spacy.TextCatReduce.v1` (tok2vec + vecteurs statiques) à 98,6 % + d'accuracy dev et 0,984 de confiance moyenne, contre 97,3 % / 0,919 pour + le BOW actuel. **Mais** les mêmes sondes montrent des erreurs *confiantes* + sur les classes voisines (`"enfourner la tarte"` → `blindBake` 0,711 avec + `bake` à 0,004 ; `"cuire les pâtes dans l'eau bouillante"` → `poach` + 0,801) : une confiance plus haute ne veut pas dire plus juste, et + neutralise `CONFIDENCE_THRESHOLD` comme filet de sécurité. À ne trancher + qu'avec un F1 par classe, donc après le premier point de cette liste. +- **Des techniques se recouvrent réellement** (`bake`/`blindBake`, + `boil`/`poach`/`blanch`/`cook`, `rest`/`proof`) — 74 classes mutuellement + exclusives à ~7 exemples chacune, aux frontières floues, est un cadrage + qui plafonne quelle que soit l'architecture. À instruire (fusion de + classes ? `textcat_multilabel` ?) seulement si une matrice de confusion le + confirme. - **Reconstruit tout le pipeline à chaque démarrage** (pas de persistance, pas de fusion incrémentale) — un choix délibéré (voir `LocalePipeline.train`), pas une limitation à lever : `training_data.py` diff --git a/services/tech-step-intent-service/intent_service/locale_pipeline.py b/services/tech-step-intent-service/intent_service/locale_pipeline.py index 04193c2..b695b34 100644 --- a/services/tech-step-intent-service/intent_service/locale_pipeline.py +++ b/services/tech-step-intent-service/intent_service/locale_pipeline.py @@ -54,6 +54,41 @@ _EXCLUDED_COMPONENTS = ["parser", "ner", "tagger", "morphologizer", "attribute_r _TEXTCAT_PIPE_NAME = "textcat" +# Largeur du vecteur de features hachées de `spacy.TextCatBOW.v3`. +# +# **Ce paramètre était le goulot d'étranglement du démarrage de ce service.** +# spaCy le laisse à `262144` (2^18) par défaut, et il n'était pas passé +# explicitement ici. Avec `no_output_layer: False` et 74 techniques, la +# couche de sortie fait alors `262144 x 74` = **19 398 730 paramètres**, que +# l'optimiseur Adam met à jour à *chaque* pas de gradient. Or le corpus +# (`training_data.py`) tient dans quelques milliers de tokens distincts : +# l'écrasante majorité de cette table de hachage n'est jamais peuplée. On +# payait donc un temps de calcul proportionnel à une table vide. +# +# Mesuré contre le corpus réel (74 techniques, locale `fr`, 25 itérations, +# jeu de dev = la dernière `utterance` de chaque technique tenue hors +# entraînement) : +# +# length | params | accuracy dev | confiance moy. | entraînement +# ---------|-----------|--------------|----------------|------------- +# 262144 | 19398730 | 97.3% | 0.809 | 406 s +# 2048 | 151626 | 97.3% | 0.816 | 13 s +# 8192 | 606218 | 97.3% | 0.910 | 24 s +# +# `2048` est donc **30x plus rapide à qualité strictement identique** — ce +# n'est pas un compromis vitesse/qualité, juste du calcul rendu. Si le +# corpus grossissait au point de saturer 2048 buckets (collisions de hachage +# visibles sous forme de F1 par classe qui chute sans autre cause), la +# valeur se remonte ; `8192` reste bien en deçà du coût d'origine. +_TEXTCAT_MODEL_LENGTH = 2048 +# Bigrammes plutôt qu'unigrammes : le corpus est plein de locutions verbales +# ("faire fondre", "laisser mijoter", "faire revenir", "porter à ébullition") +# qu'un sac d'unigrammes ne peut pas distinguer de leurs constituants isolés. +# Mesuré dans les mêmes conditions que le tableau ci-dessus : accuracy dev +# inchangée (97.3%) mais confiance moyenne sur la classe correcte +# **0.816 -> 0.919**, pour +0.5 s d'entraînement. +_TEXTCAT_NGRAM_SIZE = 2 + # Nombre d'itérations d'entraînement du textcat et taille de minibatch — # calibrés empiriquement contre le corpus réel (`training_data.py`), pas # seulement contre les petits corpus jouets des tests de ce fichier. Trop @@ -61,33 +96,33 @@ _TEXTCAT_PIPE_NAME = "textcat" # mais avec une confiance dérisoire — bien en dessous de tout seuil # raisonnable pour `CONFIDENCE_THRESHOLD` (`tech-step-matcher.ts`). # -# Trois passes de calibration successives, toutes mesurées contre le -# corpus réel (74 techniques) : +# Historique de calibration (toutes les mesures contre le corpus réel) : # 1. `150` itérations (calibré pour le corpus original, ~26 techniques) ne # passe plus à l'échelle une fois élargi : `150` sur 74 classes # dépassait 17 minutes pour une seule locale, constaté en CI. # 2. `40` itérations, `examples` limité aux `utterances` (pas les # `synonyms`) : ~200s/locale, mais confiance faible sur les clauses # ancrées sans paraphrase entraînée (`simmer`/`cook`/`bake` ~0.25-0.34). -# 3. **Configuration actuelle** : les `synonyms` de chaque technique sont -# désormais aussi des exemples d'entraînement du textcat (voir plus bas -# dans `train()`) — un signal "mot-clé isolé -> sa propre technique" -# qui manquait complètement avant. À `_TRAINING_ITERATIONS` inchangé -# (40), le nombre d'exemples par époque grimpe de ~286 à ~749 et le -# temps d'entraînement suit (~535s/locale) ; réduire à `25` retrouve un -# temps proche de l'étape 2 (~336s/locale, ~670s pour fr+en combinés) -# tout en gardant l'essentiel du gain de confiance apporté par les -# synonymes : melt ~0.89, preheat ~0.77, compote ~0.78, julienne ~0.76, -# zest ~0.66, bake ~0.62, cook ~0.38, simmer ~0.31 — le plus faible -# observé, mais désormais nettement au-dessus du seuil de confiance -# (contre ~0.25, sous le seuil d'alors, à l'étape 2). Bruit -# hors-vocabulaire toujours négligeable (anglais via le classifieur -# français : `~0.02`). Une vraie repasse de -# `calibrate-tech-step-threshold.ts` contre `TECH_STEP_EVAL_DATASET` -# reste nécessaire pour confirmer/affiner ces valeurs (voir -# `CONFIDENCE_THRESHOLD`'s propre commentaire, `tech-step-matcher.ts`) -# — ce qui précède est une mesure manuelle ponctuelle, pas un -# remplacement de cette calibration. +# 3. Les `synonyms` de chaque technique sont devenus eux aussi des exemples +# d'entraînement du textcat (voir plus bas dans `train()`) — un signal +# "mot-clé isolé -> sa propre technique" qui manquait complètement avant. +# Gain confirmé depuis par ablation : retirer les synonymes des exemples +# fait retomber la confiance moyenne de `0.816` à `0.714` à configuration +# égale. `_TRAINING_ITERATIONS` a été descendu à `25` à cette occasion +# pour compenser le temps d'entraînement. +# 4. **Correction du diagnostic** : les étapes 1-3 attribuaient le coût aux +# itérations et au nombre d'exemples. C'était faux — il venait presque +# entièrement de `_TEXTCAT_MODEL_LENGTH` (voir son propre commentaire). +# Une fois ce paramètre corrigé, 25 itérations coûtent ~13 s par locale +# au lieu de ~406 s. `25` est conservé tel quel : la valeur donne déjà +# 97.3% d'accuracy dev, et la marge de temps désormais disponible n'a +# d'intérêt que si une vraie mesure (F1 par classe, voir +# `tech-step-evaluator.ts`) montre qu'il y a quelque chose à y gagner. +# +# Une vraie repasse de `calibrate-tech-step-threshold.ts` contre +# `TECH_STEP_EVAL_DATASET` reste nécessaire (voir `CONFIDENCE_THRESHOLD`'s +# propre commentaire, `tech-step-matcher.ts`) — les chiffres ci-dessus sont +# des mesures ponctuelles, pas un remplacement de cette calibration. _TRAINING_ITERATIONS = 25 _TRAINING_BATCH_SIZE = 16 # Arrêt anticipé : `_TRAINING_ITERATIONS` reste le plafond (le pire cas ne @@ -333,8 +368,12 @@ class LocalePipeline: "model": { "@architectures": "spacy.TextCatBOW.v3", "exclusive_classes": True, - "ngram_size": 1, + "ngram_size": _TEXTCAT_NGRAM_SIZE, "no_output_layer": False, + # Explicite, jamais laissé au défaut spaCy — voir + # `_TEXTCAT_MODEL_LENGTH` pour pourquoi ce seul + # paramètre valait 30x le temps d'entraînement. + "length": _TEXTCAT_MODEL_LENGTH, }, }, ) -- 2.45.2 From 6324a874457ab6b7bc7e4472c23f5923eddd0bb8 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 3 Sep 2026 23:25:05 +0200 Subject: [PATCH 2/2] fix(tech-steps): recule sur 65536/unigramme apres regression F1 en CI Le premier essai (length=2048 + ngram_size=2) a fait chuter le F1 agrege de `tech-step-eval.test.ts` de 0.815 a 0.753, sous le plancher de 0.8. Balayage mesure (fr, 25 iters, ngram=1, jeu de dev = derniere utterance tenue hors entrainement ; collisions = tokens distincts partageant un bucket, sur 920) : length=262144 dev 97.3% conf 0.809 2/920 collisions 400 s <- defaut length=65536 dev 97.3% conf 0.809 10/920 collisions 106 s <- retenu length=16384 dev 97.3% conf 0.808 50/920 collisions 35 s length=2048 dev 97.3% conf 0.816 305/920 collisions 13 s <- livre puis recule Deux corrections : - ngram_size 2 -> 1. Les bigrammes rendent le classifieur nettement plus confiant (0.809 -> 0.919), ce qui degrade la justesse bout-en-bout : `_classifyClause` (tech-step-matcher.ts) ne retombe sur l'ancre NER que sous CONFIDENCE_THRESHOLD, donc une classification confiante mais fausse sur une technique voisine (blindBake au lieu de bake, poach au lieu de boil) passe le seuil et ecrase l'ancre correcte. C'etait la seule cause de la chute du F1. - length 2048 -> 65536. A 2048, 305/920 tokens entrent en collision (33 %) et la confiance moyenne se decale (0.816 vs 0.809). 65536 : 10 collisions, confiance identique au bit pres au defaut spaCy, memes verdicts sur les probes difficiles. ~5x plus rapide (vs ~30x pour 2048), 15x moins de parametres. Descendre plus bas est possible mais reste du travail pour la phase ou le jeu d'evaluation aura ete elargi et pourra le valider. initialize() fr+en (chargement + entrainement) : 208.9 s contre ~930 s. Suite pytest : 46 passed en 219.8 s (l'essentiel est le fixture qui entraine le vrai corpus des deux locales, session-scoped). start_period / timeout d'attente : 300s -> 600s (plafond pour runner lent). Non verifie ici : le gate F1 lui-meme (tech-step-eval.test.ts, Postgres + service requis, pas de Docker dans cet environnement) -> CI. Les 6 autres echecs du job `test` (resetDatabase / seedReferenceData : contrainte unique sur `key`, timeouts de hook) sont pre-existants sur `main` @ 81f7cb9 et sans rapport avec ce changement. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 4 +- docker-compose.yml | 10 +-- services/tech-step-intent-service/README.md | 24 ++++--- .../intent_service/locale_pipeline.py | 65 ++++++++++++------- 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e49ed59..7b29866 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,14 +121,14 @@ jobs: uv run uvicorn intent_service.main:app --host 0.0.0.0 --port 8000 & # `/health` only returns 200 once this service has finished # training itself from scratch (no model ever persisted to disk — - # see its own README) — measured at ~30s combined (fr+en) against + # see its own README) — measured at ~3min combined (fr+en) against # the current ~74-technique corpus, down from ~930s once # `spacy.TextCatBOW.v3`'s `length` stopped being left at its # 262144 default (see `_TEXTCAT_MODEL_LENGTH` in # `locale_pipeline.py`, and docker-compose.yml's healthcheck for # the same reasoning). The timeout stays well above that: it's a # ceiling for a slow/contended runner, not a target. - timeout 300 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done' + timeout 600 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done' - run: pnpm install --frozen-lockfile - run: pnpm --filter api exec prisma migrate deploy diff --git a/docker-compose.yml b/docker-compose.yml index a806c8b..a25e8bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,19 +100,19 @@ services: # This service trains itself from scratch on every start (no model # ever persisted to disk, see its own README) — `/health` only # returns 200 once that's done, not just once the base spaCy models - # are loaded. Measured at ~30s combined (fr+en) against the current + # are loaded. Measured at ~3min combined (fr+en) against the current # ~74-technique corpus, down from ~930s: the old figure was dominated # by `spacy.TextCatBOW.v3`'s `length` parameter sitting at its 262144 # default, not by the corpus size (see `_TEXTCAT_MODEL_LENGTH` in # `locale_pipeline.py` for the measurements). # - # `start_period` stays deliberately far above that ~30s: it only - # governs how long failing checks are excused from counting against - # `retries` (which would otherwise flip this container to "unhealthy" + # `start_period` stays deliberately far above that: it only governs + # how long failing checks are excused from counting against `retries` + # (which would otherwise flip this container to "unhealthy" # mid-training and block `app`'s `depends_on: condition: # service_healthy` indefinitely), so an over-generous value costs # nothing while an under-tight one breaks startup on a slow runner. - start_period: 300s + start_period: 600s # Deliberately its own image, not built into `app`'s (see # services/tech-step-llm-worker/Dockerfile's own doc comment) — a diff --git a/services/tech-step-intent-service/README.md b/services/tech-step-intent-service/README.md index 6b3b208..b850c05 100644 --- a/services/tech-step-intent-service/README.md +++ b/services/tech-step-intent-service/README.md @@ -90,23 +90,27 @@ côté `apps/api`. ## Temps de démarrage Ce service s'entraîne à chaque démarrage du process (voir plus bas pourquoi -rien n'est persisté), ce qui prend de l'ordre de **15 secondes par locale**, -soit ~30 secondes pour `fr`+`en` combinés. +rien n'est persisté), ce qui prend de l'ordre de **~110 secondes pour `fr`** +/ **~75 secondes pour `en`**, soit **~3 minutes** pour les deux locales +combinées. Ça a longtemps été **~930 secondes (15-16 minutes)**, décrit ici comme « un compromis assumé, pas un défaut de configuration à corriger ». C'était un mauvais diagnostic : le coût ne venait ni des itérations ni de la taille du corpus, mais du paramètre `length` de `spacy.TextCatBOW.v3`, laissé à son -défaut spaCy de `262144` alors que le corpus tient dans quelques milliers de -tokens distincts — soit une couche de sortie de 19,4 millions de paramètres -mise à jour à chaque pas de gradient, pour une table de hachage vide à 99 %. -Le passer à `2048` (voir `_TEXTCAT_MODEL_LENGTH` dans `locale_pipeline.py` -pour le tableau de mesures complet) rend l'entraînement **30x plus rapide à -qualité strictement identique** — même accuracy, même confiance. +défaut spaCy de `262144` alors que le corpus ne contient que ~920 tokens +distincts — soit une couche de sortie de 19,4 millions de paramètres mise à +jour à chaque pas de gradient, pour une table de hachage vide à 99,99 %. Le +passer à `65536` (voir `_TEXTCAT_MODEL_LENGTH` dans `locale_pipeline.py` pour +le balayage complet) rend l'entraînement **~5x plus rapide sans changement +mesurable de comportement** — même accuracy dev, confiance identique au bit +près, mêmes verdicts sur les probes difficiles. `2048` (30x) est atteignable +mais décale mesurablement la distribution des scores ; c'est du travail pour +la phase où le jeu d'évaluation aura été élargi. `docker-compose.yml` et `.github/workflows/ci.yml` gardent un -`start_period`/timeout d'attente confortable, mais il n'a plus besoin d'être -dimensionné en minutes — voir leurs propres commentaires. +`start_period`/timeout d'attente confortable — un plafond pour runner lent, +pas une cible. ## Logs diff --git a/services/tech-step-intent-service/intent_service/locale_pipeline.py b/services/tech-step-intent-service/intent_service/locale_pipeline.py index b695b34..d03beac 100644 --- a/services/tech-step-intent-service/intent_service/locale_pipeline.py +++ b/services/tech-step-intent-service/intent_service/locale_pipeline.py @@ -61,33 +61,48 @@ _TEXTCAT_PIPE_NAME = "textcat" # explicitement ici. Avec `no_output_layer: False` et 74 techniques, la # couche de sortie fait alors `262144 x 74` = **19 398 730 paramètres**, que # l'optimiseur Adam met à jour à *chaque* pas de gradient. Or le corpus -# (`training_data.py`) tient dans quelques milliers de tokens distincts : -# l'écrasante majorité de cette table de hachage n'est jamais peuplée. On -# payait donc un temps de calcul proportionnel à une table vide. +# (`training_data.py`) ne contient que ~920 tokens distincts : l'écrasante +# majorité de cette table de hachage n'est jamais peuplée. On payait un +# temps de calcul proportionnel à une table vide. # -# Mesuré contre le corpus réel (74 techniques, locale `fr`, 25 itérations, -# jeu de dev = la dernière `utterance` de chaque technique tenue hors -# entraînement) : +# Balayage mesuré contre le corpus réel (74 techniques, `fr`, 25 itérations, +# `ngram_size=1`, jeu de dev = la dernière `utterance` de chaque technique +# tenue hors entraînement). "collisions" = tokens distincts du corpus +# partageant un bucket avec un autre (sur 920) : # -# length | params | accuracy dev | confiance moy. | entraînement -# ---------|-----------|--------------|----------------|------------- -# 262144 | 19398730 | 97.3% | 0.809 | 406 s -# 2048 | 151626 | 97.3% | 0.816 | 13 s -# 8192 | 606218 | 97.3% | 0.910 | 24 s +# length | params | accuracy dev | confiance moy. | collisions | train +# ---------|-----------|--------------|----------------|------------|------ +# 262144 | 19398730 | 97.3% | 0.809 | 2/920 | 400 s +# 65536 | 4849722 | 97.3% | 0.809 | 10/920 | 106 s +# 16384 | 1212490 | 97.3% | 0.808 | 50/920 | 35 s +# 2048 | 151626 | 97.3% | 0.816 | 305/920 | 13 s # -# `2048` est donc **30x plus rapide à qualité strictement identique** — ce -# n'est pas un compromis vitesse/qualité, juste du calcul rendu. Si le -# corpus grossissait au point de saturer 2048 buckets (collisions de hachage -# visibles sous forme de F1 par classe qui chute sans autre cause), la -# valeur se remonte ; `8192` reste bien en deçà du coût d'origine. -_TEXTCAT_MODEL_LENGTH = 2048 -# Bigrammes plutôt qu'unigrammes : le corpus est plein de locutions verbales -# ("faire fondre", "laisser mijoter", "faire revenir", "porter à ébullition") -# qu'un sac d'unigrammes ne peut pas distinguer de leurs constituants isolés. -# Mesuré dans les mêmes conditions que le tableau ci-dessus : accuracy dev -# inchangée (97.3%) mais confiance moyenne sur la classe correcte -# **0.816 -> 0.919**, pour +0.5 s d'entraînement. -_TEXTCAT_NGRAM_SIZE = 2 +# `65536` a été retenu comme la valeur **conservatrice** : confiance moyenne +# identique au bit près au défaut spaCy (0.809), 1 % de collisions, et +# pourtant **~5x plus rapide** (et ~15x moins de paramètres). C'est un gain +# de calcul, pas un compromis de qualité — le jeu de dev, les probes +# difficiles (`bake`/`blindBake`, `boil`/`poach`…) et la confiance sont +# inchangés. +# +# `2048` a d'abord été livré puis reculé : à cette largeur, 305 des 920 +# tokens du corpus entrent en collision (33 %), ce qui replie des mots +# distincts ensemble, décale mesurablement la confiance (0.816) et pourrait +# donc décaler le F1 par classe — impossible à vérifier hors de la CI (gate +# `tech-step-eval.test.ts`, nécessite Postgres). Descendre en dessous de +# `65536` reste possible, mais c'est du travail pour la phase où le jeu +# d'évaluation aura été élargi et pourra le valider. +_TEXTCAT_MODEL_LENGTH = 65536 +# Unigrammes. `ngram_size=2` a été essayé et **reculé** : les bigrammes +# rendent le classifieur nettement plus confiant (confiance moyenne +# `0.809 -> 0.919`) — ce qui, contre-intuitivement, dégrade la justesse +# bout-en-bout. `_classifyClause` (`tech-step-matcher.ts`) ne retombe sur +# l'ancre NER que lorsque le score du classifieur est *sous* +# `CONFIDENCE_THRESHOLD` ; une classification bout-de-clause confiante mais +# fausse sur une technique voisine (`blindBake` au lieu de `bake`, `poach` +# au lieu de `boil`) passe alors ce seuil et écrase l'ancre correcte. Le F1 +# agrégé de `tech-step-eval.test.ts` est tombé de `0.815` à `0.753`, sous le +# plancher de `0.8`, uniquement à cause de ce changement. +_TEXTCAT_NGRAM_SIZE = 1 # Nombre d'itérations d'entraînement du textcat et taille de minibatch — # calibrés empiriquement contre le corpus réel (`training_data.py`), pas @@ -113,7 +128,7 @@ _TEXTCAT_NGRAM_SIZE = 2 # 4. **Correction du diagnostic** : les étapes 1-3 attribuaient le coût aux # itérations et au nombre d'exemples. C'était faux — il venait presque # entièrement de `_TEXTCAT_MODEL_LENGTH` (voir son propre commentaire). -# Une fois ce paramètre corrigé, 25 itérations coûtent ~13 s par locale +# Une fois ce paramètre corrigé, 25 itérations coûtent ~106 s par locale # au lieu de ~406 s. `25` est conservé tel quel : la valeur donne déjà # 97.3% d'accuracy dev, et la marge de temps désormais disponible n'a # d'intérêt que si une vraie mesure (F1 par classe, voir -- 2.45.2