MLOps Fundamentals : les 9 étapes clés pour passer de zéro à opérationnel

MLOps Fundamentals : l'essentiel en un article — vrai code, schémas et étapes concrètes, extraits d'un cours de 72 leçons.

MLOps Fundamentals : les 9 étapes clés pour passer de zéro à opérationnel

Tout le monde peut apprendre MLOps Fundamentals — à condition de suivre les étapes dans le bon ordre. On a condensé un cours complet de 72 leçons en un parcours clair, avec les extraits de code les plus utiles.

tl;dr
  • Installer l'environnement MLOps
  • Decouvrir le MLOps
  • Versionnement des donnees et modeles
  • Pipelines ML avec MLflow
  • Containeriser les modeles ML
~$ cat ./parcours.md # MLOps Fundamentals — 12 chapitres
01
Installer l'environnement MLOps
→ Installer Python, conda et les outils essentiels→ Configurer Git, GitHub et les bonnes pratiques+ 1 autres leçons
02
Découvrir le MLOps
→ C'est quoi le MLOps et pourquoi c'est indispensable ?→ Le cycle de vie d'un modèle ML en production+ 1 autres leçons
03
Versionnement des données et modèles
→ Pourquoi versionner les données et les modèles ?→ DVC — installation et premiers pas+ 1 autres leçons
04
Pipelines ML avec MLflow
→ MLflow Tracking : logger les expériences ML→ MLflow Model Registry : gérer le cycle de vie des modèles+ 1 autres leçons
05
Containeriser les modèles ML
→ Pourquoi Docker est essentiel pour le MLOps→ Dockerfile pour un modèle ML : build et run+ 1 autres leçons
06
Déployer avec FastAPI
→ FastAPI pour servir des modèles ML→ Construire une API de prédiction complète+ 1 autres leçons
07
CI-CD pour le ML
→ C'est quoi le CI/CD pour le ML ?→ GitHub Actions : automatiser les tests ML+ 2 autres leçons
08
Monitoring des modèles en production
→ Pourquoi les modèles se dégradent en production ?→ Détecter le Data Drift avec Evidently+ 1 autres leçons
🏁
Projet final (+ 4 chapitres en chemin)
→ Tu repars avec un projet concret et démontrable

Guide Complet du Projet Final – Étape par Étape

Cours MLOps Fundamentals • Détection de Fraude par Carte de Crédit • Pipeline MLOps End-to-End

NOTE📚 À propos de ce guide
Ce guide détaillé vous accompagne pas à pas dans la réalisation du projet final. Chaque section correspond à une étape clé du pipeline MLOps. Suivez les étapes dans l’ordre. Tout le code est fourni et expliqué. Durée estimée : 8–12 heures au total.

① Étape 1 – Initialisation du projet et environnement

Créez la structure du projet et l’environnement Conda :

bash
mkdir fraud-detection-mlops
cd fraud-detection-mlops
git init
git config user.email "vous@email.com"
git config user.name "Votre Nom"

conda create -n fraud-mlops python=3.10 -y
conda activate fraud-mlops

pip install scikit-learn xgboost pandas numpy mlflow dvc \
            fastapi uvicorn pydantic evidently \
            pytest pytest-cov httpx joblib matplotlib \
            seaborn imbalanced-learn flake8 black

conda env export > environment.yml

Créez le fichier requirements.txt :

output
scikit-learn==1.4.0
xgboost==2.0.3
pandas==2.1.4
numpy==1.26.3
mlflow==2.10.0
dvc==3.38.1
fastapi==0.109.0
uvicorn==0.27.0
pydantic==2.5.3
evidently==0.4.16
pytest==7.4.4
pytest-cov==4.1.0
httpx==0.26.0
joblib==1.3.2
matplotlib==3.8.2
seaborn==0.13.1
imbalanced-learn==0.11.0

Créez la structure des dossiers :

