Formation MLOps · Dotika · Bloc 3 · 3 h

La porte
d'entrée

Serving & API avec FastAPI — donner une adresse au cerveau, pour que n'importe quelle application puisse lui demander un verdict.

Le fil rouge · où on en est

Le cerveau existe — et personne ne peut lui parler

Bloc 2 : vous avez entraîné un détecteur de fraude, évalué honnêtement, seuil justifié en euros. Mais regardez la carte : le cerveau vit dans un cadre en pointillés — un notebook, sur un laptop. L'app de paiement, les analystes, les serveurs de la banque : personne ne peut l'interroger.

Mission du bloc 3 : donner une adresse au modèle — une API que toute la banque peut appeler.

FastAPIPydanticpytest
L'objectif du bloc

À la fin du bloc, n'importe quelle application de la banque peut demander un verdict — en temps réel

Un endpoint POST /predict qui répond en quelques millisecondes, des entrées validées, des tests qui prouvent que ça marche, une doc qui ne ment jamais. Le modèle devient un service.

Bloc 3 · 3 h · théo 0h30 / prat 2h30 · jours 1–2

Une théorie courte, puis au clavier

0h30
Stratégies de serving (batch vs temps réel) & training-serving skewLes deux façons de servir un modèle, et l'erreur invisible qui guette entre entraînement et serving.
théorie
1h20
API FastAPI : endpoint /predict, modèle chargé au démarrageLe premier guichet — une transaction entre, un verdict sort, en quelques millisecondes.
pratique
0h50
Validation Pydantic + endpoints /batch, /feedback, /healthBlinder la porte, puis ouvrir les autres guichets — dont la boucle du verdict humain.
pratique
0h20
Tests pytest & documentation SwaggerProuver que le contrat tient, et le documenter sans écrire une ligne de doc.
pratique

À cheval sur deux jours : ≈ 2 h le jour 1 (théorie + /predict), ≈ 1 h le jour 2 (validation, guichets, tests).

Acte 1 · théorie 0h30

Un cerveau muet
ne sert à rien

Il est 3 h 47. Une carte française est utilisée à Tallinn. L'app de la banque a 300 millisecondes pour décider — et votre modèle dort dans un notebook, sur un laptop éteint.

Le meilleur modèle du monde, s'il est injoignable au moment où on a besoin de lui, vaut exactement zéro.

Le mot du jour

« Servir » un modèle

Servir, c'est rendre les prédictions accessibles à ceux qui en ont besoin — au moment où ils en ont besoin.

Le data scientist ne sera pas là à 3 h 47, et personne ne relancera un notebook. Le serving, c'est faire répondre le modèle sans humain dans la boucle : une machine demande, une machine répond. Toute la question est quand et comment.

servinginferenceprediction service
Les deux façons de servir

Batch ou temps réel : deux métiers différents

Batch · scorer la nuit
Un gros calcul planifié
  • Toutes les transactions du jour, scorées à 2 h du matin, résultats déposés dans une table
  • Latence : on attend le prochain passage — des heures
  • Simple, robuste, économe — quand la décision peut attendre demain
Temps réel · répondre à la demande
Une API toujours debout
  • Une transaction arrive, un verdict repart — pendant le paiement
  • Latence : des millisecondes, exigées par le métier
  • Plus exigeant : disponibilité, validation, monitoring — tout ce bloc
Le choix

Le bon mode, c'est le métier qui le dicte

Batch suffit quand…
…la décision peut attendre : relances marketing, rapports de risque, scores de churn recalculés chaque nuit.
des heures de latence, et tout le monde est content
Temps réel obligatoire quand…
…la décision est en cours : le client attend devant le terminal, la fraude se joue maintenant ou jamais.
300 ms pour dire oui ou non
Notre détecteur sera temps réel — et beaucoup d'équipes font les deux : temps réel pour bloquer, batch pour analyser. Notre API saura faire les deux aussi.
L'ennemi invisible · training-serving skew

Entraîné sur un monde,
servi dans un autre

Le skew, c'est quand les features ne sont pas calculées pareil à l'entraînement et au serving. Le modèle reçoit des chiffres qui ressemblent aux bons — et se trompe en silence : pas de crash, pas d'erreur dans les logs, juste des verdicts faux.

