Le système est en boîte — et amnésique. On lui donne une mémoire : chaque expérience tracée, chaque modèle versionné, et une API qui sert toujours la bonne version — en le prouvant. The system is boxed — and amnesic. We give it a memory: every experiment traced, every model versioned, and an API that always serves the right version — provably.
Bloc 4 : le système vit dans une image Docker, identique partout. Mais demandez-lui d'où vient son modèle : silence. Combien d'essais avant celui-là ? Quels réglages ? Quel score ? Rien, nulle part, ne s'en souvient. Block 4: the system lives in a Docker image, identical everywhere. But ask it where its model came from: silence. How many attempts before this one? Which settings? Which score? Nothing, anywhere, remembers.
MLflow se branche sous le cerveau : chaque entraînement écrit sa page tout seul — paramètres, métriques, artefacts, commit. Le tableau markdown du bloc 2 est mort ; l'expérience laisse une trace, à chaque fois, sans discipline requise. MLflow gets wired under the brain: every training run writes its own page — parameters, metrics, artifacts, commit. Block 2's markdown table is dead; every experiment leaves a trace, every time, no discipline required.
Regardez la puce de version sur le cerveau : l'API ne charge plus un fichier balladeur, elle demande au registre « la version en Production ». Promotion tracée, rollback en trente secondes — et l'auditeur a sa réponse. Look at the version chip on the brain: the API no longer loads a wandering file, it asks the registry for "the version in Production". Traced promotion, thirty-second rollback — and the auditor gets an answer.
Mardi, bloc 2 : huit modèles, des scores griffonnés, model_final_v2_VRAI_celui_la.pkl. Maintenant l'histoire qui fait mal : le 3 mars, le régulateur demande « quel modèle a bloqué cette transaction — entraîné sur quelles données, avec quels réglages ? » Dans la salle : silence. Le modèle a changé quatre fois depuis.
Tuesday, block 2: eight models, scribbled scores, model_final_v2_REAL_this_one.pkl. Now the story that stings: on March 3rd, the regulator asks "which model blocked this transaction — trained on which data, with which settings?" In the room: silence. The model has changed four times since.
Changez un seul des quatre ingrédients : ce n'est plus le même modèle — et un .pkl seul ne raconte aucun des quatre. Une expérience non enregistrée n'a jamais eu lieu, et la mémoire humaine ne compte pas : la mémoire doit être automatique. C'est le travail de MLflow. Change any one of the four ingredients: it's no longer the same model — and a .pkl alone tells you about none of the four. An unrecorded experiment never happened, and human memory doesn't count: memory must be automatic. That's MLflow's job.
Les chimistes le font depuis deux siècles : une manipulation, une page. MLflow fait pareil pour vos modèles, sans stylo — quatre lignes dans votre train.py, et chaque entraînement écrit sa page : un run = paramètres + métriques + artefacts + commit Git.
Chemists have done it for two centuries: one experiment, one page. MLflow does the same for your models, without a pen — four lines in your train.py, and every training run writes its page: a run = parameters + metrics + artifacts + Git commit.
log_param, log_metric, log_model : le coût d'entrée tient en quatre lignes. Le carnet vit dans mlruns/, se lit dans le navigateur (mlflow ui) — aucune infra pour démarrer.log_param, log_metric, log_model: the entry cost is four lines. The logbook lives in mlruns/, reads in the browser (mlflow ui) — no infrastructure to start.Toujours : params, métriques, modèle, commit. Souvent : courbe PR, matrice, csv des pires erreurs. Jamais : les données brutes des clients — le carnet trace un pointeur et un hash, pas les transactions. Prouver sans exposer. Always: params, metrics, model, commit. Often: PR curve, matrix, worst-errors csv. Never: raw customer data — the logbook records a pointer and a hash, not the transactions. Prove without exposing.
La PR-AUC globale résume 57 000 transactions en un chiffre. Mais la banque ne perd pas d'argent « en moyenne » — elle le perd sur des segments précis. La règle d'honnêteté : segments définis avant de regarder les résultats, les mêmes pour tous les modèles. The global PR-AUC compresses 57,000 transactions into one number. But the bank doesn't lose money "on average" — it loses it on specific segments. The honesty rule: segments defined before looking at results, the same for every model.
worst_errors.csv loggé dans chaque run, recall_seg en métriques : l'analyse d'erreurs du bloc 2, avec une trace permanente.worst_errors.csv logged in every run, recall_seg as metrics: block 2's error analysis, with a permanent trace.Modèle A : PR-AUC 0,86 mais 31 % des fraudes > 1 000 € ratées. Modèle B : 0,83 mais seulement 9 % ratées. La moyenne dit A, le coût métier dit B — sans métriques par segment, vous auriez promu A sans même voir le débat. Verdict en trois regards : PR-AUC pour trier, segments pour voir où ça pèche, euros pour trancher. Model A: PR-AUC 0.86 but 31% of frauds > €1,000 missed. Model B: 0.83 but only 9% missed. The average says A, business cost says B — without per-segment metrics you'd have promoted A without even seeing the debate. Verdict in three looks: PR-AUC to rank, segments to spot the sins, euros to decide.
Fini le .pkl qu'on se passe par mail : le Model Registry donne au modèle un nom officiel — fraud-detector — des versions numérotées, et un cycle de vie standard. Chaque version reste reliée à son run : params, métriques, erreurs, tout.
No more .pkl passed around by email: the Model Registry gives the model an official name — fraud-detector — numbered versions, and a standard lifecycle. Each version stays linked to its run: params, metrics, errors, everything.
« Chaque expérience tracée, chaque modèle versionné — et l'API sert toujours la bonne version, en le prouvant. » "Every experiment traced, every model versioned — and the API always serves the right version, provably."
Le test final : « quel modèle tournait le 3 mars, entraîné comment, promu par qui ? » — vous répondez en trente secondes. Le silence de l'audit est devenu une réponse. The final test: "which model was running on March 3rd, trained how, promoted by whom?" — you answer in thirty seconds. The audit's silence has become an answer.
Quatre missions, en binômes — 3 h le jour 2, 1 h le jour 3. Les durées viennent du plan de cours : Four missions, in pairs — 3 h on day 2, 1 h on day 3. Durations come straight from the course plan:
train.py (params, métriques, artefacts), rejouer 5 runs minimum, comparer dans l'UITrack — instrument train.py (params, metrics, artifacts), replay at least 5 runs, compare in the UIrecall_seg loggé, worst_errors.csv en artefact, verdict en eurosSegments — tooled error analysis: segments fixed up front, recall_seg logged, worst_errors.csv as artifact, verdict in eurosfraud-detector, v1 et v2, staging → production → archivage, justification écrite, rollback chronométréRegistry — register fraud-detector, v1 and v2, staging → production → archiving, written justification, timed rollbackmodels:/fraud-detector/Production ; changer de stage change le modèle servi, sans toucher au codeIntegration — FastAPI loads models:/fraud-detector/Production; changing stage changes the served model, without touching the codeCinq runs tracés et comparés dans l'UI, des métriques par segment avec worst_errors.csv attaché, un registre à deux versions dont une seule en Production (justifiée par écrit), et POST /predict servi depuis le registre — preuve du changement de version à l'appui. Au bloc 6, la CI viendra garder tout ça automatiquement.
Five runs traced and compared in the UI, per-segment metrics with worst_errors.csv attached, a registry with two versions and only one in Production (justified in writing), and POST /predict served from the registry — with proof of the version switch. In block 6, CI will start guarding all of this automatically.
Tout le lab, rejouable seul : chaque commande, chaque fichier en entier, un point de contrôle par étape. Prérequis : le repo fraud-detection/ des blocs 2–3 — src/train/train.py, src/serve/app.py, data/creditcard.csv — et le venv .venv.
The whole lab, replayable on your own: every command, every file in full, one checkpoint per step. Prerequisites: the fraud-detection/ repo from blocks 2–3 — src/train/train.py, src/serve/app.py, data/creditcard.csv — and the .venv virtualenv.
Dans un terminal dédié — il reste ouvert pendant tout le lab. Alternative minimale : mlflow ui (même interface, lit le dossier local ./mlruns).
In a dedicated terminal — it stays open for the whole lab. Minimal alternative: mlflow ui (same interface, reads the local ./mlruns folder).
cd fraud-detection
source .venv/bin/activate
pip install mlflow matplotlib
mlflow server --host 127.0.0.1 --port 5000
Remplacez src/train/train.py par cette version : les mêmes choix qu'au bloc 2 (split stratifié, scaler dans un Pipeline — jamais le classifieur seul —, class_weight, coût en euros), plus le carnet de bord : params, métriques, artefact, modèle.
Replace src/train/train.py with this version: the same choices as block 2 (stratified split, scaler inside a Pipeline — never the bare classifier —, class_weight, cost in euros), plus the logbook: params, metrics, artifact, model.
import argparse
import joblib
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import mlflow
import mlflow.sklearn
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import average_precision_score, precision_recall_curve
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
def main(c, class_weight, threshold):
df = pd.read_csv("data/creditcard.csv")
X = df.drop(columns=["Class"])
y = df["Class"]
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
cw = None if class_weight == "none" else class_weight
model = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression(C=c, class_weight=cw, max_iter=1000)),
])
mlflow.set_tracking_uri("http://127.0.0.1:5000")
mlflow.set_experiment("fraud-detector")
with mlflow.start_run():
mlflow.log_params({"C": c, "class_weight": class_weight,
"threshold": threshold,
"features": "time_v1_v28_amount"})
model.fit(X_tr, y_tr)
proba = model.predict_proba(X_te)[:, 1]
pred = (proba >= threshold).astype(int)
pr_auc = average_precision_score(y_te, proba)
missed = (y_te == 1) & (pred == 0)
false_alert = (y_te == 0) & (pred == 1)
cost = X_te.loc[missed, "Amount"].sum() + 15 * false_alert.sum()
mlflow.log_metrics({"pr_auc": pr_auc, "cost_eur": float(cost)})
prec, rec, _ = precision_recall_curve(y_te, proba)
plt.plot(rec, prec)
plt.xlabel("recall")
plt.ylabel("precision")
plt.savefig("pr_curve.png")
mlflow.log_artifact("pr_curve.png")
mlflow.sklearn.log_model(model, "model")
joblib.dump(model, "models/model.joblib")
print(f"pr_auc={pr_auc:.4f} cost_eur={cost:.0f}")
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("--c", type=float, default=1.0)
p.add_argument("--class-weight", default="balanced")
p.add_argument("--threshold", type=float, default=0.5)
a = p.parse_args()
main(a.c, a.class_weight, a.threshold)
Un changement par run — la boucle scientifique du bloc 2, mais chaque page s'écrit toute seule. Dans un second terminal (venv activé) : One change per run — block 2's scientific loop, but every page now writes itself. In a second terminal (venv activated):
mkdir -p models
python src/train/train.py --c 1.0 --class-weight balanced --threshold 0.5
python src/train/train.py --c 0.1 --class-weight balanced --threshold 0.3
python src/train/train.py --c 1.0 --class-weight none --threshold 0.5
Segments fixés à l'avance, les mêmes pour tous les modèles : gros montants et nuit. Ajoutez ce bloc dans train.py, à l'intérieur du with mlflow.start_run():, après le calcul de proba — puis relancez un run.
Segments fixed up front, the same for every model: large amounts and night. Add this block to train.py, inside the with mlflow.start_run():, after proba is computed — then launch one more run.
y_true = y_te.to_numpy()
segments = {
"large_amounts": (X_te["Amount"] > 1000).to_numpy(),
"night": ((X_te["Time"] % 86400) < 6 * 3600).to_numpy(),
}
for name, mask in segments.items():
if y_true[mask].sum() > 0:
seg_auc = average_precision_score(y_true[mask], proba[mask])
mlflow.log_metric(f"pr_auc_{name}", seg_auc)
print(f"pr_auc_{name}={seg_auc:.4f} ({int(mask.sum())} tx)")
Deux voies. Voie UI : ouvrez votre meilleur run → artefact model → bouton Register model → nom fraud-detector. Voie code : copiez le Run ID depuis l'UI, puis exécutez ceci (une fois pour un run moyen → version 1, une fois pour votre meilleur run → version 2) :
Two routes. UI route: open your best run → model artifact → Register model button → name fraud-detector. Code route: copy the Run ID from the UI, then run this (once for an average run → version 1, once for your best run → version 2):
import mlflow
mlflow.set_tracking_uri("http://127.0.0.1:5000")
result = mlflow.register_model("runs:/<RUN_ID>/model", "fraud-detector")
print(f"registered: version {result.version}")
Dans MLflow récent, les alias remplacent les stages (Staging/Production) : un alias est un pointeur nommé, déplaçable en une ligne. Créez scripts/promote.py puis exécutez-le :
In recent MLflow, aliases replace stages (Staging/Production): an alias is a named pointer you can move in one line. Create scripts/promote.py then run it:
from mlflow import MlflowClient
client = MlflowClient(tracking_uri="http://127.0.0.1:5000")
client.set_registered_model_alias("fraud-detector", "production", version=2)
print("alias @production -> version 2")
mkdir -p scripts
python scripts/promote.py
Remplacez le haut de src/serve/app.py (imports, lifespan, /health) par ceci — vos endpoints /predict et schémas Pydantic du bloc 3 restent inchangés dessous, ils lisent state["model"] :
Replace the top of src/serve/app.py (imports, lifespan, /health) with this — your block 3 /predict endpoints and Pydantic schemas stay unchanged below, they read state["model"]:
import os
from contextlib import asynccontextmanager
import joblib
import mlflow
import mlflow.sklearn
from fastapi import FastAPI
state = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
mlflow.set_tracking_uri(
os.getenv("MLFLOW_TRACKING_URI", "http://127.0.0.1:5000"))
try:
state["model"] = mlflow.sklearn.load_model(
"models:/fraud-detector@production")
state["model_source"] = "registry"
except Exception as exc:
print(f"registry unavailable ({exc}), falling back to joblib")
state["model"] = joblib.load("models/model.joblib")
state["model_source"] = "joblib_fallback"
yield
state.clear()
app = FastAPI(lifespan=lifespan)
@app.get("/health")
def health():
return {"status": "ok", "model_source": state["model_source"]}
Serveur MLflow toujours ouvert dans son terminal. Dans le second terminal, relancez uvicorn puis interrogez /health — ensuite, déplacez l'alias vers la version 1 (scripts/promote.py avec version=1), relancez : le modèle servi change sans toucher au code.
MLflow server still open in its terminal. In the second terminal, restart uvicorn then query /health — next, move the alias to version 1 (scripts/promote.py with version=1) and restart: the served model changes without touching the code.
export MLFLOW_TRACKING_URI=http://127.0.0.1:5000
uvicorn src.serve.app:app --port 8000
curl http://127.0.0.1:8000/health
Aucun run dans l'UI ? Le tracking URI n'est pas défini : sans set_tracking_uri, MLflow écrit dans un ./mlruns local — vérifiez la ligne dans train.py ou la variable MLFLOW_TRACKING_URI.
Alias introuvable (RestException) ? L'alias @production n'a jamais été posé, ou une faute de frappe : re-exécutez scripts/promote.py et vérifiez le badge dans l'onglet Models.
Le modèle du registre prédit n'importe quoi ? Vous avez loggé le classifieur sans son scaler : loggez toujours un Pipeline sklearn (scaler + modèle), comme à l'étape 2.
Port 5000 déjà pris ? Sur mac, AirPlay Receiver occupe le port 5000 : désactivez-le dans Réglages, ou lancez --port 5001 et adaptez l'URI partout.
No run in the UI? The tracking URI is not set: without set_tracking_uri, MLflow writes to a local ./mlruns — check the line in train.py or the MLFLOW_TRACKING_URI variable.
Alias not found (RestException)? The @production alias was never set, or a typo: rerun scripts/promote.py and check the badge in the Models tab.
The registry model predicts nonsense? You logged the classifier without its scaler: always log an sklearn Pipeline (scaler + model), as in step 2.
Port 5000 already taken? On mac, AirPlay Receiver squats port 5000: disable it in Settings, or run --port 5001 and adapt the URI everywhere.