Implementación del proyecto completo

Lectura
45 min~11 min lectura

Implementación del Proyecto Completo: Pipeline ML de Extremo a Extremo

En esta lección culminante, integraremos todos los conocimientos adquiridos para construir, contenerizar y desplegar un pipeline completo de Machine Learning. Pasaremos de un script de entrenamiento en un entorno local a un servicio robusto, escalable y gestionado mediante Docker y Kubernetes. Este proyecto simulará un escenario realista: un servicio de predicción de precios de viviendas basado en características clave. Abordaremos la estructura del proyecto, la creación de imágenes Docker para cada componente (entrenamiento y servicio API), la orquestación con Kubernetes y la configuración para un flujo de trabajo integrado. El objetivo es que comprendas la arquitectura y las decisiones necesarias para llevar un modelo del concepto a la producción.

Asumiremos que tienes un conocimiento práctico de Python, scikit-learn, Docker (Dockerfile, comandos básicos) y los conceptos fundamentales de Kubernetes (Pods, Deployments, Services). Configuraremos un entorno que separe claramente las fases de entrenamiento y servicio, una práctica esencial para mantener la modularidad y la capacidad de mantenimiento en sistemas de ML. La lección proporcionará todo el código necesario, el cual está diseñado para ser funcional y puede ser adaptado a tus propios modelos y datos.

Arquitectura del Proyecto y Estructura de Directorios

Una arquitectura clara es la base de cualquier proyecto de software exitoso, y en MLOps esto es aún más crítico. Definiremos una estructura que segregue responsabilidades, permita el versionado de modelos y datos, y facilite la contenerización. Nuestro proyecto tendrá dos componentes principales: un módulo de entrenamiento y un módulo de servicio/inferencia. El módulo de entrenamiento es un proceso por lotes (batch) que lee datos, entrena un modelo y lo persiste en un almacenamiento. El módulo de servicio es una API REST (usaremos Flask) que carga el modelo persistido y responde a solicitudes de predicción.

La estructura de directorios propuesta es la siguiente. Notarás carpetas para el código fuente (src/), para los artefactos generados como modelos y preprocesadores (models/), para los archivos de configuración de Docker y Kubernetes (docker/, k8s/), y para los datos. Esta organización no solo es limpia, sino que también se mapea directamente a las capas dentro de nuestras imágenes Docker, optimizando el uso de la cache de construcción.


ml-pipeline-end-to-end/
├── data/
│   └── housing.csv
├── src/
│   ├── train.py
│   ├── predict.py
│   └── model_utils.py
├── models/
│   └── (aquí se guardará model.pkl y preprocessor.pkl)
├── docker/
│   ├── Dockerfile.train
│   └── Dockerfile.api
├── k8s/
│   ├── training-job.yaml
│   ├── api-deployment.yaml
│   └── api-service.yaml
└── requirements.txt

El archivo requirements.txt contendrá las dependencias comunes para ambos componentes (scikit-learn, pandas, numpy, Flask). Los Dockerfiles específicos instalarán estas dependencias y copiarán el código necesario. Los manifiestos de Kubernetes definirán cómo se ejecuta cada componente en el clúster: el entrenamiento como un Job y la API como un Deployment expuesto por un Service.

Concepto Clave: Separación de Entrenamiento e Inferencia

Imagina una fábrica de automóviles. Tienes una línea de ensamblaje y fabricación (entrenamiento) donde, usando planos y materiales (datos y algoritmos), construyes un motor perfectamente calibrado (el modelo entrenado). Esta línea opera de manera periódica o cuando llegan nuevos materiales. Por otro lado, tienes un showroom (inferencia/servicio) donde los clientes llegan con sus especificaciones (datos de entrada) y reciben una recomendación del auto que mejor se adapta (la predicción). El showroom no sabe cómo se construyó el motor; solo sabe cómo usarlo. Incluso puedes cambiar el motor por uno nuevo (actualizar el modelo) sin detener el showroom.

Esta separación es fundamental en MLOps. El entrenamiento es un proceso con estado, intensivo en recursos computacionales, que genera artefactos (el modelo). La inferencia es un proceso sin estado, optimizado para baja latencia y alta disponibilidad, que consume esos artefactos. Al contenerizarlos por separado, logramos: 1) Independencia de escalado: puedes escalar la API a decenas de réplicas sin afectar el entrenamiento, y viceversa. 2) Actualizaciones independientes: puedes mejorar la lógica de la API sin retrenar el modelo, y desplegar un nuevo modelo sin tocar el código de la API. 3) Gestión de recursos diferenciada: el Job de entrenamiento puede solicitar muchos CPUs/GPUs y memoria, mientras que los Pods de la API pueden ser más livianos.

Cómo Funciona en la Práctica: Flujo Paso a Paso

