ASI-Engineer commited on
Commit
4570c28
·
verified ·
1 Parent(s): aac75d5

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. docs/mlflow_guide.md +51 -185
  2. pyproject.toml +2 -9
  3. requirements.txt +7 -39
  4. tests/test_basic.py +30 -2
docs/mlflow_guide.md CHANGED
@@ -4,7 +4,7 @@
4
  1. [Workflow complet MLflow](#workflow-complet)
5
  2. [Comparer plusieurs runs](#comparer-runs)
6
  3. [Trouver le meilleur modèle](#meilleur-modèle)
7
- 4. [Charger un modèle pour l'API](#api-integration)
8
  5. [Best Practices](#best-practices)
9
 
10
  ---
@@ -14,7 +14,7 @@
14
  ### 🎯 Concept clé
15
  MLflow suit ce workflow :
16
  ```
17
- Entraînement → Tracking → Registry → Déploiement API
18
  ```
19
 
20
  ### Architecture actuelle du projet
@@ -26,10 +26,8 @@ mlflow.db (SQLite)
26
  MLflow UI (http://localhost:5000)
27
  ↓ (select best model)
28
  Model Registry (XGBoost_Employee_Turnover)
29
- ↓ (load)
30
- API FastAPI/Flask
31
- ↓ (serve)
32
- Prédictions
33
  ```
34
 
35
  ---
@@ -88,10 +86,10 @@ for config in configs:
88
 
89
  ## 3. Trouver le meilleur modèle
90
 
91
- ### Option A : Via l'API MLflow (recommandé pour l'API)
92
 
93
  ```python
94
- # api/get_best_model.py
95
  import mlflow
96
  from mlflow.tracking import MlflowClient
97
 
@@ -117,8 +115,8 @@ def get_best_model_from_experiment(experiment_name="Default", metric="cv_f1"):
117
  # Rechercher tous les runs de l'expérience
118
  runs = client.search_runs(
119
  experiment_ids=[experiment.experiment_id],
120
- order_by=[f"metrics.{metric} DESC"], # Trier par métrique décroissante
121
- max_results=1 # Prendre seulement le meilleur
122
  )
123
 
124
  if not runs:
@@ -128,179 +126,43 @@ def get_best_model_from_experiment(experiment_name="Default", metric="cv_f1"):
128
  print(f"🏆 Meilleur modèle trouvé:")
129
  print(f" Run ID: {best_run.info.run_id}")
130
  print(f" {metric}: {best_run.data.metrics.get(metric, 'N/A')}")
131
- print(f" Date: {best_run.info.start_time}")
132
 
133
  return best_run.info.run_id
134
 
135
- # Exemple d'utilisation
136
- if __name__ == "__main__":
137
- best_run_id = get_best_model_from_experiment("Default", "cv_f1")
138
-
139
- # Charger le modèle
140
- model_uri = f"runs:/{best_run_id}/model"
141
- model = mlflow.sklearn.load_model(model_uri)
142
- print(f"✅ Modèle chargé : {type(model)}")
143
- ```
144
-
145
- ### Option B : Via le Model Registry (pour production)
146
-
147
- ```python
148
- # api/load_production_model.py
149
- import mlflow
150
-
151
- mlflow.set_tracking_uri("sqlite:///mlflow.db")
152
-
153
- def load_production_model(model_name="XGBoost_Employee_Turnover", stage="Production"):
154
- """
155
- Charge le modèle en production depuis le Model Registry.
156
-
157
- Args:
158
- model_name: Nom du modèle dans le Registry
159
- stage: Stage du modèle ("Staging", "Production", "Archived")
160
-
161
- Returns:
162
- Modèle chargé
163
- """
164
- model_uri = f"models:/{model_name}/{stage}"
165
-
166
- try:
167
- model = mlflow.sklearn.load_model(model_uri)
168
- print(f"✅ Modèle '{model_name}' ({stage}) chargé")
169
- return model
170
- except Exception as e:
171
- print(f"⚠️ Erreur : {e}")
172
- print(f"💡 Astuce : Promouvoir une version en '{stage}' dans MLflow UI")
173
-
174
- # Fallback : Charger la dernière version
175
- model_uri = f"models:/{model_name}/latest"
176
- model = mlflow.sklearn.load_model(model_uri)
177
- print(f"✅ Fallback : Dernière version chargée")
178
- return model
179
-
180
- # Utilisation
181
- if __name__ == "__main__":
182
- model = load_production_model()
183
  ```
184
 
185
  ---
186
 
187
- ## 4. API Integration - Exemple complet
188
 
189
- ### Créer une API Flask/FastAPI avec MLflow
190
 
191
  ```python
192
- # api/app.py
193
- from fastapi import FastAPI, HTTPException
194
- from pydantic import BaseModel
195
- import mlflow
196
- import pandas as pd
197
- import numpy as np
198
-
199
- # Configuration
200
- mlflow.set_tracking_uri("sqlite:///mlflow.db")
201
- app = FastAPI(title="Employee Turnover Prediction API")
202
-
203
- # Charger le modèle au démarrage
204
- MODEL_NAME = "XGBoost_Employee_Turnover"
205
- model = None
206
-
207
- @app.on_event("startup")
208
- def load_model():
209
- global model
210
- try:
211
- # Charger le dernier modèle du Registry
212
- model_uri = f"models:/{MODEL_NAME}/latest"
213
- model = mlflow.sklearn.load_model(model_uri)
214
- print(f"✅ Modèle chargé : {MODEL_NAME}")
215
- except Exception as e:
216
- print(f"❌ Erreur chargement modèle : {e}")
217
- raise
218
-
219
- # Schéma de requête
220
- class PredictionRequest(BaseModel):
221
- features: list[float] # Liste de 50 features (après prétraitement)
222
-
223
- class Config:
224
- json_schema_extra = {
225
- "example": {
226
- "features": [0.5, 1.2, -0.3, 0.8] + [0.0] * 46 # 50 features
227
- }
228
- }
229
-
230
- class PredictionResponse(BaseModel):
231
- prediction: int # 0 ou 1
232
- probability: float # Probabilité de départ (classe 1)
233
- model_version: str
234
-
235
- # Endpoint de prédiction
236
- @app.post("/predict", response_model=PredictionResponse)
237
- def predict(request: PredictionRequest):
238
- """
239
- Prédit si un employé va quitter l'entreprise.
240
-
241
- - **features**: Liste de 50 features numériques (après prétraitement)
242
- - Retourne la prédiction (0=reste, 1=part) et la probabilité
243
- """
244
- if model is None:
245
- raise HTTPException(status_code=503, detail="Modèle non chargé")
246
-
247
- try:
248
- # Convertir en DataFrame
249
- X = pd.DataFrame([request.features])
250
-
251
- # Prédiction
252
- prediction = int(model.predict(X)[0])
253
- probability = float(model.predict_proba(X)[0][1])
254
-
255
- return PredictionResponse(
256
- prediction=prediction,
257
- probability=round(probability, 4),
258
- model_version=MODEL_NAME
259
- )
260
- except Exception as e:
261
- raise HTTPException(status_code=400, detail=f"Erreur prédiction : {str(e)}")
262
-
263
- # Endpoint de santé
264
- @app.get("/health")
265
- def health():
266
- return {
267
- "status": "ok",
268
- "model_loaded": model is not None,
269
- "model_name": MODEL_NAME
270
- }
271
-
272
- # Endpoint pour lister les modèles disponibles
273
- @app.get("/models")
274
- def list_models():
275
- from mlflow.tracking import MlflowClient
276
- client = MlflowClient()
277
-
278
- models = []
279
- for rm in client.search_registered_models():
280
- latest_versions = rm.latest_versions
281
- models.append({
282
- "name": rm.name,
283
- "versions": len(latest_versions),
284
- "latest_version": latest_versions[0].version if latest_versions else None
285
- })
286
-
287
- return {"models": models}
288
 
289
- # Lancer avec : uvicorn api.app:app --reload
290
- ```
291
 
292
- **Tester l'API** :
293
- ```bash
294
- # Installer FastAPI
295
- pip install fastapi uvicorn
296
 
297
- # Lancer le serveur
298
- uvicorn api.app:app --reload --port 8000
 
 
 
 
 
299
 
300
- # Tester
301
- curl -X POST http://localhost:8000/predict \
302
- -H "Content-Type: application/json" \
303
- -d '{"features": [0.5, 1.2, -0.3] + [0.0] * 47}'
304
  ```
305
 
306
  ---
@@ -314,7 +176,6 @@ curl -X POST http://localhost:8000/predict \
314
  # 1. Entraîner plusieurs modèles → Experiment "Development"
315
  # 2. Sélectionner le meilleur → Promouvoir en "Staging"
316
  # 3. Valider en staging → Promouvoir en "Production"
317
- # 4. API charge toujours "Production"
318
 
319
  from mlflow.tracking import MlflowClient
320
 
@@ -339,10 +200,8 @@ with mlflow.start_run():
339
  mlflow.log_param("n_features", X.shape[1])
340
  mlflow.log_param("class_imbalance_ratio", sum(y==0)/sum(y==1))
341
 
342
- # Log artifacts (graphiques, etc.)
343
  import matplotlib.pyplot as plt
344
-
345
- # Confusion matrix plot
346
  plt.figure()
347
  # ... plot code ...
348
  plt.savefig("confusion_matrix.png")
@@ -359,7 +218,6 @@ with mlflow.start_run():
359
  ```python
360
  # scripts/retrain_model.py
361
  import mlflow
362
- from datetime import datetime
363
 
364
  def retrain_and_compare():
365
  """Entraîne un nouveau modèle et le compare à la production."""
@@ -381,8 +239,6 @@ def retrain_and_compare():
381
  # 4. Si meilleur, promouvoir automatiquement
382
  if new_f1 > prod_f1:
383
  print("✅ Nouveau modèle meilleur ! Promotion en Staging...")
384
- # Enregistrer dans Registry
385
- # ... code de promotion ...
386
  else:
387
  print("⚠️ Nouveau modèle moins bon, conservation du modèle actuel")
388
  ```
@@ -397,16 +253,26 @@ def retrain_and_compare():
397
 
398
  ---
399
 
400
- ## 🎯 Prochaines étapes pour ton projet
401
 
402
- 1. **MLflow configuré** - Tracking local avec SQLite
403
- 2. ✅ **Modèle enregistré** - XGBoost_Employee_Turnover v1
404
- 3. 🔄 **TODO: Créer l'API** - FastAPI avec chargement du modèle
405
- 4. 🔄 **TODO: Tester comparaison** - Multiple runs avec hyperparams différents
406
- 5. 🔄 **TODO: CI/CD** - Auto-retraining et déploiement
407
 
408
- **Commande pour démarrer l'API** :
409
  ```bash
410
- # Créer api/app.py avec le code ci-dessus
411
- uvicorn api.app:app --reload --port 8000
 
 
 
 
 
 
 
 
 
 
 
412
  ```
 
4
  1. [Workflow complet MLflow](#workflow-complet)
5
  2. [Comparer plusieurs runs](#comparer-runs)
6
  3. [Trouver le meilleur modèle](#meilleur-modèle)
7
+ 4. [Model Registry](#model-registry)
8
  5. [Best Practices](#best-practices)
9
 
10
  ---
 
14
  ### 🎯 Concept clé
15
  MLflow suit ce workflow :
16
  ```
17
+ Entraînement → Tracking → Registry → Sélection du meilleur modèle
18
  ```
19
 
20
  ### Architecture actuelle du projet
 
26
  MLflow UI (http://localhost:5000)
27
  ↓ (select best model)
28
  Model Registry (XGBoost_Employee_Turnover)
29
+ ↓ (versions & stages)
30
+ Modèle prêt pour déploiement
 
 
31
  ```
32
 
33
  ---
 
86
 
87
  ## 3. Trouver le meilleur modèle
88
 
89
+ ### Via l'API MLflow
90
 
91
  ```python
92
+ # examples/find_best_model.py (déjà créé dans le projet)
93
  import mlflow
94
  from mlflow.tracking import MlflowClient
95
 
 
115
  # Rechercher tous les runs de l'expérience
116
  runs = client.search_runs(
117
  experiment_ids=[experiment.experiment_id],
118
+ order_by=[f"metrics.{metric} DESC"],
119
+ max_results=1
120
  )
121
 
122
  if not runs:
 
126
  print(f"🏆 Meilleur modèle trouvé:")
127
  print(f" Run ID: {best_run.info.run_id}")
128
  print(f" {metric}: {best_run.data.metrics.get(metric, 'N/A')}")
 
129
 
130
  return best_run.info.run_id
131
 
132
+ # Charger le modèle
133
+ best_run_id = get_best_model_from_experiment("Default", "cv_f1")
134
+ model_uri = f"runs:/{best_run_id}/model"
135
+ model = mlflow.sklearn.load_model(model_uri)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  ```
137
 
138
  ---
139
 
140
+ ## 4. Model Registry
141
 
142
+ ### Gérer les versions de modèles
143
 
144
  ```python
145
+ # examples/model_registry.py (déjà créé dans le projet)
146
+ from mlflow.tracking import MlflowClient
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ client = MlflowClient()
149
+ model_name = "XGBoost_Employee_Turnover"
150
 
151
+ # Lister les versions
152
+ versions = client.search_model_versions(f"name='{model_name}'")
153
+ for v in versions:
154
+ print(f"Version {v.version}: {v.current_stage}")
155
 
156
+ # Promouvoir en Production
157
+ client.transition_model_version_stage(
158
+ name=model_name,
159
+ version=1,
160
+ stage="Production",
161
+ archive_existing_versions=True
162
+ )
163
 
164
+ # Charger depuis le Registry
165
+ model = mlflow.sklearn.load_model(f"models:/{model_name}/Production")
 
 
166
  ```
167
 
168
  ---
 
176
  # 1. Entraîner plusieurs modèles → Experiment "Development"
177
  # 2. Sélectionner le meilleur → Promouvoir en "Staging"
178
  # 3. Valider en staging → Promouvoir en "Production"
 
179
 
180
  from mlflow.tracking import MlflowClient
181
 
 
200
  mlflow.log_param("n_features", X.shape[1])
201
  mlflow.log_param("class_imbalance_ratio", sum(y==0)/sum(y==1))
202
 
203
+ # Log artifacts (graphiques)
204
  import matplotlib.pyplot as plt
 
 
205
  plt.figure()
206
  # ... plot code ...
207
  plt.savefig("confusion_matrix.png")
 
218
  ```python
219
  # scripts/retrain_model.py
220
  import mlflow
 
221
 
222
  def retrain_and_compare():
223
  """Entraîne un nouveau modèle et le compare à la production."""
 
239
  # 4. Si meilleur, promouvoir automatiquement
240
  if new_f1 > prod_f1:
241
  print("✅ Nouveau modèle meilleur ! Promotion en Staging...")
 
 
242
  else:
243
  print("⚠️ Nouveau modèle moins bon, conservation du modèle actuel")
244
  ```
 
253
 
254
  ---
255
 
256
+ ## 🎯 Utilisation du projet
257
 
258
+ ### Entraîner un modèle
259
+ ```bash
260
+ python ml_model/train_model.py
261
+ ```
 
262
 
263
+ ### Lancer MLflow UI
264
  ```bash
265
+ mlflow ui --backend-store-uri sqlite:///mlflow.db --port 5000
266
+ ```
267
+
268
+ ### Exemples disponibles
269
+ ```bash
270
+ # Trouver le meilleur modèle
271
+ python examples/01_find_best_model.py
272
+
273
+ # Comparer tous les runs
274
+ python examples/02_compare_models.py
275
+
276
+ # Gérer le Model Registry
277
+ python examples/03_model_registry.py
278
  ```
pyproject.toml CHANGED
@@ -1,19 +1,13 @@
1
  [tool.poetry]
2
  name = "oc-p5"
3
  version = "0.1.0"
4
- description = "Projet OpenClassRoom mise en API d'un modèle ML"
5
  authors = ["chaton59 <v.trouillez@gmail.com>"]
6
  readme = "README.md"
7
- packages = [{include = "src"}, {include = "ml_model"}]
8
 
9
  [tool.poetry.dependencies]
10
  python = "^3.12"
11
- fastapi = "^0.123.0"
12
- uvicorn = { extras = ["standard"], version = "^0.38.0" }
13
- sqlalchemy = "^2.0.0"
14
- pydantic = "^2.0.0"
15
- psycopg = "^3.2.0"
16
- black = "^25.12.0"
17
  mlflow = "^3.8.0"
18
  scikit-learn = "1.6.1"
19
  imbalanced-learn = "0.13.0"
@@ -22,7 +16,6 @@ scipy = "^1.14.0"
22
  numpy = "^2.0.0"
23
  pandas = "^2.2.0"
24
  joblib = "^1.4.0"
25
- huggingface-hub = "^0.26.0"
26
 
27
  [tool.poetry.group.dev.dependencies]
28
  pytest = "^9.0.0"
 
1
  [tool.poetry]
2
  name = "oc-p5"
3
  version = "0.1.0"
4
+ description = "Projet OpenClassRoom - Modèle ML de prédiction du turnover avec MLflow"
5
  authors = ["chaton59 <v.trouillez@gmail.com>"]
6
  readme = "README.md"
7
+ packages = [{include = "ml_model"}]
8
 
9
  [tool.poetry.dependencies]
10
  python = "^3.12"
 
 
 
 
 
 
11
  mlflow = "^3.8.0"
12
  scikit-learn = "1.6.1"
13
  imbalanced-learn = "0.13.0"
 
16
  numpy = "^2.0.0"
17
  pandas = "^2.2.0"
18
  joblib = "^1.4.0"
 
19
 
20
  [tool.poetry.group.dev.dependencies]
21
  pytest = "^9.0.0"
requirements.txt CHANGED
@@ -1,41 +1,10 @@
1
- annotated-doc==0.0.4 ; python_version >= "3.12"
2
- annotated-types==0.7.0 ; python_version >= "3.12"
3
- anyio==4.12.0 ; python_version >= "3.12"
4
- black==25.11.0 ; python_version >= "3.12"
5
- click==8.3.1 ; python_version >= "3.12"
6
- colorama==0.4.6 ; (platform_system == "Windows" or sys_platform == "win32") and python_version >= "3.12"
7
- coverage==7.12.0 ; python_version >= "3.12"
8
- fastapi==0.123.4 ; python_version >= "3.12"
9
- flake8==7.3.0 ; python_version >= "3.12"
10
- greenlet==3.2.4 ; python_version >= "3.12" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32")
11
- h11==0.16.0 ; python_version >= "3.12"
12
- httptools==0.7.1 ; python_version >= "3.12"
13
- idna==3.11 ; python_version >= "3.12"
14
- iniconfig==2.3.0 ; python_version >= "3.12"
15
- mccabe==0.7.0 ; python_version >= "3.12"
16
- mypy-extensions==1.1.0 ; python_version >= "3.12"
17
- packaging==25.0 ; python_version >= "3.12"
18
- pathspec==0.12.1 ; python_version >= "3.12"
19
- platformdirs==4.5.0 ; python_version >= "3.12"
20
- pluggy==1.6.0 ; python_version >= "3.12"
21
- pycodestyle==2.14.0 ; python_version >= "3.12"
22
- pydantic-core==2.41.5 ; python_version >= "3.12"
23
- pydantic==2.12.5 ; python_version >= "3.12"
24
- pyflakes==3.4.0 ; python_version >= "3.12"
25
- pygments==2.19.2 ; python_version >= "3.12"
26
- pytest-cov==7.0.0 ; python_version >= "3.12"
27
- pytest==9.0.1 ; python_version >= "3.12"
28
- python-dotenv==1.2.1 ; python_version >= "3.12"
29
- pytokens==0.3.0 ; python_version >= "3.12"
30
- pyyaml==6.0.3 ; python_version >= "3.12"
31
- sqlalchemy==2.0.44 ; python_version >= "3.12"
32
- starlette==0.50.0 ; python_version >= "3.12"
33
- typing-extensions==4.15.0 ; python_version >= "3.12"
34
- typing-inspection==0.4.2 ; python_version >= "3.12"
35
- uvicorn==0.38.0 ; python_version >= "3.12"
36
- uvloop==0.22.1 ; sys_platform != "win32" and sys_platform != "cygwin" and platform_python_implementation != "PyPy" and python_version >= "3.12"
37
- watchfiles==1.1.1 ; python_version >= "3.12"
38
- websockets==15.0.1 ; python_version >= "3.12"
39
  scikit-learn==1.6.1
40
  xgboost==2.1.4
41
  imbalanced-learn==0.13.0
@@ -44,4 +13,3 @@ numpy==2.0.2
44
  pandas==2.2.3
45
  joblib==1.4.2
46
  mlflow==3.8.0
47
- huggingface-hub==0.26.5
 
1
+ # Core dependencies
2
+ black==25.11.0
3
+ flake8==7.3.0
4
+ pytest==9.0.1
5
+ pytest-cov==7.0.0
6
+
7
+ # ML dependencies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  scikit-learn==1.6.1
9
  xgboost==2.1.4
10
  imbalanced-learn==0.13.0
 
13
  pandas==2.2.3
14
  joblib==1.4.2
15
  mlflow==3.8.0
 
tests/test_basic.py CHANGED
@@ -1,3 +1,31 @@
 
 
 
 
 
1
  def test_pipeline_placeholder():
2
- """Test basique pour CI/CD (POC étape 2 ; évolutif étape 5)."""
3
- assert True # Succès simple ; ajoute cas ML (Pydantic/validation) plus tard
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests basiques pour le pipeline ML."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
  def test_pipeline_placeholder():
7
+ """Test basique pour CI/CD."""
8
+ assert True
9
+
10
+
11
+ def test_data_files_exist():
12
+ """Vérifie que les fichiers de données existent."""
13
+ data_dir = Path("data")
14
+ assert (data_dir / "extrait_sondage.csv").exists()
15
+ assert (data_dir / "extrait_eval.csv").exists()
16
+ assert (data_dir / "extrait_sirh.csv").exists()
17
+
18
+
19
+ def test_preprocess_imports():
20
+ """Vérifie que les imports ML fonctionnent."""
21
+ from ml_model.preprocess import preprocess_data, load_raw_data
22
+
23
+ assert preprocess_data is not None
24
+ assert load_raw_data is not None
25
+
26
+
27
+ def test_train_imports():
28
+ """Vérifie que le module d'entraînement s'importe."""
29
+ from ml_model.train_model import train_model
30
+
31
+ assert train_model is not None