Compare commits
No commits in common. "df0791a69489d2114ff388c2a7d87c24d111a933" and "b8411a2e91b51f2007b0d174239e384f26d99415" have entirely different histories.
df0791a694
...
b8411a2e91
4 changed files with 66 additions and 143 deletions
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
|
@ -121,14 +121,11 @@ jobs:
|
||||||
uv run uvicorn intent_service.main:app --host 0.0.0.0 --port 8000 &
|
uv run uvicorn intent_service.main:app --host 0.0.0.0 --port 8000 &
|
||||||
# `/health` only returns 200 once this service has finished
|
# `/health` only returns 200 once this service has finished
|
||||||
# training itself from scratch (no model ever persisted to disk —
|
# training itself from scratch (no model ever persisted to disk —
|
||||||
# see its own README) — measured at ~3min combined (fr+en) against
|
# see its own README) — measured at ~540s (fr) / ~390s (en),
|
||||||
# the current ~74-technique corpus, down from ~930s once
|
# ~930s combined, against the current ~74-technique corpus (see
|
||||||
# `spacy.TextCatBOW.v3`'s `length` stopped being left at its
|
# docker-compose.yml's healthcheck for the same reasoning and why
|
||||||
# 262144 default (see `_TEXTCAT_MODEL_LENGTH` in
|
# this grew slightly from the original ~670s).
|
||||||
# `locale_pipeline.py`, and docker-compose.yml's healthcheck for
|
timeout 1200 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done'
|
||||||
# the same reasoning). The timeout stays well above that: it's a
|
|
||||||
# ceiling for a slow/contended runner, not a target.
|
|
||||||
timeout 600 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done'
|
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm --filter api exec prisma migrate deploy
|
- run: pnpm --filter api exec prisma migrate deploy
|
||||||
|
|
|
||||||
|
|
@ -100,19 +100,17 @@ services:
|
||||||
# This service trains itself from scratch on every start (no model
|
# This service trains itself from scratch on every start (no model
|
||||||
# ever persisted to disk, see its own README) — `/health` only
|
# ever persisted to disk, see its own README) — `/health` only
|
||||||
# returns 200 once that's done, not just once the base spaCy models
|
# returns 200 once that's done, not just once the base spaCy models
|
||||||
# are loaded. Measured at ~3min combined (fr+en) against the current
|
# are loaded. Measured at ~540s (fr) / ~390s (en), ~930s combined,
|
||||||
# ~74-technique corpus, down from ~930s: the old figure was dominated
|
# against the current ~74-technique corpus — each technique now has
|
||||||
# by `spacy.TextCatBOW.v3`'s `length` parameter sitting at its 262144
|
# the *same* number of `utterances` per locale as every other
|
||||||
# default, not by the corpus size (see `_TEXTCAT_MODEL_LENGTH` in
|
# (equalized to the corpus's own pre-existing max, 7/5 — see
|
||||||
# `locale_pipeline.py` for the measurements).
|
# `training_data.py`'s own doc comment for why a flat, larger target
|
||||||
#
|
# like 20 was tried and reverted) — `start_period` generous enough
|
||||||
# `start_period` stays deliberately far above that: it only governs
|
# that failing checks during that whole window never count against
|
||||||
# how long failing checks are excused from counting against `retries`
|
# `retries` (which would otherwise flip this container to
|
||||||
# (which would otherwise flip this container to "unhealthy"
|
# "unhealthy" mid-training, blocking `app`'s own `depends_on:
|
||||||
# mid-training and block `app`'s `depends_on: condition:
|
# condition: service_healthy` indefinitely).
|
||||||
# service_healthy` indefinitely), so an over-generous value costs
|
start_period: 1200s
|
||||||
# nothing while an under-tight one breaks startup on a slow runner.
|
|
||||||
start_period: 600s
|
|
||||||
|
|
||||||
# Deliberately its own image, not built into `app`'s (see
|
# Deliberately its own image, not built into `app`'s (see
|
||||||
# services/tech-step-llm-worker/Dockerfile's own doc comment) — a
|
# services/tech-step-llm-worker/Dockerfile's own doc comment) — a
|
||||||
|
|
|
||||||
|
|
@ -89,28 +89,24 @@ côté `apps/api`.
|
||||||
|
|
||||||
## Temps de démarrage
|
## Temps de démarrage
|
||||||
|
|
||||||
Ce service s'entraîne à chaque démarrage du process (voir plus bas pourquoi
|
**Ce service met plusieurs minutes à devenir `healthy`** — contrairement à
|
||||||
rien n'est persisté), ce qui prend de l'ordre de **~110 secondes pour `fr`**
|
node-nlp (entraînement quasi instantané), entraîner le `textcat` sur le
|
||||||
/ **~75 secondes pour `en`**, soit **~3 minutes** pour les deux locales
|
corpus réel (~74 techniques, chaque technique entraînée sur ses `synonyms`
|
||||||
combinées.
|
en plus de ses `utterances` — voir `locale_pipeline.py`) prend de l'ordre
|
||||||
|
de 540 secondes pour `fr` / 390 secondes pour `en` (mesuré localement,
|
||||||
Ça a longtemps été **~930 secondes (15-16 minutes)**, décrit ici comme « un
|
sans GPU), donc environ 930 secondes (~15-16 minutes) pour `fr`+`en`
|
||||||
compromis assumé, pas un défaut de configuration à corriger ». C'était un
|
combinés à chaque démarrage du process — chaque technique a désormais le
|
||||||
mauvais diagnostic : le coût ne venait ni des itérations ni de la taille du
|
même nombre d'`utterances` par locale (voir `training_data.py`'s own doc
|
||||||
corpus, mais du paramètre `length` de `spacy.TextCatBOW.v3`, laissé à son
|
comment), légèrement plus qu'avant ce rééquilibrage. `docker-compose.yml`
|
||||||
défaut spaCy de `262144` alors que le corpus ne contient que ~920 tokens
|
et `.github/workflows/ci.yml` ont un `start_period`/timeout d'attente
|
||||||
distincts — soit une couche de sortie de 19,4 millions de paramètres mise à
|
généreux pour ça (`1200s`) — voir leurs propres commentaires. C'est un
|
||||||
jour à chaque pas de gradient, pour une table de hachage vide à 99,99 %. Le
|
compromis
|
||||||
passer à `65536` (voir `_TEXTCAT_MODEL_LENGTH` dans `locale_pipeline.py` pour
|
assumé, pas un défaut de configuration à corriger : moins d'itérations
|
||||||
le balayage complet) rend l'entraînement **~5x plus rapide sans changement
|
entraîne plus vite mais laisse des verdicts corrects sous
|
||||||
mesurable de comportement** — même accuracy dev, confiance identique au bit
|
`CONFIDENCE_THRESHOLD` (voir le commentaire de cette constante,
|
||||||
près, mêmes verdicts sur les probes difficiles. `2048` (30x) est atteignable
|
`apps/api/src/lib/recipe-matching/tech-step-matcher.ts`, et celui de
|
||||||
mais décale mesurablement la distribution des scores ; c'est du travail pour
|
`_TRAINING_ITERATIONS`/`_TRAINING_BATCH_SIZE` dans `locale_pipeline.py`
|
||||||
la phase où le jeu d'évaluation aura été élargi.
|
pour le détail du compromis).
|
||||||
|
|
||||||
`docker-compose.yml` et `.github/workflows/ci.yml` gardent un
|
|
||||||
`start_period`/timeout d'attente confortable — un plafond pour runner lent,
|
|
||||||
pas une cible.
|
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
|
|
@ -181,34 +177,20 @@ vraie instance de ce service tournant (voir `apps/api/.env.test`), conforme
|
||||||
|
|
||||||
## Limitations connues
|
## Limitations connues
|
||||||
|
|
||||||
- **`TECH_STEP_EVAL_DATASET` ne couvre que 26 des 74 techniques** (41 cas,
|
- **Démarrage lent** (~15-16 minutes) — voir "Temps de démarrage" ci-dessus.
|
||||||
33 `fr` / 8 `en`) — 48 techniques (65 %) n'ont *aucune* couverture
|
Une optimisation possible non explorée : parallélisation de
|
||||||
d'évaluation. Le F1 agrégé de `0.815` porte sur ~39 spans, ce qui ne
|
l'entraînement `fr`/`en` (actuellement séquentiel,
|
||||||
permet de mesurer ni le F1 par technique, ni les confusions entre
|
`PipelineRegistry.initialize`).
|
||||||
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
|
- **`CONFIDENCE_THRESHOLD` côté `apps/api` est un placeholder** depuis
|
||||||
l'élargissement du corpus à ~74 techniques (calibré à la main, pas via
|
l'élargissement du corpus à ~74 techniques (calibré à la main, pas via
|
||||||
une vraie repasse de `calibrate-tech-step-threshold.ts` contre
|
une vraie repasse de `calibrate-tech-step-threshold.ts` contre
|
||||||
`TECH_STEP_EVAL_DATASET` — voir le commentaire de cette constante).
|
`TECH_STEP_EVAL_DATASET` — voir le commentaire de cette constante).
|
||||||
Conséquence directe du point précédent.
|
- **Textcat bag-of-words** (`spacy.TextCatBOW.v3`) — suffisant pour le
|
||||||
- **Textcat bag-of-words** (`spacy.TextCatBOW.v3`) — n'exploite pas les
|
corpus actuel une fois correctement entraîné, mais n'exploite pas les
|
||||||
vecteurs de mots des modèles `md` chargés. Une mesure ponctuelle donne
|
vecteurs de mots des modèles `md` chargés. Migrable vers une architecture
|
||||||
`spacy.TextCatReduce.v1` (tok2vec + vecteurs statiques) à 98,6 %
|
tok2vec/similarité sans changer le contrat HTTP, si le F1 mesuré par
|
||||||
d'accuracy dev et 0,984 de confiance moyenne, contre 97,3 % / 0,919 pour
|
`apps/api/src/scripts/calibrate-tech-step-threshold.ts` le justifie un
|
||||||
le BOW actuel. **Mais** les mêmes sondes montrent des erreurs *confiantes*
|
jour.
|
||||||
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,
|
- **Reconstruit tout le pipeline à chaque démarrage** (pas de persistance,
|
||||||
pas de fusion incrémentale) — un choix délibéré (voir
|
pas de fusion incrémentale) — un choix délibéré (voir
|
||||||
`LocalePipeline.train`), pas une limitation à lever : `training_data.py`
|
`LocalePipeline.train`), pas une limitation à lever : `training_data.py`
|
||||||
|
|
|
||||||
|
|
@ -54,56 +54,6 @@ _EXCLUDED_COMPONENTS = ["parser", "ner", "tagger", "morphologizer", "attribute_r
|
||||||
|
|
||||||
_TEXTCAT_PIPE_NAME = "textcat"
|
_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`) 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.
|
|
||||||
#
|
|
||||||
# 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. | 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
|
|
||||||
#
|
|
||||||
# `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 —
|
# 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
|
# calibrés empiriquement contre le corpus réel (`training_data.py`), pas
|
||||||
# seulement contre les petits corpus jouets des tests de ce fichier. Trop
|
# seulement contre les petits corpus jouets des tests de ce fichier. Trop
|
||||||
|
|
@ -111,33 +61,33 @@ _TEXTCAT_NGRAM_SIZE = 1
|
||||||
# mais avec une confiance dérisoire — bien en dessous de tout seuil
|
# mais avec une confiance dérisoire — bien en dessous de tout seuil
|
||||||
# raisonnable pour `CONFIDENCE_THRESHOLD` (`tech-step-matcher.ts`).
|
# raisonnable pour `CONFIDENCE_THRESHOLD` (`tech-step-matcher.ts`).
|
||||||
#
|
#
|
||||||
# Historique de calibration (toutes les mesures contre le corpus réel) :
|
# Trois passes de calibration successives, toutes mesurées contre le
|
||||||
|
# corpus réel (74 techniques) :
|
||||||
# 1. `150` itérations (calibré pour le corpus original, ~26 techniques) ne
|
# 1. `150` itérations (calibré pour le corpus original, ~26 techniques) ne
|
||||||
# passe plus à l'échelle une fois élargi : `150` sur 74 classes
|
# passe plus à l'échelle une fois élargi : `150` sur 74 classes
|
||||||
# dépassait 17 minutes pour une seule locale, constaté en CI.
|
# dépassait 17 minutes pour une seule locale, constaté en CI.
|
||||||
# 2. `40` itérations, `examples` limité aux `utterances` (pas les
|
# 2. `40` itérations, `examples` limité aux `utterances` (pas les
|
||||||
# `synonyms`) : ~200s/locale, mais confiance faible sur les clauses
|
# `synonyms`) : ~200s/locale, mais confiance faible sur les clauses
|
||||||
# ancrées sans paraphrase entraînée (`simmer`/`cook`/`bake` ~0.25-0.34).
|
# ancrées sans paraphrase entraînée (`simmer`/`cook`/`bake` ~0.25-0.34).
|
||||||
# 3. Les `synonyms` de chaque technique sont devenus eux aussi des exemples
|
# 3. **Configuration actuelle** : les `synonyms` de chaque technique sont
|
||||||
# d'entraînement du textcat (voir plus bas dans `train()`) — un signal
|
# désormais aussi des exemples d'entraînement du textcat (voir plus bas
|
||||||
# "mot-clé isolé -> sa propre technique" qui manquait complètement avant.
|
# dans `train()`) — un signal "mot-clé isolé -> sa propre technique"
|
||||||
# Gain confirmé depuis par ablation : retirer les synonymes des exemples
|
# qui manquait complètement avant. À `_TRAINING_ITERATIONS` inchangé
|
||||||
# fait retomber la confiance moyenne de `0.816` à `0.714` à configuration
|
# (40), le nombre d'exemples par époque grimpe de ~286 à ~749 et le
|
||||||
# égale. `_TRAINING_ITERATIONS` a été descendu à `25` à cette occasion
|
# temps d'entraînement suit (~535s/locale) ; réduire à `25` retrouve un
|
||||||
# pour compenser le temps d'entraînement.
|
# temps proche de l'étape 2 (~336s/locale, ~670s pour fr+en combinés)
|
||||||
# 4. **Correction du diagnostic** : les étapes 1-3 attribuaient le coût aux
|
# tout en gardant l'essentiel du gain de confiance apporté par les
|
||||||
# itérations et au nombre d'exemples. C'était faux — il venait presque
|
# synonymes : melt ~0.89, preheat ~0.77, compote ~0.78, julienne ~0.76,
|
||||||
# entièrement de `_TEXTCAT_MODEL_LENGTH` (voir son propre commentaire).
|
# zest ~0.66, bake ~0.62, cook ~0.38, simmer ~0.31 — le plus faible
|
||||||
# Une fois ce paramètre corrigé, 25 itérations coûtent ~106 s par locale
|
# observé, mais désormais nettement au-dessus du seuil de confiance
|
||||||
# au lieu de ~406 s. `25` est conservé tel quel : la valeur donne déjà
|
# (contre ~0.25, sous le seuil d'alors, à l'étape 2). Bruit
|
||||||
# 97.3% d'accuracy dev, et la marge de temps désormais disponible n'a
|
# hors-vocabulaire toujours négligeable (anglais via le classifieur
|
||||||
# d'intérêt que si une vraie mesure (F1 par classe, voir
|
# français : `~0.02`). Une vraie repasse de
|
||||||
# `tech-step-evaluator.ts`) montre qu'il y a quelque chose à y gagner.
|
# `calibrate-tech-step-threshold.ts` contre `TECH_STEP_EVAL_DATASET`
|
||||||
#
|
# reste nécessaire pour confirmer/affiner ces valeurs (voir
|
||||||
# Une vraie repasse de `calibrate-tech-step-threshold.ts` contre
|
# `CONFIDENCE_THRESHOLD`'s propre commentaire, `tech-step-matcher.ts`)
|
||||||
# `TECH_STEP_EVAL_DATASET` reste nécessaire (voir `CONFIDENCE_THRESHOLD`'s
|
# — ce qui précède est une mesure manuelle ponctuelle, pas un
|
||||||
# propre commentaire, `tech-step-matcher.ts`) — les chiffres ci-dessus sont
|
# remplacement de cette calibration.
|
||||||
# des mesures ponctuelles, pas un remplacement de cette calibration.
|
|
||||||
_TRAINING_ITERATIONS = 25
|
_TRAINING_ITERATIONS = 25
|
||||||
_TRAINING_BATCH_SIZE = 16
|
_TRAINING_BATCH_SIZE = 16
|
||||||
# Arrêt anticipé : `_TRAINING_ITERATIONS` reste le plafond (le pire cas ne
|
# Arrêt anticipé : `_TRAINING_ITERATIONS` reste le plafond (le pire cas ne
|
||||||
|
|
@ -383,12 +333,8 @@ class LocalePipeline:
|
||||||
"model": {
|
"model": {
|
||||||
"@architectures": "spacy.TextCatBOW.v3",
|
"@architectures": "spacy.TextCatBOW.v3",
|
||||||
"exclusive_classes": True,
|
"exclusive_classes": True,
|
||||||
"ngram_size": _TEXTCAT_NGRAM_SIZE,
|
"ngram_size": 1,
|
||||||
"no_output_layer": False,
|
"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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue