perf(tech-steps): corrige la largeur du textcat — démarrage ~5× plus rapide #25
4 changed files with 61 additions and 42 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -121,14 +121,14 @@ 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 ~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
|
# the current ~74-technique corpus, down from ~930s once
|
||||||
# `spacy.TextCatBOW.v3`'s `length` stopped being left at its
|
# `spacy.TextCatBOW.v3`'s `length` stopped being left at its
|
||||||
# 262144 default (see `_TEXTCAT_MODEL_LENGTH` in
|
# 262144 default (see `_TEXTCAT_MODEL_LENGTH` in
|
||||||
# `locale_pipeline.py`, and docker-compose.yml's healthcheck for
|
# `locale_pipeline.py`, and docker-compose.yml's healthcheck for
|
||||||
# the same reasoning). The timeout stays well above that: it's a
|
# the same reasoning). The timeout stays well above that: it's a
|
||||||
# ceiling for a slow/contended runner, not a target.
|
# 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 install --frozen-lockfile
|
||||||
- run: pnpm --filter api exec prisma migrate deploy
|
- run: pnpm --filter api exec prisma migrate deploy
|
||||||
|
|
|
||||||
|
|
@ -100,19 +100,19 @@ 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 ~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
|
# ~74-technique corpus, down from ~930s: the old figure was dominated
|
||||||
# by `spacy.TextCatBOW.v3`'s `length` parameter sitting at its 262144
|
# by `spacy.TextCatBOW.v3`'s `length` parameter sitting at its 262144
|
||||||
# default, not by the corpus size (see `_TEXTCAT_MODEL_LENGTH` in
|
# default, not by the corpus size (see `_TEXTCAT_MODEL_LENGTH` in
|
||||||
# `locale_pipeline.py` for the measurements).
|
# `locale_pipeline.py` for the measurements).
|
||||||
#
|
#
|
||||||
# `start_period` stays deliberately far above that ~30s: it only
|
# `start_period` stays deliberately far above that: it only governs
|
||||||
# governs how long failing checks are excused from counting against
|
# how long failing checks are excused from counting against `retries`
|
||||||
# `retries` (which would otherwise flip this container to "unhealthy"
|
# (which would otherwise flip this container to "unhealthy"
|
||||||
# mid-training and block `app`'s `depends_on: condition:
|
# mid-training and block `app`'s `depends_on: condition:
|
||||||
# service_healthy` indefinitely), so an over-generous value costs
|
# service_healthy` indefinitely), so an over-generous value costs
|
||||||
# nothing while an under-tight one breaks startup on a slow runner.
|
# 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
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -90,23 +90,27 @@ 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 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**,
|
rien n'est persisté), ce qui prend de l'ordre de **~110 secondes pour `fr`**
|
||||||
soit ~30 secondes pour `fr`+`en` combinés.
|
/ **~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
|
Ç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
|
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
|
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
|
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
|
défaut spaCy de `262144` alors que le corpus ne contient que ~920 tokens
|
||||||
tokens distincts — soit une couche de sortie de 19,4 millions de paramètres
|
distincts — soit une couche de sortie de 19,4 millions de paramètres mise à
|
||||||
mise à jour à chaque pas de gradient, pour une table de hachage vide à 99 %.
|
jour à chaque pas de gradient, pour une table de hachage vide à 99,99 %. Le
|
||||||
Le passer à `2048` (voir `_TEXTCAT_MODEL_LENGTH` dans `locale_pipeline.py`
|
passer à `65536` (voir `_TEXTCAT_MODEL_LENGTH` dans `locale_pipeline.py` pour
|
||||||
pour le tableau de mesures complet) rend l'entraînement **30x plus rapide à
|
le balayage complet) rend l'entraînement **~5x plus rapide sans changement
|
||||||
qualité strictement identique** — même accuracy, même confiance.
|
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
|
`docker-compose.yml` et `.github/workflows/ci.yml` gardent un
|
||||||
`start_period`/timeout d'attente confortable, mais il n'a plus besoin d'être
|
`start_period`/timeout d'attente confortable — un plafond pour runner lent,
|
||||||
dimensionné en minutes — voir leurs propres commentaires.
|
pas une cible.
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,33 +61,48 @@ _TEXTCAT_PIPE_NAME = "textcat"
|
||||||
# explicitement ici. Avec `no_output_layer: False` et 74 techniques, la
|
# 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
|
# 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
|
# l'optimiseur Adam met à jour à *chaque* pas de gradient. Or le corpus
|
||||||
# (`training_data.py`) tient dans quelques milliers de tokens distincts :
|
# (`training_data.py`) ne contient que ~920 tokens distincts : l'écrasante
|
||||||
# l'écrasante majorité de cette table de hachage n'est jamais peuplée. On
|
# majorité de cette table de hachage n'est jamais peuplée. On payait un
|
||||||
# payait donc un temps de calcul proportionnel à une table vide.
|
# temps de calcul proportionnel à une table vide.
|
||||||
#
|
#
|
||||||
# Mesuré contre le corpus réel (74 techniques, locale `fr`, 25 itérations,
|
# Balayage mesuré contre le corpus réel (74 techniques, `fr`, 25 itérations,
|
||||||
# jeu de dev = la dernière `utterance` de chaque technique tenue hors
|
# `ngram_size=1`, jeu de dev = la dernière `utterance` de chaque technique
|
||||||
# entraînement) :
|
# 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
|
# length | params | accuracy dev | confiance moy. | collisions | train
|
||||||
# ---------|-----------|--------------|----------------|-------------
|
# ---------|-----------|--------------|----------------|------------|------
|
||||||
# 262144 | 19398730 | 97.3% | 0.809 | 406 s
|
# 262144 | 19398730 | 97.3% | 0.809 | 2/920 | 400 s
|
||||||
# 2048 | 151626 | 97.3% | 0.816 | 13 s
|
# 65536 | 4849722 | 97.3% | 0.809 | 10/920 | 106 s
|
||||||
# 8192 | 606218 | 97.3% | 0.910 | 24 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
|
# `65536` a été retenu comme la valeur **conservatrice** : confiance moyenne
|
||||||
# n'est pas un compromis vitesse/qualité, juste du calcul rendu. Si le
|
# identique au bit près au défaut spaCy (0.809), 1 % de collisions, et
|
||||||
# corpus grossissait au point de saturer 2048 buckets (collisions de hachage
|
# pourtant **~5x plus rapide** (et ~15x moins de paramètres). C'est un gain
|
||||||
# visibles sous forme de F1 par classe qui chute sans autre cause), la
|
# de calcul, pas un compromis de qualité — le jeu de dev, les probes
|
||||||
# valeur se remonte ; `8192` reste bien en deçà du coût d'origine.
|
# difficiles (`bake`/`blindBake`, `boil`/`poach`…) et la confiance sont
|
||||||
_TEXTCAT_MODEL_LENGTH = 2048
|
# inchangés.
|
||||||
# Bigrammes plutôt qu'unigrammes : le corpus est plein de locutions verbales
|
#
|
||||||
# ("faire fondre", "laisser mijoter", "faire revenir", "porter à ébullition")
|
# `2048` a d'abord été livré puis reculé : à cette largeur, 305 des 920
|
||||||
# qu'un sac d'unigrammes ne peut pas distinguer de leurs constituants isolés.
|
# tokens du corpus entrent en collision (33 %), ce qui replie des mots
|
||||||
# Mesuré dans les mêmes conditions que le tableau ci-dessus : accuracy dev
|
# distincts ensemble, décale mesurablement la confiance (0.816) et pourrait
|
||||||
# inchangée (97.3%) mais confiance moyenne sur la classe correcte
|
# donc décaler le F1 par classe — impossible à vérifier hors de la CI (gate
|
||||||
# **0.816 -> 0.919**, pour +0.5 s d'entraînement.
|
# `tech-step-eval.test.ts`, nécessite Postgres). Descendre en dessous de
|
||||||
_TEXTCAT_NGRAM_SIZE = 2
|
# `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
|
||||||
|
|
@ -113,7 +128,7 @@ _TEXTCAT_NGRAM_SIZE = 2
|
||||||
# 4. **Correction du diagnostic** : les étapes 1-3 attribuaient le coût aux
|
# 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
|
# itérations et au nombre d'exemples. C'était faux — il venait presque
|
||||||
# entièrement de `_TEXTCAT_MODEL_LENGTH` (voir son propre commentaire).
|
# 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à
|
# 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
|
# 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
|
# d'intérêt que si une vraie mesure (F1 par classe, voir
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue