Aller au contenu
English

Surveiller la santé du modèle

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

Ce que vous saurez faire

Détectez un problème du modèle avant que l’équipe de Grace en ressente les effets. Le service compte ses propres signaux de santé. Un court script vérifie les résultats réels quand ils arrivent trois jours plus tard.

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

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 ne sont pas testées.

À l’hôpital, une infirmière vérifie la température et le pouls de chaque patient toutes les quelques heures. Les résultats des analyses sanguines arrivent deux jours plus tard. L’infirmière n’attend pas le laboratoire : une fièvre ce soir justifie une intervention ce soir. Le laboratoire confirme ou corrige ensuite le portrait.

Un modèle en production se trouve dans la même situation. La vraie réponse pour un billet de soutien, transmis au niveau supérieur ou non, arrive 72 heures après le score. D’ici là, le service peut surveiller seulement les signes qu’il voit immédiatement.

En juillet, le modèle 1.0.0 a signalé 17,9 % des billets du 1er au 14 juillet et 21,9 % du 15 au 31 juillet. Depuis le 15 juillet, l’équipe de Grace reçoit plus de billets signalés qu’elle peut en réviser. Dans le cours d’apprentissage automatique, Priya l’a découvert après coup, dans les prédictions de juillet. La révision a révélé deux causes : le nouveau canal social et une exportation défectueuse de prior_tickets_90d (Planifier les vérifications en production).

Dans la dernière leçon, Amira a trouvé le taux de 86 % de rc1 avec grep, après l’exécution. Un fichier journal n’avertit personne tant que personne ne le lit. Le service doit compter les signaux lui-même, en continu, et indiquer quand une valeur dépasse une limite.

La santé du modèle indique s’il remplit encore son rôle avec les entrées d’aujourd’hui. Un service ne peut pas la mesurer directement. Il surveille donc des signaux : des nombres qu’il peut calculer immédiatement et qui changent quand un problème survient. Le diagramme montre chaque signal et le moment où il est disponible.

Un nouveau billet arrive. S'il ne respecte pas le contrat, le service répond 422 et ajoute un à contract_errors. S'il le respecte, le service calcule son score et écrit une ligne de journal. Immédiatement, GET /v1/monitor affiche contract_errors, unknown_categories, le taux de signalement et le score moyen des 1 000 derniers billets, ainsi que les latences p50 et p95. Après 72 heures, l'étiquette arrive : billet transmis au niveau supérieur ou non. monitoring/outcomes.py calcule alors la précision et le rappel.
Deux types de signaux. Le service compte les premiers immédiatement. Les seconds nécessitent la vraie réponse, qui arrive 72 heures plus tard.
Description textuelle du diagramme

Un nouveau billet arrive. Le contrat le vérifie d’abord. S’il ne le respecte pas, le service répond 422. Le compteur contract_errors (erreurs de contrat) augmente pour chaque champ incorrect. S’il le respecte, le service calcule son score et écrit une ligne de journal de prédiction.

Immédiatement, GET /v1/monitor affiche quatre signaux. Ce sont les erreurs de contrat, les catégories inconnues, le taux de signalement et le score moyen, puis la latence. Ils n’ont pas besoin d’étiquettes.

Après 72 heures, l’étiquette arrive : le billet a-t-il été transmis au niveau supérieur ou non? Ensuite, monitoring/outcomes.py joint les lignes de journal aux étiquettes et calcule la précision et le rappel.

Chaque signal répond à une question :

  • Erreurs de contrat : un client envoie-t-il des billets que le modèle ne peut pas évaluer? Une hausse soudaine signifie généralement qu’un client a changé.
  • Catégories inconnues : le modèle voit-il des valeurs qu’il n’a jamais apprises, comme social? Ces billets reçoivent un score, mais celui-ci est moins fiable.
  • Latence : le service est-il encore assez rapide pour le service de soutien?
  • Taux de signalement, comparé à la capacité de révision de 20 % : la contrainte de Grace, en un nombre.
  • Score moyen : s’il change, les entrées ou le modèle ont changé, même si le taux de signalement n’a pas encore changé.

Le taux de signalement et le score moyen sont des indicateurs de dérive. La dérive signifie que les données en production s’éloignent des données que le modèle a utilisées pour apprendre. Le cours d’apprentissage automatique calculait les deux mêmes signaux après coup, à partir du fichier de juillet. Ici, le service les compte pour chaque billet, au moment de calculer son score.

Un petit exemple : pourquoi une fenêtre et un minimum

Section intitulée « Un petit exemple : pourquoi une fenêtre et un minimum »

Voici les signalements des dix premiers billets après une mise en production (1 = signalé) :

0 1 0 0 1 0 0 0 1 1 → 4 of 10 flagged = 40%

40 %, c’est deux fois la capacité. Le modèle est-il défectueux? Avec 10 billets, un signalement de plus fait varier le taux de 10 points. Le taux de 40 % peut donc être dû au hasard. Avec 1 000 billets, un signalement le fait varier de 0,1 point. Le dispositif de surveillance fait donc deux choses :

  • Il calcule le taux de signalement et le score moyen sur une fenêtre : les 1 000 derniers billets évalués. Les anciens billets quittent la fenêtre, donc les nombres suivent le trafic actuel.
  • Il envoie une alerte seulement quand la fenêtre contient au moins 200 billets.

Le graphique de juillet, plus loin dans cette leçon, montre à quel point une journée peut varier.

escalation/monitor.py garde les signaux en mémoire. La première partie stocke chaque billet évalué et compte les erreurs :

escalation/monitor.py
"""Model health signals, kept in memory for the last WINDOW scored tickets.
The service cannot know at once whether a score was right: the true answer
(escalated or not) arrives 72 hours later. So it watches signals that it can
see now:
contract errors tickets rejected before scoring, by field
unknown categories values the model never saw, by feature
flag rate the share of tickets flagged, against the review capacity
mean score a change means the inputs or the model changed
latency p50 and p95 of the scoring time
Each worker process has its own monitor. With two workers, ask each one, or
send the prediction log to one place (C25 does this properly).
"""
import threading
from collections import Counter, deque
from dataclasses import dataclass
import numpy as np
WINDOW = 1000
MIN_FOR_ALERTS = 200
@dataclass
class Scored:
score: float
flag: bool
ms: float
shadow_score: float | None = None
shadow_flag: bool | None = None
class Monitor:
def __init__(self, capacity: float):
self.capacity = capacity
self.recent: deque[Scored] = deque(maxlen=WINDOW)
self.contract_errors: Counter[str] = Counter()
self.unknown: Counter[str] = Counter()
self.scored_total = 0
self.lock = threading.Lock()
def contract_error(self, fields: list[str]) -> None:
with self.lock:
self.contract_errors.update(fields)
def scored(self, item: Scored, warnings: list[str]) -> None:
with self.lock:
self.recent.append(item)
self.scored_total += 1
self.unknown.update(w.split(":")[0] for w in warnings)

deque(maxlen=WINDOW) est une liste qui garde seulement les 1 000 derniers éléments. Quand un nouvel élément arrive, le plus ancien sort. Le verrou empêche deux requêtes de modifier les compteurs en même temps. La deuxième partie calcule le rapport :

escalation/monitor.py (continued)
def report(self) -> dict:
with self.lock:
items = list(self.recent)
errors, unknown, total = dict(self.contract_errors), dict(self.unknown), self.scored_total
report = {
"scored_total": total,
"window": len(items),
"contract_errors": errors,
"unknown_categories": unknown,
"flag_rate": None,
"mean_score": None,
"latency_ms": None,
"shadow": None,
"alerts": [],
}
if not items:
return report
scores = np.array([i.score for i in items])
flags = np.array([i.flag for i in items])
ms = np.array([i.ms for i in items])
report["flag_rate"] = round(float(flags.mean()), 3)
report["mean_score"] = round(float(scores.mean()), 4)
report["latency_ms"] = {
"p50": round(float(np.percentile(ms, 50)), 2),
"p95": round(float(np.percentile(ms, 95)), 2),
}
shadowed = [i for i in items if i.shadow_score is not None]
if shadowed:
shadow_flags = np.array([i.shadow_flag for i in shadowed])
live_flags = np.array([i.flag for i in shadowed])
report["shadow"] = {
"compared": len(shadowed),
"flag_rate": round(float(shadow_flags.mean()), 3),
"mean_score": round(float(np.mean([i.shadow_score for i in shadowed])), 4),
"same_flag": round(float((shadow_flags == live_flags).mean()), 3),
}
if len(items) >= MIN_FOR_ALERTS and report["flag_rate"] > self.capacity:
report["alerts"].append(
f"flag rate {report['flag_rate']:.3f} is above the review capacity {self.capacity:.2f}"
)
if report["shadow"] and len(shadowed) >= MIN_FOR_ALERTS and report["shadow"]["flag_rate"] > self.capacity:
report["alerts"].append(
f"shadow flag rate {report['shadow']['flag_rate']:.3f} is above the review capacity {self.capacity:.2f}"
)
return report

