LangueFR
Français
English
Retour au sommaire du cours

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.

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é.

les quatre niveaux
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)
Python
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ésultats

Le 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.

bash — le principe en quatre commandes
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.

Python — MLflow
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

modes de 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.

Le décalage entraînement/servage. C'est le bug le plus fréquent et le plus coûteux en production : le prétraitement appliqué à l'inférence diffère légèrement de celui de l'entraînement — une moyenne recalculée sur d'autres données, une catégorie inconnue traitée autrement, un fuseau horaire, un arrondi. Le modèle reçoit alors des entrées qu'il n'a jamais vues sous cette forme, et se dégrade sans qu'aucune alerte ne se déclenche. La parade : sérialiser le pipeline complet (module 3), jamais l'estimateur seul, et tester l'égalité des prédictions entre le code d'entraînement et le service sur un échantillon de référence — c'est un test automatisé à part entière.

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.
Python — un test de données déclaratif
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
CI/CD pour le ML
À 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 commande

7. 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 :

\[ \text{PSI} = \sum_{i=1}^{B} (p_i - q_i)\,\ln\!\frac{p_i}{q_i} \]
Python — calculer et interpréter un PSI
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.

ce qu'il faut surveiller, par ordre de priorité
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).

quand ré-entraîner
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.

MLflowDVCCI/CDMonitoringDriftGouvernance