L'API répond 200 OK et le modèle raconte n'importe quoi. C'est le pire bug du ML : celui qu'on ne voit pas.

# à l'entraînement (bloc 2)
df["amount_log"] = np.log1p(df["Amount"])

# au serving, écrit six mois plus tard…
features = [req.amount, req.hour, ...]  # ← montant brut : skew !
Deux lignes écrites par deux personnes, à six mois d'écart. Aucune ne crashe.
S'en protéger

Quatre parades contre le skew

Parade 1
Une seule source de vérité
La fonction de features vit dans src/, importée par le train ET par l'API. Jamais deux copies du même calcul.
Parade 2
Embarquer le préprocesseur
Le scaler est sauvé avec le modèle, en pipeline : ce qui a appris ensemble est servi ensemble.
Parade 3
Tester le pipeline complet
Un test envoie une transaction brute à l'API et vérifie le score exact attendu. On l'écrit à l'acte 4.
Parade 4
Monitorer les distributions
Comparer ce qui entre en prod à ce qui a servi à l'entraînement. C'est le sujet du bloc 7 — patience.
Acte 2

FastAPI,
la porte d'entrée

Avant la techno · l'idée

Une API est un contrat

« Envoie-moi une transaction sous cette forme, je te renvoie un verdict sous cette forme. » Rien de plus, rien de moins.

L'app de paiement ne sait pas que c'est du scikit-learn derrière — et n'a pas besoin de le savoir. Tant que le contrat est tenu, on peut changer le modèle, le langage, la machine : personne ne s'en aperçoit. Ce découplage est ce qui rend le système évolutif.

Ce qu'on gagne

Trois choses qu'on gagne d'un coup

Le découplage
Le modèle évolue sans toucher aux applications ; les applications évoluent sans toucher au modèle. Chacun sa vie, le contrat entre les deux.
Le point d'entrée unique
Une seule porte à surveiller, sécuriser, mesurer. Tout ce qui entre et sort du modèle passe par là — un luxe pour le MLOps.
Le langage universel
HTTP + JSON : l'app mobile, le site web, le vieux système de la banque — tout le monde parle ça, depuis n'importe quel langage.

C'est le même geste qu'un restaurant qui ouvre une salle : la cuisine reste privée, seul le menu est public.

Feynman · 3 minutes

Un réceptionniste
et ses guichets

Un serveur web, c'est un réceptionniste : il attend qu'on lui parle. Chaque route est un guichet — POST /predict, le guichet « verdict » ; GET /health, le guichet « ça va ? ». Le client dépose un formulaire (le JSON), le réceptionniste le porte au cerveau, et rapporte la réponse.

FastAPI, c'est l'art de déclarer des guichets en trois lignes de Python.

app de paiement JSON → API POST /predict POST /batch POST /feedback GET /health modèle ← 0.93
Le client ne rencontre jamais le cerveau — seulement les guichets.
Le vocabulaire minimal

HTTP en trois idées — tout ce qu'il faut aujourd'hui

Un verbe + une route
POST /predict = « je t'envoie des données, décide ». GET /health = « donne-moi une info ». Le verbe dit l'intention, la route dit le guichet.
Du JSON dans les deux sens
Des clés, des valeurs, rien d'exotique. La lingua franca des machines — lisible aussi par les humains, ce qui sauve des soirées de debug.
Un code de statut
200 = tout va bien. 422 = ta requête est invalide. 500 = j'ai un problème. Trois nombres suffisent pour toute la journée.
POSTGETJSON200422500
La techno choisie

Pourquoi FastAPI — et pas Flask ou Django

Les types travaillent
On déclare amount: float — et FastAPI valide, convertit, documente. Le typage Python cesse d'être décoratif.
La doc s'écrit seule
Swagger généré depuis le code, toujours à jour, interactif. On la découvre à l'acte 4 — elle est déjà en train de s'écrire.
Rapide par conception
Async natif, l'un des frameworks Python les plus rapides. Largement de quoi tenir nos 300 millisecondes.

