Diseñar un Pipeline de ML con Containers

Lectura
20 min~6 min lectura

Concepto clave

Un pipeline de ML con containers es una secuencia automatizada de pasos de machine learning, donde cada paso se ejecuta dentro de un contenedor Docker independiente y se orquesta mediante Kubernetes. Imagina una cadena de montaje en una fabrica: cada estacion (container) realiza una tarea especifica (como limpieza de datos, entrenamiento o evaluacion) y pasa el resultado a la siguiente. La diferencia clave es que cada estacion es completamente portatil y aislada, garantizando que el pipeline funcione igual en tu laptop, en un servidor de pruebas o en produccion.

La ventaja principal es la reproducibilidad. En ML tradicional, los cientificos de datos suelen enfrentarse al problema de "funciona en mi maquina": un modelo entrenado localmente falla al desplegarse porque las dependencias (librerias, versiones de Python) difieren. Con containers, encapsulas todo el entorno necesario para cada paso, eliminando estas discrepancias. Ademas, Kubernetes permite escalar cada paso segun la carga: si tienes que procesar millones de datos de entrada, puedes ejecutar multiples instancias del contenedor de limpieza en paralelo, algo imposible con scripts monolíticos.

Como funciona en la practica

Vamos a disenar un pipeline simple para clasificar imagenes, con tres pasos: preprocesamiento, entrenamiento y evaluacion. En la practica, lo estructurarias asi:

  1. Paso 1: Preprocesamiento - Un contenedor Docker que lee imagenes en bruto desde un almacenamiento (como un bucket S3 o un volumen de Kubernetes), las redimensiona y normaliza, y guarda los datos procesados en otro lugar.
  2. Paso 2: Entrenamiento - Otro contenedor que toma los datos procesados, entrena un modelo de CNN (red neuronal convolucional) con TensorFlow o PyTorch, y guarda el modelo entrenado en un registro.
  3. Paso 3: Evaluacion - Un tercer contenedor que carga el modelo, prueba con un conjunto de validacion y genera metricas (precision, recall).

Kubernetes orquesta esto mediante Jobs o Argo Workflows. Cada paso se define como un Job de Kubernetes, que crea un Pod (instancia del contenedor) para ejecutarlo. Los Jobs se configuran para ejecutarse en secuencia: el paso 2 solo inicia cuando el paso 1 termina exitosamente, pasando datos a traves de volúmenes persistentes o almacenamiento en la nube. Esto es mas robusto que un script Python lineal, porque si falla un paso, Kubernetes puede reintentarlo automaticamente sin afectar los otros.

Codigo en accion

Aqui tienes un ejemplo real de un Dockerfile para el paso de entrenamiento, usando TensorFlow. Nota como se especifican todas las dependencias:

# Dockerfile para paso de entrenamiento
FROM python:3.9-slim

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
    git \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements y instalar librerias de Python
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copiar el codigo de entrenamiento
COPY train_model.py .

# Comando para ejecutar el entrenamiento
CMD ["python", "train_model.py"]

Y este es el codigo de train_model.py que se ejecuta dentro del contenedor. Antes de containerizar, muchos cientificos de datos tenian scripts como este, pero dependian de que TensorFlow estuviera instalado globalmente:

# train_model.py - Ejemplo funcional
import tensorflow as tf
import pandas as pd
import pickle
from sklearn.model_selection import train_test_split

# Cargar datos preprocesados (desde un volumen compartido)
def load_data(data_path):
    with open(data_path, 'rb') as f:
        X, y = pickle.load(f)
    return X, y

