MLOps : industrialiser un modèle
Un modèle en production n'est pas un artefact figé : c'est un système vivant, dont les entrées dérivent, dont les performances se dégradent en silence, et qui doit rester reproductible et réversible. Ce module traite de tout ce qui commence après le notebook — et qui détermine si le projet produit de la valeur ou de la dette.
📋 Au programme
1. La dette technique du ML
L'article de Sculley et al. (Google, 2015), Hidden Technical Debt in Machine Learning Systems, reste la meilleure entrée en matière. Son constat, resté célèbre par son schéma : le code du modèle occupe une petite boîte au milieu d'un ensemble bien plus vaste — collecte, vérification des données, infrastructure de servage, gestion de configuration, outillage d'analyse, monitoring, gestion des ressources. Tout ce qui entoure le modèle le dépasse largement en volume comme en coût de maintenance.
Deux formes de dette sont propres au ML.
Les frontières floues d'abord. En génie logiciel classique, une interface bien définie isole un module : on peut le modifier sans toucher au reste. Un modèle, lui, crée des dépendances non déclarées entre toutes ses entrées : modifier une source de données change son comportement sans qu'aucune interface ne le signale. Les auteurs résument cela par l'acronyme CACE — Changing Anything Changes Everything. Ajouter une variable modifie les poids de toutes les autres ; retirer une catégorie déplace toutes les frontières de décision.
Les boucles de rétroaction ensuite : un modèle de recommandation influence ce que les utilisateurs voient, donc ce qu'ils cliquent, donc les données de son prochain entraînement (modules 23 et 24). Le système apprend alors de ses propres sorties, et toute dérive devient auto-entretenue.
2. Reproductibilité
Reproductible signifie : à partir du dépôt, on peut régénérer exactement le même modèle. Cela demande de figer quatre choses, et il suffit d'en oublier une pour perdre la propriété.
Code un commit Git PRÉCIS (et non "la branche main", qui bouge)
Environnement uv.lock + image Docker taguée par DIGEST (modules 1 et 5)
— un tag comme :latest ne fige rien
Données version ou empreinte du jeu utilisé (hash, snapshot, requête figée
avec sa date). "la table clients" n'est pas une version.
Aléa graines fixées : Python, NumPy, framework, ET ordre de chargement
(un DataLoader mélange aussi)import os, random, numpy as np, torch
def figer_alea(seed: int = 42) -> None:
os.environ["PYTHONHASHSEED"] = str(seed) # hachage des chaînes, lu au démarrage
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True # au prix d'un peu de vitesse
torch.backends.cudnn.benchmark = False # sinon cuDNN choisit l'algo le plus
# rapide, qui varie d'une exécution
# à l'autre — et change les résultatsLe déterminisme complet sur GPU reste hors d'atteinte pour certaines opérations : les réductions atomiques additionnent les contributions dans un ordre qui dépend de l'ordonnancement des threads, et l'addition flottante n'est pas associative. L'objectif réaliste est donc un modèle équivalent aux fluctuations numériques près, et surtout une traçabilité complète : à partir d'une prédiction, pouvoir remonter au modèle, à son entraînement, à ses données et à son code.
3. Versionner code, données et modèles
Git gère le code, mais pas des fichiers de plusieurs gigaoctets (module 1). DVC résout cela élégamment : le fichier de données part sur un stockage distant (S3, disque réseau, NAS) et Git ne conserve qu'un petit fichier texte contenant son empreinte et son emplacement.
dvc add data/raw/transactions.parquet # crée transactions.parquet.dvc (quelques lignes) git add data/raw/transactions.parquet.dvc .gitignore # Git suit le POINTEUR dvc remote add -d stockage s3://mon-bucket/dvc dvc push # envoie les DONNÉES sur le stockage distant # six mois plus tard, pour retrouver un état exact : git checkout v1.2 && dvc checkout # le code ET les données correspondantes
Pour les modèles, un registry (MLflow Model Registry, ou un simple bucket versionné avec convention de nommage) associe à chaque version : l'artefact, ses métriques de validation, le commit qui l'a produit, les données utilisées et un état (staging, production, archivé). C'est ce qui rend un retour arrière possible en quelques minutes — la capacité opérationnelle la plus précieuse quand un déploiement se passe mal.
4. Tracking d'expériences
Après deux semaines d'itérations, personne ne se souvient de la configuration qui a produit le meilleur score. Le pire scénario — un excellent résultat qu'on ne sait plus reproduire — disparaît pour quelques lignes de code.
import mlflow
mlflow.set_experiment("prevision-prix")
with mlflow.start_run(run_name="gbm-features-v3"):
# ── ce qui DÉFINIT l'expérience ──
mlflow.log_params({"max_depth": 6, "learning_rate": 0.05,
"n_features": X.shape[1], "n_train": len(X_train)})
mlflow.log_param("git_sha", sha_courant()) # le code exact
mlflow.log_param("data_version", "dvc:8f3a91c") # les données exactes
modele.fit(X_train, y_train)
# ── ce qu'on en retient ──
mlflow.log_metrics({"rmse_cv": rmse_cv, "rmse_test": rmse_test, "mae_test": mae_test})
mlflow.log_metric("duree_entrainement_s", duree)
mlflow.log_artifact("figures/residus.png") # l'analyse d'erreurs
mlflow.sklearn.log_model(modele, "modele", registered_model_name="prix-immo")Ce qu'il faut consigner systématiquement : hyperparamètres, version du code et des données, métriques de validation et de test, durée et coût d'entraînement, et les quelques graphiques d'analyse d'erreurs. L'outil importe peu — MLflow en auto-hébergé, Weights & Biases en service — mais commencer dès le premier jour est essentiel : le tracking est impossible à reconstituer rétroactivement.
5. Packaging et servage
Batch scoring périodique, résultats écrits en base. Le plus simple, le plus
robuste, et SUFFISANT dans une large majorité des cas : personne n'a
besoin d'un score de churn à la milliseconde.
Online API synchrone (module 6). Contrainte de latence, mise à l'échelle,
disponibilité. À ne choisir que si la fraîcheur l'exige vraiment.
Streaming scoring sur flux (Kafka), pour la détection de fraude en temps réel.
Embarqué ONNX, TFLite, CoreML : inférence sur l'appareil, sans réseau.Quelques points techniques qui font gagner du temps. ONNX est un format d'échange neutre : on exporte le modèle une fois, on l'exécute partout (C++, Java, navigateur) sans dépendre de Python — utile à la fois pour la performance et pour la longévité, un modèle exporté restant lisible quand la version de scikit-learn qui l'a produit aura disparu. La quantification (module 22) divise taille et latence. Le cache de prédictions est trivial et souvent très rentable quand les mêmes entrées reviennent. Et toute réponse doit porter la version du modèle qui l'a produite.
6. Tester un système de ML
Aux tests logiciels classiques (module 1) s'ajoutent trois familles propres au ML :
- Tests de données — schéma, types, bornes, taux de valeurs manquantes, cardinalités, distributions attendues. Des outils comme Great Expectations ou pandera déclarent ces attentes et cassent le pipeline quand elles sont violées. C'est la protection la plus rentable de tout le dispositif : l'immense majorité des incidents de production vient des données, pas du modèle.
- Tests de modèle — performance minimale sur un jeu de référence figé, comportements attendus sur des cas connus (tests behavioral : invariance à une reformulation, monotonie « à surface égale, plus de pièces ne baisse pas le prix »), absence de régression par rapport à la version en production, équité entre sous-groupes (module 27).
- Tests d'intégration — la chaîne complète, de la donnée brute à la réponse de l'API, sur un mini-jeu versionné dans le dépôt.
import pandera.polars as pa
schema = pa.DataFrameSchema({
"prix": pa.Column(float, pa.Check.in_range(10_000, 5_000_000)),
"surface": pa.Column(float, pa.Check.in_range(8, 1000)),
"code_postal": pa.Column(str, pa.Check.str_matches(r"^\d{5}$")),
"n_pieces": pa.Column(int, pa.Check.in_range(1, 20), nullable=True),
})
df_valide = schema.validate(df) # lève une erreur DÉTAILLÉE si le contrat
# n'est pas respecté — et arrête le pipeline
# AVANT d'entraîner sur des données corrompuesÀ chaque commit lint, types, tests unitaires, tests d'intégration rapides
Sur la branche main entraînement complet, validation sur jeu figé,
COMPARAISON à la version en production
Si validation OK enregistrement dans le registry en "staging"
Promotion manuelle ou automatique selon le risque, puis déploiement
progressif (section 7)
Toujours possibilité de revenir en arrière en une commande7. Déploiement progressif
Un modèle validé hors ligne peut échouer en ligne — décalage de prétraitement, latence, distribution réelle différente. On ne bascule donc jamais 100 % du trafic d'un coup.
- Shadow — le nouveau modèle reçoit le trafic réel et ses prédictions sont enregistrées, mais non utilisées. Aucun risque, et l'on vérifie latence, taux d'erreur et cohérence sur les vraies données. C'est l'étape que l'on saute le plus souvent à tort, alors qu'elle coûte presque rien.
- Canary — 1 %, puis 5 %, puis 25 % du trafic, avec surveillance des métriques techniques et métier à chaque palier, et une règle d'arrêt définie à l'avance.
- A/B test — la seule façon de mesurer l'effet causal du nouveau modèle sur l'indicateur métier (module 9). Un meilleur RMSE hors ligne ne garantit en rien un meilleur résultat en ligne (module 23).
- Retour arrière — préparé, testé, exécutable en une commande. Si le retour arrière n'a jamais été essayé, il ne fonctionne pas : c'est une certitude empirique.
8. Monitoring et dérive
La performance d'un modèle se dégrade avec le temps : c'est la règle, pas l'exception. On distingue trois phénomènes, qui appellent des réponses différentes :
- Dérive des données (covariate shift) — la distribution des entrées \(P(X)\) change : nouvelle clientèle, capteur remplacé, changement d'interface qui modifie les saisies. Le modèle reste valide, mais il est utilisé hors de son domaine habituel.
- Dérive du concept — la relation \(P(Y\mid X)\) elle-même change : les comportements évoluent, une réglementation modifie les règles du jeu. Plus grave, car même un réentraînement ne rattrape le retard qu'après coup, une fois les nouvelles étiquettes disponibles.
- Dérive des étiquettes — la distribution de la cible change (le taux de fraude double).
Pour détecter une dérive sans attendre les étiquettes — qui arrivent souvent avec des semaines de retard —, on surveille les distributions d'entrée. L'indice de stabilité de population compare les proportions par intervalle entre référence et période courante :
import numpy as np
def psi(reference, courant, n_bins=10):
# les bornes viennent de la RÉFÉRENCE : on mesure le déplacement par rapport à elle
bornes = np.percentile(reference, np.linspace(0, 100, n_bins + 1))
bornes[0], bornes[-1] = -np.inf, np.inf
p = np.histogram(reference, bins=bornes)[0] / len(reference)
q = np.histogram(courant, bins=bornes)[0] / len(courant)
p, q = np.clip(p, 1e-6, None), np.clip(q, 1e-6, None) # éviter log(0)
return float(np.sum((q - p) * np.log(q / p)))
# lecture usuelle : < 0,1 stable | 0,1–0,25 à surveiller | > 0,25 dérive significative
for col in colonnes:
v = psi(ref[col], prod[col])
if v > 0.25:
print(f"ALERTE dérive sur {col} : PSI = {v:.3f}")On complète par un test de Kolmogorov-Smirnov pour les variables continues et un chi² pour les catégorielles (module 9) — en se rappelant que sur de gros volumes, ces tests détectent des écarts statistiquement significatifs mais opérationnellement négligeables. C'est l'ampleur qui doit déclencher l'alerte, pas la p-value.
1. Santé technique latence (p50, p95, p99), taux d'erreur, disponibilité, coût 2. Qualité des entrées taux de manquants, valeurs hors domaine, catégories inconnues 3. Distribution PSI par variable, dérive des embeddings pour le non-tabulaire 4. Sorties du modèle distribution des scores, taux de prédictions positives 5. Performance réelle dès que les étiquettes arrivent : métriques PAR SEGMENT 6. Impact métier LA mesure qui compte : conversion, coût évité, satisfaction
Les couches 1 à 4 sont disponibles immédiatement ; la 5 arrive en différé ; la 6 exige souvent une expérimentation. Un tableau de bord qui ne montre que la couche 1 donne une fausse impression de contrôle — un modèle peut répondre en 20 ms avec une disponibilité parfaite tout en étant devenu complètement faux.
9. Orchestration et ré-entraînement
Les pipelines (extraction, variables, entraînement, validation, déploiement) sont décrits comme des graphes de tâches et planifiés par un orchestrateur : Airflow (le standard historique, très répandu), Prefect ou Dagster (plus modernes, Dagster étant construit autour des actifs de données, ce qui correspond bien au ML), ou Kubeflow si l'on vit déjà dans Kubernetes (module 5).
Périodique chaque semaine ou chaque mois. Simple, prévisible, souvent suffisant.
Sur déclenchement quand le monitoring détecte une dérive au-delà d'un seuil (section 8)
Sur volume quand N nouvelles observations ÉTIQUETÉES sont disponibles
En continu apprentissage en ligne. Réservé aux cas qui l'exigent vraiment :
difficile à déboguer, très exposé aux boucles de rétroaction.Un ré-entraînement automatique n'est acceptable qu'avec des garde-fous : validation sur un jeu figé, comparaison obligatoire avec le modèle en place, et refus automatique de promotion si la performance recule. Sans cela, un pipeline automatisé propage en production un modèle entraîné sur des données corrompues — plus vite et plus sûrement qu'un humain ne l'aurait fait.
10. Gouvernance et checklist
La model card documente, en une page : l'usage prévu et les usages exclus, les données d'entraînement et leurs biais connus, les performances globales et par sous-groupe, les limites, et le contact responsable. Ce n'est pas de la bureaucratie : c'est ce qui permet à quelqu'un d'autre — ou à soi-même dans un an — de savoir si le modèle peut être utilisé dans un contexte donné.
Le cadre réglementaire européen ajoute des obligations concrètes, détaillées au module 27 : le RGPD encadre les données personnelles et prévoit un droit à l'explication pour les décisions automatisées ; l'AI Act classe les systèmes par niveau de risque et impose documentation, traçabilité et supervision humaine pour les usages à haut risque. Ces exigences se traitent en amont — journalisation des prédictions, explicabilité, tests d'équité — bien plus facilement qu'après coup.
Checklist avant mise en production
- Le pipeline complet (prétraitement inclus) est sérialisé et versionné.
- Le modèle est reproductible : commit, environnement, version des données, graines.
- Les prédictions du service sont identiques à celles du code d'entraînement sur un échantillon de référence.
- Les tests de données bloquent le pipeline en cas de schéma ou de distribution anormale.
- Une baseline est déployée en parallèle pour comparaison.
- La latence et le coût par prédiction sont mesurés sous charge réaliste.
- Le monitoring couvre santé technique, qualité des entrées, dérive et sorties.
- Les alertes ont un destinataire et un mode d'emploi, pas seulement un seuil.
- Le retour arrière a été testé pour de vrai.
- La model card existe, et les performances par sous-groupe ont été vérifiées.
Ce qu'il faut retenir du module
- Le code du modèle est la petite partie ; le système autour est le vrai travail.
- Reproductibilité = code + environnement + données + aléa, tous versionnés ensemble.
- Tracker chaque expérience dès le premier jour : c'est rétroactivement impossible.
- Sérialiser le pipeline entier, jamais l'estimateur seul.
- Les tests de données sont la protection la plus rentable qui existe.
- Shadow, puis canary, puis A/B ; retour arrière testé.
- Surveiller la dérive sans attendre les étiquettes ; alerter sur l'ampleur, pas sur la p-value.
- Le ré-entraînement automatique exige des garde-fous, sinon il automatise l'erreur.