El flujo operativo completo, desde el código hasta un servicio en ejecución, sigue una secuencia lógica. Primero, desarrollamos y probamos localmente los scripts de Python (train.py y predict.py). Luego, construimos las imágenes Docker para cada uno. Para el entrenamiento, la imagen se ejecutará como un Job de Kubernetes que, al completarse, guardará el modelo en un volumen persistente. Finalmente, desplegamos la API, que monta ese mismo volumen para acceder al modelo, y la exponemos al mundo exterior.

Paso 1: Desarrollo Local. Escribimos el código de entrenamiento que carga datos, realiza ingeniería de características (usando un ColumnTransformer), entrena un modelo de regresión (RandomForest) y guarda ambos objetos (preprocesador y modelo) en archivos .pkl. El código de la API crea un endpoint `/predict` que carga estos archivos, aplica la transformación a los datos de entrada y devuelve la predicción. Paso 2: Construcción de Imágenes. Usamos `docker build` con los Dockerfiles correspondientes para crear `ml-pipeline-train:latest` y `ml-pipeline-api:latest`. Paso 3: Despliegue en Kubernetes. Aplicamos los manifiestos YAML en orden: primero el PersistentVolumeClaim para el almacenamiento del modelo, luego el Job de entrenamiento que lo usa, y finalmente el Deployment y Service para la API. Kubernetes se encarga de orquestar la ejecución, garantizando que la API solo intente cargar el modelo una vez que el Job haya terminado exitosamente (esto puede gestionarse con initContainers o comprobaciones de readiness).

Código en Acción: Scripts Esenciales y Configuraciones

A continuación, se presentan los archivos de código centrales. Comenzamos con el script de entrenamiento. Este es un ejemplo completo y funcional que simula un problema de regresión.


# src/train.py
import pandas as pd
import joblib
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_absolute_error

def train():
    # 1. Cargar datos (simulados para el ejemplo)
    data = pd.read_csv('/data/housing.csv')
    X = data.drop('price', axis=1)
    y = data['price']

    # 2. Definir preprocesador
    numeric_features = ['sqft', 'bedrooms']
    categorical_features = ['neighborhood']

    preprocessor = ColumnTransformer(
        transformers=[
            ('num', StandardScaler(), numeric_features),
            ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features)
        ])

    # 3. Definir pipeline con modelo
    model = Pipeline(steps=[
        ('preprocessor', preprocessor),
        ('regressor', RandomForestRegressor(n_estimators=100, random_state=42))
    ])

    # 4. Entrenar
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    model.fit(X_train, y_train)

    # 5. Evaluar
    predictions = model.predict(X_test)
    mae = mean_absolute_error(y_test, predictions)
    print(f"Modelo entrenado. MAE en test: {mae}")

    # 6. Persistir el modelo completo (pipeline)
    joblib.dump(model, '/models/model.pkl')
    print("Modelo guardado en /models/model.pkl")

if __name__ == '__main__':
    train()

Ahora, el archivo de la API. Este script utiliza Flask para crear un servicio web sencillo.


# src/predict.py
from flask import Flask, request, jsonify
import joblib
import pandas as pd

app = Flask(__name__)

# Cargar el modelo al iniciar la app
MODEL_PATH = '/models/model.pkl'
model = None

def load_model():
    global model
    model = joblib.load(MODEL_PATH)
    print("Modelo cargado desde", MODEL_PATH)

@app.before_first_request
def before_first_request():
    load_model()

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'healthy'}), 200

@app.route('/predict', methods=['POST'])
def predict():
    try:
        # Obtener datos JSON
        input_data = request.get_json()
        # Convertir a DataFrame (asumiendo estructura conocida)
        df = pd.DataFrame([input_data])
        # Realizar predicción
        prediction = model.predict(df)[0]
        return jsonify({'predicted_price': prediction})
    except Exception as e:
        return jsonify({'error': str(e)}), 400

if __name__ == '__main__':
    # Nota: En producción, usa un WSGI server como Gunicorn
    load_model()  # Carga también para ejecución directa
    app.run(host='0.0.0.0', port=5000)

Finalmente, un ejemplo del Dockerfile para la API. El Dockerfile para el entrenamiento sería similar, cambiando el punto de entrada.


# docker/Dockerfile.api
FROM python:3.9-slim

WORKDIR /app

# Copiar requirements e instalar dependencias
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copiar el código de la aplicación
COPY src/ ./src/

# Exponer puerto y definir comando
EXPOSE 5000
CMD ["python", "src/predict.py"]

Errores Comunes y Cómo Evitarlos

Al implementar este pipeline, varios obstáculos suelen aparecer. Reconocerlos de antemano te ahorrará horas de depuración.

