Aller au contenu
English

Versions et déploiement progressif

  • Leçon 13
  • 45 min
  • Module 5 : Mettre en production, surveiller et rétablir

Ce que vous saurez faire

Mettez une nouvelle version du modèle en production sans risque pour le centre de soutien. Vous donnez un nom et une empreinte à chaque mise en production. Vous testez le candidat sur de vrais billets pendant que l’ancien modèle répond toujours.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • ~/projects/ticket-model du cours d’apprentissage automatique, avec model/ entraîné par python train.py (Empaqueter les artefacts).
  • ~/projects/escalation-service à la fin du module 4, avec ses fichiers .venv, .env et l’image escalation-service:1.0.0. Les 42 tests réussissent.
  • Dans .env, WORKERS=1 et CPUS=1.0. L’exercice du module 4 se terminait avec CPUS=0.5 : rétablissez la valeur, car les temps de ce module ont été mesurés avec un CPU.
  • Si votre projet manque ou ne fonctionne plus, copiez l’instantané nextia-learning-labs/C17/snapshots/end-of-m04. Utiliser un instantané présente les étapes.

Testé avec Python 3.14.6 et Docker Desktop (Docker Engine 29.6.1) sur macOS, avec une puce Apple. Les étapes pour Windows et Linux n’ont pas été testées.

Un nouveau pilote vole d’abord avec un commandant de bord

Section intitulée « Un nouveau pilote vole d’abord avec un commandant de bord »

Un nouveau pilote de ligne a réussi tous ses examens. Malgré cela, il effectue ses premiers vols avec un commandant de bord expérimenté à ses côtés. Le nouveau pilote fait le travail, et le commandant peut reprendre les commandes à tout moment. La compagnie observe le nouveau pilote sur de vrais trajets avant que des passagers dépendent de lui seul.

Une nouvelle version de modèle a besoin de la même chose. Ses tests sur les anciennes données sont ses examens. La question est de savoir comment elle se comporte sur les billets d’aujourd’hui.

Le cours d’apprentissage automatique prévoyait un réentraînement : ajuster le même pipeline sur les billets jusqu’en mai, puis choisir le seuil sur ceux de juin (Planifier les vérifications en production). Le script retrain.py le fait. Placez-le dans ~/projects/ticket-model, à côté de train.py, car il utilise les données et fonctions du cours d’apprentissage automatique. (L’instantané end-of-m05 en conserve une copie dans training/retrain.py.)