Devenu le standard de facto pour servir des modèles ML en Python — la question d'entretien n'est plus « pourquoi FastAPI ? » mais « pourquoi pas ? ».

Le premier guichet

/predict tient en dix lignes

# src/api/main.py
from fastapi import FastAPI
app = FastAPI(title="Fraud Detector")

@app.post("/predict")
def predict(txn: Transaction):
    score = model.predict_proba(txn.to_features())[0, 1]
    return {"fraud_score": round(score, 4),
            "verdict": "ALERT" if score >= 0.31 else "OK"}

Le décorateur déclare le guichet, la fonction fait le travail. Et regardez le seuil 0,31 : c'est celui que vous avez justifié en euros au bloc 2 — la décision métier voyage avec le code.

Le contrat, en vrai

Ce qui part, ce qui revient

# requête → POST /predict
{ "amount": 1499.90, "hour": 3, "v14": -8.31, "v12": -5.20, ... }

# réponse ← HTTP 200 · 11 ms
{ "fraud_score": 0.93, "verdict": "ALERT", "model_version": "logreg-v3" }

Ce couple requête/réponse est le contrat. Tout le reste du bloc — validation, tests, documentation — ne sert qu'à une chose : le rendre incassable.

Histoire vraie · le bug du premier jour

2 secondes ou 8 millisecondes

Le modèle rechargé à chaque requête
joblib.load() écrit dans la fonction predict : le modèle entier relu depuis le disque à chaque transaction. Ça marche — lentement.
~2 s par requête. Le paiement a expiré.
Le modèle chargé au démarrage
Chargé une fois quand l'API démarre, gardé en mémoire, réutilisé par toutes les requêtes suivantes.
~8 ms par requête — 250× plus vite
Même modèle, même machine : seule la place d'une ligne de code change. C'est LE réflexe serving de la journée.
Le réflexe, en code

Charger une fois, répondre mille fois

# exécuté UNE fois, au démarrage de l'API
@asynccontextmanager
async def lifespan(app: FastAPI):
    ml["model"] = joblib.load("models/model.pkl")  # ~2 s, une seule fois
    yield  # ← l'API vit ici, le modèle déjà en mémoire
    ml.clear()  # à l'extinction

app = FastAPI(lifespan=lifespan)

Les 2 secondes de chargement sont payées une seule fois, au démarrage. Ensuite, chaque requête trouve le modèle déjà là.

Async · la version simple

Le réceptionniste ne reste
pas au téléphone

Quand une base de données met 80 ms à répondre, un serveur classique attend, bras croisés. Un serveur async passe au client suivant et revient quand la réponse arrive. Même réceptionniste, zéro temps mort.

Pour nous, aujourd'hui : def suffit. Retenez juste pourquoi async existe — et que FastAPI le maîtrise nativement.

3 requêtes, chacune attend la base 80 ms sync attente… async l'attente des uns sert aux autres même machine — trois fois moins de temps total
Async ne rend rien plus rapide — il recycle l'attente.
Démo live · à vous de jouer

Interrogez le modèle

en attente d'une requête…
Requête → POST /predict
# cliquez sur un bouton pour envoyer
Réponse
# la réponse s'affichera ici
Acte 3

Blinder
la porte

Premier jour en production : une transaction arrive sans montant. La deuxième affiche −50 €. La troisième dit « mille euros » — en toutes lettres.

Le monde extérieur est créatif. Une API qui gobe tout transmet le chaos directement au modèle : garbage in, garbage out — avec un code 200 pour faire joli.

Pydantic · le videur

Personne n'entre
sans papiers valides

Pydantic vérifie chaque requête à la porte : les champs attendus, du bon type, dans les bonnes bornes. Ce qui passe est propre — garanti. Ce qui ne passe pas est refusé avant même de toucher le modèle.

On déclare le contrat une fois, en Python typé. Le videur, lui, ne dort jamais.

class Transaction(BaseModel):
    amount: float = Field(gt=0, le=100_000)
    hour: int = Field(ge=0, le=23)
    v14: float
    v12: float
    # champ manquant ou invalide → refusé
La classe EST le contrat — lisible par un humain et exécutée par la machine.
Le refus automatique

422 : le refus poli, précis, gratuit

