TP3 : synthese (reponses aux questions) + README
This commit is contained in:
46
README.md
46
README.md
@@ -56,3 +56,49 @@ Resultats et comparaisons : https://mlflow.192-168-122-143.nip.io (experience `t
|
||||
## Livrable
|
||||
|
||||
Synthese des resultats et reponses aux questions : `SYNTHESE.md`.
|
||||
|
||||
---
|
||||
|
||||
# TP03 - Exposer un modele via une API REST (Model Registry + FastAPI)
|
||||
|
||||
Prolonge le TP02 : on enregistre le meilleur modele dans le **MLflow Model Registry**, on le
|
||||
**promeut via un alias**, puis on l'expose par une **API REST FastAPI**.
|
||||
|
||||
## Prerequis (en plus du TP02)
|
||||
|
||||
`.env` complete avec les creds S3 Garage (voir `.env.example`) : `AWS_ACCESS_KEY_ID`,
|
||||
`AWS_SECRET_ACCESS_KEY`, `MLFLOW_S3_ENDPOINT_URL`. Necessaires pour `log_model` (upload de
|
||||
l'artefact) et pour le chargement du modele par l'API (download depuis `s3://mlflow-artifacts`).
|
||||
|
||||
## Enregistrer et promouvoir (Partie 1)
|
||||
|
||||
```bash
|
||||
set -a; source .env; set +a
|
||||
py -m lab.modeling.cli full --register # v1 -> Registry (champion vise)
|
||||
py -m lab.modeling.cli mixed --register # v2 -> Registry (comparaison)
|
||||
py -m lab.registry.cli versions # lister versions + alias
|
||||
py -m lab.registry.cli promote --version 1 --alias champion
|
||||
```
|
||||
|
||||
`log_model` est appele a **chaque** run (artefact sauvegarde) ; `--register` empile en plus une
|
||||
version dans le Registry sous le nom `electricity-consumption`.
|
||||
|
||||
## Servir l'API (Parties 2 a 4)
|
||||
|
||||
```bash
|
||||
./serve.sh # uvicorn 0.0.0.0:8000, charge models:/...@champion
|
||||
curl -s localhost:8000/health # {"status":"ok"}
|
||||
curl -s -X POST localhost:8000/predict \
|
||||
-H 'content-type: application/json' -d '{"client_id":"MT_124"}'
|
||||
curl -s -X POST localhost:8000/predict/batch \
|
||||
-H 'content-type: application/json' -d '{"client_ids":["MT_124","MT_158"]}'
|
||||
```
|
||||
|
||||
- Endpoints : `GET /health`, `POST /predict`, `POST /predict/batch`, Swagger `GET /docs`.
|
||||
- Feature store **simule** par un dictionnaire (`lab/serving/features.py`) ; client inconnu -> **404**.
|
||||
- Depuis le poste (cert ENI de confiance) : **https://api.192-168-122-143.nip.io/docs**
|
||||
(reverse-proxy Caddy vers uvicorn). Unite systemd transitoire : `sudo systemctl status tp03-api`.
|
||||
|
||||
## Livrable TP03
|
||||
|
||||
Reponses aux questions et recap : `SYNTHESE_TP03.md`.
|
||||
|
||||
181
SYNTHESE_TP03.md
Normal file
181
SYNTHESE_TP03.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# TP03 - Synthèse : exposer un modèle ML via une API REST
|
||||
|
||||
Fil rouge : prédiction de la consommation électrique (kWh). On sélectionne le meilleur modèle
|
||||
du TP02, on l'enregistre dans le **MLflow Model Registry**, on le **promeut via un alias**, puis
|
||||
on l'expose par une **API REST FastAPI**.
|
||||
|
||||
## Ce qui a été construit
|
||||
|
||||
- **`log_model`** ajouté à chaque run d'entraînement (`lab/modeling/cli.py`, `lab/modeling_ridge/cli.py`) :
|
||||
sauvegarde l'artefact complet du modèle sur S3 (`s3://mlflow-artifacts`).
|
||||
- **Modèle enregistré** : `electricity-consumption` (Registry MLflow). Versions créées avec le flag
|
||||
`--register` : v1 = `full` (6 features, RMSE val 6.576), v2 = `mixed` (4 features, RMSE 6.671).
|
||||
- **Promotion** : alias `champion` -> **v1** (`full`), via `lab/registry/cli.py promote`.
|
||||
- **API FastAPI** (`lab/serving/`) : `GET /health`, `POST /predict`, `POST /predict/batch`, Swagger `/docs`.
|
||||
Le modèle est chargé au démarrage par `models:/electricity-consumption@champion`.
|
||||
- **Exposition** : `https://api.192-168-122-143.nip.io/docs` (reverse-proxy Caddy -> uvicorn `:8000`).
|
||||
|
||||
---
|
||||
|
||||
## Partie 1 - Sélection et enregistrement du modèle
|
||||
|
||||
**1.1 - Que contiennent les artefacts du modèle sauvegardé par MLflow, pourquoi sont-ils utiles ?**
|
||||
Sauvegarder un modèle ne se limite pas à ses coefficients : MLflow enregistre tout l'environnement
|
||||
d'exécution. Contenu observé (`s3://mlflow-artifacts/1/models/<id>/artifacts/`) :
|
||||
- `model.pkl` : le modèle sérialisé (poids/coefficients) ;
|
||||
- `MLmodel` : métadonnées (flavors `sklearn`/`pyfunc`, **signature** = schéma entrées/sorties) ;
|
||||
- `requirements.txt`, `conda.yaml`, `python_env.yaml` : versions exactes des dépendances ;
|
||||
- `input_example.json`, `serving_input_example.json` : exemple d'entrée.
|
||||
Utiles pour **recharger le modèle partout** (`load_model`), **reproduire l'environnement** (mêmes
|
||||
versions -> mêmes prédictions), et connaître le **contrat d'E/S** (signature).
|
||||
|
||||
**1.2 - Quel mécanisme vous permet de promouvoir un modèle ?**
|
||||
L'**alias** du Model Registry : `MlflowClient().set_registered_model_alias(name, "champion", version)`
|
||||
fait pointer un alias mobile vers une version précise. (Les anciens *stages* Staging/Production sont
|
||||
dépréciés en MLflow 3.x au profit des alias + tags.)
|
||||
|
||||
**1.3 - Plusieurs environnements de production (un par région), plusieurs modèles : comment les identifier ?**
|
||||
Avec des **alias** et **tags** spécifiques : par ex. des alias `production-eu`, `production-us`,
|
||||
`champion-north`... sur un même modèle enregistré, et/ou un modèle enregistré par région, complétés par
|
||||
des **tags** (région, environnement) portés par le modèle ou la version. Le Registry gère plusieurs
|
||||
alias par modèle et des tags arbitraires : l'API cible alors `models:/<nom>@<alias-région>`.
|
||||
|
||||
---
|
||||
|
||||
## Partie 2 - Service de prédiction
|
||||
|
||||
### Étape 1 - Création de l'API
|
||||
|
||||
**2.1 - Quel endpoint de vérification ? Quel code de statut attendu ?**
|
||||
`GET /health`, qui renvoie **200 OK** avec `{"status":"ok"}`.
|
||||
|
||||
**2.2 - À quoi sert un endpoint de santé ?**
|
||||
À vérifier que le service est vivant et prêt (*liveness/readiness*), sans exécuter de vraie prédiction.
|
||||
Il est sondé par l'orchestrateur / load-balancer / monitoring pour router le trafic, redémarrer un
|
||||
conteneur en échec, ou alerter.
|
||||
|
||||
### Étape 2 - Requête de prédiction
|
||||
|
||||
**2.3 - Quelles informations le client doit-il fournir ? Pourquoi ?**
|
||||
L'**identifiant client** (`client_id`) et, en paramètre, la **date** de prédiction. Ce sont les seules
|
||||
informations du **contexte métier** que le client connaît : elles disent *qui* et *quand*. Elles servent
|
||||
de clé pour retrouver le reste côté serveur.
|
||||
|
||||
**2.4 - Quelles informations le client ne peut-il pas fournir ? Comment le service les récupère-t-il ?**
|
||||
Les **features calculées** (lags, moyennes glissantes) issues de l'historique de consommation : le
|
||||
client ne les possède/calcule pas. Le service les **récupère lui-même** depuis un feature store / une
|
||||
base, à partir du `client_id` (+ date). Ici, c'est **simulé par un dictionnaire Python** (`lab/serving/features.py`).
|
||||
|
||||
**2.5 - Quelle méthode HTTP pour le endpoint de prédiction ? Pourquoi ?**
|
||||
**POST** : la requête transporte un **corps JSON structuré** (et potentiellement volumineux en batch),
|
||||
et déclenche un **calcul** (action, non une simple lecture cacheable de ressource comme le ferait GET).
|
||||
|
||||
### Étape 3 - Récupération des features
|
||||
|
||||
**2.6 - Rappel des features nécessaires à la prédiction.**
|
||||
Modèle promu = stratégie `full`, soit les **6 features** :
|
||||
`lag_1d`, `lag_7d`, `lag_30d`, `lag_365d`, `rolling_mean_7d`, `rolling_mean_30d`.
|
||||
|
||||
**2.7 - Dans un système réel, d'où proviennent ces features ?**
|
||||
D'un **feature store / pipeline de features** : un job (batch ou streaming) calcule lags et moyennes
|
||||
glissantes depuis la série temporelle brute, les stocke dans une base (ex. Feast), et les sert à
|
||||
l'inférence. Elles doivent être calculées **de façon identique à l'entraînement** (éviter le
|
||||
*training/serving skew*).
|
||||
|
||||
**2.8 - Pourquoi séparer récupération des features et calcul de la prédiction ?**
|
||||
**Séparation des responsabilités** : la source des features peut évoluer (BDD, cache, feature store)
|
||||
sans toucher au modèle ; le modèle reste une **fonction pure** `features -> prédiction`, testable et
|
||||
réutilisable. « L'application du modèle n'est qu'une étape de la chaîne de prédiction. »
|
||||
|
||||
**2.9 (Bonus) - Si les features ne peuvent pas être calculées/récupérées ?**
|
||||
L'API ne doit pas planter : elle renvoie une erreur explicite. Client/features introuvables -> **404** ;
|
||||
payload invalide -> 422 ; feature store indisponible (panne transitoire) -> **503**. Toujours un JSON
|
||||
d'erreur clair.
|
||||
|
||||
### Étape 4 - Chargement du modèle depuis le Registry
|
||||
|
||||
**2.10 - Pourquoi un alias plutôt qu'un numéro de version ?**
|
||||
L'alias (`champion`) est **stable et mobile** : l'API charge toujours `models:/electricity-consumption@champion`
|
||||
et l'on **re-pointe l'alias** vers une nouvelle version sans modifier ni redéployer le code. Un numéro
|
||||
de version est **figé** : chaque changement de modèle imposerait d'éditer la config et de redéployer.
|
||||
L'alias **découple** « quel modèle est en prod » (décision côté MLflow) du code de service.
|
||||
|
||||
**2.11 - Avantages du Registry par rapport à un simple fichier modèle ?**
|
||||
Versioning centralisé (historique de toutes les versions), **alias/tags**, **lignée** vers le run
|
||||
d'entraînement (params/métriques), chargement par URI depuis n'importe où, workflow de promotion,
|
||||
traçabilité/audit, environnement (requirements) attaché. Un simple fichier n'offre rien de tout cela
|
||||
(pas d'historique, pas de métadonnées, distribution manuelle et fragile).
|
||||
|
||||
**2.12 (Bonus) - Si les requirements du modèle sont incohérents avec l'environnement de l'API ?**
|
||||
Risque : incompatibilité de versions (scikit-learn, numpy) -> erreur de désérialisation ou écarts
|
||||
numériques silencieux. **Architecture** : **isoler le modèle dans son propre runtime** construit à
|
||||
partir de son `requirements.txt` (image conteneur dédiée par modèle, ex. `mlflow models build-docker` /
|
||||
MLflow serving), l'API l'appelant via HTTP ; ou figer l'environnement de l'API depuis les requirements
|
||||
du modèle. On **découple** l'API des dépendances du modèle.
|
||||
|
||||
### Étape 5 - Endpoint de prédiction
|
||||
|
||||
**2.13 - Étapes lorsqu'une requête de prédiction arrive.**
|
||||
1. Valider le payload (schéma Pydantic). 2. Récupérer les features du client (404 si inconnu).
|
||||
3. Assembler le vecteur de features **dans l'ordre attendu** par le modèle. 4. `model.predict`.
|
||||
5. Formater et renvoyer la réponse JSON (200).
|
||||
|
||||
**2.14 - Quel format de réponse ? Quel statut HTTP ?**
|
||||
JSON : `{client_id, prediction_kwh, model_name, model_version}`, statut **200 OK**.
|
||||
|
||||
---
|
||||
|
||||
## Partie 3 (Bonus) - Gestion des erreurs
|
||||
|
||||
**3.1 - L'application doit-elle échouer ou intercepter cette erreur ?**
|
||||
**Intercepter.** Un `client_id` inconnu est une **erreur cliente** (mauvaise entrée), pas un bug
|
||||
serveur : le service reste debout et renvoie une réponse d'erreur propre.
|
||||
|
||||
**3.2 - Quel code HTTP est adapté ?**
|
||||
**404 Not Found** (la ressource/le client demandé n'existe pas). (422 si le payload lui-même est
|
||||
malformé.) Implémenté via `HTTPException(status_code=404, ...)`.
|
||||
|
||||
---
|
||||
|
||||
## Partie 4 (Bonus) - Prédictions en batch
|
||||
|
||||
**4.1 - Un modèle doit-il forcément être exposé par API ? Dans quel cas ?**
|
||||
Non. L'API (temps réel) convient aux prédictions **à la demande, individuelles, à faible latence**
|
||||
(appli interactive). Pour de **gros volumes calculés périodiquement** (ex. tous les clients chaque
|
||||
nuit), l'**inférence batch** (job planifié qui écrit les résultats en base) est plus adaptée et moins
|
||||
coûteuse. On expose par API quand on a besoin de prédictions fraîches, unitaires et synchrones.
|
||||
|
||||
**4.2 - Quelles briques restent identiques entre batch et temps réel ?**
|
||||
Le **modèle** (même artefact du Registry), la **logique/définition des features**, le **préprocessing**,
|
||||
le **code de prédiction** (`features -> prédiction`). Ce qui diffère : le **déclencheur/orchestration**
|
||||
(requête HTTP vs job planifié), les **E/S** (un JSON unitaire vs une table en masse) et le profil
|
||||
latence/débit.
|
||||
|
||||
**4.3 - Comment récupérer les informations de prédiction ?**
|
||||
En **masse** : lire les features de tous les clients pour la date depuis le feature store / une table
|
||||
(BDD ou parquet), appeler `model.predict` sur le **lot entier** (vectorisé), puis écrire les résultats
|
||||
en base/fichier. Ici, `POST /predict/batch` prend une liste de `client_ids` et renvoie la liste des
|
||||
prédictions (+ `unknown_client_ids` pour les clients ignorés).
|
||||
|
||||
---
|
||||
|
||||
## Reproduire
|
||||
|
||||
Sur la VM, depuis `/home/user/tp` :
|
||||
|
||||
```bash
|
||||
set -a; source .env; set +a # MLflow + creds S3 (Garage)
|
||||
|
||||
# Partie 1 : enregistrer + promouvoir
|
||||
python -m lab.modeling.cli full --register # v1 (champion)
|
||||
python -m lab.modeling.cli mixed --register # v2 (comparaison)
|
||||
python -m lab.registry.cli versions # consulter les versions/alias
|
||||
python -m lab.registry.cli promote --version 1 --alias champion
|
||||
|
||||
# Partie 2-4 : servir l'API
|
||||
./serve.sh # uvicorn :8000 (Swagger /docs)
|
||||
curl -s localhost:8000/health
|
||||
curl -s -X POST localhost:8000/predict -H 'content-type: application/json' -d '{"client_id":"MT_124"}'
|
||||
```
|
||||
|
||||
Depuis le poste (certificat ENI de confiance) : **https://api.192-168-122-143.nip.io/docs**.
|
||||
Reference in New Issue
Block a user