Compare commits
5 Commits
dataset-v1
...
e063effc5d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e063effc5d | ||
|
|
b0bd6cdb07 | ||
|
|
9a52395c91 | ||
|
|
e6a05fbbc7 | ||
|
|
d8c2e85083 |
@@ -11,5 +11,12 @@ MLFLOW_EXPERIMENT_NAME=tp02_electricity_consumption
|
|||||||
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||||
AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
|
# --- S3 Garage : artefacts MLflow (TP03) ---
|
||||||
|
# Necessaire pour log_model (upload de l'artefact) ET pour le chargement du modele
|
||||||
|
# par l'API (download depuis s3://mlflow-artifacts). Cle S3 "mlflow" (RWO sur le bucket).
|
||||||
|
AWS_ACCESS_KEY_ID=GKxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
AWS_SECRET_ACCESS_KEY=change-me
|
||||||
|
MLFLOW_S3_ENDPOINT_URL=https://garage.192-168-122-143.nip.io
|
||||||
|
|
||||||
# --- Import du package lab ---
|
# --- Import du package lab ---
|
||||||
PYTHONPATH=/home/user/tp
|
PYTHONPATH=/home/user/tp
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -56,3 +56,49 @@ Resultats et comparaisons : https://mlflow.192-168-122-143.nip.io (experience `t
|
|||||||
## Livrable
|
## Livrable
|
||||||
|
|
||||||
Synthese des resultats et reponses aux questions : `SYNTHESE.md`.
|
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`.
|
||||||
|
|||||||
200
SYNTHESE.md
Normal file
200
SYNTHESE.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# TP02 - Synthèse : comparer et tracer des expérimentations ML
|
||||||
|
|
||||||
|
Fil rouge : prédiction de la consommation électrique (kWh). Datasets versionnés avec **DVC**
|
||||||
|
(remote S3 = Garage), expériences tracées avec **MLflow** (expérience `tp02_electricity_consumption`,
|
||||||
|
13 runs : 10 régressions linéaires + 3 Ridge).
|
||||||
|
|
||||||
|
Deux versions de dataset produites et versionnées (tags git + DVC) :
|
||||||
|
|
||||||
|
| Version DVC | Tag git | Train | Validation | Test |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| v1 `full_history` | `dataset-v1-full-history` | 2011-2012 | 2013 | 2014 |
|
||||||
|
| v2 `recent_history` | `dataset-v2-recent-history` | 2013 | jan-mai 2014 | juin-déc 2014 |
|
||||||
|
|
||||||
|
Métriques comparées : **RMSE** et **MAE** sur la **validation** (le test reste réservé).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 1 - Comparaison de stratégies de features (split `full_history`)
|
||||||
|
|
||||||
|
Stratégies testées (hypothèse -> features) :
|
||||||
|
|
||||||
|
| Stratégie | Hypothèse | Features |
|
||||||
|
|---|---|---|
|
||||||
|
| `short_memory` | la conso dépend surtout de la veille | lag_1d |
|
||||||
|
| `seasonality` | conso plus saisonnière que journalière | lag_7d, lag_30d |
|
||||||
|
| `tendency` | conso surtout tendancielle | rolling_mean_7d, rolling_mean_30d |
|
||||||
|
| `mixed` | mémoire courte + tendance | lag_1d, lag_7d, lag_30d, rolling_mean_30d |
|
||||||
|
| `full` | toutes les features | les 6 |
|
||||||
|
|
||||||
|
Résultats (validation, `full_history`) :
|
||||||
|
|
||||||
|
| Stratégie | RMSE | MAE |
|
||||||
|
|---|---|---|
|
||||||
|
| `full` | **7.41** | **4.07** |
|
||||||
|
| `mixed` | 7.53 | 4.11 |
|
||||||
|
| `short_memory` | 8.82 | 4.58 |
|
||||||
|
| `seasonality` | 9.14 | 5.04 |
|
||||||
|
| `tendency` | 20.79 | 14.66 |
|
||||||
|
|
||||||
|
**1.1 - Quelle stratégie obtient les meilleures performances ?**
|
||||||
|
`full` (6 features) puis `mixed` (4 features), quasi à égalité. Toutes deux combinent lags courts
|
||||||
|
et tendance. `tendency` (moyennes glissantes seules) est de loin la pire.
|
||||||
|
|
||||||
|
**1.2 - Ajouter davantage de features améliore-t-il systématiquement le modèle ?**
|
||||||
|
Non. `short_memory` (1 feature) fait 8.82 alors que `tendency` (2 features) fait 20.79 : c'est la
|
||||||
|
**pertinence** des features, pas leur nombre, qui compte. Et `full` (6) n'améliore `mixed` (4) que
|
||||||
|
marginalement (7.41 vs 7.53) : rendements décroissants, les features supplémentaires apportent peu.
|
||||||
|
|
||||||
|
**1.3 - Quelles hypothèses semblent validées ?**
|
||||||
|
- « La conso dépend de la veille » : validée, `lag_1d` est le prédicteur dominant (voir coefficients).
|
||||||
|
- « Plutôt tendancielle » (moyennes glissantes seules) : **invalidée** (pire modèle).
|
||||||
|
- La conso combine **mémoire court terme + tendance** : validée (mixed/full gagnants).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 2 - Comparaison des stratégies d'entraînement (split)
|
||||||
|
|
||||||
|
Chaque stratégie ré-entraînée sur `recent_history`. Matrice RMSE (validation) :
|
||||||
|
|
||||||
|
| Stratégie | `full_history` | `recent_history` |
|
||||||
|
|---|---|---|
|
||||||
|
| `short_memory` | 8.82 | 8.30 |
|
||||||
|
| `seasonality` | 9.14 | 7.60 |
|
||||||
|
| `tendency` | 20.79 | 18.56 |
|
||||||
|
| `mixed` | 7.53 | 6.67 |
|
||||||
|
| `full` | 7.41 | **6.58** |
|
||||||
|
|
||||||
|
La stratégie `full` a été relancée sur **la première version du dataset (v1) restaurée via DVC**
|
||||||
|
(`git checkout dataset-v1-full-history -- ... && dvc checkout`), illustrant la reproductibilité.
|
||||||
|
|
||||||
|
**2.1 - Quel split obtient les meilleures performances ?**
|
||||||
|
`recent_history` : RMSE plus bas pour **les 5 stratégies** (env. -12 %).
|
||||||
|
|
||||||
|
**2.2 - Davantage d'historique ou données plus récentes ?**
|
||||||
|
Ici, **données plus récentes**. Nuance importante : la comparaison n'est pas strictement iso car les
|
||||||
|
fenêtres de validation diffèrent (2013 pour `full_history` vs début 2014 pour `recent_history`).
|
||||||
|
Prédire début 2014 à partir de 2013 (année adjacente) est plus « facile » que prédire 2013 à partir
|
||||||
|
de 2011-2012. Enseignement : la **proximité temporelle train/validation** (moins de dérive) prime sur
|
||||||
|
le simple volume d'historique ancien.
|
||||||
|
|
||||||
|
**2.3 - Avantages / inconvénients de chaque approche ?**
|
||||||
|
- `full_history` : + capte les cycles longs (saisonnalité annuelle), robustesse au bruit ponctuel ;
|
||||||
|
- inclut des données anciennes possiblement obsolètes (data/concept drift), plus coûteux, validation lointaine.
|
||||||
|
- `recent_history` : + colle au régime actuel (moins de drift), moins coûteux ;
|
||||||
|
- moins de données (variance accrue), saisonnalités longues moins fiables (`lag_365d`), sensible aux
|
||||||
|
événements récents atypiques.
|
||||||
|
|
||||||
|
**2.4 - Bonus : coefficients du modèle** (linéaire `full`, `recent_history`), par influence décroissante :
|
||||||
|
|
||||||
|
| Feature | Coefficient |
|
||||||
|
|---|---|
|
||||||
|
| lag_1d | +0.460 |
|
||||||
|
| lag_7d | +0.371 |
|
||||||
|
| rolling_mean_7d | +0.288 |
|
||||||
|
| rolling_mean_30d | -0.268 |
|
||||||
|
| lag_365d | +0.090 |
|
||||||
|
| lag_30d | +0.058 |
|
||||||
|
|
||||||
|
- Les plus influentes : `lag_1d` (la veille) et `lag_7d` (semaine dernière), puis la paire
|
||||||
|
`rolling_mean_7d` (+) / `rolling_mean_30d` (-) qui agit en **différentiel de tendance** court/moyen terme.
|
||||||
|
- Les peu utilisées : `lag_30d` et `lag_365d` (mensuel/annuel apportent peu une fois les autres présentes).
|
||||||
|
- Cela **confirme les hypothèses de la Partie 1** : mémoire court terme + tendance courte dominent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 3 - Influence des hyperparamètres avec Ridge
|
||||||
|
|
||||||
|
Sur le meilleur couple (features `full`, split `recent_history`), alphas `[1, 1e3, 1e9]` :
|
||||||
|
|
||||||
|
| alpha | RMSE (val) | MAE (val) | RMSE (train) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **6.576** | **3.677** | 7.397 |
|
||||||
|
| 1e3 | **6.576** | **3.677** | 7.397 |
|
||||||
|
| 1e9 | 7.059 | 4.185 | 8.115 |
|
||||||
|
|
||||||
|
Coefficients selon alpha :
|
||||||
|
|
||||||
|
| Feature | alpha=1 | alpha=1e3 | alpha=1e9 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| lag_1d | 0.460 | 0.460 | 0.284 |
|
||||||
|
| lag_7d | 0.371 | 0.371 | 0.263 |
|
||||||
|
| lag_30d | 0.058 | 0.058 | 0.158 |
|
||||||
|
| lag_365d | 0.090 | 0.090 | 0.174 |
|
||||||
|
| rolling_mean_7d | 0.288 | 0.287 | 0.069 |
|
||||||
|
| rolling_mean_30d | -0.268 | -0.268 | 0.043 |
|
||||||
|
|
||||||
|
**3.1 - Quelle valeur d'alpha obtient les meilleures performances ?**
|
||||||
|
`alpha=1` (identique à `alpha=1e3`). `alpha=1e9` dégrade nettement (RMSE 7.06).
|
||||||
|
|
||||||
|
**3.2 - Que se passe-t-il sur les coefficients quand alpha augmente ?**
|
||||||
|
Ils sont **contraints vers zéro** (régularisation L2). À `alpha=1` et `1e3` : quasi identiques, car la
|
||||||
|
pénalité reste négligeable devant ~4,5 M d'échantillons. À `alpha=1e9` : forte contraction, les
|
||||||
|
coefficients dominants s'écrasent (`lag_1d` 0.46 -> 0.28, `rolling_mean_7d` 0.29 -> 0.07,
|
||||||
|
`rolling_mean_30d` -0.27 -> +0.04) ; le modèle devient plus « plat » (biais accru) -> RMSE augmente.
|
||||||
|
|
||||||
|
**3.3 - Pourquoi est-il indispensable de logger les hyperparamètres ?**
|
||||||
|
Pour la **reproductibilité et la comparabilité** : une métrique n'a de sens qu'associée à ses HP (alpha,
|
||||||
|
features, split). Sans, impossible de reproduire un run, d'expliquer un écart de performance ou de
|
||||||
|
comparer objectivement. MLflow lie chaque métrique à ses paramètres -> traçabilité complète.
|
||||||
|
|
||||||
|
**3.4 - Bonus : que représente le bruit ? Faut-il l'apprendre ?**
|
||||||
|
Le bruit = variations aléatoires non reproductibles (erreurs de mesure, aléas ponctuels). Il **ne faut
|
||||||
|
pas** l'apprendre : le modèle mémoriserait des accidents particuliers (overfitting) au lieu de la relation
|
||||||
|
générale, et généraliserait mal. Exemple : un pic de conso dû à une canicule exceptionnelle un 15 août
|
||||||
|
ne doit pas devenir une règle.
|
||||||
|
|
||||||
|
**3.5 - L'ordre de grandeur des coefficients a-t-il un effet ? Et avec du bruit ?**
|
||||||
|
Oui : de grands coefficients rendent la prédiction très sensible aux petites variations des features
|
||||||
|
(amplification). En présence de bruit, ils **amplifient ce bruit** -> prédictions instables, variance
|
||||||
|
élevée (overfitting). Des coefficients plus petits lissent la réponse.
|
||||||
|
|
||||||
|
**3.6 - Rôle de l'hyperparamètre alpha ?**
|
||||||
|
alpha règle le **compromis biais/variance** en pénalisant la magnitude des coefficients : alpha faible ->
|
||||||
|
modèle libre (variance élevée, risque d'overfitting) ; alpha élevé -> coefficients contraints (biais
|
||||||
|
élevé, risque d'underfitting). C'est le levier de régularisation pour améliorer la généralisation face
|
||||||
|
au bruit.
|
||||||
|
|
||||||
|
**3.7 - Comment utiliser un dataset de test dans le choix d'un hyperparamètre ?**
|
||||||
|
On ne choisit **jamais** alpha sur le test. On règle alpha sur la **validation** (comparaison des alphas),
|
||||||
|
puis on mesure **une seule fois** le modèle retenu sur le **test** (jamais vu) pour une estimation non
|
||||||
|
biaisée de la généralisation. Utiliser le test pour régler alpha revient à le « fuiter » -> estimation
|
||||||
|
trop optimiste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 4 - Réflexion en production
|
||||||
|
|
||||||
|
**4.1 - Sur quelles données entraîner ?**
|
||||||
|
Sur une **fenêtre glissante de données récentes** représentatives du régime actuel (ici `recent_history`
|
||||||
|
l'emporte), en réintégrant régulièrement les dernières observations, tout en gardant assez d'historique
|
||||||
|
pour capter les saisonnalités utiles (semaine, éventuellement année si stable). Compromis récence/volume.
|
||||||
|
|
||||||
|
**4.2 - À quelle fréquence réentraîner ?**
|
||||||
|
Réentraînement **périodique** (hebdomadaire à mensuel selon le coût et la vitesse de dérive) **et**
|
||||||
|
déclenché **par événement** (dégradation des métriques ou détection de drift). La forte saisonnalité de
|
||||||
|
la conso justifie au minimum un rythme régulier accompagné d'une surveillance.
|
||||||
|
|
||||||
|
**4.3 - Quels indicateurs signalent un nouvel entraînement ?**
|
||||||
|
- **Dégradation des métriques** en production (RMSE/MAE qui remontent vs baseline).
|
||||||
|
- **Data drift** : la distribution des features d'entrée s'éloigne de celle d'entraînement.
|
||||||
|
- **Concept drift** : la relation features -> cible change (nouveaux usages, réglementation, météo extrême).
|
||||||
|
- Écart croissant entre distributions **train vs production** (surveillance type Evidently/Grafana).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Livrables - synthèse finale
|
||||||
|
|
||||||
|
- **Stratégies de features testées** : `short_memory`, `seasonality`, `tendency`, `mixed`, `full`.
|
||||||
|
- **Résultats** : voir matrices ci-dessus (validation RMSE/MAE).
|
||||||
|
- **Meilleur run MLflow** : features `full` sur le split `recent_history` (RMSE **6.576**, MAE **3.677**) ;
|
||||||
|
en linéaire comme en Ridge `alpha` ≤ 1e3 (résultats indistinguables ; run Ridge `alpha=1e3`
|
||||||
|
`id 57cac1f5...`).
|
||||||
|
- **Impact du changement de split** : `recent_history` améliore **toutes** les stratégies (~ -12 % RMSE),
|
||||||
|
résultat à nuancer (fenêtres de validation différentes -> proximité temporelle avantageuse).
|
||||||
|
- **Stratégie recommandée en production** : features **`mixed`** (parcimonie : quasi identique à `full`
|
||||||
|
mais 4 features au lieu de 6, `lag_30d`/`lag_365d` contribuant peu), entraînée sur une **fenêtre de
|
||||||
|
données récentes**, avec **Ridge `alpha` modéré (1 à 1e3)**.
|
||||||
|
- **Raisons** : meilleure performance en validation, modèle plus simple donc plus robuste et
|
||||||
|
maintenable, données récentes = moins de dérive, régularisation légère = filet de sécurité contre le
|
||||||
|
bruit sans coût de performance.
|
||||||
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**.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
outs:
|
outs:
|
||||||
- md5: 2f70e749c483c8c74cde844a69d52631
|
- md5: 6ecb52ceb1d8322a91454aca4d902646
|
||||||
size: 114542933
|
size: 68171008
|
||||||
hash: md5
|
hash: md5
|
||||||
path: test.parquet
|
path: test.parquet
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
outs:
|
outs:
|
||||||
- md5: b08dfd4b6aa3f59d68220a35110df0b0
|
- md5: 24bf315461302605f8fd229eb263f499
|
||||||
size: 216998722
|
size: 115206905
|
||||||
hash: md5
|
hash: md5
|
||||||
path: train.parquet
|
path: train.parquet
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
outs:
|
outs:
|
||||||
- md5: 24bf315461302605f8fd229eb263f499
|
- md5: 5b3f656605f48a8aabeb5b21a6ae27ca
|
||||||
size: 115206905
|
size: 48074392
|
||||||
hash: md5
|
hash: md5
|
||||||
path: validation.parquet
|
path: validation.parquet
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class SplitStrategy(StrEnum):
|
|||||||
# Strategie de split active : pilote a la fois le decoupage produit par split/cli.py
|
# Strategie de split active : pilote a la fois le decoupage produit par split/cli.py
|
||||||
# et le parametre "split_strategy" logge dans MLflow. On la modifie (et on committe)
|
# et le parametre "split_strategy" logge dans MLflow. On la modifie (et on committe)
|
||||||
# a chaque changement de version de dataset pour synchroniser DVC et Git.
|
# a chaque changement de version de dataset pour synchroniser DVC et Git.
|
||||||
CHOSEN_SPLIT_STRATEGY = SplitStrategy.FULL_HISTORY
|
CHOSEN_SPLIT_STRATEGY = SplitStrategy.RECENT_HISTORY
|
||||||
|
|
||||||
DatasetPart = Literal["train", "test", "validation"]
|
DatasetPart = Literal["train", "test", "validation"]
|
||||||
|
|
||||||
@@ -70,3 +70,18 @@ MODELLING_FEATURES: dict[ModellingStrategy, list] = {
|
|||||||
|
|
||||||
# Valeurs d'alpha demandees par l'enonce (Partie 3)
|
# Valeurs d'alpha demandees par l'enonce (Partie 3)
|
||||||
RIDGE_ALPHAS = [1, 1e3, 1e9]
|
RIDGE_ALPHAS = [1, 1e3, 1e9]
|
||||||
|
|
||||||
|
|
||||||
|
# --- TP03 : Model Registry + service de prediction ---
|
||||||
|
# Nom sous lequel les modeles sont enregistres dans le MLflow Model Registry.
|
||||||
|
# On garde un nom stable pour retrouver le modele et empiler ses versions.
|
||||||
|
REGISTERED_MODEL_NAME = "electricity-consumption"
|
||||||
|
|
||||||
|
# Alias pointant vers la version promue (chargee par l'API). On identifie le modele
|
||||||
|
# a servir par son alias (mobile) plutot que par un numero de version (fige).
|
||||||
|
MODEL_ALIAS = "champion"
|
||||||
|
|
||||||
|
# Strategie de features du modele expose par l'API : "full" (les 6 features).
|
||||||
|
# L'ordre des colonnes servies doit correspondre a celui de l'entrainement.
|
||||||
|
SERVING_STRATEGY = ModellingStrategy.FULL
|
||||||
|
SERVING_FEATURES = MODELLING_FEATURES[SERVING_STRATEGY]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import mlflow
|
import mlflow
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import typer
|
import typer
|
||||||
|
from mlflow.models import infer_signature
|
||||||
from sklearn import linear_model
|
from sklearn import linear_model
|
||||||
from sklearn import metrics
|
from sklearn import metrics
|
||||||
|
|
||||||
@@ -17,6 +18,11 @@ logger = logging.getLogger(__name__)
|
|||||||
@app.command()
|
@app.command()
|
||||||
def main(
|
def main(
|
||||||
strategy: constants.ModellingStrategy,
|
strategy: constants.ModellingStrategy,
|
||||||
|
register: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--register/--no-register",
|
||||||
|
help="Enregistrer le modele dans le Model Registry (cree une nouvelle version).",
|
||||||
|
),
|
||||||
):
|
):
|
||||||
training_file_path = constants.DATASET_DIR / "train.parquet"
|
training_file_path = constants.DATASET_DIR / "train.parquet"
|
||||||
validation_file_path = constants.DATASET_DIR / "validation.parquet"
|
validation_file_path = constants.DATASET_DIR / "validation.parquet"
|
||||||
@@ -72,6 +78,24 @@ def main(
|
|||||||
):
|
):
|
||||||
mlflow.log_metric(f"coef_{feature_name}", float(coefficient))
|
mlflow.log_metric(f"coef_{feature_name}", float(coefficient))
|
||||||
|
|
||||||
|
# Sauvegarde de l'artefact du modele (poids + signature + environnement
|
||||||
|
# d'execution : requirements.txt, conda.yaml, MLmodel). --register empile
|
||||||
|
# une nouvelle version dans le Model Registry pour les meilleures experiences.
|
||||||
|
signature = infer_signature(X_train, train_predictions)
|
||||||
|
mlflow.sklearn.log_model(
|
||||||
|
sk_model=model,
|
||||||
|
name="model",
|
||||||
|
signature=signature,
|
||||||
|
input_example=X_train.iloc[:5],
|
||||||
|
registered_model_name=(
|
||||||
|
constants.REGISTERED_MODEL_NAME if register else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if register:
|
||||||
|
logger.info(
|
||||||
|
f"Model registered as '{constants.REGISTERED_MODEL_NAME}' (nouvelle version)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import mlflow
|
import mlflow
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import typer
|
import typer
|
||||||
|
from mlflow.models import infer_signature
|
||||||
from sklearn import linear_model
|
from sklearn import linear_model
|
||||||
from sklearn import metrics
|
from sklearn import metrics
|
||||||
|
|
||||||
@@ -17,6 +18,11 @@ logger = logging.getLogger(__name__)
|
|||||||
@app.command()
|
@app.command()
|
||||||
def main(
|
def main(
|
||||||
strategy: constants.ModellingStrategy = constants.ModellingStrategy.MIXED,
|
strategy: constants.ModellingStrategy = constants.ModellingStrategy.MIXED,
|
||||||
|
register: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--register/--no-register",
|
||||||
|
help="Enregistrer chaque modele (par alpha) dans le Model Registry.",
|
||||||
|
),
|
||||||
):
|
):
|
||||||
training_file_path = constants.DATASET_DIR / "train.parquet"
|
training_file_path = constants.DATASET_DIR / "train.parquet"
|
||||||
validation_file_path = constants.DATASET_DIR / "validation.parquet"
|
validation_file_path = constants.DATASET_DIR / "validation.parquet"
|
||||||
@@ -74,10 +80,16 @@ def main(
|
|||||||
):
|
):
|
||||||
mlflow.log_metric(f"coef_{feature_name}", float(coefficient))
|
mlflow.log_metric(f"coef_{feature_name}", float(coefficient))
|
||||||
|
|
||||||
|
# Sauvegarde de l'artefact du modele (voir lab/modeling/cli.py).
|
||||||
|
signature = infer_signature(X_train, train_predictions)
|
||||||
mlflow.sklearn.log_model(
|
mlflow.sklearn.log_model(
|
||||||
sk_model=model,
|
sk_model=model,
|
||||||
name="electricity_consumption_model",
|
name="model",
|
||||||
registered_model_name="electricity_consumption_model",
|
signature=signature,
|
||||||
|
input_example=X_train.iloc[:5],
|
||||||
|
registered_model_name=(
|
||||||
|
constants.REGISTERED_MODEL_NAME if register else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
lab/registry/__init__.py
Normal file
0
lab/registry/__init__.py
Normal file
63
lab/registry/cli.py
Normal file
63
lab/registry/cli.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""Gestion du MLflow Model Registry : consulter les versions et promouvoir via alias.
|
||||||
|
|
||||||
|
La promotion = pointer un alias (mobile) vers une version precise (figee) du modele.
|
||||||
|
L'API charge ensuite le modele par `models:/<nom>@<alias>` sans connaitre le numero.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from mlflow import MlflowClient
|
||||||
|
|
||||||
|
from .. import constants
|
||||||
|
|
||||||
|
app = typer.Typer(help="MLflow Model Registry (versions + promotion par alias).")
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def versions(
|
||||||
|
name: str = constants.REGISTERED_MODEL_NAME,
|
||||||
|
):
|
||||||
|
"""Lister les versions du modele enregistre, avec leurs alias."""
|
||||||
|
client = MlflowClient()
|
||||||
|
results = client.search_model_versions(f"name = '{name}'")
|
||||||
|
if not results:
|
||||||
|
logger.warning(f"Aucune version pour le modele '{name}'.")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
# Les alias sont portes par le modele enregistre (dict alias -> version),
|
||||||
|
# pas par les objets renvoyes par search_model_versions.
|
||||||
|
alias_by_version: dict[str, list[str]] = {}
|
||||||
|
for alias, version in client.get_registered_model(name).aliases.items():
|
||||||
|
alias_by_version.setdefault(str(version), []).append(alias)
|
||||||
|
|
||||||
|
for mv in sorted(results, key=lambda v: int(v.version)):
|
||||||
|
aliases = ", ".join(alias_by_version.get(str(mv.version), [])) or "-"
|
||||||
|
run = client.get_run(mv.run_id) if mv.run_id else None
|
||||||
|
strategy = run.data.params.get("strategy", "?") if run else "?"
|
||||||
|
val_rmse = run.data.metrics.get("validation_rmse") if run else None
|
||||||
|
rmse_txt = f"{val_rmse:.3f}" if val_rmse is not None else "?"
|
||||||
|
typer.echo(
|
||||||
|
f"v{mv.version:<3} | alias: {aliases:<12} | strategy={strategy:<12}"
|
||||||
|
f" | validation_rmse={rmse_txt} | run={mv.run_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def promote(
|
||||||
|
version: int = typer.Option(..., help="Numero de version a promouvoir."),
|
||||||
|
alias: str = typer.Option(constants.MODEL_ALIAS, help="Alias a (re)pointer."),
|
||||||
|
name: str = constants.REGISTERED_MODEL_NAME,
|
||||||
|
):
|
||||||
|
"""Promouvoir une version : (re)pointer l'alias vers cette version."""
|
||||||
|
client = MlflowClient()
|
||||||
|
client.set_registered_model_alias(name=name, alias=alias, version=str(version))
|
||||||
|
mv = client.get_model_version_by_alias(name=name, alias=alias)
|
||||||
|
logger.info(f"Alias '{alias}' -> {name} v{mv.version} (source: {mv.source})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
0
lab/serving/__init__.py
Normal file
0
lab/serving/__init__.py
Normal file
99
lab/serving/api.py
Normal file
99
lab/serving/api.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""API REST de prediction de consommation electrique (FastAPI).
|
||||||
|
|
||||||
|
Le service orchestre la chaine de prediction :
|
||||||
|
1. recevoir la requete (client_id, date) ;
|
||||||
|
2. recuperer les features du client (feature store simule) ;
|
||||||
|
3. charger le modele promu (Model Registry, au demarrage) ;
|
||||||
|
4. calculer la prediction et repondre en JSON.
|
||||||
|
|
||||||
|
Lancement : uvicorn lab.serving.api:app --host 0.0.0.0 --port 8000
|
||||||
|
Swagger UI : /docs
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
|
||||||
|
from . import features, registry
|
||||||
|
from .schemas import (
|
||||||
|
BatchPredictionRequest,
|
||||||
|
BatchPredictionResponse,
|
||||||
|
PredictionRequest,
|
||||||
|
PredictionResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Modele charge une fois au demarrage (couteux) et reutilise a chaque requete.
|
||||||
|
_state: dict[str, registry.LoadedModel] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
_state["model"] = registry.load_champion()
|
||||||
|
yield
|
||||||
|
_state.clear()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Electricity Consumption Prediction API",
|
||||||
|
description="Expose le modele promu (MLflow Model Registry) via une API REST.",
|
||||||
|
version="1.0.0",
|
||||||
|
lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_model() -> registry.LoadedModel:
|
||||||
|
model = _state.get("model")
|
||||||
|
if model is None: # modele indisponible au demarrage
|
||||||
|
raise HTTPException(status_code=503, detail="Modele non charge.")
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def _predict(client_id: str, model: registry.LoadedModel) -> PredictionResponse:
|
||||||
|
feats = features.get_features(client_id) # peut lever UnknownClientError
|
||||||
|
prediction = model.predict_one(feats)
|
||||||
|
return PredictionResponse(
|
||||||
|
client_id=client_id,
|
||||||
|
prediction_kwh=prediction,
|
||||||
|
model_name=model.name,
|
||||||
|
model_version=model.version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health", summary="Verification de l'etat du service")
|
||||||
|
def health() -> dict[str, str]:
|
||||||
|
"""Endpoint de sante : renvoie 200 si le service repond."""
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/predict", response_model=PredictionResponse, summary="Prediction unitaire")
|
||||||
|
def predict(request: PredictionRequest) -> PredictionResponse:
|
||||||
|
model = _get_model()
|
||||||
|
try:
|
||||||
|
return _predict(request.client_id, model)
|
||||||
|
except features.UnknownClientError:
|
||||||
|
# Partie 3 : client inconnu -> erreur cliente, pas un plantage du service.
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Client inconnu : aucune feature pour '{request.client_id}'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/predict/batch",
|
||||||
|
response_model=BatchPredictionResponse,
|
||||||
|
summary="Prediction en batch (plusieurs clients)",
|
||||||
|
)
|
||||||
|
def predict_batch(request: BatchPredictionRequest) -> BatchPredictionResponse:
|
||||||
|
model = _get_model()
|
||||||
|
predictions: list[PredictionResponse] = []
|
||||||
|
unknown: list[str] = []
|
||||||
|
for client_id in request.client_ids:
|
||||||
|
try:
|
||||||
|
predictions.append(_predict(client_id, model))
|
||||||
|
except features.UnknownClientError:
|
||||||
|
unknown.append(client_id)
|
||||||
|
return BatchPredictionResponse(predictions=predictions, unknown_client_ids=unknown)
|
||||||
66
lab/serving/features.py
Normal file
66
lab/serving/features.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""Recuperation des features de prediction.
|
||||||
|
|
||||||
|
Dans un systeme reel, ces valeurs proviendraient d'un feature store / d'une base
|
||||||
|
alimentee par le pipeline de calcul de features (lags, moyennes glissantes) sur
|
||||||
|
l'historique de consommation. On separe volontairement cette etape du calcul de la
|
||||||
|
prediction : la source des features peut changer sans toucher au modele.
|
||||||
|
|
||||||
|
Ici on SIMULE cette recuperation par un simple dictionnaire Python, comme demande
|
||||||
|
par l'enonce. Les valeurs sont des observations reelles (derniere ligne connue de
|
||||||
|
quelques clients dans data/test.parquet).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .. import constants
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownClientError(KeyError):
|
||||||
|
"""Aucune feature disponible pour ce client (identifiant inconnu)."""
|
||||||
|
|
||||||
|
|
||||||
|
# feature store simule : client_id -> {feature: valeur}
|
||||||
|
FEATURE_STORE: dict[str, dict[str, float]] = {
|
||||||
|
"MT_124": {
|
||||||
|
"lag_1d": 107.656,
|
||||||
|
"lag_7d": 25.120,
|
||||||
|
"lag_30d": 70.574,
|
||||||
|
"lag_365d": 25.120,
|
||||||
|
"rolling_mean_7d": 65.870,
|
||||||
|
"rolling_mean_30d": 71.310,
|
||||||
|
},
|
||||||
|
"MT_156": {
|
||||||
|
"lag_1d": 13.149,
|
||||||
|
"lag_7d": 13.929,
|
||||||
|
"lag_30d": 21.577,
|
||||||
|
"lag_365d": 8.935,
|
||||||
|
"rolling_mean_7d": 16.648,
|
||||||
|
"rolling_mean_30d": 19.720,
|
||||||
|
},
|
||||||
|
"MT_158": {
|
||||||
|
"lag_1d": 30.739,
|
||||||
|
"lag_7d": 16.608,
|
||||||
|
"lag_30d": 34.094,
|
||||||
|
"lag_365d": 6.574,
|
||||||
|
"rolling_mean_7d": 19.067,
|
||||||
|
"rolling_mean_30d": 21.486,
|
||||||
|
},
|
||||||
|
"MT_159": {
|
||||||
|
"lag_1d": 23.305,
|
||||||
|
"lag_7d": 24.741,
|
||||||
|
"lag_30d": 21.386,
|
||||||
|
"lag_365d": 5.333,
|
||||||
|
"rolling_mean_7d": 11.707,
|
||||||
|
"rolling_mean_30d": 13.619,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_features(client_id: str) -> dict[str, float]:
|
||||||
|
"""Renvoyer les features du client, ordonnees comme a l'entrainement du modele.
|
||||||
|
|
||||||
|
Leve UnknownClientError si le client est inconnu.
|
||||||
|
"""
|
||||||
|
if client_id not in FEATURE_STORE:
|
||||||
|
raise UnknownClientError(client_id)
|
||||||
|
raw = FEATURE_STORE[client_id]
|
||||||
|
# On respecte l'ordre des colonnes attendu par le modele (SERVING_FEATURES).
|
||||||
|
return {feature: raw[feature] for feature in constants.SERVING_FEATURES}
|
||||||
43
lab/serving/registry.py
Normal file
43
lab/serving/registry.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""Chargement du modele promu depuis le MLflow Model Registry (par alias)."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import mlflow
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .. import constants
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoadedModel:
|
||||||
|
"""Modele charge + metadonnees de version, garde en memoire par l'API."""
|
||||||
|
|
||||||
|
model: mlflow.pyfunc.PyFuncModel
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
|
||||||
|
def predict_one(self, features: dict[str, float]) -> float:
|
||||||
|
"""Prediction pour un jeu de features (1 ligne)."""
|
||||||
|
frame = pd.DataFrame([features], columns=constants.SERVING_FEATURES)
|
||||||
|
return float(self.model.predict(frame)[0])
|
||||||
|
|
||||||
|
|
||||||
|
def load_champion() -> LoadedModel:
|
||||||
|
"""Charger la version pointee par l'alias `MODEL_ALIAS` du modele enregistre.
|
||||||
|
|
||||||
|
On identifie le modele par `models:/<nom>@<alias>` : le meme code sert n'importe
|
||||||
|
quelle version promue, sans redeploiement, en changeant seulement l'alias cote MLflow.
|
||||||
|
"""
|
||||||
|
name = constants.REGISTERED_MODEL_NAME
|
||||||
|
alias = constants.MODEL_ALIAS
|
||||||
|
uri = f"models:/{name}@{alias}"
|
||||||
|
logger.info(f"Chargement du modele {uri}")
|
||||||
|
|
||||||
|
client = mlflow.MlflowClient()
|
||||||
|
version = client.get_model_version_by_alias(name=name, alias=alias)
|
||||||
|
model = mlflow.pyfunc.load_model(uri)
|
||||||
|
logger.info(f"Modele charge : {name} v{version.version}")
|
||||||
|
return LoadedModel(model=model, name=name, version=str(version.version))
|
||||||
44
lab/serving/schemas.py
Normal file
44
lab/serving/schemas.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""Schemas Pydantic du service de prediction (contrat d'entree/sortie de l'API)."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class PredictionRequest(BaseModel):
|
||||||
|
"""Ce que le client fournit : QUI et QUAND, pas les features (calculees cote serveur)."""
|
||||||
|
|
||||||
|
client_id: str = Field(
|
||||||
|
...,
|
||||||
|
description="Identifiant du client (ex. 'MT_124').",
|
||||||
|
examples=["MT_124"],
|
||||||
|
)
|
||||||
|
date: datetime.date | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Date de la prediction (parametre de requete). Optionnelle.",
|
||||||
|
examples=["2015-01-01"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchPredictionRequest(BaseModel):
|
||||||
|
"""Prediction pour plusieurs clients en une seule requete (inference batch)."""
|
||||||
|
|
||||||
|
client_ids: list[str] = Field(
|
||||||
|
...,
|
||||||
|
description="Liste d'identifiants clients.",
|
||||||
|
examples=[["MT_124", "MT_156", "MT_158"]],
|
||||||
|
)
|
||||||
|
date: datetime.date | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PredictionResponse(BaseModel):
|
||||||
|
client_id: str
|
||||||
|
prediction_kwh: float = Field(description="Consommation predite (kWh).")
|
||||||
|
model_name: str
|
||||||
|
model_version: str
|
||||||
|
|
||||||
|
|
||||||
|
class BatchPredictionResponse(BaseModel):
|
||||||
|
predictions: list[PredictionResponse]
|
||||||
|
# Clients ignores (features introuvables) : on ne fait pas echouer tout le lot.
|
||||||
|
unknown_client_ids: list[str] = Field(default_factory=list)
|
||||||
10
serve.sh
Executable file
10
serve.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Lance l'API de prediction (TP03). A executer depuis la racine du depot (/home/user/tp).
|
||||||
|
# Charge .env (MLflow + creds S3 pour recuperer le modele promu depuis le Registry).
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
exec /opt/venvs/mlops/bin/uvicorn lab.serving.api:app --host 0.0.0.0 --port 8000
|
||||||
Reference in New Issue
Block a user