# Entrenar un modelo simple
def train(X_train, y_train):
    model = tf.keras.Sequential([
        tf.keras.layers.Dense(128, activation='relu', input_shape=(X_train.shape[1],)),
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Dense(10, activation='softmax')
    ])
    model.compile(optimizer='adam',
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    model.fit(X_train, y_train, epochs=10, batch_size=32, verbose=1)
    return model

if __name__ == "__main__":
    # Ruta a datos preprocesados (configurable via variables de entorno)
    data_path = "/data/processed_data.pkl"
    X, y = load_data(data_path)
    X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
    
    model = train(X_train, y_train)
    
    # Guardar modelo entrenado
    model.save("/output/model.h5")
    print("Modelo entrenado y guardado en /output/model.h5")

Despues de containerizar, este script se ejecuta en un entorno aislado. Para orquestar con Kubernetes, crearias un Job que monte un volumen en /data (con los datos preprocesados) y otro en /output (para guardar el modelo).

Errores comunes

  • Contenedores demasiado grandes: Incluir librerias innecesarias en el Dockerfile, como Jupyter o herramientas de desarrollo, lo que aumenta el tiempo de construccion y despliegue. Solucion: Usa imagenes base ligeras (como python:3.9-slim) y solo instala lo esencial para el paso especifico.
  • Falta de gestion de datos entre pasos: Pasar datos entre contenedores via sistemas de archivos locales, lo que falla en Kubernetes si los Pods se ejecutan en nodos diferentes. Solucion: Usa almacenamiento persistente (como PersistentVolumes en Kubernetes) o servicios en la nube (S3, GCS) para compartir datos.
  • No configurar limites de recursos: Ejecutar contenedores sin limites de CPU o memoria, causando que un paso agote los recursos del cluster y afecte a otros pipelines. Solucion: Define requests y limits en los manifiestos de Kubernetes para cada Job.
  • Ignorar el registro de logs: No capturar logs de los contenedores, dificultando la depuracion cuando falla un paso. Solucion: Asegurate de que tu codigo Python imprima a stdout/stderr, y usa herramientas como Fluentd o el logging de Kubernetes para centralizar logs.
  • Acoplamiento estrecho entre pasos: Disenar pipelines donde un paso depende directamente de la salida de otro en formato hardcodeado, haciendo cambios costosos. Solucion: Usa interfaces estandarizadas (como archivos JSON o protocolos como gRPC) y parametriza rutas con variables de entorno.

Checklist de dominio

  1. Puedo escribir un Dockerfile para un paso de ML que instale solo las dependencias necesarias.
  2. Se configurar un volumen persistente en Kubernetes para compartir datos entre Jobs.
  3. Entiendo como usar variables de entorno en contenedores para parametrizar rutas de datos.
  4. Puedo definir un Job de Kubernetes que ejecute un contenedor y maneje reintentos en fallos.
  5. Se leer logs de contenedores en Kubernetes usando kubectl logs.
  6. Puedo explicar la diferencia entre un pipeline monolítico y uno con containers en terminos de escalabilidad.
  7. He probado un pipeline localmente con Docker Compose antes de desplegar en Kubernetes.

Containerizar y ejecutar un paso de preprocesamiento de datos

En este ejercicio, vas a crear un contenedor Docker para un paso de preprocesamiento de datos y ejecutarlo localmente, simulando como funcionaria en un pipeline. Sigue estos pasos:

  1. Prepara los archivos: Crea un directorio llamado ml-pipeline-exercise. Dentro, crea dos archivos:
    • preprocess.py: Un script Python que lee un archivo CSV llamado raw_data.csv (simulado), limpia los datos (ej., llena valores nulos con la media), y guarda el resultado en processed_data.csv.
    • requirements.txt: Lista las dependencias, como pandas==1.3.3.
  2. Escribe el Dockerfile: Crea un archivo Dockerfile que:
    • Use una imagen base ligera de Python (ej., python:3.9-slim).
    • Copie requirements.txt e instale las dependencias con pip.
    • Copie preprocess.py.
    • Defina un comando para ejecutar preprocess.py.
  3. Construye y ejecuta el contenedor: En la terminal, navega al directorio y ejecuta:
    • docker build -t preprocess-step .
    • docker run -v $(pwd):/data preprocess-step (esto monta el directorio actual en /data dentro del contenedor, simulando un volumen).
  4. Verifica el resultado: Despues de ejecutar, deberias ver un archivo processed_data.csv en tu directorio local, generado por el contenedor.

Objetivo: Asegurate de que el contenedor funcione sin errores y produzca el archivo de salida. Esto te prepara para integrarlo en un pipeline de Kubernetes mas adelante.

Pistas
  • Usa pandas.read_csv() y .to_csv() en preprocess.py para manejar los archivos.
  • En el Dockerfile, recuerda establecer WORKDIR para organizar los archivos dentro del contenedor.
  • Si el contenedor falla, revisa los logs con docker logs para depurar.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.