Connectez maintenant ce code au service, dans trois fichiers.

escalation/main.py crée un Monitor par processus. Ajoutez l’importation, puis créez-le après le chargement des ensembles, avec la capacité provenant des métadonnées de l’ensemble :

escalation/main.py
from escalation.monitor import Monitor
...
logger.error("Start-up error: %s", error)
raise
app.state.monitor = Monitor(app.state.bundle.metadata["review_capacity"])

Ajoutez aussi une ligne dans create_app(), après app.state.settings = settings. Une erreur de contrat peut survenir dans un test avant le démarrage. Il faut un dispositif de surveillance pour la compter :

escalation/main.py
app.state.settings = settings
app.state.monitor = Monitor(0.2) # replaced at start-up; here so that errors can count

escalation/errors.py compte les champs de chaque erreur de contrat, dans invalid_request :

escalation/errors.py
fields = sorted({field_name(e) for e in exc.errors()})
request.app.state.monitor.contract_error(fields)

escalation/routes.py transmet chaque billet évalué au dispositif de surveillance. Ajoutez import time et from escalation.monitor import Scored en haut. Modifiez ensuite score() :

escalation/routes.py
def score(ticket: TicketIn, request: Request, response: Response) -> ScoreOut:
started = time.perf_counter()
state = request.app.state
[result], timings = score_tickets(state.bundle, [ticket])
response.headers["Server-Timing"] = timings.header()
item = Scored(result.score, result.flag, (time.perf_counter() - started) * 1000)
record = { ... } # as before
if state.shadow is not None: # score with the candidate too, but never answer with it
[shadow], _ = score_tickets(state.shadow, [ticket])
item.shadow_score, item.shadow_flag = shadow.score, shadow.flag
record |= {"shadow_version": state.shadow.version, "shadow_score": shadow.score, "shadow_flag": shadow.flag}
state.monitor.scored(item, result.warnings)
predictions.info(json.dumps(record))

Ajoutez ensuite la route, après GET /v1/model. Elle utilise la même clé d’API que les autres routes /v1 :

escalation/routes.py
@router.get("/monitor", tags=["model"], summary="Model health signals for the recent tickets")
def monitor(request: Request) -> dict:
report = request.app.state.monitor.report()
report["model_version"] = request.app.state.bundle.version
return report

Enfin, deux tests dans tests/test_api.py vérifient les compteurs. Ajoutez une ligne à la fin de chaque test :

tests/test_api.py
def test_a_contract_error_has_the_error_shape(client, ticket):
...
assert client.get("/v1/monitor").json()["contract_errors"] == {"created_hour": 1, "priority": 1}
def test_an_unknown_category_is_scored_with_a_warning(client, ticket):
...
assert client.get("/v1/monitor").json()["unknown_categories"] == {"channel": 1}

Exemple détaillé : l’exécution en parallèle, surveillée par le service

Section intitulée « Exemple détaillé : l’exécution en parallèle, surveillée par le service »

Amira reconstruit l’image. Elle répète l’exécution en parallèle de la dernière leçon : 1.0.0 en production, rc1 en parallèle, les 1 400 billets de juillet dans l’ordre. Puis elle interroge le service :

Terminal
curl http://127.0.0.1:8000/v1/monitor

Prédisez : la fenêtre contient les 1 000 derniers billets, surtout de la deuxième moitié de juillet. Le taux de signalement en production dépasse-t-il les 20,1 % du mois complet, ou est-il plus bas?

La sortie, mise en forme :

GET /v1/monitor
{
"scored_total": 1400,
"window": 1000,
"contract_errors": {},
"unknown_categories": {"channel": 91},
"flag_rate": 0.212,
"mean_score": 0.1299,
"latency_ms": {"p50": 2.17, "p95": 3.07},
"shadow": {"compared": 1000, "flag_rate": 0.87, "mean_score": 0.4371, "same_flag": 0.342},
"alerts": [
"flag rate 0.212 is above the review capacity 0.20",
"shadow flag rate 0.870 is above the review capacity 0.20"
],
"model_version": "1.0.0"
}

