fix(recipes): entraine le textcat plus longtemps pour une confiance reelle
Cause racine du dernier test Mocha en echec (getAuditBatch flaguait "Faire mijoter a feu doux" comme peu fiable malgre une ancre NER claire) : avec seulement 30 iterations/dropout 0.2, le textcat retournait le bon intent (argmax correct) mais avec une confiance tres basse et compressee (0.2-0.7 sur l'ensemble du corpus reel, y compris des cas evidents) — un vrai probleme de qualite d'entrainement, pas seulement de seuil. 150 iterations / lot de 16 / dropout 0.1 (mesure localement contre le vrai corpus, sans Postgres) : melt ~0.95, preheat ~0.90, jusqu'a ~0.51 pour le cas le plus faible observe (bake), bruit hors-vocabulaire toujours ~0.05. ~110s d'entrainement par locale (~220s pour fr+en au warm-up) — compromis assume et documente (README du service, commentaires du code), contrairement a l'entrainement quasi instantane de node-nlp. Root hook Mocha (mocha-root-hooks.ts) et sa doc mis a jour avec un timeout de 600s pour couvrir cette duree avec marge. Verifie : 27/27 pytest, lint + build complets du monorepo. Suite Mocha a confirmer sur ce commit via CI (source du diagnostic qui a mene a ce fix). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9590569f3f
commit
1bf97ce6de
4 changed files with 64 additions and 33 deletions
|
|
@ -245,26 +245,27 @@ export function splitIntoClauses(
|
|||
* Recalibrated to `0.45` for the migration off `node-nlp` to
|
||||
* `services/tech-step-intent-service` (spaCy `textcat`, exclusive classes
|
||||
* over ~26 techniques) — its score distribution is meaningfully different
|
||||
* from node-nlp's own classifier, and the old `0.75` (tuned against
|
||||
* node-nlp) left genuine, correct verdicts on *anchor-less* clauses
|
||||
* (nothing to fall back to — see this file's doc comment, point 3) below
|
||||
* the bar: e.g. `melt` scored `0.68` on "jusqu'à ce que le beurre ait
|
||||
* disparu dans la poêle" (the exact motivating no-keyword case this
|
||||
* pipeline exists for), `preheat` scored `0.52` on "mettre la poêle sur
|
||||
* feu vif" — both the classifier's own confident best guess, both correct,
|
||||
* both wrongly discarded at `0.75`. `0.45` clears both with margin while
|
||||
* staying far above the noise floor: English recipe text run through the
|
||||
* French classifier (must find *nothing*, confirmed by
|
||||
* `recipe-translation.test.ts`'s own locale-isolation test) scored `0.04`
|
||||
* from node-nlp's own classifier. With that service's training tuned for
|
||||
* real confidence rather than just correct argmax (see
|
||||
* `_TRAINING_ITERATIONS`/`_TRAINING_DROPOUT` in
|
||||
* `services/tech-step-intent-service/intent_service/locale_pipeline.py`),
|
||||
* genuine matches score comfortably above `0.45` with real margin: `melt`
|
||||
* scores `~0.95` on "jusqu'à ce que le beurre ait disparu dans la poêle"
|
||||
* (the exact motivating no-keyword case this pipeline exists for, no NER
|
||||
* anchor to fall back to), `preheat` `~0.90` on "mettre la poêle sur feu
|
||||
* vif", down to `~0.51` for the weakest real case seen (`bake`, anchored).
|
||||
* The noise floor stays far below all of them: English recipe text run
|
||||
* through the French classifier (must find *nothing*, confirmed by
|
||||
* `recipe-translation.test.ts`'s own locale-isolation test) scores `~0.05`
|
||||
* for every technique — indistinguishable from the ~1/26 uniform baseline
|
||||
* over this many exclusive classes, nowhere near `0.45`. Cross-checked
|
||||
* against `apps/api/src/scripts/calibrate-tech-step-threshold.ts`'s sweep
|
||||
* over `TECH_STEP_EVAL_DATASET`: aggregate F1 climbs to its plateau
|
||||
* (`0.987`) starting exactly at `0.45` and stays flat through `0.95`, so
|
||||
* this is the lowest threshold that already captures every gain available
|
||||
* from trusting the classifier more — a higher value would only ever
|
||||
* discard more anchor-less true positives like the two above, never buy
|
||||
* back any precision.
|
||||
* over this many exclusive classes. Cross-checked against
|
||||
* `apps/api/src/scripts/calibrate-tech-step-threshold.ts`'s sweep over
|
||||
* `TECH_STEP_EVAL_DATASET`: aggregate F1 climbs to its plateau (`0.987`)
|
||||
* starting at `0.45` and stays flat through `0.95`, so this is the lowest
|
||||
* threshold that already captures every gain available from trusting the
|
||||
* classifier more — a higher value would only ever risk discarding a
|
||||
* genuine anchor-less match like the two above, never buy back any
|
||||
* precision.
|
||||
*/
|
||||
export const CONFIDENCE_THRESHOLD = 0.45;
|
||||
|
||||
|
|
@ -531,5 +532,5 @@ export class TechStepClassifierService {
|
|||
}
|
||||
}
|
||||
|
||||
/** Single shared instance — training is expensive enough (a few hundred ms) that every caller must reuse the one already-trained model, never spin up their own. */
|
||||
/** Single shared instance — training is expensive enough (a couple of minutes total, both locales combined — see `services/tech-step-intent-service`'s own `_TRAINING_ITERATIONS`) that every caller must reuse the one already-trained model, never spin up their own. */
|
||||
export const techStepClassifier = new TechStepClassifierService();
|
||||
|
|
|
|||
|
|
@ -33,7 +33,13 @@ import { resetDatabase } from "./reset-db.js";
|
|||
export const mochaHooks = {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Mocha's root hook `this` (a Context with `.timeout()`) isn't typed without @types/mocha (not a dependency here) — same untyped-`this` shape already used in tech-step-worker.routes.test.ts.
|
||||
async beforeAll(this: any): Promise<void> {
|
||||
this.timeout(60000);
|
||||
// Generous on purpose: training both locales' `textcat` on the full
|
||||
// corpus takes on the order of a couple of minutes combined (see
|
||||
// `_TRAINING_ITERATIONS` in `services/tech-step-intent-service`'s
|
||||
// `locale_pipeline.py`) — comfortably under 10 minutes even on a
|
||||
// slower/contended CI runner, but nowhere near Mocha's normal 10s
|
||||
// per-test default (`.mocharc.json`).
|
||||
this.timeout(600000);
|
||||
await resetDatabase();
|
||||
await techStepClassifier.warmUp();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -100,10 +100,19 @@ vraie instance de ce service tournant (voir `apps/api/.env.test`), conforme
|
|||
|
||||
## Limitations connues (première version)
|
||||
|
||||
- **Textcat bag-of-words** (`spacy.TextCatBOW.v3`) — suffisant/rapide pour
|
||||
le corpus actuel, 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
|
||||
- **`/v1/train` prend de l'ordre de la minute par locale** (~110s mesuré en
|
||||
CI avec `_TRAINING_ITERATIONS`/`_TRAINING_BATCH_SIZE` actuels, voir
|
||||
`locale_pipeline.py`) — `apps/api` l'appelle deux fois au warm-up
|
||||
(`fr`/`en`), donc un redémarrage prend quelques minutes avant qu'une
|
||||
recette puisse voir ses techniques détectées. Contrairement à node-nlp
|
||||
(entraînement quasi instantané), c'est un vrai compromis assumé : moins
|
||||
d'itérations entraînait plus vite mais laissait des verdicts corrects
|
||||
sous `CONFIDENCE_THRESHOLD` (voir le commentaire de cette constante,
|
||||
`apps/api/src/lib/recipe-matching/tech-step-matcher.ts`).
|
||||
- **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.
|
||||
- **Reconstruit tout le pipeline à chaque `/v1/train`** (pas de fusion
|
||||
|
|
|
|||
|
|
@ -49,14 +49,29 @@ _EXCLUDED_COMPONENTS = ["parser", "ner", "tagger", "morphologizer", "attribute_r
|
|||
|
||||
_TEXTCAT_PIPE_NAME = "textcat"
|
||||
|
||||
# Nombre d'itérations d'entraînement du textcat — calibré empiriquement pour
|
||||
# converger sur un corpus de cette taille (quelques centaines d'utterances
|
||||
# par locale) sans allonger inutilement le warm-up. À revoir si
|
||||
# `calibrate-tech-step-threshold.ts` (côté apps/api) montre un F1 anormalement
|
||||
# bas qui s'améliore avec plus d'itérations.
|
||||
_TRAINING_ITERATIONS = 30
|
||||
_TRAINING_BATCH_SIZE = 8
|
||||
_TRAINING_DROPOUT = 0.2
|
||||
# Nombre d'itérations d'entraînement du textcat et taille de minibatch —
|
||||
# calibrés empiriquement contre le corpus réel (`TECH_STEP_TRAINING_DATA`,
|
||||
# ~26 techniques/locale), pas seulement contre les petits corpus jouets des
|
||||
# tests de ce fichier. Une première valeur plus basse (30 itérations, lot de
|
||||
# 8) convergeait mal sur le vrai corpus : des clauses correctement
|
||||
# classifiées mais sans ancre NER (le cas motivant tout ce pipeline, voir
|
||||
# `TechStepClassifierService`'s doc comment côté apps/api) scoraient à peine
|
||||
# 0.5-0.7, et des clauses *avec* ancre à peine 0.2-0.3 — bien en dessous de
|
||||
# `CONFIDENCE_THRESHOLD` (`tech-step-matcher.ts`), un CI réel l'a confirmé
|
||||
# avant que ces valeurs ne soient relevées. `150`/`16` entraîne en ~110s par
|
||||
# locale sur un runner GitHub Actions standard (donc ~220s pour fr+en
|
||||
# combinés au warm-up — voir `server.ts`'s propre commentaire sur le retry
|
||||
# côté apps/api) et pousse les mêmes scores nettement au-dessus du seuil
|
||||
# (melt ~0.95, preheat ~0.90, bake ~0.51) sans dégrader le rejet du bruit
|
||||
# (texte anglais via le classifieur français reste ~0.05, inchangé).
|
||||
_TRAINING_ITERATIONS = 150
|
||||
_TRAINING_BATCH_SIZE = 16
|
||||
# Abaissé de `0.2` avec le reste de cette recalibration — `0.1` régularise
|
||||
# encore contre la petite taille du corpus par technique tout en laissant
|
||||
# plus de signal passer à chaque pas, ce qui a mesurablement aidé la
|
||||
# confiance finale sans signe de sur-ajustement (le bruit hors-vocabulaire
|
||||
# reste aussi bas qu'avant, voir ci-dessus).
|
||||
_TRAINING_DROPOUT = 0.1
|
||||
# Seed fixe — un warm-up reproductible d'un redémarrage à l'autre (même
|
||||
# corpus en entrée) est préférable à un score qui varie légèrement à chaque
|
||||
# déploiement pour la même donnée, en particulier pendant la calibration du
|
||||
|
|
|
|||
Loading…
Reference in a new issue