bash
mkdir -p data/raw data/processed src api monitoring/reports tests models .github/workflows
output
__pycache__/
*.pyc
*.pyo
.env
models/
mlruns/
*.pkl
data/raw/
data/processed/
monitoring/reports/*.html
monitoring/reports/*.json
.coverage
htmlcov/
bash
dvc init
git add .
git commit -m "feat: init project structure and DVC"

② Étape 2 – Génération et versionnement des données

Créez src/generate_synthetic_data.py (si vous n’avez pas accès au dataset Kaggle) :

output
"""Génère un dataset synthétique de fraude carte de crédit."""
import pandas as pd
import numpy as np

def generate_fraud_dataset(n_samples=50000, fraud_ratio=0.002, random_state=42):
    np.random.seed(random_state)
    n_fraud = int(n_samples * fraud_ratio)
    n_legit = n_samples - n_fraud

    legit = pd.DataFrame({
        f'V{i}': np.random.normal(0, 1, n_legit) for i in range(1, 29)
    })
    legit['Amount'] = np.abs(np.random.exponential(scale=88, size=n_legit))
    legit['Time'] = np.sort(np.random.uniform(0, 172800, n_legit))
    legit['Class'] = 0

    fraud = pd.DataFrame({
        f'V{i}': np.random.normal(np.random.uniform(-3, 3), 2, n_fraud) for i in range(1, 29)
    })
    fraud['Amount'] = np.abs(np.random.exponential(scale=122, size=n_fraud))
    fraud['Time'] = np.random.uniform(0, 172800, n_fraud)
    fraud['Class'] = 1

    df = pd.concat([legit, fraud], ignore_index=True)
    df = df.sample(frac=1, random_state=random_state).reset_index(drop=True)
    return df

if __name__ == "__main__":
    df = generate_fraud_dataset()
    df.to_csv("data/raw/creditcard.csv", index=False)
    print(f"Dataset généré : {len(df)} lignes")
    print(f"Fraudes : {df['Class'].sum()} ({df['Class'].mean()*100:.3f}%)")

Dockerfile pour un modèle ML : build et run

Cours MLOps Fundamentals — Chapitre 04 — Containeriser les modèles ML

NOTE🎯 Objectifs d'apprentissage
  • Comprendre chaque instruction d'un Dockerfile pour un modèle ML
  • Écrire un Dockerfile complet pour une API scikit-learn avec FastAPI
  • Construire une image Docker avec docker build
  • Exécuter et tester un conteneur ML avec docker run
  • Déboguer les problèmes courants de conteneurs ML

1. Structure du projet ML à conteneuriser

Avant d'écrire le Dockerfile, voyons la structure du projet que nous allons conteneuriser. Il s'agit d'une API de classification de vins basée sur un modèle Random Forest entraîné avec scikit-learn, servi via FastAPI.

output
wine-classifier/
â└â─â─ Dockerfile              # Notre fichier de configuration Docker
â└â─â─ .dockerignore           # Fichiers à exclure de l'image
â└â─â─ requirements.txt        # Dépendances Python
â└â─â─ src/
â├â─â─   app.py               # Application FastAPI (point d'entrée)
â├â─â─   predict.py           # Logique de prédiction
â├â─â─   preprocessing.py     # Pré-traitement des features
â└â─â─ models/
    â└â─â─ rf_classifier.pkl   # Modèle Random Forest sérialisé
    â└â─â─ scaler.pkl           # StandardScaler sauvegardé

Le fichier requirements.txt

Le requirements.txt liste toutes les dépendances Python avec leurs versions pinées. C'est crucial pour la reproductibilité : sans version fixée, pip install pourrait télécharger une version plus récente et incompatible.

output
# requirements.txt
# Framework web
fastapi==0.110.0
uvicorn[standard]==0.27.1

# Machine Learning
scikit-learn==1.4.1
numpy==1.26.4
pandas==2.2.1

# Sérialisation du modèle
joblib==1.3.2

# Validation des données
pydantic==2.6.3
WARNING⚠ Toujours pinér les versions !
Éviter scikit-learn>=1.0 ou scikit-learn sans version. Si scikit-learn publie la version 2.0 avec des changements d'API, votre conteneur cassera au prochain rebuild. Utilisez toujours scikit-learn==1.4.1.

L'application FastAPI (src/app.py)

output
# src/app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib
import numpy as np
import os

app = FastAPI(
    title="Wine Quality Classifier API",
    description="API de classification de la qualité du vin basée sur Random Forest",
    version="1.0.0"
)

# Chargement du modèle au démarrage (pas à chaque requête)
MODEL_PATH = os.getenv("MODEL_PATH", "/app/models/rf_classifier.pkl")
SCALER_PATH = os.getenv("SCALER_PATH", "/app/models/scaler.pkl")

model = joblib.load(MODEL_PATH)
scaler = joblib.load(SCALER_PATH)

class WineFeatures(BaseModel):
    fixed_acidity: float
    volatile_acidity: float
    citric_acid: float
    residual_sugar: float
    chlorides: float
    free_sulfur_dioxide: float
    total_sulfur_dioxide: float
    density: float
    pH: float
    sulphates: float
    alcohol: float

@app.get("/health")
def health_check():
    return {"status": "ok", "model": "rf_classifier", "version": "1.0.0"}

@app.post("/predict")
def predict(features: WineFeatures):
    try:
        X = np.array([[
            features.fixed_acidity, features.volatile_acidity,
            features.citric_acid, features.residual_sugar,
            features.chlorides, features.free_sulfur_dioxide,
            features.total_sulfur_dioxide, features.density,
            features.pH, features.sulphates, features.alcohol
        ]])
        X_scaled = scaler.transform(X)
        prediction = model.predict(X_scaled)[0]
        probability = model.predict_proba(X_scaled)[0].max()
        return {
            "quality": int(prediction),
            "confidence": round(float(probability), 4),
            "label": "bon vin" if prediction >= 6 else "vin moyen"
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

2. Le Dockerfile complet et commenté

Voici le Dockerfile complet. Nous allons analyser chaque instruction en détail immédiatement après.

output
# ============================================================
# Dockerfile pour un modèle scikit-learn servi avec FastAPI
# ============================================================

# Étape 1 : Image de base
FROM python:3.11-slim

# Étape 2 : Métadonnées de l'image
LABEL maintainer="mlops-team@exemple.com"
LABEL version="1.0.0"
LABEL description="Wine Quality Classifier - Random Forest API"

# Étape 3 : Variables d'environnement système
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Étape 4 : Répertoire de travail dans le conteneur
WORKDIR /app

# Étape 5 : Dépendances système (si nécessaire)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# Étape 6 : Copier ET installer les dépendances Python (couche cachée)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Étape 7 : Copier le code source
COPY src/ ./src/

# Étape 8 : Copier le modèle ML
COPY models/ ./models/

# Étape 9 : Exposer le port de l'API
EXPOSE 8000

# Étape 10 : Utilisateur non-root pour la sécurité
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

# Étape 11 : Commande de démarrage
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]

3. Analyse détaillée de chaque instruction

3.1 FROM python:3.11-slim

FROM définit l'image de base. python:3.11-slim est la version allégée de l'image Python officielle :

Pour scikit-learn, slim est le bon compromis : assez petit, mais compatible avec les extensions C de numpy/scipy.

WARNING⚠ Éviter :latest
FROM python:latest peut passer de Python 3.11 à 3.12 du jour au lendemain et casser votre code. Toujours spécifier la version exacte : python:3.11-slim ou même python:3.11.8-slim.
TIP💡 Pour GPU
Si votre modèle utilise le GPU (TensorFlow, PyTorch), utilisez :
FROM nvidia/cuda:12.1-cudnn8-runtime-ubuntu22.04

3.2 Variables d'environnement ENV

output
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1
Variable Effet Pourquoi l'activer ?
PYTHONDONTWRITEBYTECODE=1 Pas de fichiers .pyc Réduit la taille de l'image, évite les fichiers inutiles
PYTHONUNBUFFERED=1 Logs en temps réel Indispensable pour voir les logs dans docker logs immédiatement
PIP_NO_CACHE_DIR=1 Pas de cache pip dans l'image Réduit la taille finale de l'image (peut sauver 100-200 Mo)
PIP_DISABLE_PIP_VERSION_CHECK=1 Pas de vérification de mise à jour pip Accélère le build, évite les avertissements parasites

3.3 WORKDIR /app

WORKDIR définit le répertoire de travail dans le conteneur. Toutes les instructions suivantes (COPY, RUN, CMD) s'exécutent depuis ce répertoire. Si le répertoire n'existe pas, Docker le crée automatiquement.

TIP💡 Bonne pratique : Toujours utiliser /app comme répertoire de travail pour les applications Python. Éviter de travailler directement dans / ou /root.

Optimisation des images Docker ML

Cours MLOps Fundamentals — Chapitre 04 — Containeriser les modèles ML

NOTE🎯 Objectifs d'apprentissage
  • Comprendre pourquoi la taille des images Docker ML est un enjeu critique en production
  • Maîtriser les multi-stage builds pour séparer construction et exécution
  • Configurer un .dockerignore efficace pour les projets ML
  • Exploiter le cache de couches Docker à son maximum
  • Appliquer les meilleures pratiques pour des images ML de production

1. Pourquoi la taille des images ML est critique

Contrairement à une application web classique (quelques dizaines de Mo), une image Docker ML peut facilement atteindre 2 à 8 Go. Cette taille a des conséquences directes sur votre workflow MLOps :

🚬 Déploiement lent

Télécharger une image de 5 Go sur un cluster Kubernetes prend 10 à 20 minutes. En cas d'auto-scaling sous charge, c'est inacceptable.

💸 Coût de stockage

Stocker 50 versions d'une image de 4 Go dans AWS ECR = 200 Go × 0.10$/Go/mois = 20$/mois rien que pour le stockage.

🔐 Surface d'attaque

Chaque outil supplémentaire (compilateurs, utilitaires système) est une potentielle vulnérabilité de sécurité. Une image minimale est plus sécurisée.

2. Comparaison : images grasses vs images slim

Approche Image de base Taille typique Temps de pull (100 Mbit/s) Vulnérabilités
🔴 Image grasse (naïve) python:3.11 + tous les outils ~2.1 Go ~170 s Très nombreuses
🟡 Image slim (bonne pratique) python:3.11-slim ~700 Mo ~56 s Modérées
🟢 Multi-stage slim Build : python:3.11-slim
Run : python:3.11-slim
~350 Mo ~28 s Peu nombreuses
🆕 Distroless gcr.io/distroless/python3 ~180 Mo ~14 s Très peu
🟢 Alpine (avec précautions) python:3.11-alpine ~100 Mo ~8 s Minimales
WARNING⚠ Alpine et les bibliothèques ML
Alpine utilise musl libc au lieu de glibc. Numpy, scikit-learn, pandas et PyTorch sont compilés pour glibc. L'utilisation d'Alpine force la compilation depuis les sources, ce qui prend 30 à 60 minutes et peut échouer. Réserver Alpine aux microservices sans dépendances C. Pour le ML, préférer python:3.11-slim.

3. Le .dockerignore : votre première ligne de défense

Avant même le multi-stage build, le .dockerignore est l'optimisation la plus simple et la plus immédiate. Il empêche Docker d'envoyer des fichiers inutiles (voire dangereux) au dáemon de build.

TIP💡 Le contexte de build Docker
Quand vous exécutez docker build ., Docker envoie tout le répertoire courant au déamon Docker (appelé « contexte de build »). Sans .dockerignore, cela inclut votre dataset de 10 Go, les notebooks Jupyter, les environnements virtuels Python, les fichiers de configuration secrets…
output
# .dockerignore pour un projet ML

# Contrôle de version
.git/
.gitignore
.github/

# Environnements Python virtuels (critique ! peut faire des centaines de Mo)
.venv/
venv/
env/
ENV/
__pycache__/
*.py[cod]
*.pyo
.pytest_cache/
.mypy_cache/

# Données ML (ne jamais embarquer dans l'image !)
data/
datasets/
*.csv
*.parquet
*.arrow
*.feather

# Notebooks Jupyter (pas nécessaires en production)
*.ipynb
.ipynb_checkpoints/

# Fichiers de configuration locale
.env
.env.local
.env.development
*.env

# Documentation et tests (inutiles en production)
docs/
tests/
README.md
CHANGELOG.md

# Artefacts de build
dist/
build/
*.egg-info/
htmlcov/
.coverage

# IDE et éditeurs
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db

# Logs et fichiers temporaires
logs/
*.log
tmp/
temp/

# Fichiers MLflow et expériences
mlruns/
mlflow.db

# Modèles de test (garder seulement le modèle de production)
models/experiments/
models/checkpoints/

Mesurer l'impact du .dockerignore

bash
# Vérifier la taille du contexte de build AVANT .dockerignore
docker build -t test-before . 2>&1 | head -5
# Sending build context to Docker daemon  4.521GB  <-- sans .dockerignore !

# Vérifier la taille du contexte de build APRES .dockerignore
docker build -t test-after . 2>&1 | head -5
# Sending build context to Docker daemon  12.34MB  <-- avec .dockerignore

4. Les multi-stage builds pour le ML

Un multi-stage build utilise plusieurs instructions FROM dans un seul Dockerfile. Chaque étape produit une couche intermédiaire, et seule la dernière étape est conservée dans l'image finale. Cela permet de :

4.1 Multi-stage build pour un modèle scikit-learn

output
# ============================================================
# Multi-stage Dockerfile pour Wine Classifier (production)
# ============================================================

# ---- ÉTAPE 1 : Builder ----
# Cette étape installe tout, y compris les outils de compilation
FROM python:3.11-slim AS builder

# Variables d'environnement pour le build
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /build

# Installer les outils de build système (ne seront PAS dans l'image finale)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    g++ \
    build-essential \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# Copier et installer dans un répertoire local isolé (--prefix)
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt


# ---- ÉTAPE 2 : Image de production (finale) ----
# Image finale ultra-légère : ne contient PAS gcc, build-essential, etc.
FROM python:3.11-slim AS production

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Copier uniquement les bibliothèques installées depuis le builder
COPY --from=builder /install /usr/local

# Dépendances système runtime (minimales)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgomp1 \
    && rm -rf /var/lib/apt/lists/*

# Copier le code source et le modèle
COPY src/ ./src/
COPY models/ ./models/

# Sécurité : utilisateur non-root
RUN useradd --create-home --shell /bin/bash appuser && \
    chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
TIP💡 Gain de taille typique avec multi-stage
En éliminant gcc, g++, build-essential et les fichiers intermédiaires de compilation de l'image finale, vous gagnez généralement entre 200 et 600 Mo selon les dépendances C/C++ utilisées.

4.2 Multi-stage build avec étape de test

output
# ============================================================
# Multi-stage Dockerfile avec étape de test intégrée
# ============================================================

FROM python:3.11-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt


# ---- Étape de test ----
FROM base AS test
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY src/ ./src/
COPY models/ ./models/
COPY tests/ ./tests/
RUN pytest tests/ -v --tb=short


# ---- Étape de production ----
FROM base AS production
COPY src/ ./src/
COPY models/ ./models/
RUN useradd --create-home appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]
va-plus-loin

Cet article couvre les extraits les plus utiles — le cours complet MLOps Fundamentals (13 chapitres, 72 leçons, exercices corrigés et projet final) t'emmène jusqu'au bout.

./acceder-au-cours-complet cours gratuit : Maîtriser Claude Code

FAQ

Combien de temps pour apprendre MLOps Fundamentals ?
Avec une progression structurée (13 chapitres, 72 leçons courtes et pratiques), on atteint un niveau opérationnel en quelques semaines à raison de 30 à 60 minutes par jour. L'important est de pratiquer chaque notion immédiatement.
Faut-il des prérequis ?
Mieux vaut être à l'aise avec les fondamentaux du domaine : ce contenu va en profondeur, avec des cas réels.
Par où commencer concrètement ?
Reproduis les commandes de cet article, puis suis le cours complet MLOps Fundamentals : il enchaîne les 72 leçons dans l'ordre, avec exercices et projet final.

📬 Tu veux recevoir ce type de guide chaque semaine ? Abonne-toi gratuitement — code réel, zéro blabla.