Lisez-la depuis le haut :

  • contract_errors est vide : chaque billet de juillet respectait le contrat.
  • unknown_categories (catégories inconnues) : 91 billets provenaient de channel (canal) social. Ce compteur couvre les 1 400 billets, pas seulement la fenêtre. Ce sont les mêmes 91 billets trouvés dans le cours d’apprentissage automatique.
  • flag_rate (taux de signalement) 0,212 : le modèle en production dépasse la capacité sur les 1 000 derniers billets. C’est plus que les 20,1 % du mois, car la fenêtre contient surtout des billets de fin juillet. La première alerte l’indique.
  • mean_score (score moyen) 0,1299 : comparez-le à 0,120, le score moyen de mai dans le cours d’apprentissage automatique. Il a augmenté.
  • latency_ms (latence en millisecondes) : le temps de calcul du score en production à l’intérieur du service, sans HTTP. Il n’inclut pas le travail en parallèle.
  • shadow (modèle en parallèle) : rc1 signalerait 87 % des billets. Son signalement correspond à celui du modèle en production pour seulement 34,2 % des billets. La deuxième alerte l’indique.

Envoyez maintenant un billet incorrect avec priority (priorité) "high" (élevée) et created_hour (heure de création) à 24. Utilisez la commande curl de Connecter l’API, en changeant ces deux valeurs. Dans un nouveau conteneur, le dispositif de surveillance affiche alors :

"scored_total":0, ... "contract_errors":{"created_hour":1,"priority":1}

Le service n’a jamais calculé le score du billet, mais le dispositif de surveillance a compté chaque champ incorrect.

Prédisez : chaque jour de juillet compte entre 13 et 67 billets. Le modèle 1.0.0 reste-t-il près de 20 % chaque jour?

Le graphique montre la part des billets de chaque jour que chaque version signale. Les scores sont les scores réels des trois ensembles, calculés avec le même code que le service.

  • 1.0.0 (en production)
  • 1.1.0-rc1
  • 1.1.0
  • Capacité de 20 %
Le taux de signalement quotidien de trois versions sur les 1 400 billets de juillet. rc1 dépasse largement la capacité chaque jour. 1.0.0 et 1.1.0 varient ensemble autour de cette limite et la dépassent la plupart des jours depuis le 15 juillet.
Description textuelle du graphique

Un point par jour de juillet 2026 pour chaque version, avec une ligne pointillée à la capacité de 20 %.

La version candidate 1.1.0-rc1 signale entre 70,8 % et 94,5 % des billets de chaque jour. Elle dépasse largement la ligne chaque jour.

La version 1.0.0, le modèle en production, varie entre 4,5 % (4 juillet, 22 billets) et 35,3 % (24 juillet). Du 1er au 14 juillet, elle dépasse 20 % pendant 5 jours sur 14. Du 15 au 21 juillet, elle dépasse 20 % pendant 6 jours sur 7, entre 27,3 % et 30,3 %. L’exception est le 20 juillet, avec 17,2 %.

La version 1.1.0 suit de près 1.0.0, un peu plus bas la plupart des jours. Elle dépasse aussi 20 % la plupart des jours depuis le 15 juillet. Sur le mois, 1.0.0 signale 20,1 %, 1.1.0 signale 19,4 % et rc1 signale 86,1 %.

Trois choses ressortent :

  1. rc1 fonctionne mal chaque jour, sans exception. Aucune fenêtre ni aucun minimum n’est nécessaire pour le voir. Une exécution en parallèle d’une journée aurait suffi.
  2. Une journée donne un signal variable pour 1.0.0. Le 4 juillet, il a signalé 4,5 % de 22 billets; le 24 juillet, 35,3 %. Une règle fondée sur une seule journée produirait de fausses alertes. C’est pourquoi le dispositif de surveillance utilise une fenêtre de 1 000 billets. Le déclencheur du cours d’apprentissage automatique attendait 5 jours ouvrables consécutifs.
  3. 1.1.0 ne corrige pas la fin de juillet. Il varie avec 1.0.0 parce que la cause vient des entrées, pas du modèle. Le réentraînement seul ne répare pas une exportation défectueuse.

