From 9312d3b60f6f25247f3092d34b360e21177761c1 Mon Sep 17 00:00:00 2001 From: Valentin Date: Mon, 21 Sep 2026 14:52:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(ml):=20enregistrer=20le=20mod=C3=A8le=20da?= =?UTF-8?q?ns=20le=20MLflow=20Model=20Registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- ml/.dockerignore | 5 +++++ ml/Dockerfile | 3 +++ ml/README.md | 31 +++++++++++++++++++++++++++++++ ml/docker-compose.mlflow.yml | 33 +++++++++++++++++++++++++++++++++ ml/tests/test_train.py | 1 + 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 ml/.dockerignore create mode 100644 ml/Dockerfile create mode 100644 ml/docker-compose.mlflow.yml diff --git a/.gitignore b/.gitignore index 47574d1..81c0338 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,7 @@ ml/models/* !ml/models/.gitkeep ml/mlruns/ ml/mlartifacts/ -ml/mlflow.db +ml/mlflow.db* # IDE et OS .idea/ diff --git a/ml/.dockerignore b/ml/.dockerignore new file mode 100644 index 0000000..456e579 --- /dev/null +++ b/ml/.dockerignore @@ -0,0 +1,5 @@ +.venv +data +mlruns +mlflow.db* +models diff --git a/ml/Dockerfile b/ml/Dockerfile new file mode 100644 index 0000000..77b75ed --- /dev/null +++ b/ml/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3.12-slim +RUN pip install --no-cache-dir mlflow==3.16.1 psycopg2-binary +EXPOSE 5000 diff --git a/ml/README.md b/ml/README.md index 4d69363..b11a273 100644 --- a/ml/README.md +++ b/ml/README.md @@ -57,6 +57,37 @@ validation. La coupure est **chronologique**, jamais un tirage aleatoire de lign aleatoire laisserait des lignes de validation "voir" des lignes d'entrainement via leurs lags/moyennes glissantes, une fuite qui masquerait un surapprentissage. +## Serveur MLflow (conteneur) + +Un serveur MLflow (PostgreSQL pour les metadonnees, volume pour les artefacts) se lance avec +Docker. Prerequis : Docker Desktop demarre. + +``` +cd ml +docker compose -f docker-compose.mlflow.yml up -d --build +``` + +Interface : http://localhost:5000. Entrainer vers ce serveur : + +``` +uv run python -m enervision_ml.train --csv data/all_sites_combined.csv --mlflow-tracking-uri http://localhost:5000 +``` + +Arreter : `docker compose -f docker-compose.mlflow.yml down` (ajouter `-v` pour effacer aussi les +runs et les modeles). + +Pour voir les runs dans l'interface (MLflow 3.x) : + +- Passer le selecteur en haut a gauche sur **Model training**. Le mode **GenAI** affiche des + traces LLM et reste vide pour un entrainement LightGBM. +- **Runs** liste les entrainements, **Models** les artefacts de modele de chaque run (tous nommes + `model`), et **Model registry** les versions numerotees de `consumption-forecast-lightgbm`. + +Limites : les identifiants PostgreSQL (`mlflow` / `mlflow`) du compose ne conviennent qu'au +developpement local. Un deploiement partage demandera des secrets, de l'authentification et un +stockage d'artefacts dedie (S3/MinIO). Le port 5000 doit etre libre : arreter `mlflow ui` avant, +ou changer le mapping (`"5001:5000"`) dans le compose. + ## Commandes ```bash diff --git a/ml/docker-compose.mlflow.yml b/ml/docker-compose.mlflow.yml new file mode 100644 index 0000000..981a2f9 --- /dev/null +++ b/ml/docker-compose.mlflow.yml @@ -0,0 +1,33 @@ +services: + mlflow-db: + image: postgres:16 + environment: + POSTGRES_USER: mlflow + POSTGRES_PASSWORD: mlflow + POSTGRES_DB: mlflow + volumes: + - mlflow-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mlflow"] + interval: 5s + retries: 10 + + mlflow: + build: . + depends_on: + mlflow-db: + condition: service_healthy + ports: + - "5000:5000" + volumes: + - mlflow-artifacts:/mlartifacts + command: > + mlflow server + --host 0.0.0.0 --port 5000 + --backend-store-uri postgresql://mlflow:mlflow@mlflow-db:5432/mlflow + --artifacts-destination /mlartifacts + --serve-artifacts + +volumes: + mlflow-db-data: + mlflow-artifacts: diff --git a/ml/tests/test_train.py b/ml/tests/test_train.py index 047e069..7f22d5c 100644 --- a/ml/tests/test_train.py +++ b/ml/tests/test_train.py @@ -77,6 +77,7 @@ def test_train_runs_end_to_end_on_synthetic_data_and_beats_a_dummy_baseline( assert baseline_metrics["n_observations"] == model_metrics["n_observations"] assert model_metrics["mae"] < baseline_metrics["mae"] + def test_train_raises_when_the_validation_window_is_empty(tmp_path: Path) -> None: depart = datetime(2026, 1, 1, tzinfo=UTC) frame = make_frame("site-a", heures=50, depart=depart) # trop court pour un lag de 168h