MLflow desde Cero: Rastrea Experimentos y Modelos para Proyectos de ML Reproducibles en Python
Contexto del Problema
Como desarrolladores que inician o se consolidan en el mundo del Machine Learning (ML), rápidamente nos enfrentamos a un desafío común: la gestión del caos. Imagina que estás entrenando un modelo. Pruebas diferentes algoritmos, ajustas hiperparámetros, experimentas con distintas técnicas de preprocesamiento de datos. Cada intento genera un conjunto de resultados: métricas de rendimiento, el modelo entrenado, los parámetros utilizados, e incluso los datos de entrada. Sin una estrategia clara, este proceso se convierte en un laberinto de archivos con nombres confusos (modelo_final_v2_mejorado_este_si.pkl), hojas de cálculo manuales y la constante pregunta: "¿Qué versión de este modelo fue la que dio el mejor resultado? ¿Y con qué parámetros la entrené?".
Esta falta de trazabilidad y reproducibilidad no solo ralentiza el desarrollo, sino que también dificulta la colaboración en equipo, la depuración de errores y, en última instancia, la puesta en producción de modelos fiables. Necesitamos una forma sistemática de registrar cada "experimento" que realizamos, capturando todos los detalles relevantes de manera automática y organizada. Aquí es donde herramientas como MLflow se vuelven indispensables.
Conceptos Clave
MLflow es una plataforma de código abierto para gestionar el ciclo de vida completo del Machine Learning, incluyendo experimentación, reproducibilidad y despliegue. Aunque es una herramienta robusta con varios componentes, en este artículo nos centraremos en su módulo de Tracking, que es el corazón de la gestión de experimentos.
¿Qué es MLflow Tracking?
MLflow Tracking es una API y una interfaz de usuario (UI) para registrar y consultar información sobre tus experimentos de ML. Piensa en ello como un "cuaderno de laboratorio" digital y automatizado para tus modelos.
Componentes Principales de MLflow Tracking:
- Experimentos (Experiments): Un experimento es una colección de "runs" (ejecuciones). Puedes agrupar runs relacionados bajo un mismo experimento. Por ejemplo, todos los intentos de entrenar un modelo de clasificación para un problema específico podrían pertenecer al mismo experimento.
- Ejecuciones (Runs): Una ejecución corresponde a una única ejecución de tu código de ML. Cada run registra los parámetros de entrada, las métricas de salida, el código fuente y cualquier artefacto generado (como el modelo entrenado o gráficos).
- Parámetros (Parameters): Son los valores de entrada clave para tu código de ML. Esto incluye hiperparámetros del modelo (ej. tasa de aprendizaje, número de épocas), rutas a los datos, etc. Se registran como pares clave-valor.
- Métricas (Metrics): Son los valores numéricos que quieres evaluar para cada run. Esto puede ser la precisión, el F1-score, el error cuadrático medio (MSE), la pérdida (loss), etc. MLflow permite registrar métricas a lo largo del tiempo (útil para ver la evolución de la pérdida durante el entrenamiento).
- Artefactos (Artifacts): Son los archivos de salida de tu run. Esto incluye el modelo entrenado serializado, gráficos, imágenes, archivos de texto, o cualquier otro archivo que sea relevante para el experimento. MLflow los almacena y los asocia con el run específico.
El objetivo principal es que, al finalizar un run, tengas toda la información necesaria para entender qué se hizo, cómo se hizo y cuáles fueron los resultados, permitiendo la reproducibilidad y la comparación sencilla entre diferentes intentos.
Implementación Paso a Paso
Vamos a empezar con un ejemplo práctico. Entrenaremos un modelo de Regresión Logística con el famoso dataset Iris y usaremos MLflow para registrar todo el proceso.
Paso 1: Instalación de MLflow
Primero, asegúrate de tener MLflow y las librerías necesarias instaladas. Si no las tienes, puedes instalarlas con pip:
pip install mlflow scikit-learn pandas matplotlib
Paso 2: Configuración Básica y Primer Run
Para este ejemplo, MLflow guardará los datos de tracking localmente en un directorio llamado mlruns/. No necesitamos una configuración compleja para empezar.
Crea un archivo llamado train_iris.py y añade el siguiente código:
import mlflow
import mlflow.sklearn
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score
from sklearn.datasets import load_iris
import warnings
warnings.filterwarnings("ignore") # Para ignorar warnings de convergencia de sklearn
# 1. Cargar el dataset Iris
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target
# 2. Dividir los datos en conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 3. Definir parámetros del modelo
# Usaremos una variable de entorno para simular un parámetro configurable
# En un entorno real, esto podría venir de un archivo de configuración o CLI
alpha = float(os.environ.get("ALPHA", 0.1)) # Ejemplo de parámetro configurable
l1_ratio = float(os.environ.get("L1_RATIO", 0.5)) # Ejemplo de parámetro configurable
# 4. Iniciar un run de MLflow
# El "with" statement asegura que el run se cierre correctamente
with mlflow.start_run():
# Loguear parámetros
mlflow.log_param("solver", "saga")
mlflow.log_param("max_iter", 1000)
mlflow.log_param("alpha", alpha)
mlflow.log_param("l1_ratio", l1_ratio)
# 5. Entrenar el modelo
model = LogisticRegression(
solver="saga",
max_iter=1000,
penalty="elasticnet",
l1_ratio=l1_ratio,
C=alpha, # C es el inverso de la fuerza de regularización
random_state=42
)
model.fit(X_train, y_train)
# 6. Realizar predicciones y calcular métricas
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred, average='weighted')
# 7. Loguear métricas
mlflow.log_metric("accuracy", accuracy)
mlflow.log_metric("f1_score", f1)
# 8. Loguear el modelo
# Esto guarda el modelo y sus dependencias para poder cargarlo después
mlflow.sklearn.log_model(model, "iris_logistic_regression_model")
print(f"Run MLflow completado. ID del Run: {mlflow.active_run().info.run_id}")
print(f"Accuracy: {accuracy:.4f}")
print(f"F1-Score: {f1:.4f}")
print("Script finalizado.")
```
Paso 3: Ejecutar el Script y Ver la UI de MLflow
Para ejecutar el script, simplemente abre tu terminal en el directorio donde guardaste train_iris.py y ejecuta:
python train_iris.py
Verás una salida similar a:
Run MLflow completado. ID del Run: [algún_id_alfanumérico]
Accuracy: 1.0000
F1-Score: 1.0000
Script finalizado.
Ahora, para ver los resultados en la interfaz de usuario de MLflow, ejecuta en la misma terminal:
mlflow ui
Abre tu navegador y ve a http://localhost:5000. Deberías ver la interfaz de MLflow con tu primer experimento y run registrado. Podrás ver los parámetros, métricas y el modelo como un artefacto descargable.
Mini Proyecto / Aplicación Sencilla: Comparando Hiperparámetros
Para demostrar el verdadero poder de MLflow Tracking, vamos a modificar nuestro script para ejecutar múltiples runs, probando diferentes valores para el parámetro de regularización C (que hemos mapeado a alpha en nuestro código) y el l1_ratio de la Regresión Logística. Esto simulará una búsqueda de hiperparámetros.
Crea un nuevo archivo llamado hyperparameter_tuning.py:
import mlflow
import mlflow.sklearn
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score
from sklearn.datasets import load_iris
import warnings
import os
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore")
# Cargar el dataset Iris
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Definir el nombre del experimento
mlflow.set_experiment("Iris_Logistic_Regression_Tuning")
# Valores a probar para los hiperparámetros
alphas = [0.01, 0.1, 1.0, 10.0]
l1_ratios = [0.0, 0.25, 0.5, 0.75, 1.0] # 0.0 es L2, 1.0 es L1
results = []
for alpha in alphas:
for l1_ratio in l1_ratios:
with mlflow.start_run():
# Loguear parámetros
mlflow.log_param("solver", "saga")
mlflow.log_param("max_iter", 1000)
mlflow.log_param("C_alpha", alpha)
mlflow.log_param("l1_ratio", l1_ratio)
# Entrenar el modelo
# Cuidado: l1_ratio solo es aplicable con penalty='elasticnet'
# Para l1_ratio=0.0 (L2) o l1_ratio=1.0 (L1), penalty debe ser 'l2' o 'l1' respectivamente
# Aquí simplificamos usando elasticnet y ajustando l1_ratio
if l1_ratio == 0.0: # L2 regularization
penalty_type = "l2"
current_l1_ratio = None # l1_ratio no se usa con penalty='l2'
elif l1_ratio == 1.0: # L1 regularization
penalty_type = "l1"
current_l1_ratio = None # l1_ratio no se usa con penalty='l1'
else: # Elastic Net regularization
penalty_type = "elasticnet"
current_l1_ratio = l1_ratio
model = LogisticRegression(
solver="saga",
max_iter=1000,
penalty=penalty_type,
l1_ratio=current_l1_ratio,
C=alpha,
random_state=42
)
model.fit(X_train, y_train)
# Realizar predicciones y calcular métricas
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred, average='weighted')
# Loguear métricas
mlflow.log_metric("accuracy", accuracy)
mlflow.log_metric("f1_score", f1)
# Loguear el modelo
mlflow.sklearn.log_model(model, "iris_logistic_regression_model")
# Guardar un gráfico como artefacto
fig, ax = plt.subplots()
ax.bar(iris.feature_names, model.coef_)
ax.set_title(f"Coeficientes del modelo (C={alpha}, L1_ratio={l1_ratio})")
plt.tight_layout()
plt.savefig("feature_coefficients.png")
mlflow.log_artifact("feature_coefficients.png")
plt.close(fig) # Cerrar la figura para liberar memoria
os.remove("feature_coefficients.png") # Limpiar el archivo local
print(f"Run completado: C={alpha}, L1_ratio={l1_ratio}, Accuracy={accuracy:.4f}, F1={f1:.4f}")
results.append({"C": alpha, "l1_ratio": l1_ratio, "accuracy": accuracy, "f1_score": f1, "run_id": mlflow.active_run().info.run_id})
print("Todos los runs de ajuste de hiperparámetros han finalizado.")
# Opcional: Imprimir los mejores resultados
best_run = max(results, key=lambda x: x['f1_score'])
print(f"\nMejor F1-Score: {best_run['f1_score']:.4f} con C={best_run['C']} y L1_ratio={best_run['l1_ratio']} (Run ID: {best_run['run_id']})")
```
Ejecuta este script:
python hyperparameter_tuning.py
Mientras se ejecuta, puedes mantener mlflow ui abierto en tu navegador. Verás cómo se van añadiendo nuevos runs al experimento "Iris_Logistic_Regression_Tuning". Una vez que todos los runs hayan terminado, podrás:
- Seleccionar múltiples runs y compararlos lado a lado.
- Ordenar los runs por métricas (ej. F1-Score) para encontrar el mejor modelo.
- Descargar los modelos entrenados o los gráficos de coeficientes de cada run.
Esto te permite visualizar rápidamente qué combinación de hiperparámetros produjo los mejores resultados, sin tener que rastrear manualmente cada intento.
Cargar un Modelo Logueado para Inferencia
Una de las grandes ventajas es que puedes cargar un modelo directamente desde MLflow para usarlo en inferencia, sin preocuparte por las dependencias o la serialización.
Crea un archivo predict_model.py:
import mlflow
import pandas as pd
from sklearn.datasets import load_iris
# ID del run del modelo que quieres cargar (reemplaza con uno de tus runs)
# Puedes obtener este ID de la UI de MLflow o de la salida de tu script
RUN_ID = "[REEMPLAZA_CON_EL_ID_DEL_MEJOR_RUN]"
# Ruta al artefacto del modelo dentro del run
# Por defecto, mlflow.sklearn.log_model guarda el modelo en "model"
MODEL_PATH = f"runs:/{RUN_ID}/iris_logistic_regression_model"
print(f"Cargando modelo desde: {MODEL_PATH}")
try:
# Cargar el modelo usando la sintaxis de URI de MLflow
loaded_model = mlflow.sklearn.load_model(MODEL_PATH)
print("Modelo cargado exitosamente.")
# Preparar nuevos datos para inferencia (usaremos una muestra del dataset Iris)
iris = load_iris()
new_data = pd.DataFrame(iris.data[:5], columns=iris.feature_names)
print("\nNuevos datos para predicción:")
print(new_data)
# Realizar predicciones
predictions = loaded_model.predict(new_data)
probabilities = loaded_model.predict_proba(new_data)
print("\nPredicciones:")
print(predictions)
print("\nProbabilidades:")
print(probabilities)
except Exception as e:
print(f"Error al cargar o usar el modelo: {e}")
print("Asegúrate de que el RUN_ID sea correcto y que el modelo exista en esa ruta.")
```
Importante: Reemplaza [REEMPLAZA_CON_EL_ID_DEL_MEJOR_RUN] con el ID de un run real de tu experimento (puedes copiarlo de la UI de MLflow).
Ejecuta el script:
python predict_model.py
Esto demuestra cómo MLflow no solo te ayuda a rastrear, sino también a reutilizar tus modelos de manera sencilla y reproducible.
Errores Comunes y Depuración
-
"No active run" error: Si intentas loguear parámetros o métricas fuera de un bloque
with mlflow.start_run():o sin haber llamado amlflow.start_run()explícitamente, MLflow no sabrá a qué run asociar la información. Siempre asegúrate de que haya un run activo.# Incorrecto # mlflow.log_param("param", 1) # Correcto with mlflow.start_run(): mlflow.log_param("param", 1) -
Problemas con el Tracking URI: Por defecto, MLflow usa
./mlruns. Si quieres usar una ubicación diferente (por ejemplo, una base de datos remota o un servidor de MLflow), debes configurarlo conmlflow.set_tracking_uri("tu_uri_aqui"). Si no puedes ver tus runs, verifica que el URI sea el correcto y que el servidor de tracking esté activo si es remoto.# Ejemplo para un servidor remoto # os.environ["MLFLOW_TRACKING_URI"] = "http://localhost:5000" # mlflow.set_tracking_uri("http://localhost:5000") -
Dependencias del modelo: Cuando logueas un modelo con
mlflow.sklearn.log_model()(o sus equivalentes para otras librerías), MLflow intenta inferir y guardar las dependencias del entorno. Sin embargo, a veces puede haber problemas si el entorno de carga no tiene las mismas versiones de librerías. Asegúrate de que tu entorno de inferencia sea lo más parecido posible al de entrenamiento, o considera usar MLflow Projects para empaquetar tu código y entorno. -
Archivos de artefactos no encontrados: Si logueas un artefacto con
mlflow.log_artifact("mi_archivo.png"), el archivo debe existir en la ruta especificada en el momento de la llamada. Si el archivo se elimina antes de que MLflow lo copie, o si la ruta es incorrecta, el artefacto no se guardará. Recuerda que MLflow copia el archivo, no lo mueve. -
Confusión entre Experiments y Runs: Recuerda que un experimento es una colección de runs. Puedes establecer el experimento actual con
mlflow.set_experiment("NombreDeMiExperimento"). Si no lo haces, MLflow usará un experimento por defecto llamado "Default". Organizar tus runs en experimentos lógicos te ayudará mucho a navegar la UI.
Aprendizaje Futuro / Próximos Pasos
MLflow Tracking es solo la punta del iceberg. Aquí hay algunas áreas para explorar a medida que te sientas más cómodo:
-
MLflow Projects: Aprende a empaquetar tu código de ML en un formato reproducible usando MLflow Projects. Esto te permite especificar dependencias, puntos de entrada y ejecutar tu código en diferentes entornos con un solo comando (
mlflow run). -
MLflow Models: Explora cómo MLflow estandariza el formato de los modelos para que puedan ser desplegados en diversas plataformas (Docker, Azure ML, SageMaker, etc.) con herramientas integradas.
-
MLflow Model Registry: Una característica crucial para la gestión del ciclo de vida del modelo. Te permite gestionar versiones de modelos, transicionar modelos entre etapas (Staging, Production), y anotar modelos con descripciones y tags.
-
Tracking Remoto: Configura MLflow para usar una base de datos (como PostgreSQL) y un almacenamiento de artefactos (como S3 o Azure Blob Storage) para centralizar el tracking de experimentos en un equipo o en la nube. Esto es fundamental para entornos de producción.
-
Integración con otras herramientas: MLflow se integra bien con librerías populares como Keras, PyTorch, XGBoost, y plataformas de orquestación como Apache Airflow o Kubeflow.
Dominar MLflow te proporcionará una base sólida para construir flujos de trabajo de ML más robustos, reproducibles y colaborativos, llevándote un paso más cerca de las buenas prácticas de MLOps.