Afficher retrain.py (enregistrez-le dans ~/projects/ticket-model)
retrain.py
"""Retrain the escalation model with the newer tickets, for a new version.
Nextia Learning, C17, Module 5. Put this file in your C05 ticket-model folder,
next to train.py, and run it there:
python retrain.py --out model-1.1.0
C05 decided (Plan production checks) to retrain with the May and June tickets
after the warranty policy changed. This script fits the same pipeline on
train + valid (July 2024 to May 2026), and chooses the threshold for the 20%
review capacity on the June tickets (test), which the new model has not seen.
It writes the same three files as train.py, so make_bundle.py can read them.
--balanced weight the rare escalations more (class_weight="balanced")
--threshold 0.19 keep a fixed threshold instead of choosing a new one
Release candidate 1.1.0-rc1 was made with both options. Together they are a
mistake: balanced weights move every score up, so the old threshold flags
far more tickets than the senior team can review.
"""
import argparse
import json
import platform
from datetime import datetime, timezone
from pathlib import Path
import joblib
import numpy as np
import pandas as pd
import sklearn
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import average_precision_score, roc_auc_score
from ticket_model import CATEGORIES, DATA, FEATURES, NUMBERS, TARGET, build_pipeline, load
from train import REVIEW_CAPACITY, choose_threshold, sha256
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--balanced", action="store_true", help='use class_weight="balanced"')
parser.add_argument("--threshold", type=float, help="keep this threshold (not recommended)")
args = parser.parse_args()
fit_on = pd.concat([load("train"), load("valid")], ignore_index=True)
june = load("test")
model = LogisticRegression(max_iter=1000, class_weight="balanced") if args.balanced else None
pipeline = build_pipeline(model).fit(fit_on[FEATURES], fit_on[TARGET])
scores = pipeline.predict_proba(june[FEATURES])[:, 1]
threshold = args.threshold if args.threshold is not None else choose_threshold(scores, REVIEW_CAPACITY)
flagged = scores >= threshold
caught = int((flagged & (june[TARGET] == 1)).sum())
results = {
"rows": len(june), "escalated": int(june[TARGET].sum()),
"roc_auc": round(roc_auc_score(june[TARGET], scores), 3),
"average_precision": round(average_precision_score(june[TARGET], scores), 3),
"flagged": int(flagged.sum()), "flag_rate": round(flagged.mean(), 3),
"precision": round(caught / max(flagged.sum(), 1), 3),
"recall": round(caught / june[TARGET].sum(), 3),
}
args.out.mkdir(exist_ok=False)
joblib.dump(pipeline, args.out / "escalation_model.joblib")
categories = pipeline.named_steps["prepare"].named_transformers_["categories"].categories_
info = {
"model": "Larkfield ticket escalation: logistic regression in a scikit-learn pipeline, "
"retrained with the tickets up to May 2026",
"trained_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
"target": f"{TARGET}: 1 if the ticket is escalated within 72 hours of creation",
"prediction_time": "when the ticket is created",
"features": {
"categories": {name: [str(v) for v in values] for name, values in zip(CATEGORIES, categories)},
"numbers": NUMBERS,
},
"threshold": threshold,
"threshold_chosen_on": "fixed by hand" if args.threshold is not None else "test (June 2026)",
"review_capacity": REVIEW_CAPACITY,
"valid": results,
"evaluated_on": "test (June 2026)",
"train_rows": len(fit_on),
"data_sha256": {name: sha256(DATA / name) for name in
("train.csv", "valid.csv", "test.csv", "extra_features.csv")},
"artifact_sha256": sha256(args.out / "escalation_model.joblib"),
"versions": {"python": platform.python_version(), "pandas": pd.__version__,
"scikit-learn": sklearn.__version__, "numpy": np.__version__, "joblib": joblib.__version__},
}
(args.out / "model_info.json").write_text(json.dumps(info, indent=2) + "\n", encoding="utf-8")
print(f"Trained on {len(fit_on)} tickets (to the end of May). Threshold {threshold}"
f"{' (fixed by hand)' if args.threshold is not None else ' for a review capacity of 20%'}.")
print("June: ROC AUC {roc_auc}, average precision {average_precision}, flagged {flagged} of {rows} "
"({flag_rate:.1%}), precision {precision}, recall {recall}.".format(**results))
print(f"Saved {args.out}/escalation_model.joblib and model_info.json.")
if __name__ == "__main__":
main()

La nouvelle version est 1.1.0 : le nombre du milieu augmente, car le modèle change, mais le contrat reste le même. Tomás a essayé une autre idée : donner plus de poids aux escalades rares, avec class_weight="balanced". Il a gardé l’ancien seuil, 0,19. Il appelle sa version 1.1.0-rc1. « rc » signifie release candidate (version candidate) : une version qui pourrait être mise en production. Priya a créé la version prévue, 1.1.0, sans ces deux options :

Terminal, in ~/projects/ticket-model
python retrain.py --out model-1.1.0
python retrain.py --out model-1.1.0-rc1 --balanced --threshold 0.19

La sortie :

Trained on 18118 tickets (to the end of May). Threshold 0.2 for a review capacity of 20%.
June: ROC AUC 0.749, average precision 0.346, flagged 193 of 1002 (19.3%), precision 0.363, recall 0.446.
Saved model-1.1.0/escalation_model.joblib and model_info.json.
Trained on 18118 tickets (to the end of May). Threshold 0.19 (fixed by hand).
June: ROC AUC 0.751, average precision 0.343, flagged 851 of 1002 (84.9%), precision 0.177, recall 0.962.
Saved model-1.1.0-rc1/escalation_model.joblib and model_info.json.

Tomás : « L’AUC est de 0,751, la meilleure des deux. Le contrat et les vérifications de parité passent, et tous les tests réussissent. On la met en production. »