Requête invalide ? FastAPI répond tout seul — sans une ligne de code de votre part — et désigne exactement le champ fautif.

# amount: -50 → HTTP 422 Unprocessable Entity
{ "detail": [{
    "loc": ["body", "amount"],
    "msg": "Input should be greater than 0",
    "type": "greater_than" }] }

Le développeur de l'app de paiement sait immédiatement quoi corriger — sans vous appeler à 3 h du matin. Une bonne erreur est une documentation.

Guichet 2 · /batch

Scorer 10 000 transactions d'un coup

Les analystes veulent re-scorer l'historique de la nuit. 10 000 appels à /predict ? Non — un appel, une liste.

@app.post("/batch")
def batch(txns: list[Transaction]):
    X = to_features(txns)
    scores = model.predict_proba(X)[:, 1]  # vectorisé : un seul passage
    return {"scores": scores.round(4).tolist(), "count": len(txns)}

NumPy score 10 000 lignes presque aussi vite qu'une seule — le serving batch et le temps réel de l'acte 1 cohabitent dans la même API.

Guichet 3 · /feedback · la boucle

Le retour de
la vérité terrain

L'analyste examine l'alerte et rend le verdict humain : vraie fraude ou fausse alerte. /feedback enregistre cette réponse. C'est la donnée la plus précieuse du système — celle qui, demain, ré-entraînera le modèle.

Souvenez-vous de la carte système : cette flèche retour transforme un modèle figé en système qui apprend. On la referme complètement au bloc 8.

class Feedback(BaseModel):
    transaction_id: str
    true_label: int = Field(ge=0, le=1)  # le verdict humain

@app.post("/feedback")
def feedback(fb: Feedback):
    store.append(fb)  # stocké pour le ré-entraînement
    return {"status": "recorded"}
Six lignes aujourd'hui — la boucle d'apprentissage de demain.
Guichet 4 · /health

Le pouls du service

GET /health répond « je suis vivant, le modèle est chargé ». Deux lignes de code — une importance démesurée.

Qui appelle ce guichet ? Pas des humains : des machines. Le load balancer, la supervision — et bientôt Kubernetes, qui l'interrogera toutes les dix secondes pour décider s'il faut redémarrer votre service. Rendez-vous au bloc 7.

GET /health{"status": "ok", "model_loaded": true}
L'API complète · vue d'ensemble

Quatre guichets, quatre clients

POST /predict
L'app de paiement
Une transaction, un verdict, quelques millisecondes. Le cœur du service — et du métier.
POST /batch
Les analystes
Des milliers de transactions d'un coup, vectorisées. Le mode batch, offert par la même API.
POST /feedback
Le verdict humain qui revient
La vérité terrain, capturée à la source. Future matière première du ré-entraînement.
GET /health
Les machines qui surveillent
Le pouls, lu par le load balancer aujourd'hui — par Kubernetes au bloc 7.
Acte 4

Prouver que
ça marche

pytest · TestClient

Tester l'API
sans la lancer

TestClient fait tourner l'API en mémoire : pas de serveur à démarrer, pas de port à ouvrir. Chaque test envoie une vraie requête HTTP et vérifie la réponse — le tout en millisecondes.

Ces tests sont un investissement : au bloc 6, la CI les rejouera à chaque commit, automatiquement. Écrivez-les comme si un robot allait les lire — parce que c'est le cas.

def test_predict_ok():
    r = client.post("/predict", json=TXN_OK)
    assert r.status_code == 200
    assert 0 <= r.json()["fraud_score"] <= 1

def test_montant_negatif_refuse():
    r = client.post("/predict", json={**TXN_OK, "amount": -50})
    assert r.status_code == 422
On teste le contrat — pas les entrailles.
La checklist

Quatre tests qui couvrent le contrat

Test 1
Le cas nominal
Une transaction valide → 200, un score entre 0 et 1, un verdict cohérent avec le seuil.
Test 2
Le videur
Montant négatif, champ manquant → 422, avec le bon champ désigné. On teste aussi les refus.
Test 3
Le batch
100 transactions envoyées → 100 scores rendus, dans l'ordre. Le contrat vaut aussi pour les listes.
Test 4
Le pouls
/health → 200 et model_loaded: true. Le test le plus court — et le premier que la prod regardera.
Swagger · /docs

