thamu _

Reproduzierbares Experiment-Tracking mit MLflow und UV

von Thamu Mnyulwa 7 Min. Lesezeit

Die meisten MLflow-Tutorials beginnen mit pip install mlflow und enden mit einem Screenshot der Tracking UI.

Das ist hilfreich, aber es ueberspringt den Teil, der Reproduzierbarkeit meistens zuerst bricht: das Environment. Ein geloggter Run sagt dir, welche Parameter und welche Metrik verwendet wurden. Wenn du aber den Dependency-Stand nicht wiederherstellen kannst, der diesen Run erzeugt hat, ist der Run nur noch ein Protokoll von etwas, das du nicht mehr verlaesslich ausfuehren kannst.

Dieser Artikel kombiniert MLflow mit uv, einem schnellen Python Package- und Project-Manager. Ziel ist ein Workflow, bei dem ein Kollege das Repository klonen, das gleiche gelockte Environment schnell aufloesen, das Training erneut ausfuehren und dieselbe registrierte Modellversion laden kann.

Alles hier ist fuer MLflow 3.x geschrieben. Die Model-Logging- und Registry-APIs haben sich zwischen MLflow 2 und 3 geaendert. Pruefe also deine installierte Version, falls ein Aufruf anders aussieht als in den Dokumentationen, die du im Kopf hast.

Warum uv in diese Diskussion gehoert

Reproduzierbarkeit hat zwei Haelften.

MLflow kuemmert sich um die Experiment-Haelfte: was ausgefuehrt wurde, welche Parameter verwendet wurden, welche Metriken entstanden sind und wo das Modellartefakt liegt.

Die Environment-Haelfte bestimmt aber, wie dein Code sich tatsaechlich verhaelt. Genau diese Haelfte wird in vielen Tracking-Artikeln zu wenig behandelt.

uv schliesst diese Luecke mit einem Lockfile. Wenn du Dependencies mit uv hinzufuegst, schreibt es uv.lock. Darin steht der aufgeloeste Dependency Graph, nicht nur die Pakete, die du direkt genannt hast. Wer uv sync gegen dieses Lockfile ausfuehrt, bekommt den Dependency-Stand, den das Lockfile beschreibt.

Das ist die Eigenschaft, die hinter jedem MLflow-Run stehen sollte: Das Experiment ist geloggt, und das Environment, das den Code interpretiert hat, liegt versioniert daneben.

uv ist ausserdem schnell genug, dass ein frischer Environment-Sync kein Grund mehr ist, ein altes virtuelles Environment weiterzuverwenden. Das ist wichtig, weil veraltete Environments eine stille Quelle von Drift sind.

Projekt aufsetzen

Starte mit einem frischen uv-Projekt. Dadurch entstehen pyproject.toml, eine Projektstruktur und ein Lockfile, sobald Dependencies hinzugefuegt werden.

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

uv add loest den Dependency Graph auf, aktualisiert uv.lock und installiert in eine projektlokale .venv. Committe sowohl pyproject.toml als auch uv.lock.

Das Lockfile ist der Vertrag. pyproject.toml sagt, was dein Projekt braucht. uv.lock sagt, was konkret aufgeloest wurde.

Um Befehle im gelockten Environment auszufuehren, nutze uv run.

uv run python train.py

Fuer CI- oder Review-Workflows bevorzuge ich die strengere Variante:

uv run --locked python train.py

Damit schlaegt der Lauf fehl, wenn das Lockfile nicht aktuell ist, statt es waehrend des Laufs still zu aktualisieren.

Wenn du macOS nutzt und spaeter eine lokale MLflow UI starten willst, beachte, dass der Standard-Port 5000 mit AirPlay Receiver kollidieren kann. Nutze bei Bedarf einen anderen Port, zum Beispiel 5001.

Ein registry-freundliches lokales Backend

Viele aeltere Beispiele nutzen eine File-Tracking-URI wie file:./mlruns. Fuer ein kleines Tracking-only-Demo ist das in Ordnung. Fuer verlaesslichere Workflows empfiehlt die aktuelle MLflow-Dokumentation aber ein datenbankgestuetztes Backend, und die Model Registry braucht Registry-Metadaten, die sauber verwaltet werden koennen.

Fuer ein lokales Reproduzierbarkeitsbeispiel ist SQLite pragmatisch:

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