Regardez les billets signalés en juin : 851 sur 1 002 (84,9 %). L’équipe chevronnée de Grace peut en examiner 20 %. Avec rc1 en production, l’équipe recevrait quatre fois sa capacité.

Faites une prédiction : l’AUC ROC mesure la capacité du modèle à classer les billets escaladés au-dessus des autres. Un modèle peut-il avoir la même AUC et signaler quatre fois plus de billets?

Le graphique montre six vrais billets de juillet, évalués par les deux modèles. Les billets suivent l’ordre des scores de 1.0.0.

  • 1.0.0
  • 1.1.0-rc1
  • Seuil de 0,19
Six billets de juillet. Les deux modèles les classent dans le même ordre, donc le classement est aussi bon. Mais rc1 a augmenté chaque score, et quatre des six dépassent maintenant 0,19.
Description textuelle du graphique

Six billets de juillet, numérotés de 1 à 6 selon l’ordre de leur score de 1.0.0. Les scores de 1.0.0 sont 0,020, 0,026, 0,038, 0,069, 0,106 et 0,255. Les scores de rc1 pour les mêmes billets sont 0,124, 0,173, 0,224, 0,344, 0,502 et 0,734. Les deux courbes montent de gauche à droite : les deux modèles classent donc les billets dans le même ordre. Une ligne pointillée marque le seuil de 0,19. Le modèle 1.0.0 signale un billet sur six (le billet 6). Le candidat rc1 en signale quatre (les billets 3 à 6).

L’AUC compare seulement l’ordre, donc elle reste la même. Mais les poids de classes équilibrés demandent au modèle de traiter les escalades comme si elles étaient aussi fréquentes que les autres billets. Chaque score augmente donc. Le seuil de 0,19 a été choisi pour les anciens scores, et il est maintenant beaucoup trop bas.

La leçon est courte : le seuil appartient à la version du modèle. Un nouveau modèle a besoin d’un seuil choisi sur ses propres scores, comme le fait retrain.py sans --threshold. Comparer le taux de signalement à la capacité permet de détecter le problème de rc1. Vérifier l’AUC ne le permet pas.

Avant tout test sur de vrais billets, chaque candidat devient un paquet, dans son propre dossier :

Terminal, in ~/projects/escalation-service
python make_bundle.py ../ticket-model --version 1.1.0-rc1 --model-dir model-1.1.0-rc1
python make_bundle.py ../ticket-model --version 1.1.0 --model-dir model-1.1.0

La sortie (les paquets du cours dans les exercices) :

Made bundles/escalation-1.1.0-rc1: 14 parity cases, threshold 0.19.
Bundle digest (MODEL_SHA256): fa04bc40bea99723095ec4db600b7060fbfce0f2ec461534bb06c1829b47fe06
Made bundles/escalation-1.1.0: 14 parity cases, threshold 0.2.
Bundle digest (MODEL_SHA256): 382077148b6cad58238e4832f0951c6556f82832bc6e7de1d3a2335bd6d36af0

Vos empreintes sont différentes, car metadata.json contient votre propre valeur trained_at. Utilisez vos empreintes partout ci-dessous.

Il y a maintenant trois versions. Omar demande : « Laquelle répond au centre de soutien aujourd’hui, et qui l’a approuvée? » Après un mois, personne ne peut répondre de mémoire. Une voiture pose le même problème, et un carnet d’entretien le résout. Il indique chaque entretien, sa date, le travail effectué et la personne qui a signé. Amira consigne donc ces renseignements.

Un registre de modèles est le carnet d’entretien d’un modèle. Il décrit chaque version : ce qu’elle est, où elle se trouve, son état et les preuves justifiant cet état. Des outils comme MLflow et Azure Machine Learning tiennent un registre pour une équipe. MLOps et LLMOps : exploiter l’IA en production en utilise un. Pour un seul modèle, un fichier dans le projet suffit. Amira crée RELEASES.md :

