Compare commits

...

4 commits

Author SHA1 Message Date
df0791a694 Merge pull request 'perf(tech-steps): corrige la largeur du textcat — démarrage ~5× plus rapide' (#25) from perf/nlp-textcat-width into main
All checks were successful
CI / build (push) Successful in 3m36s
CI / lint (push) Successful in 3m40s
CI / intent-service-test (push) Successful in 4m28s
CI / e2e (push) Successful in 8m56s
CI / test (push) Successful in 15m47s
Reviewed-on: #25
2026-09-04 08:14:40 +02:00
dfd5a2f650 merge(main): rattrape main (#23 stepper + optimiseur)
All checks were successful
CI / lint (push) Successful in 2m54s
CI / build (push) Successful in 2m50s
CI / intent-service-test (push) Successful in 4m26s
CI / e2e (push) Successful in 8m5s
CI / test (push) Successful in 14m48s
2026-09-03 23:26:20 +02:00
6324a87445 fix(tech-steps): recule sur 65536/unigramme apres regression F1 en CI
Some checks failed
CI / e2e (push) Waiting to run
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / intent-service-test (push) Has been cancelled
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 <noreply@anthropic.com>
2026-09-03 23:25:05 +02:00
393300205d perf(tech-steps): corrige la largeur du textcat, entrainement 32x plus rapide
Some checks failed
CI / build (push) Successful in 1m27s
CI / e2e (push) Successful in 6m39s
CI / lint (push) Successful in 1m41s
CI / intent-service-test (push) Successful in 3m4s
CI / test (push) Failing after 12m43s
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 <noreply@anthropic.com>
2026-09-03 18:01:06 +02:00
4 changed files with 143 additions and 66 deletions

View file

@ -121,11 +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 ~540s (fr) / ~390s (en), # see its own README) — measured at ~3min combined (fr+en) against
# ~930s combined, against the current ~74-technique corpus (see # the current ~74-technique corpus, down from ~930s once
# docker-compose.yml's healthcheck for the same reasoning and why # `spacy.TextCatBOW.v3`'s `length` stopped being left at its
# this grew slightly from the original ~670s). # 262144 default (see `_TEXTCAT_MODEL_LENGTH` in
timeout 1200 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done' # `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 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

View file

@ -100,17 +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 ~540s (fr) / ~390s (en), ~930s combined, # are loaded. Measured at ~3min combined (fr+en) against the current
# against the current ~74-technique corpus — each technique now has # ~74-technique corpus, down from ~930s: the old figure was dominated
# the *same* number of `utterances` per locale as every other # by `spacy.TextCatBOW.v3`'s `length` parameter sitting at its 262144
# (equalized to the corpus's own pre-existing max, 7/5 — see # default, not by the corpus size (see `_TEXTCAT_MODEL_LENGTH` in
# `training_data.py`'s own doc comment for why a flat, larger target # `locale_pipeline.py` for the measurements).
# like 20 was tried and reverted) — `start_period` generous enough #
# that failing checks during that whole window never count against # `start_period` stays deliberately far above that: it only governs
# `retries` (which would otherwise flip this container to # how long failing checks are excused from counting against `retries`
# "unhealthy" mid-training, blocking `app`'s own `depends_on: # (which would otherwise flip this container to "unhealthy"
# condition: service_healthy` indefinitely). # mid-training and block `app`'s `depends_on: condition:
start_period: 1200s # service_healthy` indefinitely), so an over-generous value costs
# 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

View file

@ -89,24 +89,28 @@ côté `apps/api`.
## Temps de démarrage ## Temps de démarrage
**Ce service met plusieurs minutes à devenir `healthy`** — contrairement à Ce service s'entraîne à chaque démarrage du process (voir plus bas pourquoi
node-nlp (entraînement quasi instantané), entraîner le `textcat` sur le rien n'est persisté), ce qui prend de l'ordre de **~110 secondes pour `fr`**
corpus réel (~74 techniques, chaque technique entraînée sur ses `synonyms` / **~75 secondes pour `en`**, soit **~3 minutes** pour les deux locales
en plus de ses `utterances` — voir `locale_pipeline.py`) prend de l'ordre combinées.
de 540 secondes pour `fr` / 390 secondes pour `en` (mesuré localement,
sans GPU), donc environ 930 secondes (~15-16 minutes) pour `fr`+`en` Ça a longtemps été **~930 secondes (15-16 minutes)**, décrit ici comme « un
combinés à chaque démarrage du process — chaque technique a désormais le compromis assumé, pas un défaut de configuration à corriger ». C'était un
même nombre d'`utterances` par locale (voir `training_data.py`'s own doc mauvais diagnostic : le coût ne venait ni des itérations ni de la taille du
comment), légèrement plus qu'avant ce rééquilibrage. `docker-compose.yml` corpus, mais du paramètre `length` de `spacy.TextCatBOW.v3`, laissé à son
et `.github/workflows/ci.yml` ont un `start_period`/timeout d'attente défaut spaCy de `262144` alors que le corpus ne contient que ~920 tokens
généreux pour ça (`1200s`) — voir leurs propres commentaires. C'est un distincts — soit une couche de sortie de 19,4 millions de paramètres mise à
compromis jour à chaque pas de gradient, pour une table de hachage vide à 99,99 %. Le
assumé, pas un défaut de configuration à corriger : moins d'itérations passer à `65536` (voir `_TEXTCAT_MODEL_LENGTH` dans `locale_pipeline.py` pour
entraîne plus vite mais laisse des verdicts corrects sous le balayage complet) rend l'entraînement **~5x plus rapide sans changement
`CONFIDENCE_THRESHOLD` (voir le commentaire de cette constante, mesurable de comportement** — même accuracy dev, confiance identique au bit
`apps/api/src/lib/recipe-matching/tech-step-matcher.ts`, et celui de près, mêmes verdicts sur les probes difficiles. `2048` (30x) est atteignable
`_TRAINING_ITERATIONS`/`_TRAINING_BATCH_SIZE` dans `locale_pipeline.py` mais décale mesurablement la distribution des scores ; c'est du travail pour
pour le détail du compromis). 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 — un plafond pour runner lent,
pas une cible.
## Logs ## Logs
@ -177,20 +181,34 @@ vraie instance de ce service tournant (voir `apps/api/.env.test`), conforme
## Limitations connues ## Limitations connues
- **Démarrage lent** (~15-16 minutes) — voir "Temps de démarrage" ci-dessus. - **`TECH_STEP_EVAL_DATASET` ne couvre que 26 des 74 techniques** (41 cas,
Une optimisation possible non explorée : parallélisation de 33 `fr` / 8 `en`) — 48 techniques (65 %) n'ont *aucune* couverture
l'entraînement `fr`/`en` (actuellement séquentiel, d'évaluation. Le F1 agrégé de `0.815` porte sur ~39 spans, ce qui ne
`PipelineRegistry.initialize`). 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 - **`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).
- **Textcat bag-of-words** (`spacy.TextCatBOW.v3`) — suffisant pour le Conséquence directe du point précédent.
corpus actuel une fois correctement entraîné, mais n'exploite pas les - **Textcat bag-of-words** (`spacy.TextCatBOW.v3`) — n'exploite pas les
vecteurs de mots des modèles `md` chargés. Migrable vers une architecture vecteurs de mots des modèles `md` chargés. Une mesure ponctuelle donne
tok2vec/similarité sans changer le contrat HTTP, si le F1 mesuré par `spacy.TextCatReduce.v1` (tok2vec + vecteurs statiques) à 98,6 %
`apps/api/src/scripts/calibrate-tech-step-threshold.ts` le justifie un d'accuracy dev et 0,984 de confiance moyenne, contre 97,3 % / 0,919 pour
jour. 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, - **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`