Damit liegen die Metadaten in einer lokalen Datenbank, waehrend Modellartefakte weiter lokal gespeichert werden. In einem Team-Setup wird daraus typischerweise ein Remote Tracking Server mit PostgreSQL, MySQL oder einem Managed MLflow Service, plus Object Storage fuer Artefakte.

Das Trainingsskript

Hier ist ein vollstaendiges, selbststaendiges Trainingsskript. Es trainiert einen Klassifikator, loggt Parameter und Accuracy, speichert Input Example und Model Signature und registriert das Modell unter einem Namen in der Registry.

Speichere es als 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}")

Fuehre es im gelockten Environment aus:

uv run --locked python train.py

Drei Details sind wichtig.

Erstens ist name="model" der MLflow-3-Stil fuer Model Logging. Wenn du MLflow-2-Code kennst, der artifact_path="model" uebergibt, ist genau dieser Parameter verschoben worden.

Zweitens ist die Signature keine Dekoration. infer_signature betrachtet die Trainingsinputs und die Modellvorhersagen, um ein Input- und Output-Schema zu speichern. Dieses Schema gibt spaeteren Loadern und Serving-Pfaden etwas Konkretes, gegen das sie validieren koennen.

Drittens hebt registered_model_name="iris-rf" das geloggte Modell direkt in die Registry. Jeder erfolgreiche Lauf erzeugt eine neue Modellversion unter demselben registrierten Modellnamen.

Das Modell deterministisch laden

Registrierung ist nur dann hilfreich, wenn das Laden eindeutig ist.

Wenn das Modell als iris-rf registriert ist, lade eine spezifische Version per URI. Entscheidend ist das finale /1: Es pinnt die exakte Modellversion.

Speichere das als 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())

Dann ausfuehren:

uv run --locked python load_model.py

Nutze explizite Versionsnummern, wenn Reproduzierbarkeit das Ziel ist. Alias-URIs wie models:/iris-rf@champion sind fuer Deployments hilfreich, weil du ein neues Modell promoten kannst, ohne Application Code zu aendern. Aber Aliases sind absichtlich beweglich. Eine gepinnte Versions-URI ist stabil.

Den Run inspizieren

Du brauchst die UI nicht, um zu bestaetigen, was geloggt wurde. Die UI ist trotzdem hilfreich, wenn du Runs visuell inspizieren willst.

Starte sie auf einem alternativen Port:

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

Fuer Reports und CI-Checks ist die Client API besser:

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)

Das ist reproduzierbarer als Screenshots. Du kannst direkt gegen die Run-Metadaten pruefen.

Wie Reproduzierbarkeit jetzt aussieht

Der Workflow gibt dir zwei gelockte Ebenen, die sich gegenseitig verstaerken.

uv.lock fixiert das Environment. Ein Reviewer kann uv sync --locked oder uv run --locked ... ausfuehren und weiss, dass der Dependency Graph nicht waehrend des Reviews veraendert wird.

MLflow fixiert den Experiment Record. Jeder Run traegt Parameter, Metriken, Modellartefakte und Schema. Die Registry gibt jedem Modell eine stabile versionierte Adresse.

Zusammen beantworten sie die Frage, die viele Tracking-Setups untergraebt. Nicht nur: “Was hat dieser Run erzeugt?” Sondern: “Kann ich diesen Run von Null wieder aufsetzen und dieselbe Modellversion laden?”

Mit dem Lockfile in Version Control neben dem Trainingscode und dem Modell als gepinnte Registry-Version wird die Antwort operativ statt nur wuenschenswert.

Kurze Checkliste

  • Committe pyproject.toml und uv.lock zusammen. Das Lockfile macht das Environment reproduzierbar, nicht pyproject.toml allein.
  • Nutze uv run --locked in CI- und Review-Workflows, damit Dependency Drift frueh fehlschlaegt.
  • Logge Parameter, Metriken, Model Signature und Input Example mit jedem Modell.
  • Registriere Modelle und lade sie fuer Reproduzierbarkeit ueber explizite Versionen.
  • Reserviere Registry-Aliases fuer Deployment-Workflows, bei denen ein beweglicher Pointer sinnvoll ist.
  • Pruefe die API gegen deine installierte MLflow-Major-Version, besonders wenn du Code von MLflow 2 nach MLflow 3 verschiebst.

Quellen

Share