RELEASES.md
# Releases of the escalation model
| Version | Bundle digest (MODEL_SHA256) | Status | Since | Evidence | Approved by |
|---|---|---|---|---|---|
| 1.0.0 | `8f233029aa4c...3b74` | live | 2026-10-08 | C05 validation: ROC AUC 0.752, flagged 19.3% of May. Parity 14 of 14. | Grace, Priya |
| 1.1.0-rc1 | `fa04bc40bea9...fe06` | candidate | 2026-10-08 | June: ROC AUC 0.751, flagged 84.9%. | not yet |
| 1.1.0 | `382077148b6c...6af0` | candidate | 2026-10-08 | June: ROC AUC 0.749, flagged 19.3%. | not yet |

Écrivez les empreintes complètes de 64 caractères dans votre fichier; cette page les abrège. Chaque ligne identifie le modèle par sa version et son empreinte. La version sert aux personnes. L’empreinte prouve quels octets ont été exécutés, comme celle de l’image dans L’essentiel de Docker et du nuage pour les développeurs en IA. Une version ne change jamais : une correction crée une nouvelle ligne.

La colonne Status (état) suit un parcours fixe. Le diagramme montre chaque état et la vérification qui fait passer une version au suivant.

Les états d'une version de modèle. make_bundle.py crée un candidat. Quand les tests et la parité réussissent, il passe en parallèle; si une vérification échoue, il est retiré. Depuis l'état en parallèle, il passe en production si les signaux sont bons et si Grace approuve. Si les signaux échouent, il est retiré. Quand une nouvelle version passe en production, l'ancienne devient une version fiable. Un retour en arrière remet la version fiable en production. Elle est retirée quand la nouvelle version a fait ses preuves. Une alerte après la mise en production déclenche un retour en arrière. La version est ensuite retirée quand la cause est connue.
Les états de mise en production d'une version de modèle. Chaque flèche représente une vérification ou une décision, et chacune est inscrite dans RELEASES.md.
Description textuelle du diagramme

Une version commence comme candidate quand make_bundle.py crée son paquet. Si les tests et les vérifications de parité réussissent, elle passe en parallèle. Si une vérification échoue, elle est retirée.

En parallèle, la version évalue de vrais billets, mais ne répond jamais. Si ses signaux sont bons et que Grace approuve, elle passe en production. Si ses signaux échouent, elle est retirée.

Quand une nouvelle version passe en production, l’ancienne devient une version fiable. Elle reste prête, pour que l’équipe puisse y revenir. Un retour en arrière la remet en production. Quand la nouvelle version a fait ses preuves, la version fiable est retirée.

Si une version en production provoque une alerte, elle fait l’objet d’un retour en arrière. Elle est retirée plus tard, quand la cause est connue. La leçon 3 permet de pratiquer cette branche.

En parallèle : évaluer avec les deux, répondre avec un seul

Section intitulée « En parallèle : évaluer avec les deux, répondre avec un seul »

Comment rc1 peut-il traiter de vrais billets sans risque pour l’équipe de Grace? Faites comme la compagnie aérienne : laissez-le travailler à côté du modèle expérimenté, qui garde les commandes.

Un déploiement en parallèle exécute le candidat à côté du modèle en production. Pour chaque requête, le service évalue le billet avec les deux modèles. Il répond avec le modèle en production seulement et inscrit les deux scores dans le journal de prédiction. Le centre de soutien ne voit aucun changement. L’équipe observe le comportement du candidat sur le trafic réel. Les points de terminaison gérés de Choisir l’infrastructure et mesurer offrent la même idée sous le nom de mise en miroir du trafic.

Le centre de soutien envoie le billet T-120631 à escalation-service. Le service interroge 1.0.0, le modèle en production : 0,082, non signalé. Il demande ensuite à 1.1.0-rc1, le modèle en parallèle, d'évaluer le même billet : 0,373, signalé. La ligne du journal conserve les deux réponses. Le centre de soutien reçoit seulement la réponse en production : score de 0,082, indicateur false, modèle 1.0.0.
Une requête en parallèle tirée de l'exécution réelle : un billet, deux modèles, une réponse. Le score du candidat va seulement dans le journal.
Description textuelle du diagramme