View file

@ -54,6 +54,56 @@ _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
@ -61,33 +111,33 @@ _TEXTCAT_PIPE_NAME = "textcat"
# 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`).
# #
# Trois passes de calibration successives, toutes mesurées contre le # Historique de calibration (toutes les mesures contre le corpus réel) :
# 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. **Configuration actuelle** : les `synonyms` de chaque technique sont # 3. Les `synonyms` de chaque technique sont devenus eux aussi des exemples
# désormais aussi des exemples d'entraînement du textcat (voir plus bas # d'entraînement du textcat (voir plus bas dans `train()`) — un signal
# dans `train()`) — un signal "mot-clé isolé -> sa propre technique" # "mot-clé isolé -> sa propre technique" qui manquait complètement avant.
# qui manquait complètement avant. À `_TRAINING_ITERATIONS` inchangé # Gain confirmé depuis par ablation : retirer les synonymes des exemples
# (40), le nombre d'exemples par époque grimpe de ~286 à ~749 et le # fait retomber la confiance moyenne de `0.816` à `0.714` à configuration
# temps d'entraînement suit (~535s/locale) ; réduire à `25` retrouve un # égale. `_TRAINING_ITERATIONS` a été descendu à `25` à cette occasion
# temps proche de l'étape 2 (~336s/locale, ~670s pour fr+en combinés) # pour compenser le temps d'entraînement.
# tout en gardant l'essentiel du gain de confiance apporté par les # 4. **Correction du diagnostic** : les étapes 1-3 attribuaient le coût aux
# synonymes : melt ~0.89, preheat ~0.77, compote ~0.78, julienne ~0.76, # itérations et au nombre d'exemples. C'était faux — il venait presque
# zest ~0.66, bake ~0.62, cook ~0.38, simmer ~0.31 — le plus faible # entièrement de `_TEXTCAT_MODEL_LENGTH` (voir son propre commentaire).
# observé, mais désormais nettement au-dessus du seuil de confiance # Une fois ce paramètre corrigé, 25 itérations coûtent ~106 s par locale
# (contre ~0.25, sous le seuil d'alors, à l'étape 2). Bruit # au lieu de ~406 s. `25` est conservé tel quel : la valeur donne déjà
# hors-vocabulaire toujours négligeable (anglais via le classifieur # 97.3% d'accuracy dev, et la marge de temps désormais disponible n'a
# français : `~0.02`). Une vraie repasse de # d'intérêt que si une vraie mesure (F1 par classe, voir
# `calibrate-tech-step-threshold.ts` contre `TECH_STEP_EVAL_DATASET` # `tech-step-evaluator.ts`) montre qu'il y a quelque chose à y gagner.
# reste nécessaire pour confirmer/affiner ces valeurs (voir #
# `CONFIDENCE_THRESHOLD`'s propre commentaire, `tech-step-matcher.ts`) # Une vraie repasse de `calibrate-tech-step-threshold.ts` contre
# — ce qui précède est une mesure manuelle ponctuelle, pas un # `TECH_STEP_EVAL_DATASET` reste nécessaire (voir `CONFIDENCE_THRESHOLD`'s
# remplacement de cette calibration. # propre commentaire, `tech-step-matcher.ts`) — les chiffres ci-dessus sont
# 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
@ -333,8 +383,12 @@ class LocalePipeline:
"model": { "model": {
"@architectures": "spacy.TextCatBOW.v3", "@architectures": "spacy.TextCatBOW.v3",
"exclusive_classes": True, "exclusive_classes": True,
"ngram_size": 1, "ngram_size": _TEXTCAT_NGRAM_SIZE,
"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,
}, },
}, },
) )