1. Volúmenes no compartidos entre Job y Deployment: El Job de entrenamiento escribe el modelo en un volumen, pero el Pod de la API no puede leerlo porque monta un volumen diferente o una subruta incorrecta. Solución: Asegúrate de que ambos recursos de Kubernetes (Job y Deployment) referencien el mismo PersistentVolumeClaim (PVC) y monten el mismo mountPath (e.g., /models). Verifica los permisos de escritura/lectura del PVC.

2. La API inicia antes de que el modelo exista: Kubernetes despliega la API inmediatamente, pero el Job de entrenamiento puede tardar minutos. La API falla al intentar cargar un archivo inexistente. Solución: Implementa un initContainer en el Deployment de la API que espere a que el archivo model.pkl esté presente en el volumen. También puedes agregar lógica de reintento (retry) en el código de carga del modelo dentro de la aplicación.

3. Imágenes Docker demasiado grandes: Usar imágenes base como python:3.9 sin optimización resulta en imágenes de más de 1GB, lo que ralentiza el despliegue y transferencia. Solución: Utiliza imágenes base más ligeras (python:3.9-slim o python:3.9-alpine), realiza una instalación en múltiples etapas (multi-stage builds) si es necesario, y limpia la cache de pip en la misma capa RUN.

4. Configuración hardcodeada en el código: Rutas de archivo (/models/model.pkl), puertos, o parámetros de conexión están escritos directamente en los scripts. Esto hace que el código no sea portable entre entornos (local, staging, producción). Solución: Utiliza variables de entorno. Lee configuraciones desde os.environ o desde archivos de configuración que puedan ser inyectados vía ConfigMaps y Secrets de Kubernetes. Por ejemplo, MODEL_PATH = os.getenv('MODEL_PATH', '/models/model.pkl').

5. Falta de monitoreo y logs: Una vez desplegado, no sabes si las predicciones son correctas, cuál es la latencia, o si el Job de entrenamiento falló silenciosamente. Solución: Estructura los logs de tu aplicación (usando la librería logging de Python) y asegúrate de que salgan a stdout/stderr para que los capturen los recolectores de logs de Kubernetes (como Fluentd). Implementa endpoints de salud (/health) y métricas (usando Prometheus client) en tu API. Considera integrar un dashboard como Grafana.

Tip Crítico: Nunca subas datos sensibles o modelos grandes a tu repositorio de código. Usa un bucket de objetos (como AWS S3 o Google Cloud Storage) para los datos de entrenamiento y los artefactos del modelo. Tu Job de entrenamiento puede descargarlos al inicio, y tu API puede cargar el modelo desde allí (quizás a través de un volumen CSI). Esto mantiene tus imágenes ligeras y seguras.

Checklist de Dominio

Antes de considerar esta lección completamente integrada, verifica que puedes realizar o comprender cada uno de los siguientes puntos:

  • Puedo explicar la diferencia arquitectural entre un componente de entrenamiento (batch/Job) y uno de inferencia (servicio/Deployment) y justificar su separación.
  • He construido y ejecutado localmente al menos dos imágenes Docker distintas: una para el entrenamiento de un modelo y otra para servir predicciones vía API.
  • Puedo escribir un manifiesto de Kubernetes para un Job que complete una tarea (como entrenar un modelo) y guarde su salida en un volumen persistente.
  • Puedo escribir un manifiesto de Kubernetes para un Deployment que exponga una API, monte el volumen persistente con el modelo, y sea accesible a través de un Service (tipo ClusterIP o LoadBalancer).
  • Comprendo y puedo implementar una estrategia para manejar la dependencia de orden: asegurar que la API no se inicie antes de que el modelo esté disponible (usando initContainers, readinessProbes, o lógica en la aplicación).
  • Sé cómo depurar problemas comunes: verificar logs de Pods (kubectl logs), describir recursos para ver eventos (kubectl describe), y acceder a un Pod en ejecución para inspeccionar archivos (kubectl exec).
  • Puedo configurar variables de entorno en mis Deployments de Kubernetes para hacer mi aplicación configurable (por ejemplo, la ruta del modelo o el nivel de log).
  • Reconozco al menos tres buenas prácticas para optimizar imágenes Docker en el contexto de aplicaciones de ML (uso de imágenes slim, limpieza de cache, multi-stage builds para compilaciones nativas).

La implementación de un pipeline de ML de extremo a extremo es un hito significativo. Has pasado de escribir scripts monolíticos a diseñar un sistema distribuido, resiliente y preparado para escalar. Los conceptos y patrones vistos aquí—separación de responsabilidades, contenerización, orquestación, y gestión de estado—son directamente transferibles a proyectos más complejos que involucren pipelines de feature engineering, experimentación con múltiples modelos, y despliegues canary. Recuerda que la infraestructura es tan importante como el modelo mismo; un gran modelo es inútil si no puede ser desplegado de manera confiable. Continúa iterando, automatizando (con CI/CD) y monitoreando para construir sistemas de ML verdaderamente robustos.

De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión