fix(recipes): entraine le textcat sur les synonymes en plus des utterances
Suite a une suggestion de revue de code : le textcat n'apprenait jusqu'ici que sur entry.utterances, jamais sur entry.synonyms (deja utilises pour le PhraseMatcher). Ajouter le mot-cle isole comme exemple positif de sa propre technique ameliore radicalement la confiance sur les cas ancres sans paraphrase entrainee. Mesures sur le vrai corpus (74 techniques) : - 40 iterations + synonymes (749 exemples vs 286 avant) : gain de confiance massif (simmer 0.25->0.60, cook 0.33->0.60, bake 0.34->0.86) mais temps d'entrainement multiplie par 2.6 (~535s/locale, ~17min combine pour fr+en — inacceptable). - 15 iterations + synonymes : retour a un temps raisonnable (~205s) mais qualite pire qu'avant (simmer/cook repassent sous le seuil de confiance) — les exemples supplementaires ne compensent pas la perte d'epoques a ce point. - 25 iterations + synonymes (retenu) : ~336s/locale (~670s combine), meilleur compromis — tous les cas mesures s'ameliorent par rapport a la config precedente (simmer 0.25->0.31, cook 0.33->0.38, bake 0.34->0.62, zest 0.64->0.66, julienne 0.56->0.76, compote 0.76->0.78), bruit hors-vocabulaire toujours negligeable (~0.02). CONFIDENCE_THRESHOLD releve de 0.2 a 0.25 (le cas le plus faible mesure est maintenant 0.31, avec plus de marge qu'avant). docker-compose.yml (start_period 900s) et la CI (timeout 900s) ajustes pour le nouveau temps de demarrage (~11 min pour fr+en combines, contre ~7 min avant). Deux autres pistes de la meme revue examinees et non retenues avec justification : classe __OTHER__/negatifs hors-domaine (le bruit mesure est deja bas, ~0.02, sans le symptome que cette classe corrige) et boost de score post-traitement si le NER confirme l'intention predite (casserait la garantie "score brut, jamais corrige par l'ancre" que services/tech-step-llm-worker's audit de faible confiance depend explicitement d'avoir, voir le commentaire de TechStepClauseClassification dans tech-step-matcher.ts). Verifie : 28/28 pytest (dont le vrai corpus complet, ~10.5 min pour la suite complete), lint + build du monorepo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
19e0507852
commit
f2fd3b961a
8 changed files with 101 additions and 71 deletions
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
|
@ -96,13 +96,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 ~200s per locale (~400s for
|
# see its own README) — measured at ~335s per locale (~670s for
|
||||||
# fr+en combined) against the current ~74-technique corpus, so
|
# fr+en combined) against the current ~74-technique corpus,
|
||||||
# this wait is generous rather than the fast "base models only"
|
# trained on each technique's own synonyms in addition to its
|
||||||
# check it used to be before that service trained itself at
|
# example phrases, so this wait is generous rather than the fast
|
||||||
# startup (see docker-compose.yml's healthcheck for the same
|
# "base models only" check it used to be before that service
|
||||||
# reasoning).
|
# trained itself at startup (see docker-compose.yml's healthcheck
|
||||||
timeout 600 bash -c 'until curl -sf http://localhost:8000/health > /dev/null; do sleep 2; done'
|
# for the same reasoning).
|
||||||
|
timeout 900 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
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ pnpm --filter api prisma:seed
|
||||||
# Microservice de détection des techniques (spaCy) — requis, `pnpm dev:api`
|
# Microservice de détection des techniques (spaCy) — requis, `pnpm dev:api`
|
||||||
# ne peut plus détecter aucune technique de cuisine sans lui. Lance-le en
|
# ne peut plus détecter aucune technique de cuisine sans lui. Lance-le en
|
||||||
# premier et laisse-le tourner : il s'entraîne lui-même à chaque démarrage
|
# premier et laisse-le tourner : il s'entraîne lui-même à chaque démarrage
|
||||||
# (~7 minutes pour le corpus actuel, voir son propre README) avant de
|
# (~11 minutes pour le corpus actuel, voir son propre README) avant de
|
||||||
# répondre quoi que ce soit sur /health.
|
# répondre quoi que ce soit sur /health.
|
||||||
cd services/tech-step-intent-service
|
cd services/tech-step-intent-service
|
||||||
uv sync
|
uv sync
|
||||||
|
|
|
||||||
|
|
@ -249,22 +249,24 @@ export function splitIntoClauses(
|
||||||
* changes (more exclusive classes generally means a *lower* natural
|
* changes (more exclusive classes generally means a *lower* natural
|
||||||
* confidence ceiling, softmax mass spread thinner).
|
* confidence ceiling, softmax mass spread thinner).
|
||||||
*
|
*
|
||||||
* Currently `0.2`, set against the corpus as expanded to ~74 techniques
|
* Currently `0.25`, set against the corpus as expanded to ~74 techniques
|
||||||
* (`services/tech-step-intent-service/intent_service/training_data.py`,
|
* (`services/tech-step-intent-service/intent_service/training_data.py`,
|
||||||
* `_TRAINING_ITERATIONS = 40`) from manual spot-checks, not yet a real
|
* `_TRAINING_ITERATIONS = 25`, `textcat` trained on each technique's own
|
||||||
* `calibrate-tech-step-threshold.ts` sweep against
|
* `synonyms` in addition to its `utterances` — see that constant's own
|
||||||
|
* comment for the calibration history) from manual spot-checks, not yet a
|
||||||
|
* real `calibrate-tech-step-threshold.ts` sweep against
|
||||||
* `TECH_STEP_EVAL_DATASET` (needs Postgres — see that script's own doc
|
* `TECH_STEP_EVAL_DATASET` (needs Postgres — see that script's own doc
|
||||||
* comment): observed real-case scores ranged `0.25`-`0.89` (`simmer`
|
* comment): observed real-case scores ranged `0.31`-`0.89` (`simmer`
|
||||||
* lowest, still correct and anchored anyway; `melt` highest, the
|
* lowest, still correct in argmax and anchored anyway; `melt` highest, the
|
||||||
* motivating anchor-less case), against a noise floor around `0.02`
|
* motivating anchor-less case), against a noise floor around `0.02`
|
||||||
* (English text through the French classifier). `0.2` sits with margin
|
* (English text through the French classifier). `0.25` sits with real
|
||||||
* above the noise floor and below every real case seen so far, but **this
|
* margin above the noise floor and below every real case seen so far, but
|
||||||
* is a placeholder pending the real eval-dataset sweep** — do not treat it
|
* **this is a placeholder pending the real eval-dataset sweep** — do not
|
||||||
* as load-bearing precision the way the previous `0.45` (calibrated
|
* treat it as load-bearing precision the way the original `0.45`
|
||||||
* against the ~26-technique corpus, `TECH_STEP_EVAL_DATASET` F1 plateauing
|
* (calibrated against the ~26-technique corpus, `TECH_STEP_EVAL_DATASET`
|
||||||
* exactly there) was.
|
* F1 plateauing exactly there) was.
|
||||||
*/
|
*/
|
||||||
export const CONFIDENCE_THRESHOLD = 0.2;
|
export const CONFIDENCE_THRESHOLD = 0.25;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One clause's full classification detail — the finer-grained sibling of
|
* One clause's full classification detail — the finer-grained sibling of
|
||||||
|
|
|
||||||
|
|
@ -94,14 +94,15 @@ 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 ~200s per locale (~400s for fr+en combined)
|
# are loaded. Measured at ~335s per locale (~670s for fr+en combined)
|
||||||
# against the current ~74-technique corpus
|
# against the current ~74-technique corpus, trained on each
|
||||||
# (`intent_service/training_data.py`) — `start_period` generous
|
# technique's own synonyms in addition to its example phrases
|
||||||
# enough that failing checks during that whole window never count
|
# (`intent_service/locale_pipeline.py`'s `_TRAINING_ITERATIONS`) —
|
||||||
# against `retries` (which would otherwise flip this container to
|
# `start_period` generous enough that failing checks during that
|
||||||
# "unhealthy" mid-training, blocking `app`'s own
|
# whole window never count against `retries` (which would otherwise
|
||||||
# `depends_on: condition: service_healthy` indefinitely).
|
# flip this container to "unhealthy" mid-training, blocking `app`'s
|
||||||
start_period: 600s
|
# own `depends_on: condition: service_healthy` indefinitely).
|
||||||
|
start_period: 900s
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,11 @@ côté `apps/api`.
|
||||||
|
|
||||||
**Ce service met plusieurs minutes à devenir `healthy`** — contrairement à
|
**Ce service met plusieurs minutes à devenir `healthy`** — contrairement à
|
||||||
node-nlp (entraînement quasi instantané), entraîner le `textcat` sur le
|
node-nlp (entraînement quasi instantané), entraîner le `textcat` sur le
|
||||||
corpus réel (~74 techniques) prend de l'ordre de 200 secondes par locale
|
corpus réel (~74 techniques, chaque technique entraînée sur ses `synonyms`
|
||||||
(mesuré localement, sans GPU), donc environ 400 secondes (~7 minutes) pour
|
en plus de ses `utterances` — voir `locale_pipeline.py`) prend de l'ordre
|
||||||
`fr`+`en` combinés à chaque démarrage du process. `docker-compose.yml` et
|
de 335 secondes par locale (mesuré localement, sans GPU), donc environ 670
|
||||||
|
secondes (~11 minutes) pour `fr`+`en` combinés à chaque démarrage du
|
||||||
|
process. `docker-compose.yml` et
|
||||||
`.github/workflows/ci.yml` ont un `start_period`/timeout d'attente
|
`.github/workflows/ci.yml` ont un `start_period`/timeout d'attente
|
||||||
généreux pour ça — voir leurs propres commentaires. C'est un compromis
|
généreux pour ça — voir leurs propres commentaires. C'est un compromis
|
||||||
assumé, pas un défaut de configuration à corriger : moins d'itérations
|
assumé, pas un défaut de configuration à corriger : moins d'itérations
|
||||||
|
|
@ -149,7 +151,7 @@ vraie instance de ce service tournant (voir `apps/api/.env.test`), conforme
|
||||||
|
|
||||||
## Limitations connues
|
## Limitations connues
|
||||||
|
|
||||||
- **Démarrage lent** (~7 minutes) — voir "Temps de démarrage" ci-dessus.
|
- **Démarrage lent** (~11 minutes) — voir "Temps de démarrage" ci-dessus.
|
||||||
Une optimisation possible non explorée : parallélisation de
|
Une optimisation possible non explorée : parallélisation de
|
||||||
l'entraînement `fr`/`en` (actuellement séquentiel,
|
l'entraînement `fr`/`en` (actuellement séquentiel,
|
||||||
`PipelineRegistry.initialize`).
|
`PipelineRegistry.initialize`).
|
||||||
|
|
|
||||||
|
|
@ -56,29 +56,37 @@ _TEXTCAT_PIPE_NAME = "textcat"
|
||||||
# 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
|
||||||
# peu d'itérations laisse des clauses correctement classifiées (bon argmax)
|
# peu d'itérations laisse des clauses correctement classifiées (bon argmax)
|
||||||
# mais avec une confiance dérisoire (`0.02`-`0.08` observé à 5-15
|
# mais avec une confiance dérisoire — bien en dessous de tout seuil
|
||||||
# itérations) — bien en dessous de tout seuil raisonnable pour
|
# raisonnable pour `CONFIDENCE_THRESHOLD` (`tech-step-matcher.ts`).
|
||||||
# `CONFIDENCE_THRESHOLD` (`tech-step-matcher.ts`).
|
|
||||||
#
|
#
|
||||||
# `150` convenait au corpus original (~26 techniques) mais ne passe plus à
|
# Trois passes de calibration successives, toutes mesurées contre le
|
||||||
# l'échelle une fois le corpus élargi à ~74 : le temps d'entraînement croît
|
# corpus réel (74 techniques) :
|
||||||
# avec le nombre de classes autant qu'avec les itérations (mesuré :
|
# 1. `150` itérations (calibré pour le corpus original, ~26 techniques) ne
|
||||||
# ~150s pour seulement 30 itérations sur 74 classes, contre ~110s pour 150
|
# passe plus à l'échelle une fois élargi : `150` sur 74 classes
|
||||||
# itérations sur 26 classes) — `150` sur 74 classes dépassait 17 minutes
|
# dépassait 17 minutes pour une seule locale, constaté en CI.
|
||||||
# rien que pour une locale, constaté en CI. `40` est le meilleur compromis
|
# 2. `40` itérations, `examples` limité aux `utterances` (pas les
|
||||||
# trouvé empiriquement sur ce corpus élargi : ~200s par locale (~400s pour
|
# `synonyms`) : ~200s/locale, mais confiance faible sur les clauses
|
||||||
# fr+en combinés au démarrage), avec des scores exploitables sur tous les
|
# ancrées sans paraphrase entraînée (`simmer`/`cook`/`bake` ~0.25-0.34).
|
||||||
# cas testés à la main (melt ~0.89, preheat ~0.76, compote ~0.76, zest
|
# 3. **Configuration actuelle** : les `synonyms` de chaque technique sont
|
||||||
# ~0.64, julienne ~0.56, cook/bake ~0.33, simmer ~0.25 — le plus faible
|
# désormais aussi des exemples d'entraînement du textcat (voir plus bas
|
||||||
# observé, toujours correct en argmax et de toute façon ancré par NER) et
|
# dans `train()`) — un signal "mot-clé isolé -> sa propre technique"
|
||||||
# un bruit hors-vocabulaire qui reste négligeable (anglais via le
|
# qui manquait complètement avant. À `_TRAINING_ITERATIONS` inchangé
|
||||||
# classifieur français : `~0.02`). Une vraie repasse de
|
# (40), le nombre d'exemples par époque grimpe de ~286 à ~749 et le
|
||||||
# `calibrate-tech-step-threshold.ts` contre `TECH_STEP_EVAL_DATASET` reste
|
# temps d'entraînement suit (~535s/locale) ; réduire à `25` retrouve un
|
||||||
# nécessaire pour confirmer/affiner ces deux valeurs (voir
|
# temps proche de l'étape 2 (~336s/locale, ~670s pour fr+en combinés)
|
||||||
# `CONFIDENCE_THRESHOLD`'s propre commentaire, `tech-step-matcher.ts`) — ce
|
# tout en gardant l'essentiel du gain de confiance apporté par les
|
||||||
# qui suit est une mesure manuelle ponctuelle, pas un remplacement de cette
|
# synonymes : melt ~0.89, preheat ~0.77, compote ~0.78, julienne ~0.76,
|
||||||
# calibration.
|
# zest ~0.66, bake ~0.62, cook ~0.38, simmer ~0.31 — le plus faible
|
||||||
_TRAINING_ITERATIONS = 40
|
# 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.
|
||||||
|
_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
|
||||||
# change pas), un corpus/locale qui converge plus vite n'a pas à payer les
|
# change pas), un corpus/locale qui converge plus vite n'a pas à payer les
|
||||||
|
|
@ -87,12 +95,12 @@ _TRAINING_BATCH_SIZE = 16
|
||||||
# sous la meilleure perte vue jusqu'ici ; `_EARLY_STOPPING_PATIENCE` époques
|
# sous la meilleure perte vue jusqu'ici ; `_EARLY_STOPPING_PATIENCE` époques
|
||||||
# consécutives sans progrès arrêtent l'entraînement.
|
# consécutives sans progrès arrêtent l'entraînement.
|
||||||
#
|
#
|
||||||
# Mesuré contre le corpus réel (74 techniques) : ne se déclenche jamais dans
|
# Mesuré contre le corpus réel (74 techniques, budget de 40 itérations,
|
||||||
# le budget actuel de 40 itérations — la perte continue de baisser
|
# avant le passage à 25) : ne s'est jamais déclenché — la perte continuait
|
||||||
# significativement sur toute la plage (cohérent avec la confiance qui
|
# de baisser significativement sur toute la plage (cohérent avec la
|
||||||
# grimpe encore nettement entre 15 et 40 itérations, voir le commentaire de
|
# confiance qui grimpait encore nettement entre 15 et 40 itérations, voir
|
||||||
# `_TRAINING_ITERATIONS`). Ce n'est donc pas un gain de temps aujourd'hui,
|
# le commentaire de `_TRAINING_ITERATIONS`). Ce n'est donc pas un gain de
|
||||||
# mais un filet de sécurité peu coûteux pour la suite : si
|
# temps aujourd'hui, mais un filet de sécurité peu coûteux pour la suite : si
|
||||||
# `_TRAINING_ITERATIONS` est un jour augmenté pour une meilleure confiance,
|
# `_TRAINING_ITERATIONS` est un jour augmenté pour une meilleure confiance,
|
||||||
# ceci évite de payer des itérations supplémentaires une fois la
|
# ceci évite de payer des itérations supplémentaires une fois la
|
||||||
# convergence réellement atteinte, sans qu'il faille retrouver le bon
|
# convergence réellement atteinte, sans qu'il faille retrouver le bon
|
||||||
|
|
@ -223,8 +231,11 @@ class LocalePipeline:
|
||||||
def train(self, entries: list[TrainEntry]) -> tuple[int, int, int]:
|
def train(self, entries: list[TrainEntry]) -> tuple[int, int, int]:
|
||||||
"""Reconstruit le `textcat` et le `PhraseMatcher` de ce pipeline à
|
"""Reconstruit le `textcat` et le `PhraseMatcher` de ce pipeline à
|
||||||
partir de `entries` (le tokenizer/les vecteurs restent ceux chargés
|
partir de `entries` (le tokenizer/les vecteurs restent ceux chargés
|
||||||
par `preload()`). Retourne `(label_count, utterance_count,
|
par `preload()`). Retourne `(label_count, example_count,
|
||||||
synonym_count)` pour la réponse `/v1/train`.
|
synonym_count)` pour la journalisation (`pipeline_registry.py`) —
|
||||||
|
`example_count` est le nombre réel d'exemples donnés au `textcat`
|
||||||
|
(`utterances` *et* `synonyms` combinés, voir plus bas), pas
|
||||||
|
seulement `entry.utterances`.
|
||||||
|
|
||||||
`entries` vide retombe à `is_trained == False` plutôt que de lever —
|
`entries` vide retombe à `is_trained == False` plutôt que de lever —
|
||||||
un appelant qui n'a rien à entraîner pour cette locale obtient le
|
un appelant qui n'a rien à entraîner pour cette locale obtient le
|
||||||
|
|
@ -298,10 +309,16 @@ class LocalePipeline:
|
||||||
textcat.add_label(entry.uid)
|
textcat.add_label(entry.uid)
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
for utterance in entry.utterances:
|
|
||||||
doc = nlp.make_doc(utterance)
|
|
||||||
cats = {other.uid: 0.0 for other in entries}
|
cats = {other.uid: 0.0 for other in entries}
|
||||||
cats[entry.uid] = 1.0
|
cats[entry.uid] = 1.0
|
||||||
|
# `synonyms` (déjà utilisés pour le `PhraseMatcher` ci-dessus)
|
||||||
|
# sont aussi de bonnes phrases d'entraînement pour le
|
||||||
|
# `textcat` — un texte réduit au mot-clé lui-même ("fondre",
|
||||||
|
# "faire fondre") est le cas le plus net qui soit pour sa
|
||||||
|
# propre technique, et n'était auparavant vu par le textcat
|
||||||
|
# que noyé dans le contexte plus riche des `utterances`.
|
||||||
|
for text in (*entry.synonyms, *entry.utterances):
|
||||||
|
doc = nlp.make_doc(text)
|
||||||
examples.append(Example.from_dict(doc, {"cats": cats}))
|
examples.append(Example.from_dict(doc, {"cats": cats}))
|
||||||
|
|
||||||
# Graine le RNG Python *et* celui de numpy/thinc sous-jacent à
|
# Graine le RNG Python *et* celui de numpy/thinc sous-jacent à
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,16 @@ class PipelineRegistry:
|
||||||
for locale, pipeline in self._pipelines.items():
|
for locale, pipeline in self._pipelines.items():
|
||||||
pipeline.preload()
|
pipeline.preload()
|
||||||
entries = [TrainEntry(**entry) for entry in entries_for_locale(locale)]
|
entries = [TrainEntry(**entry) for entry in entries_for_locale(locale)]
|
||||||
label_count, utterance_count, synonym_count = pipeline.train(entries)
|
label_count, example_count, synonym_count = pipeline.train(entries)
|
||||||
logger.info(
|
logger.info(
|
||||||
"tech-step NLP pipeline trained",
|
"tech-step NLP pipeline trained",
|
||||||
extra={
|
extra={
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
"labelCount": label_count,
|
"labelCount": label_count,
|
||||||
"utteranceCount": utterance_count,
|
# Nombre réel d'exemples donnés au textcat (utterances
|
||||||
|
# *et* synonyms combinés — voir `LocalePipeline.train`),
|
||||||
|
# pas seulement le compte d'`utterances` du corpus.
|
||||||
|
"exampleCount": example_count,
|
||||||
"synonymCount": synonym_count,
|
"synonymCount": synonym_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,15 @@ _ENTRIES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_train_returns_label_utterance_and_synonym_counts():
|
def test_train_returns_label_example_and_synonym_counts():
|
||||||
pipeline = LocalePipeline("fr")
|
pipeline = LocalePipeline("fr")
|
||||||
label_count, utterance_count, synonym_count = pipeline.train(_ENTRIES)
|
label_count, example_count, synonym_count = pipeline.train(_ENTRIES)
|
||||||
assert label_count == 2
|
assert label_count == 2
|
||||||
assert utterance_count == sum(len(entry.utterances) for entry in _ENTRIES)
|
# `example_count` couvre les utterances *et* les synonyms (voir
|
||||||
|
# LocalePipeline.train — les synonymes sont aussi des exemples
|
||||||
|
# d'entraînement pour le textcat, pas seulement pour le PhraseMatcher).
|
||||||
|
expected_examples = sum(len(entry.utterances) + len(entry.synonyms) for entry in _ENTRIES)
|
||||||
|
assert example_count == expected_examples
|
||||||
assert synonym_count == sum(len(entry.synonyms) for entry in _ENTRIES)
|
assert synonym_count == sum(len(entry.synonyms) for entry in _ENTRIES)
|
||||||
assert pipeline.is_trained is True
|
assert pipeline.is_trained is True
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue