thamu _

Suivi d'experiences reproductible avec MLflow et UV

par Thamu Mnyulwa 7 min de lecture

La plupart des tutoriels MLflow commencent par pip install mlflow et se terminent par une capture de la Tracking UI.

C’est utile, mais cela evite souvent la partie qui casse la reproductibilite en premier : l’environnement. Un run logge indique les parametres et la metrique. Mais si tu ne peux pas reconstruire le jeu de dependances qui a produit ce run, il devient le souvenir d’une execution que tu ne peux plus relancer avec confiance.

Cet article associe MLflow a uv, un gestionnaire rapide de packages et de projets Python. L’objectif est un workflow ou un collegue peut cloner le repo, reconstruire rapidement le meme environnement verrouille, relancer le script d’entrainement et charger la meme version de modele enregistree.

Tout ce qui suit est ecrit pour MLflow 3.x. Les APIs de model logging et de registry ont change entre MLflow 2 et 3. Verifie donc ta version installee si un appel ne correspond pas aux exemples que tu connais.

Pourquoi uv fait partie du sujet

La reproductibilite a deux moities.

MLflow gere la moitie experimentale : ce qui a ete execute, les parametres utilises, les metriques produites et l’emplacement de l’artefact modele.

L’autre moitie est l’environnement qui determine le comportement reel du code. C’est cette partie que beaucoup d’articles sur l’experiment tracking ne traitent pas assez.

uv traite cette moitie avec un lockfile. Quand tu ajoutes des dependances avec uv, il ecrit uv.lock. Ce fichier contient le graphe de dependances resolu, pas seulement les packages que tu as nommes directement. Une personne qui lance uv sync avec ce lockfile obtient le jeu de dependances decrit par le lock.

C’est exactement la propriete que tu veux derriere chaque run MLflow : l’experience est loggee, et l’environnement qui a interprete le code est versionne a cote.

uv est aussi assez rapide pour que reconstruire un environnement ne soit plus une excuse pour reutiliser une vieille .venv. C’est important, car les environnements obsoletes sont une source silencieuse de drift.

Mettre en place le projet

Commence avec un projet uv propre. Cela cree pyproject.toml, une structure de projet, puis un lockfile lorsque les dependances sont ajoutees.

uv init mlflow-repro
cd mlflow-repro
uv add "mlflow>=3,<4" scikit-learn pandas

uv add resout le graphe de dependances, met a jour uv.lock et installe dans une .venv locale au projet. Committe pyproject.toml et uv.lock ensemble.

Le lockfile est le contrat. pyproject.toml dit ce dont le projet a besoin. uv.lock dit ce qui a ete resolu concretement.

Pour executer une commande dans l’environnement verrouille, prefixe-la avec uv run.

uv run python train.py

Pour la CI ou les reviews, je prefere la variante plus stricte :

uv run --locked python train.py

Elle echoue si le lockfile n’est pas a jour, au lieu de le modifier silencieusement pendant l’execution.

Si tu es sur macOS et que tu veux lancer une UI MLflow locale, note que le port par defaut 5000 peut entrer en conflit avec AirPlay Receiver. Utilise un autre port si besoin, par exemple 5001.

Utiliser un backend local compatible avec la registry

Beaucoup d’exemples plus anciens utilisent une tracking URI fichier comme file:./mlruns. Pour une petite demo de tracking uniquement, c’est acceptable. Pour un workflow plus fiable, la documentation actuelle de MLflow recommande un backend base de donnees, et la Model Registry a besoin de metadonnees de registry gerees proprement.

Pour un exemple local reproductible, SQLite est le choix pragmatique :

TRACKING_URI = "sqlite:///mlflow.db"

Les metadonnees restent dans une base locale, tandis que les artefacts modele restent sur le disque local. Dans un contexte d’equipe, la meme forme devient souvent un Tracking Server distant avec PostgreSQL, MySQL ou un service MLflow manage, plus un object storage pour les artefacts.

Le script d’entrainement

Voici un script complet et autonome. Il entraine un classifieur, logge les parametres et l’accuracy, attache un input example et une model signature, puis enregistre le modele sous un nom dans la registry.

Sauvegarde-le sous train.py.

import mlflow
import mlflow.sklearn
from mlflow.models import infer_signature
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

TRACKING_URI = "sqlite:///mlflow.db"

mlflow.set_tracking_uri(TRACKING_URI)
mlflow.set_registry_uri(TRACKING_URI)
mlflow.set_experiment("iris-repro")

X, y = load_iris(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y,
)

params = {
    "n_estimators": 200,
    "max_depth": 4,
    "random_state": 42,
}

with mlflow.start_run() as run:
    mlflow.log_params(params)

    model = RandomForestClassifier(**params)
    model.fit(X_train, y_train)

    accuracy = model.score(X_test, y_test)
    mlflow.log_metric("accuracy", accuracy)

    signature = infer_signature(X_train, model.predict(X_train))

    model_info = mlflow.sklearn.log_model(
        model,
        name="model",
        signature=signature,
        input_example=X_train.iloc[:2],
        registered_model_name="iris-rf",
    )

    print(f"accuracy: {accuracy:.4f}")
    print(f"run_id: {run.info.run_id}")
    print(f"model_uri: {model_info.model_uri}")
    print(f"registered_version: {model_info.registered_model_version}")

Execute-le dans l’environnement verrouille :

uv run --locked python train.py

Trois details meritent d’etre explicites.

D’abord, name="model" est le style MLflow 3 pour logger un modele. Si tu as du code MLflow 2 avec artifact_path="model", c’est ce parametre qui a change.

Ensuite, la signature n’est pas decorative. infer_signature inspecte les inputs d’entrainement et les predictions du modele pour enregistrer un schema d’entree et de sortie. Ce schema donne aux loaders et aux chemins de serving quelque chose de concret a valider.

Enfin, registered_model_name="iris-rf" promeut le modele logge dans la registry pendant la meme operation. Chaque execution reussie cree une nouvelle version de modele sous le meme nom de modele enregistre.

Recharger le modele de facon deterministe

Une registration n’est utile que si le chargement est sans ambiguite.

Avec le modele enregistre comme iris-rf, charge une version specifique par URI. Le point important est le /1 final : il pinne la version exacte du modele.

Sauvegarde ceci sous load_model.py.

import mlflow
import mlflow.sklearn
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

TRACKING_URI = "sqlite:///mlflow.db"

mlflow.set_tracking_uri(TRACKING_URI)
mlflow.set_registry_uri(TRACKING_URI)

X, y = load_iris(return_X_y=True, as_frame=True)
_, X_test, _, _ = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y,
)

model = mlflow.sklearn.load_model("models:/iris-rf/1")

predictions = model.predict(X_test.iloc[:3])
print(predictions.tolist())

Puis execute :

uv run --locked python load_model.py

Utilise des versions explicites lorsque l’objectif est la reproductibilite. Les URIs avec alias, comme models:/iris-rf@champion, sont utiles pour le deploiement parce qu’elles permettent de promouvoir un nouveau modele sans modifier le code applicatif. Mais les alias sont volontairement mobiles. Une URI de version pinnee est stable.

Inspecter le run

Tu n’as pas besoin de la UI pour confirmer ce qui a ete logge, meme si elle aide pour une inspection visuelle.

Demarre-la sur un port alternatif :

uv run mlflow ui --backend-store-uri sqlite:///mlflow.db --port 5001

Pour les rapports et les checks CI, utilise plutot l’API client :

import mlflow

client = mlflow.tracking.MlflowClient(tracking_uri="sqlite:///mlflow.db")
run = client.get_run("YOUR_RUN_ID")

print(run.data.params)
print(run.data.metrics)

C’est plus reproductible qu’une capture d’ecran. Tu peux faire des assertions directement sur les metadonnees du run.

A quoi ressemble la reproductibilite maintenant

Le workflow donne deux couches verrouillees qui se renforcent.

uv.lock fixe l’environnement. Un reviewer peut lancer uv sync --locked ou uv run --locked ... et savoir que le graphe de dependances ne change pas pendant la review.

MLflow fixe l’enregistrement experimental. Chaque run porte les parametres, les metriques, les artefacts modele et le schema. La registry donne a chaque modele une adresse stable et versionnee.

Ensemble, ils repondent a la question qui fragilise beaucoup de setups d’experiment tracking. Pas seulement : “qu’est-ce que ce run a produit ?” Mais : “est-ce que je peux reconstruire ce run depuis zero et charger la meme version de modele ?”

Avec le lockfile en version control a cote du code d’entrainement, et le modele pinne par version dans la registry, la reponse devient operationnelle plutot qu’aspirationnelle.

Une courte checklist

  • Committe pyproject.toml et uv.lock ensemble. Le lockfile rend l’environnement reproductible, pas pyproject.toml seul.
  • Utilise uv run --locked dans la CI et les reviews pour faire echouer rapidement le dependency drift.
  • Logge les parametres, les metriques, une model signature et un input example avec chaque modele.
  • Enregistre les modeles et recharge-les par version explicite pour la reproductibilite.
  • Reserve les alias de registry aux workflows de deploiement ou un pointeur mobile est utile.
  • Verifie l’API contre ta version majeure de MLflow, surtout lorsque tu migres du code MLflow 2 vers MLflow 3.

Sources

Share