Le billet T-120631 arrive au service. Le modèle en production 1.0.0 lui donne un score de 0,082, sous le seuil. Le modèle en parallèle 1.1.0-rc1 donne au même billet un score de 0,373, au-dessus du seuil. Le service écrit une ligne de journal avec les deux scores. Le centre de soutien reçoit 0,082 et « non signalé », du modèle 1.0.0. L’avis du modèle en parallèle ne change rien pour le centre de soutien.

Le service a besoin de trois petits changements. Chacun indique un fichier et la raison.

escalation/settings.py lit deux nouveaux paramètres facultatifs. Ajoutez les deux champs à Settings, après model_sha256 :

escalation/settings.py
model_bundle: Path
model_sha256: str
shadow_bundle: Path | None
shadow_sha256: str | None

Dans load_settings(), lisez-les après la vérification de MODEL_SHA256, puis passez-les à Settings :

escalation/settings.py
shadow = env.get("SHADOW_BUNDLE") or None
shadow_sha = env.get("SHADOW_SHA256", "").strip().lower() or None
if shadow and (shadow_sha is None or len(shadow_sha) != 64):
raise SettingsError("SHADOW_BUNDLE is set, so SHADOW_SHA256 must be its digest")
settings = Settings(
model_bundle=Path(env.get("MODEL_BUNDLE", "bundles/escalation-1.0.0")),
model_sha256=sha,
shadow_bundle=Path(shadow) if shadow else None,
shadow_sha256=shadow_sha,

Le paquet en parallèle passe les mêmes six barrières que le paquet en production, avec sa propre empreinte. Un candidat n’obtient pas plus de confiance parce qu’il fonctionne seulement en parallèle. Son chargement exécute quand même du code dans le service en production.

escalation/main.py charge le modèle en parallèle au démarrage, dans le même try, et le nomme dans le journal :

escalation/main.py
try:
app.state.bundle = load_bundle(settings.model_bundle, settings.model_sha256)
app.state.shadow = None
if settings.shadow_bundle:
app.state.shadow = load_bundle(settings.shadow_bundle, settings.shadow_sha256)
except BundleError as error:
logger.error("Start-up error: %s", error)
raise
# ... app.state.jobs = JobRunner(...) as before ...
shadow = f", shadow {app.state.shadow.version}" if app.state.shadow else ""
logger.info(
"model %s loaded (bundle %s...)%s in %.0f ms",
app.state.bundle.version,
app.state.bundle.digest[:12],
shadow,
(time.perf_counter() - started) * 1000,
)

escalation/routes.py évalue avec le modèle en parallèle dans POST /v1/score, après le score en production et avant la ligne du journal. Ajoutez ce bloc après le dictionnaire record = {...} :

escalation/routes.py
if state.shadow is not None: # score with the candidate too, but never answer with it
[shadow], _ = score_tickets(state.shadow, [ticket])
record |= {"shadow_version": state.shadow.version, "shadow_score": shadow.score, "shadow_flag": shadow.flag}

La réponse après le bloc ne change pas : elle utilise result, le score en production. Dans le même fichier, ModelInfo reçoit un champ, shadow_version: str | None. GET /v1/model le remplit avec shadow.version if shadow else None, où shadow est request.app.state.shadow. Ainsi, toute personne peut demander au service quel candidat il exécute en parallèle.

Exemple détaillé : rc1 en parallèle sur les billets de juillet

Section intitulée « Exemple détaillé : rc1 en parallèle sur les billets de juillet »

Amira envoie les 1 400 billets de juillet au service, dans l’ordre, avec 1.0.0 en production et rc1 en parallèle. Les billets de juillet sont synthétiques, comme toutes les données de Larkfield. compose.yaml ne transmet pas les paramètres du modèle en parallèle. Elle démarre donc le conteneur avec docker run pour ce test.

  1. Arrêtez le service Compose, car il utilise le port 8000 :

    Terminal, in ~/projects/escalation-service
    docker compose down
  2. Reconstruisez l’image. Elle contient maintenant les trois paquets et le nouveau code :

    Terminal
    docker compose build

    L’étiquette reste escalation-service:1.0.0, car compose.yaml la définit. Désormais, l’image contient plusieurs paquets, et les paramètres en choisissent un au démarrage. Dans Compose, MODEL_VERSION choisit le dossier du paquet, et MODEL_SHA256 doit être son empreinte. Au module 4, l’étiquette nommait le code et son unique modèle. Maintenant, l’étiquette nomme le code, et l’empreinte nomme le modèle. La leçon 3 utilise cela pour revenir à un ancien modèle sans reconstruire l’image.

  3. Démarrez le service avec rc1 en parallèle. Utilisez vos propres empreintes :

    Terminal
    docker run -d --name escalation-shadow -p 127.0.0.1:8000:8000 --cpus 1 --memory 512m \
    -e MODEL_SHA256=8f233029aa4c896dce1a5b3dfa970cd085088386bbe7784519f15a5b1a433b74 \
    -e SHADOW_BUNDLE=bundles/escalation-1.1.0-rc1 \
    -e SHADOW_SHA256=fa04bc40bea99723095ec4db600b7060fbfce0f2ec461534bb06c1829b47fe06 \
    escalation-service:1.0.0
  4. Envoyez chaque billet de juillet une fois, dans l’ordre du fichier. --warmup 0 fait compter chaque requête :

    Terminal, with .venv active
    python bench/bench.py http://127.0.0.1:8000 --requests 1400 --warmup 0
  5. Enregistrez le journal du conteneur, avec les lignes de prédiction, dans un fichier :

    Terminal
    docker logs escalation-shadow > predictions.log 2>&1

La ligne de démarrage du journal nomme les deux modèles :

INFO escalation: model 1.0.0 loaded (bundle 8f233029aa4c...), shadow 1.1.0-rc1 in 937 ms

Le service vérifie et charge maintenant deux paquets, donc son démarrage prend plus de temps. Voici une ligne de prédiction du journal :

INFO escalation.predictions: {"request_id": "657a0d114f22", "ticket_id": "T-120631", "model_version": "1.0.0", "score": 0.08204913759363192, "flag": false, "shadow_version": "1.1.0-rc1", "shadow_score": 0.37341856790793543, "shadow_flag": true}

Faites une prédiction : 1.0.0 a signalé 20,1 % des billets de juillet dans le traitement par lots du module 2. Combien des 1 400 billets chaque modèle a-t-il signalés ici?

Comptez les lignes avec un signalement :

Terminal
grep -c '"flag": true' predictions.log
grep -c '"shadow_flag": true' predictions.log

La sortie :

282
1205

Le modèle en production a signalé 282 billets (20,1 %), comme le traitement par lots. rc1 en aurait signalé 1 205 (86,1 %). rc1 a signalé tous les billets signalés par 1.0.0, et 923 de plus. L’équipe de Grace n’a vu aucun de ces signalements supplémentaires : chaque réponse venait de 1.0.0. Amira inscrit le résultat dans RELEASES.md, et rc1 ne passe pas en production.

Supprimez le conteneur de test. Les prochaines leçons utilisent de nouveau Compose :

Terminal
docker rm -f escalation-shadow

Déploiement canari : une petite part des vraies réponses

Section intitulée « Déploiement canari : une petite part des vraies réponses »

Un modèle en parallèle ne répond jamais. Il ne peut donc pas montrer les effets des réponses du candidat. Pour bien des équipes, l’étape suivante est un déploiement canari. La nouvelle version répond à une petite part des vraies requêtes, par exemple 1 sur 10. L’ancienne version répond au reste. Si les chiffres du canari se dégradent, peu d’utilisateurs sont touchés. Le nom vient des oiseaux que les mineurs emportaient dans les mines. Un problème se manifeste chez l’oiseau avant d’atteindre les personnes.

Quel effet aurait un canari de rc1 à 10 % sur l’équipe de Grace? Calculez-le avec les taux de juillet. 90 % des billets obtiennent le taux de 20,1 % de 1.0.0, et 10 % obtiennent celui de 86,1 % de rc1 :

0.9 × 20.1% + 0.1 × 86.1% = 18.1% + 8.6% = 26.7% of all tickets flagged

Même à 10 %, rc1 pousse l’équipe au-delà de sa capacité. Et chaque signalement correspond à un vrai billet qu’une personne chevronnée doit ouvrir.

En parallèle Canari
Qui reçoit la réponse du candidat Personne (journal seulement) Une petite part des vraies requêtes
Risque pour les utilisateurs Aucun Faible, mais réel
Ce que vous apprenez Comment il évalue de vraies entrées Comment il évalue, et les effets de ses réponses
Coût Travail d’évaluation doublé pour chaque requête Une seconde version à exécuter et à surveiller

Utilisez d’abord un déploiement en parallèle quand une mauvaise réponse coûte quelque chose à une personne, comme un signalement ici. Utilisez un canari après de bons résultats en parallèle, pour apprendre ce qu’un journal ne peut pas montrer. Un canari exige un moyen de répartir le trafic. Si vous avez suivi CI/CD, tests et mises en production sûres, sa leçon Mise en production progressive le fait avec des révisions et des poids de trafic. Ce cours n’en a pas besoin.

Exercice guidé : deux candidats, un registre, un modèle en parallèle

Section intitulée « Exercice guidé : deux candidats, un registre, un modèle en parallèle »
  1. Enregistrez retrain.py dans ~/projects/ticket-model, à côté de train.py. Il se trouve dans Le candidat réussit tous les tests.
  2. Dans ~/projects/ticket-model, activez son .venv. Exécutez les deux commandes retrain.py. Comparez vos deux lignes de juin avec la sortie ci-dessus.
  3. Dans ~/projects/escalation-service, créez les deux paquets, comme dans Une mise en production a une version et une empreinte. Copiez les deux empreintes.
  4. Créez RELEASES.md avec trois lignes. Utilisez vos trois empreintes. L’empreinte de 1.0.0 est la valeur MODEL_SHA256 dans votre .env.
  5. Modifiez settings.py, main.py et routes.py, comme dans En parallèle : évaluer avec les deux, répondre avec un seul.
  6. Exécutez python -m pytest. Vous devriez voir 42 passed (42 tests réussis) : les tests s’exécutent sans modèle en parallèle.
  7. Faites les cinq étapes de l’exemple détaillé. Comptez ensuite les signalements.
  8. Avant de supprimer le conteneur, exécutez curl http://127.0.0.1:8000/v1/model, comme dans Connecter l’API. Vérifiez que shadow_version vaut "1.1.0-rc1".
  9. Dans RELEASES.md, ajoutez le résultat en parallèle aux preuves de rc1. Supprimez ensuite le conteneur.
Afficher les résultats attendus

L’étape 2 donne les deux lignes de juin de la sortie ci-dessus : 19,3 % signalés pour 1.1.0, 84,9 % pour rc1. Avec le même ordinateur et les mêmes versions, vos modèles sont identiques à ceux du cours, octet pour octet. Seules les empreintes des paquets diffèrent.

L’étape 7 donne 282 et 1205. L’étape 8 donne (version abrégée) :

{"model_version":"1.0.0","bundle_sha256":"8f233029aa4c...","threshold":0.19,...,"shadow_version":"1.1.0-rc1"}

La preuve de rc1 à l’étape 9 peut être : « Exécution en parallèle, juillet : 1 205 sur 1 400 signalés (86,1 %). Non mis en production. »

Redémarrez le conteneur avec 1.1.0 en mode fantôme plutôt que rc1, puis envoyez les billets de juillet. Avant de compter, faites une prédiction : 1.1.0 signale-t-il plus ou moins de billets que 1.0.0?

Afficher la réponse
INFO escalation: model 1.0.0 loaded (bundle 8f233029aa4c...), shadow 1.1.0 in 726 ms

Les comptes sont 282 et 272. La version 1.1.0 signale 272 billets (19,4 %), soit un peu moins que le modèle en production. Priya a choisi son seuil, 0,20, à partir de ses propres scores de juin. Son taux de signalement reste donc près de 20 %. C’est le résultat réel : 1.1.0 diffère seulement un peu de 1.0.0. La prochaine leçon montre qu’il dépasse lui aussi 20 % à la fin de juillet.

Erreur courante

Le service ne démarre pas : SHADOW_SHA256 manque

Symptôme : vous démarrez le conteneur avec SHADOW_BUNDLE, mais sans -e SHADOW_SHA256=.... Le conteneur s’arrête immédiatement avec le code de sortie 1. docker logs affiche :

Start-up error: SHADOW_BUNDLE is set, so SHADOW_SHA256 must be its digest

Cause : load_settings() refuse un paquet en mode fantôme sans son empreinte. Sans l’empreinte, le service ne peut pas vérifier les fichiers du candidat avant de les charger.

Solution : ajoutez -e SHADOW_SHA256= avec l’empreinte de RELEASES.md, puis redémarrez. Copiez l’empreinte du registre, pas du dossier du paquet : un dossier ne peut pas garantir sa propre intégrité.

Pour un vrai service d’IA : un candidat en mode fantôme s’exécute dans le service en production. Un fichier candidat défectueux ou modifié peut arrêter le service en production ou y exécuter du code. Un modèle en mode fantôme exige donc les mêmes vérifications de confiance qu’un modèle en production.

Vérifiez votre compréhension

rc1 a une aire sous la courbe ROC de 0,751 en juin et réussit tous les tests. Pourquoi ne convenait-il toujours pas à Larkfield?

Afficher la réponse

L’aire sous la courbe (AUC) mesure seulement l’ordre des scores. Les poids de classes équilibrés ont augmenté tous les scores, mais ont conservé leur ordre. L’AUC est donc restée la même. L’ancien seuil de 0,19 convient aux anciens scores. rc1 a donc signalé 84,9 % des billets de juin et 86,1 % de ceux de juillet. C’est quatre fois la capacité de l’équipe chevronnée, soit 20 %. Le seuil appartient à la version du modèle. La vérification avant la mise en production doit comparer le taux de signalement à la capacité.

Vérifiez votre compréhension

Omar demande pourquoi RELEASES.md conserve l'empreinte alors que le nom de la version y figure déjà. Que répondez-vous?

Afficher la réponse

La version est un nom que les gens choisissent. Deux paquets différents peuvent porter le même nom, par exemple un paquet recréé sur un autre ordinateur. L’empreinte vient des fichiers eux-mêmes. Elle prouve donc exactement quels octets ont été exécutés. Le service vérifie l’empreinte au démarrage. Vous pouvez ainsi comparer le registre au service en cours d’exécution.

Vérifiez votre compréhension

Grace dit : aucun candidat ne doit envoyer un seul billet de plus à mon équipe avant qu'on voie ses chiffres. Mode fantôme ou déploiement canari? Pourquoi?

Afficher la réponse

Mode fantôme. Dans un déploiement en mode fantôme, le candidat attribue un score à chaque billet, mais seul le modèle en production répond. Les signalements du candidat vont donc seulement dans le journal. Un déploiement canari répond à une part des vrais billets. Un déploiement canari de rc1 à 10 % signalerait environ 26,7 % de tous les billets.

Récapitulatif

  • retrain.py a créé deux candidats. rc1 classait aussi bien que 1.0.0 (AUC de 0,751), mais signalait 86 % des billets de juillet : le seuil appartient à la version du modèle.
  • Chaque mise en production a une version et une empreinte. RELEASES.md est votre registre de modèles : version, empreinte, état, date, preuves et personne ayant approuvé.
  • Une version passe par des états fixes : candidate, en mode fantôme, en production, reconnue comme fiable, retirée ou annulée.
  • Un modèle en mode fantôme attribue un score à chaque requête avec le candidat et répond avec le modèle en production. Ici, cela ajoutait environ 2 ms par requête.
  • Un déploiement canari répond à une petite part des vraies requêtes. Utilisez-le après un mode fantôme réussi, quand une mauvaise réponse est acceptable pour quelques utilisateurs.

Ensuite, le service surveille ses propres chiffres. Personne n’a donc besoin de grep pour voir que quelque chose ne va pas.

Leçon suivante : Surveiller la santé du modèle