La doc qui
ne ment jamais

Ouvrez /docs : chaque guichet, chaque champ, chaque exemple — générés depuis le code. La doc manuelle ment au bout de deux semaines ; celle-ci est reconstruite à chaque démarrage, depuis la seule source de vérité : le code.

Et elle est interactive : le bouton « Try it out » envoie de vraies requêtes. La démo de tout à l'heure ? Offerte avec le framework.

# http://localhost:8000/docs

Fraud Detector  v0.3.0

POST  /predict   Score une transaction
POST  /batch     Score une liste de transactions
POST  /feedback  Enregistre un verdict humain
GET   /health    Le pouls du service
Écrite par personne, à jour pour toujours.
Avant de coder · l'essentiel

Cinq réflexes à emporter

Réflexe 1
Le mode de serving vient du métier
Batch quand la décision peut attendre, temps réel quand elle est en cours. Jamais l'inverse.
Réflexe 2
Le modèle se charge au démarrage
Jamais dans la fonction de prédiction. 2 secondes une fois — pas 2 secondes à chaque requête.
Réflexe 3
Valider à la porte
Pydantic déclare le contrat, le 422 fait le reste. Rien de sale ne touche le modèle.
Réflexe 4
Mêmes features au train et au serving
Une seule source de vérité pour le calcul des features — sinon le skew s'installe en silence.

Et le cinquième : tester le contrat, pas les entrailles — c'est le contrat que les autres consomment.

Acte 5 · lab

À vous :
2h30 au clavier

Mission 1 / 3 · 1h20 · pratique

Ouvrir le guichet /predict

Dans src/api/main.py : créer l'app FastAPI, charger votre modèle du bloc 2 au démarrage (lifespan), déclarer POST /predict qui renvoie fraud_score et verdict — avec votre seuil justifié en euros. Lancer avec uvicorn, appeler avec curl.

Livrable : uvicorn src.serve.app:app tourne, et une transaction envoyée en curl reçoit un verdict en moins de 100 ms.

FastAPIuvicornlifespanjoblib.loadcurl

Piège classique : si votre premier appel prend 2 secondes, vous savez exactement où chercher.

Mission 2 / 3 · 0h50 · pratique

Blinder la porte, ouvrir les autres guichets

Écrire le modèle Pydantic Transaction avec ses bornes (montant > 0, heure 0–23), puis ajouter /batch (une liste, scorée vectorisée), /feedback (le verdict humain, stocké) et /health. Et vérifier le videur : envoyez exprès une transaction cassée.

Livrable : les quatre guichets répondent, et une transaction invalide est refusée en 422 avec le champ fautif désigné.

PydanticBaseModelField(gt=0)list[Transaction]422

Binômes : un qui code, un qui attaque l'API avec des requêtes tordues — puis on échange.

Mission 3 / 3 · 0h20 · pratique

Prouver et documenter

Écrire les quatre tests de la checklist avec TestClient — nominal, 422, batch, health — puis ouvrir /docs et essayer chaque guichet depuis Swagger. Commitez le tout : ces tests seront la matière première de la CI au bloc 6.

Livrable : pytest vert (4 tests minimum) et l'API entièrement décrite dans /docs.

pytestTestClient/docsTry it out

20 minutes suffisent : l'API est petite, les tests sont courts — c'est exactement pour ça qu'on les écrit maintenant.

Le fil rouge · fin du bloc 3

Le modèle a une adresse

Regardez la carte : la porte d'entrée est apparue. Les transactions passent par l'API, le modèle répond en temps réel, et le verdict humain revient par /feedback. Le cerveau n'est plus muet.

Mais cette API tourne sur UNE machine — la vôtre. « Ça marche chez moi » n'est pas une stratégie de production.

API en ligne ✓validée ✓testée ✓portable… pas encore
La suite

Faire tourner
partout

Bloc 4 — Conteneurisation avec Docker : votre API, ses dépendances et son modèle dans une boîte que n'importe quelle machine sait exécuter.

Pas à pas ↗