Versions et déploiement progressif
- Leçon 13
- 45 min
- Module 5 : Mettre en production, surveiller et rétablir
Leçon 13 sur 15 · Déployer des modèles d'IA pour de vrais utilisateursVersions et déploiement progressif
1. Définir le contrat d'inférence
2. Choisir un mode de service
3. Créer le service de prédiction
4. Choisir l'infrastructure et mesurer
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 main
Ce que vous devez savoir
- Le plan de réentraînement d’Apprentissage automatique : du problème à un modèle fiable : réentraîner avec les billets jusqu’en mai, choisir le seuil sur ceux de juin. Voir Planifier les vérifications en production : un déclencheur de réentraînement écrit.
- Le seuil de 0,19 et la capacité de révision de 20 %, dans Seuils et calibration.
- Les paquets et leurs empreintes, dans Empaqueter les dépendances du modèle et Confiance et intégrité des artefacts.
- La ligne du journal de prédiction et
GET /v1/model, dans Connecter l’API. La parité, dans Vérifier la parité. compose.yaml,.envetbench/bench.py, dans Paramètres de ressources du conteneur et Mesurer la performance du service.
Ce qu’il vous faut sur votre ordinateur
~/projects/ticket-modeldu cours d’apprentissage automatique, avecmodel/entraîné parpython train.py(Empaqueter les artefacts).~/projects/escalation-serviceà la fin du module 4, avec ses fichiers.venv,.envet l’imageescalation-service:1.0.0. Les 42 tests réussissent.- Dans
.env,WORKERS=1etCPUS=1.0. L’exercice du module 4 se terminait avecCPUS=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 candidat réussit tous les tests
Section intitulée « Le candidat réussit tous les tests »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 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 ticketsafter the warranty policy changed. This script fits the same pipeline ontrain + 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 amistake: balanced weights move every score up, so the old threshold flagsfar more tickets than the senior team can review."""
import argparseimport jsonimport platformfrom datetime import datetime, timezonefrom pathlib import Path
import joblibimport numpy as npimport pandas as pdimport sklearnfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import average_precision_score, roc_auc_score
from ticket_model import CATEGORIES, DATA, FEATURES, NUMBERS, TARGET, build_pipeline, loadfrom 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 :
python retrain.py --out model-1.1.0python retrain.py --out model-1.1.0-rc1 --balanced --threshold 0.19La 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é.
Pourquoi l’AUC n’a pas détecté le problème
Section intitulée « Pourquoi l’AUC n’a pas détecté le problème »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
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.
Une mise en production a une version et une empreinte
Section intitulée « Une mise en production a une version et une empreinte »Avant tout test sur de vrais billets, chaque candidat devient un paquet, dans son propre dossier :
python make_bundle.py ../ticket-model --version 1.1.0-rc1 --model-dir model-1.1.0-rc1python make_bundle.py ../ticket-model --version 1.1.0 --model-dir model-1.1.0La 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): fa04bc40bea99723095ec4db600b7060fbfce0f2ec461534bb06c1829b47fe06Made bundles/escalation-1.1.0: 14 parity cases, threshold 0.2.Bundle digest (MODEL_SHA256): 382077148b6cad58238e4832f0951c6556f82832bc6e7de1d3a2335bd6d36af0Vos 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 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.
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.
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 :
model_bundle: Path model_sha256: str shadow_bundle: Path | None shadow_sha256: str | NoneDans load_settings(), lisez-les après la vérification de MODEL_SHA256, puis passez-les à Settings :
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 :
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 = {...} :
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.
-
Arrêtez le service Compose, car il utilise le port 8000 :
Terminal, in ~/projects/escalation-service docker compose down -
Reconstruisez l’image. Elle contient maintenant les trois paquets et le nouveau code :
Terminal docker compose buildL’étiquette reste
escalation-service:1.0.0, carcompose.yamlla définit. Désormais, l’image contient plusieurs paquets, et les paramètres en choisissent un au démarrage. Dans Compose,MODEL_VERSIONchoisit le dossier du paquet, etMODEL_SHA256doit ê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. -
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.0PowerShell 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 -
Envoyez chaque billet de juillet une fois, dans l’ordre du fichier.
--warmup 0fait compter chaque requête :Terminal, with .venv active python bench/bench.py http://127.0.0.1:8000 --requests 1400 --warmup 0 -
Enregistrez le journal du conteneur, avec les lignes de prédiction, dans un fichier :
Terminal docker logs escalation-shadow > predictions.log 2>&1PowerShell docker logs escalation-shadow 2>&1 | Out-File -Encoding utf8 predictions.log
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 msLe 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 :
grep -c '"flag": true' predictions.loggrep -c '"shadow_flag": true' predictions.log(Select-String '"flag": true' predictions.log).Count(Select-String '"shadow_flag": true' predictions.log).CountLa sortie :
2821205Le 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 :
docker rm -f escalation-shadowDé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 flaggedMê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 »- Enregistrez
retrain.pydans~/projects/ticket-model, à côté detrain.py. Il se trouve dans Le candidat réussit tous les tests. - Dans
~/projects/ticket-model, activez son.venv. Exécutez les deux commandesretrain.py. Comparez vos deux lignes de juin avec la sortie ci-dessus. - 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. - Créez
RELEASES.mdavec trois lignes. Utilisez vos trois empreintes. L’empreinte de 1.0.0 est la valeurMODEL_SHA256dans votre.env. - Modifiez
settings.py,main.pyetroutes.py, comme dans En parallèle : évaluer avec les deux, répondre avec un seul. - Exécutez
python -m pytest. Vous devriez voir42 passed(42 tests réussis) : les tests s’exécutent sans modèle en parallèle. - Faites les cinq étapes de l’exemple détaillé. Comptez ensuite les signalements.
- Avant de supprimer le conteneur, exécutez
curl http://127.0.0.1:8000/v1/model, comme dans Connecter l’API. Vérifiez queshadow_versionvaut"1.1.0-rc1". - 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. »
À vous de jouer : 1.1.0 en mode fantôme
Section intitulée « À vous de jouer : 1.1.0 en mode fantôme »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 msLes 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 digestCause : 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é.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
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.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
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.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
retrain.pya 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.mdest 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