Files
ENI-ml-mlops/SYNTHESE_TP04.md
2026-07-23 15:32:41 +02:00

10 KiB

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 : 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 :

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).