72 heures après chaque billet, le service de soutien sait s’il a été transmis au niveau supérieur. Mei fournit les étiquettes dans july_labels.csv, comme dans le cours d’apprentissage automatique. Copiez-le dans le projet :

Terminal, in ~/projects/escalation-service
cp ../ticket-model/data/july_labels.csv data/

monitoring/outcomes.py lit les lignes de prédiction d’un journal et les joint aux étiquettes selon ticket_id. Ensuite, il calcule la précision et le rappel pour chaque version du modèle et pour le modèle en parallèle.

Afficher monitoring/outcomes.py
monitoring/outcomes.py
"""When the true answers arrive, check how the served model really did.
docker compose logs escalation --no-log-prefix > predictions.log
python monitoring/outcomes.py predictions.log data/july_labels.csv
Reads the prediction lines that the service writes (one JSON object per
scored ticket, from the logger escalation.predictions), joins them with the
labels by ticket_id, and prints precision and recall for each model version
(and for the shadow model, if there was one). Tickets without a label yet are
counted, not guessed.
"""
import argparse
import json
import pandas as pd
MARK = "escalation.predictions: "
def read_predictions(path) -> pd.DataFrame:
rows = []
with open(path, encoding="utf-8") as log:
for line in log:
if MARK in line:
rows.append(json.loads(line.split(MARK, 1)[1]))
return pd.DataFrame(rows).drop_duplicates("ticket_id", keep="last")
def report(name: str, flags: pd.Series, truth: pd.Series) -> str:
caught = int((flags & truth).sum())
precision = caught / flags.sum() if flags.sum() else float("nan")
recall = caught / truth.sum() if truth.sum() else float("nan")
return (f"{name}: flagged {int(flags.sum())} of {len(flags)} ({flags.mean():.1%}), "
f"precision {precision:.3f}, recall {recall:.3f}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("predictions")
parser.add_argument("labels")
args = parser.parse_args()
predictions = read_predictions(args.predictions)
labels = pd.read_csv(args.labels)
joined = predictions.merge(labels, on="ticket_id", how="left")
waiting = joined["escalated_72h"].isna().sum()
known = joined.dropna(subset=["escalated_72h"])
truth = known["escalated_72h"].astype(int) == 1
print(f"{len(predictions)} predictions, {len(known)} with a label, {waiting} still waiting for one.")
print(f"Escalated: {int(truth.sum())} ({truth.mean():.1%}).")
for version, group in known.groupby("model_version"):
print(report(f"Model {version}", group["flag"].astype(bool), truth[group.index]))
if "shadow_version" in known:
for version, group in known.dropna(subset=["shadow_version"]).groupby("shadow_version"):
print(report(f"Shadow {version}", group["shadow_flag"].astype(bool), truth[group.index]))
if __name__ == "__main__":
main()

Exécutez-le avec le journal de l’exécution en parallèle :

Terminal, with .venv active
python monitoring/outcomes.py predictions.log data/july_labels.csv

La sortie :

1400 predictions, 1400 with a label, 0 still waiting for one.
Escalated: 196 (14.0%).
Model 1.0.0: flagged 282 of 1400 (20.1%), precision 0.333, recall 0.480
Shadow 1.1.0-rc1: flagged 1205 of 1400 (86.1%), precision 0.159, recall 0.974

Ce sont les nombres qui comptent le plus pour Grace. 1.0.0 détecte 48 % des transmissions au niveau supérieur. Un billet signalé sur trois est réellement transmis au niveau supérieur. rc1 en détecte 97 %, mais seulement un billet signalé sur six est réellement transmis au niveau supérieur. Il « trouve » plus de transmissions parce qu’il signale presque tout.

Omar : « Alors, pourquoi ne pas surveiller seulement la précision et le rappel? » Parce qu’ils arrivent tard. Les étiquettes du 31 juillet arrivent le 3 août. Le taux de signalement a dépassé 20 % le 15 juillet, immédiatement.

Le dispositif de surveillance est petit par choix. Comprenez ses limites avant de lui faire confiance :

  • Il vit dans la mémoire d’un seul processus. Chaque processus a son propre dispositif de surveillance, et un redémarrage remet tous les compteurs à zéro. Le cas d’échec ci-dessous montre deux processus.
  • La fenêtre contient les 1 000 derniers billets. À environ 45 billets par jour, cela représente environ trois semaines. Le minimum de 200 billets représente quatre ou cinq jours. Un changement soudain apparaît lentement : le premier jour, il représente moins de 5 % de la fenêtre.
  • Il alerte seulement au-dessus de la capacité. Une version qui signale trop peu de billets ne produit aucune alerte. Son rappel baisse, et seul outcomes.py le montre, trois jours plus tard.
  • Un signal montre qu’un changement a eu lieu, pas pourquoi. La cause de fin juillet se trouvait dans l’exportation des données. Une personne a dû la trouver.

Une équipe qui gère plusieurs services envoie les lignes de prédiction et les signaux vers un endroit central. Elle y utilise des tableaux de bord et des alertes. Le cours MLOps et LLMOps : exploiter l’IA en production fait cela. L’idée reste la même.

Exercice guidé : surveiller l’exécution en parallèle

Section intitulée « Exercice guidé : surveiller l’exécution en parallèle »
  1. Ajoutez escalation/monitor.py et modifiez main.py, errors.py, routes.py et tests/test_api.py, comme dans monitor.py.
  2. Exécutez python -m pytest. Vous devriez voir 42 passed (42 tests réussis).
  3. Ajoutez monitoring/outcomes.py et copiez july_labels.csv dans data/.
  4. Construisez l’image avec docker compose build.
  5. Démarrez le conteneur en parallèle et envoyez les billets de juillet. Suivez les étapes 3 et 4 de Versions et déploiement progressif : exemple détaillé.
  6. Appelez GET /v1/monitor. Comparez flag_rate, shadow et alerts (alertes) à la sortie ci-dessus.
  7. Envoyez le billet d’exemple avec created_hour à 24, avec la commande curl de Connecter l’API. Appelez de nouveau GET /v1/monitor et trouvez le compteur dans contract_errors.
  8. Enregistrez le journal dans predictions.log, puis exécutez outcomes.py.
  9. Supprimez le conteneur avec docker rm -f escalation-shadow.
Afficher les résultats attendus

L’étape 6 donne le JSON de l’exemple détaillé : "flag_rate": 0.212, le "flag_rate": 0.87 du modèle en parallèle et deux alertes. Vos valeurs latency_ms sont différentes : elles dépendent de votre ordinateur.

L’étape 7 ajoute "contract_errors": {"created_hour": 1}, et scored_total (total évalué) reste à 1 400.

L’étape 8 donne les quatre lignes de Quand les étiquettes arrivent.

À vous de jouer : certaines étiquettes ne sont pas encore arrivées

Section intitulée « À vous de jouer : certaines étiquettes ne sont pas encore arrivées »

Le 23 juillet, les étiquettes existent seulement pour les billets de soutien du 1er au 20 juillet. Copiez july_labels.csv en gardant seulement ces billets, dans un fichier nommé labels_to_20_july.csv. Il contient 887 lignes. Prédisez ce que la première ligne de outcomes.py affiche, puis exécutez-le avec ce fichier.

Afficher la réponse

Voici une façon de créer le fichier dans une session Python avec pandas :

import pandas as pd
tickets = pd.read_csv("data/july_tickets.csv", parse_dates=["created_at"])
labels = pd.read_csv("data/july_labels.csv")
early = tickets.loc[tickets["created_at"].dt.day <= 20, "ticket_id"]
labels[labels["ticket_id"].isin(early)].to_csv("labels_to_20_july.csv", index=False)

La sortie de python monitoring/outcomes.py predictions.log labels_to_20_july.csv :

1400 predictions, 887 with a label, 513 still waiting for one.
Escalated: 129 (14.5%).
Model 1.0.0: flagged 179 of 887 (20.2%), precision 0.341, recall 0.473
Shadow 1.1.0-rc1: flagged 761 of 887 (85.8%), precision 0.164, recall 0.969

Le script compte les 513 billets sans étiquette. Il ne devine pas leurs étiquettes et ne les compte pas comme « non transférés ». Sinon, le rappel semblerait meilleur et la précision moins bonne qu’ils ne le sont.

Erreur courante

Deux processus, deux réponses différentes

Symptôme : avec WORKERS=2 et CPUS=2.0 dans .env, Amira envoie les 1 400 billets de juillet. Elle appelle ensuite GET /v1/monitor six fois. Les réponses ne concordent pas :

scored_total 935, flag_rate 0.206, unknown {'channel': 62}, alerts: ['flag rate 0.206 is above the review capacity 0.20'] (4 times)
scored_total 465, flag_rate 0.191, unknown {'channel': 29}, alerts: [] (2 times)

Cause : chaque worker (processus) est un processus distinct, avec son propre Monitor dans sa propre mémoire. Chaque requête va à l’un des deux processus. 935 + 465 = 1 400 : chaque processus a reçu une partie du trafic et répond seulement pour cette partie. L’un déclenche une alerte, l’autre non.

Solution : pour ce cours, utilisez un seul processus, comme le module 4 l’a retenu pour la charge de Larkfield. Avec plusieurs processus ou conteneurs, calculez les signaux à partir des journaux de prédictions de tous ces processus ou conteneurs. C’est ce que fait outcomes.py. Un système de surveillance centralisé le fait pour vous (voir le cours sur le MLOps).

Pour un vrai service d’IA : une alerte qui dépend du processus qui répond est pire qu’aucune alerte. Elle ressemble à une décision. Sachez où se trouvent vos comptes avant d’agir selon ces résultats.

Vérifiez votre compréhension

Le système de surveillance affiche flag_rate 0.40 après 10 billets, sans alerte. Est-ce un bogue? Expliquez.

Afficher la réponse

Non. Avec 10 billets, un seul signalement fait varier le taux de 10 points. Le taux de 40 % peut donc être dû au hasard. Le système de surveillance déclenche une alerte seulement quand sa fenêtre contient au moins 200 billets. Le graphique quotidien de juillet montre le même effet. Selon la journée, la version 1.0.0 a signalé entre 4,5 % et 35,3 % des billets. Mais sur le mois, elle en a signalé 20,1 %.

Vérifiez votre compréhension

Quels signaux de santé le service peut-il calculer immédiatement, et lesquels nécessitent les étiquettes? Pourquoi cette différence compte-t-elle?

Afficher la réponse

Immédiatement : les erreurs de contrat, les catégories inconnues, la latence, le taux de signalement et le score moyen. Ces signaux ne nécessitent aucune étiquette. La précision et le rappel nécessitent de savoir si chaque billet a été transféré. Cette information est connue 72 heures plus tard. Les premiers signaux servent d’avertissement; la précision et le rappel servent de confirmation. En juillet, le taux de signalement a dépassé 20 % le 15 juillet. Mais les étiquettes du 31 juillet sont arrivées le 3 août.

Vérifiez votre compréhension

Une nouvelle version signale 9 % des billets, soit beaucoup moins qu'avant. Le système de surveillance déclenche-t-il une alerte? Qu'est-ce qui révélerait le problème?

Afficher la réponse

Non. monitor.py déclenche une alerte seulement quand le taux de signalement dépasse la capacité de révision. Le score moyen baisse, ce qu’une personne peut voir dans /v1/monitor, mais aucune alerte ne l’indique. Quand les étiquettes arrivent, outcomes.py montre le faible rappel : le modèle manque des transferts. Une équipe pourrait ajouter une limite inférieure aux alertes, par exemple 10 %.

Récapitulatif

  • On surveille la santé du modèle au moyen de signaux. Immédiatement : les erreurs de contrat, les catégories inconnues, la latence, le taux de signalement comparé à la capacité et le score moyen. Plus tard, avec les étiquettes : la précision et le rappel.
  • Le taux de signalement et le score moyen sont des indicateurs de dérive. Ils changent quand les données de production s’éloignent des données d’entraînement.
  • monitor.py compte les signaux sur les 1 000 derniers billets et déclenche des alertes seulement après 200 billets. Les petits échantillons sont trop variables. GET /v1/monitor affiche ces signaux.
  • Pendant l’exécution en parallèle, le système de surveillance a déclenché deux alertes : 1.0.0 à 21,2 % et rc1 à 87 %.
  • outcomes.py joint le journal de prédictions aux étiquettes : 1.0.0 avait une précision de 0,333 et un rappel de 0,480 en juillet.
  • Le système de surveillance réside dans un seul processus : deux processus donnent deux réponses différentes.

Ensuite, rc1 est volontairement mis en production dans un exercice, et vous revenez à la version précédente.

Leçon suivante : Retour en arrière et retrait