Compare commits
4 Commits
e063effc5d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead28bf663 | ||
|
|
7c5660c455 | ||
|
|
67566ef076 | ||
|
|
47109efad9 |
50
.forgejo/workflows/pipeline.yml
Normal file
50
.forgejo/workflows/pipeline.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: pipeline
|
||||||
|
|
||||||
|
# Partie 4 : le pipeline ML est rejoue automatiquement a chaque Pull Request
|
||||||
|
# vers la branche principale (Q4.1). Autres declencheurs possibles (Q4.4) :
|
||||||
|
# push sur main, tag de release, planification (schedule/cron), declenchement
|
||||||
|
# manuel (workflow_dispatch).
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
train-and-register:
|
||||||
|
runs-on: docker
|
||||||
|
# Le runner (mode docker, reseau mlops-net) lance le job dans ce conteneur.
|
||||||
|
# On y monte en lecture seule les donnees source du fil rouge et le magasin
|
||||||
|
# de certificats du host (pour faire confiance a la CA interne ENI MLOps).
|
||||||
|
container:
|
||||||
|
image: node:20-bookworm
|
||||||
|
volumes:
|
||||||
|
- /data/modelling:/data/modelling:ro
|
||||||
|
- /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
||||||
|
steps:
|
||||||
|
- name: Recuperation du code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Installation de Python et des dependances
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends python3-venv
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/pip install --upgrade pip
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Execution du pipeline (split -> train --register -> promote)
|
||||||
|
run: .venv/bin/python pipeline.py
|
||||||
|
env:
|
||||||
|
# Serveur MLflow (basic auth) : URLs HTTPS via Caddy, resolues sur mlops-net.
|
||||||
|
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
|
||||||
|
MLFLOW_TRACKING_USERNAME: ${{ secrets.MLFLOW_TRACKING_USERNAME }}
|
||||||
|
MLFLOW_TRACKING_PASSWORD: ${{ secrets.MLFLOW_TRACKING_PASSWORD }}
|
||||||
|
MLFLOW_EXPERIMENT_NAME: ${{ secrets.MLFLOW_EXPERIMENT_NAME }}
|
||||||
|
# Artefacts sur S3 Garage (log_model).
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
MLFLOW_S3_ENDPOINT_URL: ${{ secrets.MLFLOW_S3_ENDPOINT_URL }}
|
||||||
|
# CA interne ENI MLOps (requests/boto3 n'utilisent pas le store systeme par defaut).
|
||||||
|
REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
|
||||||
|
AWS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
|
||||||
|
# Import du package lab depuis la racine du depot.
|
||||||
|
PYTHONPATH: ${{ github.workspace }}
|
||||||
31
README.md
31
README.md
@@ -102,3 +102,34 @@ curl -s -X POST localhost:8000/predict/batch \
|
|||||||
## Livrable TP03
|
## Livrable TP03
|
||||||
|
|
||||||
Reponses aux questions et recap : `SYNTHESE_TP03.md`.
|
Reponses aux questions et recap : `SYNTHESE_TP03.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# TP04 - Automatiser le pipeline avec la CI/CD (pipeline.py + Forgejo Actions)
|
||||||
|
|
||||||
|
Prolonge les TP precedents : les etapes manuelles (split -> entrainement -> enregistrement/promotion
|
||||||
|
Registry) sont orchestrees en une seule commande, puis rejouees automatiquement en CI.
|
||||||
|
|
||||||
|
## Pipeline (Parties 2-3)
|
||||||
|
|
||||||
|
`pipeline.py` (racine) enchaine, via des sous-processus (`check=True`, arret au premier echec) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a; source .env; set +a
|
||||||
|
python pipeline.py # split -> train full --register -> promote champion
|
||||||
|
```
|
||||||
|
|
||||||
|
- 1. `lab.split.cli` : (re)cree `data/{train,validation,test}.parquet` (deterministe) ;
|
||||||
|
- 2. `lab.modeling.cli full --register` : entraine + `log_model` + nouvelle version au Registry ;
|
||||||
|
- 3. `lab.registry.cli promote` : repointe l'alias `champion` sur cette version.
|
||||||
|
|
||||||
|
`requirements.txt` fige les dependances runtime (env de reference de la VM, Python 3.14).
|
||||||
|
|
||||||
|
## CI/CD (Partie 4)
|
||||||
|
|
||||||
|
`.forgejo/workflows/pipeline.yml` : declenche sur **pull_request vers `main`**, installe les
|
||||||
|
dependances puis lance `pipeline.py`. Execute par les runners du **Forgejo de la VM**
|
||||||
|
(`forgejo.192-168-122-143.nip.io`), mode docker sur `mlops-net` (les donnees `/data/modelling` et la
|
||||||
|
CA du host sont montees en lecture seule). Secrets (MLflow + S3 Garage) dans les secrets Actions du repo.
|
||||||
|
|
||||||
|
Livrable : `SYNTHESE_TP04.md`.
|
||||||
|
|||||||
160
SYNTHESE_TP04.md
Normal file
160
SYNTHESE_TP04.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# TP04 - Synthèse : automatisation d'un pipeline ML avec CI/CD
|
||||||
|
|
||||||
|
Fil rouge : prédiction de la consommation électrique (kWh). Aux TP02/TP03, chaque étape (split,
|
||||||
|
entraînement, `log_model`, enregistrement + promotion au Model Registry) était lancée **à la main**.
|
||||||
|
Le TP04 **automatise** cet enchaînement en un pipeline reproductible et idempotent (`pipeline.py`),
|
||||||
|
puis le **déclenche automatiquement via Forgejo Actions** à chaque Pull Request vers `main`.
|
||||||
|
|
||||||
|
## Ce qui a été construit
|
||||||
|
|
||||||
|
- **`pipeline.py`** (racine du dépôt) : orchestre en une seule commande `split -> train --register ->
|
||||||
|
promote`. Chaque étape est un sous-processus `subprocess.run(..., check=True)` : un échec stoppe le
|
||||||
|
pipeline avec un code de sortie non nul. Réutilise **telles quelles** les CLI du package `lab/`
|
||||||
|
(mêmes commandes qu'en manuel) ; ne fait qu'enchaîner et calculer le numéro de version à promouvoir.
|
||||||
|
- **`requirements.txt`** : dépendances runtime figées sur l'environnement de référence de la VM
|
||||||
|
(venv `/opt/venvs/mlops`, Python 3.14 ; mlflow 3.13.0, scikit-learn 1.9.0, pandas, pyarrow, boto3, s3fs).
|
||||||
|
- **`.forgejo/workflows/pipeline.yml`** : workflow CI déclenché sur `pull_request` vers `main`, qui
|
||||||
|
installe les dépendances puis exécute `pipeline.py`.
|
||||||
|
- **Repo + CI sur le Forgejo de la VM** (`forgejo.192-168-122-143.nip.io/trainer-admin/ENI-ml-mlops`) :
|
||||||
|
les 2 runners existants (mode docker, réseau `mlops-net`) exécutent le job. Le code de référence reste
|
||||||
|
archivé publiquement sur `git.lidge.fr` (comme aux TP02/TP03).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 1 - Analyse de la pipeline existante
|
||||||
|
|
||||||
|
**1.1 - Quelles étapes sont nécessaires pour produire un nouveau modèle ?**
|
||||||
|
1. **Préparation des données** (`lab/split/cli.py`) : lire le dataset source `/data/modelling/{features,
|
||||||
|
target}.parquet` et le découper en `train` / `validation` / `test` selon la stratégie de split figée.
|
||||||
|
2. **Entraînement** (`lab/modeling/cli.py`) : entraîner sur `train`, évaluer sur `validation`, logguer
|
||||||
|
paramètres et métriques dans MLflow, sauvegarder l'artefact du modèle (`log_model`).
|
||||||
|
3. **Enregistrement au Registry** (option `--register`) : empiler une **nouvelle version** du modèle
|
||||||
|
`electricity-consumption`.
|
||||||
|
4. **Promotion** (`lab/registry/cli.py`) : (re)pointer l'alias `champion` vers cette nouvelle version
|
||||||
|
(mise à disposition pour l'API de service).
|
||||||
|
|
||||||
|
**1.2 - Quelle étape dépend directement du résultat de la précédente ?**
|
||||||
|
Le pipeline est une **chaîne linéaire** : `train` dépend de `split` (il lit `train.parquet` et
|
||||||
|
`validation.parquet` produits par le split) ; l'`enregistrement`/`log_model` dépend de `train` (il
|
||||||
|
sauvegarde le modèle entraîné) ; la `promotion` dépend de l'enregistrement (elle a besoin du **numéro
|
||||||
|
de version** qui vient d'être créé). Chaque étape consomme la sortie de la précédente
|
||||||
|
(parquets -> modèle -> version -> alias).
|
||||||
|
|
||||||
|
**1.3 - Quels artefacts sont produits tout au long du pipeline ?**
|
||||||
|
- Les jeux de données `data/{train,validation,test}.parquet` (versionnables par DVC) ;
|
||||||
|
- un **run MLflow** avec ses paramètres (`strategy`, `split_strategy`, `features`) et métriques
|
||||||
|
(`train/validation_rmse`, `train/validation_mae`, coefficients) ;
|
||||||
|
- l'**artefact du modèle** sur `s3://mlflow-artifacts` (`model.pkl`, `MLmodel` + signature,
|
||||||
|
`requirements.txt`/`conda.yaml`/`python_env.yaml`, `input_example`) ;
|
||||||
|
- une **version** au Model Registry (`electricity-consumption` vN) ;
|
||||||
|
- l'**alias** `champion` pointant vers cette version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 2 - Construction d'une pipeline automatisé
|
||||||
|
|
||||||
|
**2.1 - Quels avantages par rapport à une exécution manuelle ?**
|
||||||
|
Reproductibilité (mêmes étapes, même ordre, mêmes paramètres à chaque exécution), suppression des
|
||||||
|
erreurs humaines (étape oubliée, mauvais ordre, paramètre incohérent), rapidité (**une seule
|
||||||
|
commande**), traçabilité, et surtout **exécutabilité par une machine** : le pipeline devient
|
||||||
|
déclenchable par une plateforme de CI/CD. Le script documente aussi le processus de façon vivante.
|
||||||
|
|
||||||
|
**2.2 - Que se passe-t-il si une étape échoue ?**
|
||||||
|
Le pipeline **s'arrête immédiatement** : chaque étape est lancée avec `check=True`, donc un échec lève
|
||||||
|
une exception et le script sort avec un **code non nul**. Les étapes suivantes ne sont pas exécutées
|
||||||
|
(on n'enregistre pas un modèle à partir d'un split incomplet). En CI, ce code non nul fait **échouer le
|
||||||
|
job** (signal rouge visible). On corrige la cause, puis on relance.
|
||||||
|
|
||||||
|
**2.3 - Pourquoi est-il important qu'un pipeline soit ré-exécutable sans effets de bord (idempotence) ?**
|
||||||
|
Pour pouvoir le **rejouer en confiance** (reprise après échec, ré-entraînement périodique, exécution en
|
||||||
|
CI) sans corrompre l'état ni accumuler d'effets parasites. Ici : le **split est déterministe** (mêmes
|
||||||
|
dates -> mêmes fichiers, réécrits proprement) ; `--register` **empile une nouvelle version** proprement
|
||||||
|
(le versioning est le comportement attendu, pas un doublon anarchique) ; l'alias `champion` est
|
||||||
|
**repointé** (mobile), jamais dupliqué. Rejouer le pipeline redonne donc un état cohérent et prévisible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 3 - Exécution de la pipeline
|
||||||
|
|
||||||
|
Exécution de référence sur la VM (`set -a; source .env; set +a; python pipeline.py`).
|
||||||
|
|
||||||
|
**3.1 - Quels artefacts ont été produits ?**
|
||||||
|
- `data/train.parquet` (~115 Mo, 4 485 120 lignes), `validation.parquet` (~48 Mo, 1 855 488),
|
||||||
|
`test.parquet` (~68 Mo, 2 629 632) ;
|
||||||
|
- un nouveau run dans l'expérience MLflow `tp02_electricity_consumption` ;
|
||||||
|
- une **nouvelle version** de `electricity-consumption` (stratégie `full`, `validation_rmse = 6.576`) ;
|
||||||
|
- l'alias `champion` repointé vers cette nouvelle version.
|
||||||
|
|
||||||
|
**3.2 - Comment vérifier qu'un nouveau modèle a bien été enregistré dans MLflow ?**
|
||||||
|
- En CLI : `python -m lab.registry.cli versions` (liste les versions, leurs alias et le
|
||||||
|
`validation_rmse`) ;
|
||||||
|
- dans l'**UI MLflow** (`https://mlflow.192-168-122-143.nip.io` -> Models -> `electricity-consumption`) :
|
||||||
|
la nouvelle version apparaît, l'alias `champion` pointe dessus ;
|
||||||
|
- par l'API : `MlflowClient().search_model_versions(...)` / `get_model_version_by_alias(name, "champion")`.
|
||||||
|
|
||||||
|
**3.3 - Quels éléments garantissent la reproductibilité ?**
|
||||||
|
- Le **code** versionné (git) et le **workflow** figé ;
|
||||||
|
- la **configuration** figée dans `lab/constants.py` (`CHOSEN_SPLIT_STRATEGY`, listes de features,
|
||||||
|
`SERVING_STRATEGY`) et le **split déterministe** par dates ;
|
||||||
|
- les **données** versionnées par DVC (remote S3 Garage) ;
|
||||||
|
- les **dépendances épinglées** (`requirements.txt`) + l'environnement capturé par MLflow dans
|
||||||
|
l'artefact (`requirements.txt`/`conda.yaml`) ;
|
||||||
|
- le **tracking MLflow** (paramètres, métriques, artefacts) qui permet de retrouver exactement chaque run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 4 - Automatisation avec Forgejo Actions
|
||||||
|
|
||||||
|
**4.1 - Quel événement déclenche le workflow ?**
|
||||||
|
Une **Pull Request vers la branche principale** : `on: pull_request: branches: [main]`. L'ouverture
|
||||||
|
(et chaque mise à jour) d'une PR ciblant `main` lance le workflow. (Démonstration réalisée : PR #1
|
||||||
|
`tp04-ci-pipeline -> main`, workflow **vert**, une nouvelle version du modèle enregistrée par la CI.)
|
||||||
|
|
||||||
|
**4.2 - Quel est le rôle de Forgejo Actions dans cette architecture ?**
|
||||||
|
C'est la **plateforme de CI/CD intégrée au forge**. Elle détecte l'événement (PR), planifie un job et
|
||||||
|
l'assigne à un **runner**, fournit l'environnement d'exécution (conteneur), **injecte les secrets**,
|
||||||
|
exécute les étapes (récupération du code, installation des dépendances, exécution de `pipeline.py`) et
|
||||||
|
**rapporte le statut** (vert/rouge) rattaché à la PR. C'est l'orchestrateur qui automatise l'exécution
|
||||||
|
du pipeline à chaque changement proposé, sert de garde-fou (une PR qui casse le pipeline est visible
|
||||||
|
avant le merge) et centralise la traçabilité.
|
||||||
|
|
||||||
|
**4.3 - Différence entre le pipeline Python (partie 2) et le workflow Forgejo Actions ?**
|
||||||
|
`pipeline.py` porte le **quoi** : la logique métier (l'enchaînement `split -> train -> register ->
|
||||||
|
promote`), exécutable partout (poste, VM, CI). Le workflow porte le **quand** et le **où** :
|
||||||
|
l'orchestration CI (quel événement déclenche, sur quel runner, dans quel environnement/conteneur, avec
|
||||||
|
quels secrets) et le reporting. Le workflow ne contient **aucune logique ML** : il prépare
|
||||||
|
l'environnement et appelle `pipeline.py`. Cette séparation des responsabilités permet de changer le
|
||||||
|
déclencheur ou l'infrastructure sans toucher à la logique, et inversement.
|
||||||
|
|
||||||
|
**4.4 - Quels autres événements pourraient déclencher automatiquement le pipeline ?**
|
||||||
|
Un **push** sur `main` (après merge), la création d'un **tag/release**, une **planification**
|
||||||
|
(`schedule`/cron, pour un ré-entraînement périodique), un **déclenchement manuel** (`workflow_dispatch`),
|
||||||
|
ou un **événement externe** (webhook signalant l'arrivée de nouvelles données, appel depuis un autre
|
||||||
|
workflow).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Détails d'implémentation (Forgejo Actions sur la VM)
|
||||||
|
|
||||||
|
- Le job tourne dans un conteneur `node:20-bookworm` sur le réseau `mlops-net` : il résout les services
|
||||||
|
par leur nom et joint MLflow/Garage via Caddy en HTTPS (`https://mlflow...`, `https://garage...`),
|
||||||
|
hôtes autorisés par `MLFLOW_SERVER_ALLOWED_HOSTS`.
|
||||||
|
- Deux volumes montés en lecture seule : les **données source** `/data/modelling` (le split en a besoin)
|
||||||
|
et le **magasin de certificats** du host `/etc/ssl/certs/ca-certificates.crt` (confiance envers la CA
|
||||||
|
interne ENI MLOps ; les runners ont été autorisés à monter ces chemins via `valid_volumes`).
|
||||||
|
- Les identifiants sont fournis en **secrets Actions** du repo (MLflow basic auth + clés S3 Garage),
|
||||||
|
jamais committés (`.env` reste gitignoré). Python et les dépendances sont installés dans le job
|
||||||
|
(`python3-venv` + `pip install -r requirements.txt`).
|
||||||
|
|
||||||
|
## Reproduire
|
||||||
|
|
||||||
|
Sur la VM, `~/tp` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a; source .env; set +a
|
||||||
|
python pipeline.py # split -> train full --register -> promote champion
|
||||||
|
python -m lab.registry.cli versions # verifier la nouvelle version + alias champion
|
||||||
|
```
|
||||||
|
|
||||||
|
En CI : ouvrir une Pull Request vers `main` sur `forgejo.192-168-122-143.nip.io/trainer-admin/ENI-ml-mlops`
|
||||||
|
-> le workflow `pipeline` s'exécute et enregistre une nouvelle version du modèle (onglet **Actions**).
|
||||||
178
SYNTHESE_TP05.md
Normal file
178
SYNTHESE_TP05.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# TP05 - Synthèse : monitoring, dérive et redéploiement
|
||||||
|
|
||||||
|
Fil rouge : prédiction de la consommation électrique (kWh, 128 clients portugais, pas de 15 min).
|
||||||
|
|
||||||
|
Scénario : un modèle a été **entraîné sur 2011** et tourne toujours en production plusieurs années plus
|
||||||
|
tard. En ingénieur MLOps, on vérifie si les données **2014** observées en production ont **dérivé** par
|
||||||
|
rapport à l'entraînement, on analyse cette dérive avec **Evidently**, puis on mesure son **impact sur les
|
||||||
|
performances** (2012 -> 2014) pour décider d'une stratégie de maintenance.
|
||||||
|
|
||||||
|
## Ce qui a été construit
|
||||||
|
|
||||||
|
- **`tp_module5_monitoring_derive.ipynb`** (notebook exécuté) : Partie 1 (moyennes + histogrammes de
|
||||||
|
`lag_30d` 2011 vs 2014), Partie 2 (rapport de dérive Evidently `DataDriftPreset`), Partie 3 (modèle
|
||||||
|
linéaire entraîné sur 2011, évalué sur 2011-2014). Réutilise `lab/constants.py` (chemins, cible, features).
|
||||||
|
- **`tp05_evidently_drift_2011_vs_2014.html`** : rapport Evidently interactif complet (à ouvrir dans un
|
||||||
|
navigateur).
|
||||||
|
- **Choix technique** : la feature `lag_365d` (consommation 365 jours plus tôt) est **exclue** car elle
|
||||||
|
est indéfinie (NaN) sur **toute** l'année 2011 (première année, pas d'historique 2010). On travaille donc
|
||||||
|
sur les 5 features définies sur les deux périodes : `lag_1d, lag_7d, lag_30d, rolling_mean_7d,
|
||||||
|
rolling_mean_30d`. Cet ensemble sert à la fois au rapport de dérive et au modèle entraîné sur 2011.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 1 - Observer une dérive
|
||||||
|
|
||||||
|
Feature `lag_30d`, sur les années complètes 2011 (train) et 2014 (production) :
|
||||||
|
|
||||||
|
| Année | Moyenne `lag_30d` | Effectif |
|
||||||
|
|--------------|-------------------|------------|
|
||||||
|
| 2011 (train) | **62.041 kWh** | 4 116 352 |
|
||||||
|
| 2014 (prod) | **54.054 kWh** | 4 485 120 |
|
||||||
|
| Écart | **-7.986 kWh** | **-12.9 %** |
|
||||||
|
|
||||||
|
**1.1 - Les deux distributions semblent-elles similaires ?**
|
||||||
|
Non. Elles gardent la même **forme** générale (distribution asymétrique étalée vers la droite : beaucoup de
|
||||||
|
petites consommations, une longue queue de fortes valeurs), mais l'histogramme 2014 est **décalé vers les
|
||||||
|
valeurs plus faibles** et sa moyenne est nettement plus basse. Visuellement, le décalage est net.
|
||||||
|
|
||||||
|
**1.2 - La moyenne a-t-elle évolué entre 2011 et 2014 ?**
|
||||||
|
Oui, franchement : de **62.04 kWh** à **54.05 kWh**, soit **-7.99 kWh (-12.9 %)**. La consommation moyenne
|
||||||
|
(observée via `lag_30d`) a **baissé d'environ 13 %** entre l'entraînement et la production.
|
||||||
|
|
||||||
|
**1.3 - Cette évolution paraît-elle suffisamment importante pour parler de dérive ?**
|
||||||
|
Une baisse de ~13 % de la moyenne est un signal **fort et cohérent** d'un changement de distribution : c'est
|
||||||
|
un indice sérieux de dérive des données. Attention toutefois : la moyenne seule ne **prouve** pas une dérive
|
||||||
|
statistique (deux distributions peuvent avoir des moyennes proches et des formes différentes, ou l'inverse).
|
||||||
|
Il faut confirmer avec une analyse sur toute la distribution, avec un test et un seuil objectifs (Partie 2).
|
||||||
|
|
||||||
|
**1.4 - Cette première analyse est-elle suffisante pour conclure sur l'état du modèle ? Pourquoi ?**
|
||||||
|
Non. Elle ne porte que sur **une** feature (`lag_30d`), via un **seul** indicateur (moyenne + comparaison
|
||||||
|
visuelle), **sans test statistique ni seuil**, et elle ne dit **rien** des autres features ni de l'**impact
|
||||||
|
réel sur la performance** du modèle. C'est un premier indice, pas une conclusion. Il faut (a) un outil de
|
||||||
|
monitoring qui teste toutes les features (Partie 2) et (b) mesurer la performance dans le temps (Partie 3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 2 - Analyse avec un outil de monitoring (Evidently)
|
||||||
|
|
||||||
|
Rapport `DataDriftPreset` comparant 2011 (référence) et 2014 (courant), échantillon de 200 000 lignes par
|
||||||
|
période (seed fixe). Test choisi automatiquement par Evidently pour de grands échantillons numériques :
|
||||||
|
**distance de Wasserstein normalisée**, seuil **0.1**.
|
||||||
|
|
||||||
|
| Feature | Test | Drift score | Seuil | Dérive ? |
|
||||||
|
|--------------------|-------------------------------|-------------|-------|----------|
|
||||||
|
| lag_1d | Wasserstein distance (normed) | 0.165 | 0.1 | Oui |
|
||||||
|
| lag_7d | Wasserstein distance (normed) | 0.167 | 0.1 | Oui |
|
||||||
|
| lag_30d | Wasserstein distance (normed) | 0.167 | 0.1 | Oui |
|
||||||
|
| rolling_mean_7d | Wasserstein distance (normed) | 0.188 | 0.1 | Oui |
|
||||||
|
| rolling_mean_30d | Wasserstein distance (normed) | 0.191 | 0.1 | Oui |
|
||||||
|
|
||||||
|
Verdict global : **`dataset_drift = True`**, **5 / 5 features en dérive (100 %)**.
|
||||||
|
|
||||||
|
**2.1 - Combien de features présentent une dérive ?**
|
||||||
|
**Les 5** features analysées présentent une dérive (5 / 5, soit 100 %). Toutes dépassent le seuil de 0.1.
|
||||||
|
|
||||||
|
**2.2 - Toutes les features évoluent-elles de la même manière ?**
|
||||||
|
Toutes dérivent, mais **pas avec la même intensité**. Les distances de Wasserstein vont de **0.165**
|
||||||
|
(`lag_1d`) à **0.191** (`rolling_mean_30d`). Les **moyennes glissantes** (`rolling_mean_7d` 0.188,
|
||||||
|
`rolling_mean_30d` 0.191) dérivent un peu **plus** que les **lags bruts** (0.165-0.167) : les indicateurs
|
||||||
|
lissés/tendanciels captent davantage la baisse durable du niveau de consommation.
|
||||||
|
|
||||||
|
**2.3 - Identifiez deux métriques ou indicateurs du rapport. À quoi servent-ils ?**
|
||||||
|
- **Le drift score par feature** (ici la distance de Wasserstein normalisée) : il **quantifie** l'écart
|
||||||
|
entre la distribution de référence (2011) et la distribution courante (2014) pour chaque variable, et le
|
||||||
|
compare à un **seuil** (0.1). Il sert à **détecter et localiser** la dérive, feature par feature.
|
||||||
|
- **Le verdict global "Dataset Drift" + la part de colonnes en dérive** : il **agrège** les décisions par
|
||||||
|
feature (nombre / pourcentage de colonnes dérivées, ici 5/5 = 100 %) en une conclusion **au niveau du
|
||||||
|
dataset entier**. Il sert à **trancher globalement** (le dataset a-t-il dérivé, oui/non).
|
||||||
|
(Le rapport affiche aussi, par feature, les **histogrammes de distribution référence vs courant**, utiles
|
||||||
|
pour visualiser la nature du décalage.)
|
||||||
|
|
||||||
|
**2.4 - Le rapport conclut-il à une dérive globale du dataset ? Justifiez.**
|
||||||
|
**Oui.** `dataset_drift = True`. Le critère par défaut d'Evidently (déclarer une dérive du dataset quand la
|
||||||
|
**part de colonnes en dérive dépasse 50 %**) est **largement** franchi : **100 %** des colonnes dérivent.
|
||||||
|
Le rapport conclut donc sans ambiguïté à une dérive globale des données d'entrée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partie 3 - Mesurer l'impact sur les performances
|
||||||
|
|
||||||
|
Modèle (régression linéaire) entraîné **sur 2011 uniquement**, évalué sur chaque année complète :
|
||||||
|
|
||||||
|
| Année | Rôle | RMSE (kWh) | MAE (kWh) |
|
||||||
|
|-------|-------------------|------------|-----------|
|
||||||
|
| 2011 | train (référence) | 7.904 | 4.450 |
|
||||||
|
| 2012 | évaluation (prod) | 7.595 | 4.178 |
|
||||||
|
| 2013 | évaluation (prod) | 7.486 | 4.095 |
|
||||||
|
| 2014 | évaluation (prod) | **7.101** | **3.911** |
|
||||||
|
|
||||||
|
**Résultat marquant : l'erreur ne se dégrade pas, elle diminue légèrement d'année en année.**
|
||||||
|
|
||||||
|
**3.1 - Peut-on comparer 2011 vs 2012 au même titre que 2012 vs 2013 ?**
|
||||||
|
Non. **2011 est l'année d'entraînement** : l'erreur y est mesurée **in-sample** (le modèle a déjà vu ces
|
||||||
|
données), donc **optimiste**. Comparer 2011 (in-sample) à 2012 (hors échantillon) mélange deux régimes
|
||||||
|
différents, alors que 2012 vs 2013 compare **deux années hors échantillon** entre elles (comparaison
|
||||||
|
homogène). De plus, le **niveau de consommation change** d'une année à l'autre : comparer des **RMSE bruts**
|
||||||
|
est biaisé par l'échelle de la cible (voir 3.3). 2011 doit donc être traité comme **référence de train**,
|
||||||
|
pas comme un point de comparaison équivalent aux années de production.
|
||||||
|
|
||||||
|
**3.2 - Observe-t-on une dégradation progressive ?**
|
||||||
|
**Non.** En valeur absolue, la performance **s'améliore** légèrement : RMSE 7.90 -> 7.60 -> 7.49 -> 7.10 et
|
||||||
|
MAE 4.45 -> 4.18 -> 4.10 -> 3.91 de 2011 à 2014. Aucune dégradation observée sur ces métriques.
|
||||||
|
|
||||||
|
**3.3 - Cette (absence de) dégradation est-elle cohérente avec la dérive détectée ?**
|
||||||
|
À première vue c'est surprenant (dérive nette en Partie 2, mais pas de perte de performance), et c'est
|
||||||
|
**instructif** : **une dérive des données ne signifie pas automatiquement une dégradation du modèle**.
|
||||||
|
Explication : la consommation a **baissé** (~-13 % sur `lag_30d`, Partie 1), donc la **cible** (kWh) est plus
|
||||||
|
petite en 2014 ; comme RMSE et MAE sont des erreurs **absolues** en kWh, elles **diminuent mécaniquement**
|
||||||
|
quand l'échelle de la cible diminue. Pendant ce temps, la **relation features -> cible** (les lags prédisent
|
||||||
|
la consommation courante) est restée **stable**. On a donc une dérive des données **sans** dérive de la
|
||||||
|
relation. Pour comparer rigoureusement les années, il faudrait une métrique **indépendante de l'échelle**
|
||||||
|
(MAPE, R², RMSE normalisé).
|
||||||
|
|
||||||
|
**3.4 - Peut-on conclure que le modèle 2011 est encore fiable en 2014 ?**
|
||||||
|
Sur la base des erreurs absolues, il **ne s'est pas dégradé** (il fait même un peu mieux en kWh). Mais on ne
|
||||||
|
peut pas **conclure** à sa pleine fiabilité sur ces seuls chiffres : (1) les métriques absolues sont
|
||||||
|
**trompeuses** quand l'échelle de la cible change ; (2) on n'a mesuré que l'**amplitude** de l'erreur, pas un
|
||||||
|
éventuel **biais systématique** (sur/sous-estimation) ni la performance par segment (client, saison).
|
||||||
|
Verdict prudent : **pas de signe de dégradation**, mais à **confirmer** avec des métriques relatives et une
|
||||||
|
analyse des résidus avant de déclarer le modèle fiable.
|
||||||
|
|
||||||
|
**3.5 - Que faudrait-il faire ensuite : surveiller, réentraîner, ou redéployer ?**
|
||||||
|
**Surveiller.** La dérive des données est réelle, mais **sans impact mesuré** sur la performance : rien
|
||||||
|
n'impose un réentraînement ou un redéploiement immédiat (ce serait du travail et du risque pour un gain non
|
||||||
|
démontré). On met en place un **monitoring continu** (dérive des features + performance suivie avec une
|
||||||
|
métrique relative + dérive des prédictions et de la cible dès que la vérité terrain arrive) et on **déclenche
|
||||||
|
un réentraînement seulement si/quand** la performance se dégrade réellement ou qu'un concept drift apparaît.
|
||||||
|
|
||||||
|
**3.6 - Quel type de dérive semble en jeu ? Quelles analyses préconisez-vous ?**
|
||||||
|
Type : **data drift** (dérive des covariables / *covariate shift*). Les distributions des features d'entrée
|
||||||
|
ont changé (baisse durable du niveau de consommation), tandis que la **relation** features -> cible semble
|
||||||
|
**stable** (pas de perte de performance) : **pas de concept drift évident**. Analyses préconisées :
|
||||||
|
- suivre une **métrique de performance indépendante de l'échelle** (MAPE, R², nRMSE) dans le temps, plutôt
|
||||||
|
que le RMSE brut ;
|
||||||
|
- monitorer aussi la **dérive des prédictions** et de la **cible** (*target drift*), pas seulement des
|
||||||
|
features en entrée ;
|
||||||
|
- **analyser les résidus** (biais moyen, hétéroscédasticité) et **segmenter** (par client, par saison) ;
|
||||||
|
- surtout, dès que la **vérité terrain** est disponible (avec délai), **recalculer la performance réelle en
|
||||||
|
production** pour détecter précocement un éventuel **concept drift** : c'est la fermeture de la *feedback
|
||||||
|
loop* MLOps (surveiller -> alerter -> réentraîner -> redéployer).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reproduire
|
||||||
|
|
||||||
|
Sur la VM, dans `~/tp` (venv `/opt/venvs/mlops`). Le TP05 est **100 % local** (données `/data/modelling`) :
|
||||||
|
aucun secret réseau (MLflow, S3) n'est nécessaire.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ré-exécuter le notebook (régénère les sorties + le rapport HTML)
|
||||||
|
/opt/venvs/mlops/bin/jupyter nbconvert --to notebook --execute --inplace tp_module5_monitoring_derive.ipynb
|
||||||
|
```
|
||||||
|
|
||||||
|
Consultation interactive (service `jupyter-tp`, token `mlops`) :
|
||||||
|
|
||||||
|
```
|
||||||
|
https://jupyter.192-168-122-143.nip.io/lab/tree/tp_module5_monitoring_derive.ipynb?token=mlops
|
||||||
|
```
|
||||||
70
pipeline.py
Normal file
70
pipeline.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""Pipeline ML automatise : split -> entrainement (--register) -> promotion.
|
||||||
|
|
||||||
|
Orchestre en une seule commande (`python pipeline.py`) les etapes du fil rouge
|
||||||
|
menant a la mise a disposition du modele dans le MLflow Model Registry :
|
||||||
|
|
||||||
|
1. split : (re)cree data/{train,validation,test}.parquet depuis /data/modelling
|
||||||
|
(strategie de split figee dans lab/constants.py) ;
|
||||||
|
2. train : entraine la strategie de service (`full`) et enregistre une
|
||||||
|
nouvelle version du modele dans le Registry (log_model + --register) ;
|
||||||
|
3. promote : (re)pointe l'alias `champion` vers cette nouvelle version.
|
||||||
|
|
||||||
|
Chaque etape est un sous-processus lance avec `check=True` : si une etape echoue,
|
||||||
|
le pipeline s'arrete immediatement avec un code de sortie non nul et n'execute
|
||||||
|
aucune etape suivante (Partie 2, Q2.2). Le pipeline est reexecutable sans effet
|
||||||
|
de bord indesirable (idempotence, Q2.3) : le split est deterministe, `--register`
|
||||||
|
empile proprement une nouvelle version, et l'alias `champion` est repointe
|
||||||
|
(jamais duplique).
|
||||||
|
|
||||||
|
Les etapes reutilisent telles quelles les CLI du package `lab/` (memes commandes
|
||||||
|
qu'une execution manuelle des TP precedents) : le pipeline ne fait que les
|
||||||
|
enchainer et calculer le numero de la version a promouvoir.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from mlflow import MlflowClient
|
||||||
|
|
||||||
|
from lab import constants
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [pipeline] %(message)s")
|
||||||
|
logger = logging.getLogger("pipeline")
|
||||||
|
|
||||||
|
|
||||||
|
def run_step(title: str, module_args: list[str]) -> None:
|
||||||
|
"""Execute une etape (module `lab`) dans un sous-processus ; stoppe si elle echoue."""
|
||||||
|
logger.info("=== ETAPE %s : python -m %s", title, " ".join(module_args))
|
||||||
|
subprocess.run([sys.executable, "-m", *module_args], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def latest_model_version(name: str) -> int:
|
||||||
|
"""Numero de la derniere version enregistree du modele (celle qui vient d'etre creee)."""
|
||||||
|
client = MlflowClient()
|
||||||
|
versions = client.search_model_versions(f"name = '{name}'")
|
||||||
|
if not versions:
|
||||||
|
raise RuntimeError(f"Aucune version enregistree pour le modele '{name}'.")
|
||||||
|
return max(int(mv.version) for mv in versions)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
strategy = constants.SERVING_STRATEGY
|
||||||
|
name = constants.REGISTERED_MODEL_NAME
|
||||||
|
alias = constants.MODEL_ALIAS
|
||||||
|
|
||||||
|
# 1. Preparation des jeux de donnees (deterministe).
|
||||||
|
run_step("split", ["lab.split.cli"])
|
||||||
|
|
||||||
|
# 2. Entrainement + enregistrement d'une nouvelle version dans le Model Registry.
|
||||||
|
run_step("train", ["lab.modeling.cli", strategy.value, "--register"])
|
||||||
|
|
||||||
|
# 3. Promotion : repointer l'alias vers la version qui vient d'etre enregistree.
|
||||||
|
version = latest_model_version(name)
|
||||||
|
run_step("promote", ["lab.registry.cli", "promote", "--version", str(version), "--alias", alias])
|
||||||
|
|
||||||
|
logger.info("Pipeline termine avec succes : %s v%s -> alias '%s'", name, version, alias)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
14
requirements.txt
Normal file
14
requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Dependances runtime du pipeline (Partie 4 : "installer les dependances").
|
||||||
|
# Versions figees sur l'environnement de reference de la VM (venv /opt/venvs/mlops,
|
||||||
|
# Python 3.14) pour garantir la reproductibilite et la coherence avec le serveur
|
||||||
|
# MLflow (3.13.0) et l'API de service (chargement du modele enregistre).
|
||||||
|
mlflow==3.13.0
|
||||||
|
scikit-learn==1.9.0
|
||||||
|
scipy==1.17.1
|
||||||
|
numpy==2.4.6
|
||||||
|
pandas==2.3.3
|
||||||
|
pyarrow==24.0.0
|
||||||
|
typer==0.26.7
|
||||||
|
boto3==1.43.0
|
||||||
|
botocore==1.43.0
|
||||||
|
s3fs==2026.6.0
|
||||||
646
tp05_evidently_drift_2011_vs_2014.html
Normal file
646
tp05_evidently_drift_2011_vs_2014.html
Normal file
File diff suppressed because one or more lines are too long
597
tp_module5_monitoring_derive.ipynb
Normal file
597
tp_module5_monitoring_derive.ipynb
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user