MLOps Fundamentals: las 9 etapas clave para pasar de cero a operativo
MLOps Fundamentals : lo esencial en un artículo — código real, diagramas y pasos concretos, extractos de un curso de 72 lecciones.
Todo el mundo puede aprender MLOps Fundamentals — siempre que siga los pasos en el orden correcto. Hemos condensado un curso completo de 72 lecciones en un recorrido claro, con los extractos de código más útiles.
- Instalar el entorno MLOps
- Descubrir MLOps
- Versionado de datos y modelos
- Pipelines ML con MLflow
- Containerizar los modelos ML
Guía completa del proyecto final – Paso a paso
Curso MLOps Fundamentals • Detección de fraude con tarjeta de crédito • Pipeline MLOps End-to-End
Esta guía detallada te acompaña paso a paso en la realización del proyecto final. Cada sección corresponde a una etapa clave del pipeline MLOps. Sigue los pasos en orden. Todo el código está proporcionado y explicado. Duración estimada: 8–12 horas en total.
① Paso 1 – Inicialización del proyecto y entorno
Crea la estructura del proyecto y el entorno Conda:
mkdir fraud-detection-mlops
cd fraud-detection-mlops
git init
git config user.email "vous@email.com"
git config user.name "Votre Nom"
conda create -n fraud-mlops python=3.10 -y
conda activate fraud-mlops
pip install scikit-learn xgboost pandas numpy mlflow dvc \
fastapi uvicorn pydantic evidently \
pytest pytest-cov httpx joblib matplotlib \
seaborn imbalanced-learn flake8 black
conda env export > environment.ymlCrea el archivo requirements.txt:
scikit-learn==1.4.0 xgboost==2.0.3 pandas==2.1.4 numpy==1.26.3 mlflow==2.10.0 dvc==3.38.1 fastapi==0.109.0 uvicorn==0.27.0 pydantic==2.5.3 evidently==0.4.16 pytest==7.4.4 pytest-cov==4.1.0 httpx==0.26.0 joblib==1.3.2 matplotlib==3.8.2 seaborn==0.13.1 imbalanced-learn==0.11.0
Crea la estructura de carpetas:
mkdir -p data/raw data/processed src api monitoring/reports tests models .github/workflows
__pycache__/ *.pyc *.pyo .env models/ mlruns/ *.pkl data/raw/ data/processed/ monitoring/reports/*.html monitoring/reports/*.json .coverage htmlcov/
dvc init git add . git commit -m "feat: init project structure and DVC"
② Paso 2 – Generación y versionado de los datos
Crea src/generate_synthetic_data.py (si no tienes acceso al dataset de Kaggle):
"""Genera un dataset sintético de fraude con tarjeta de crédito."""
import pandas as pd
import numpy as np
def generate_fraud_dataset(n_samples=50000, fraud_ratio=0.002, random_state=42):
np.random.seed(random_state)
n_fraud = int(n_samples * fraud_ratio)
n_legit = n_samples - n_fraud
legit = pd.DataFrame({
f'V{i}': np.random.normal(0, 1, n_legit) for i in range(1, 29)
})
legit['Amount'] = np.abs(np.random.exponential(scale=88, size=n_legit))
legit['Time'] = np.sort(np.random.uniform(0, 172800, n_legit))
legit['Class'] = 0
fraud = pd.DataFrame({
f'V{i}': np.random.normal(np.random.uniform(-3, 3), 2, n_fraud) for i in range(1, 29)
})
fraud['Amount'] = np.abs(np.random.exponential(scale=122, size=n_fraud))
fraud['Time'] = np.random.uniform(0, 172800, n_fraud)
fraud['Class'] = 1
df = pd.concat([legit, fraud], ignore_index=True)
df = df.sample(frac=1, random_state=random_state).reset_index(drop=True)
return df
if __name__ == "__main__":
df = generate_fraud_dataset()
df.to_csv("data/raw/creditcard.csv", index=False)
print(f"Dataset generado: {len(df)} filas")
print(f"Fraudes: {df['Class'].sum()} ({df['Class'].mean()*100:.3f}%)")Dockerfile para un modelo ML: build y run
Curso MLOps Fundamentals — Capítulo 04 — Containerizar los modelos ML
- Comprender cada instrucción de un Dockerfile para un modelo ML
- Escribir un Dockerfile completo para una API scikit-learn con FastAPI
- Construir una imagen Docker con
docker build - Ejecutar y probar un contenedor ML con
docker run - Depurar los problemas comunes de contenedores ML
1. Estructura del proyecto ML a containerizar
Antes de escribir el Dockerfile, veamos la estructura del proyecto que vamos a containerizar. Se trata de una API de clasificación de vinos basada en un modelo Random Forest entrenado con scikit-learn, servido a través de FastAPI.
wine-classifier/
â└â─â─ Dockerfile # Nuestro archivo de configuración Docker
â└â─â─ .dockerignore # Archivos a excluir de la imagen
â└â─â─ requirements.txt # Dependencias Python
â└â─â─ src/
â├â─â─ app.py # Aplicación FastAPI (punto de entrada)
â├â─â─ predict.py # Lógica de predicción
â├â─â─ preprocessing.py # Preprocesamiento de features
â└â─â─ models/
â└â─â─ rf_classifier.pkl # Modelo Random Forest serializado
â└â─â─ scaler.pkl # StandardScaler guardadoEl archivo requirements.txt
El requirements.txt lista todas las dependencias Python con sus versiones fijadas. Es crucial para la reproducibilidad: sin versión fijada, pip install podría descargar una versión más reciente e incompatible.
# requirements.txt # Framework web fastapi==0.110.0 uvicorn[standard]==0.27.1 # Machine Learning scikit-learn==1.4.1 numpy==1.26.4 pandas==2.2.1 # Serialización del modelo joblib==1.3.2 # Validación de datos pydantic==2.6.3
Evita
scikit-learn>=1.0 o scikit-learn sin versión. Si scikit-learn publica la versión 2.0 con cambios de API, tu contenedor fallará en el próximo rebuild. Usa siempre scikit-learn==1.4.1.La aplicación FastAPI (src/app.py)
# src/app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib
import numpy as np
import os
app = FastAPI(
title="Wine Quality Classifier API",
description="API de clasificación de la calidad del vino basada en Random Forest",
version="1.0.0"
)
# Carga del modelo al inicio (no en cada petición)
MODEL_PATH = os.getenv("MODEL_PATH", "/app/models/rf_classifier.pkl")
SCALER_PATH = os.getenv("SCALER_PATH", "/app/models/scaler.pkl")
model = joblib.load(MODEL_PATH)
scaler = joblib.load(SCALER_PATH)
class WineFeatures(BaseModel):
fixed_acidity: float
volatile_acidity: float
citric_acid: float
residual_sugar: float
chlorides: float
free_sulfur_dioxide: float
total_sulfur_dioxide: float
density: float
pH: float
sulphates: float
alcohol: float
@app.get("/health")
def health_check():
return {"status": "ok", "model": "rf_classifier", "version": "1.0.0"}
@app.post("/predict")
def predict(features: WineFeatures):
try:
X = np.array([[
features.fixed_acidity, features.volatile_acidity,
features.citric_acid, features.residual_sugar,
features.chlorides, features.free_sulfur_dioxide,
features.total_sulfur_dioxide, features.density,
features.pH, features.sulphates, features.alcohol
]])
X_scaled = scaler.transform(X)
prediction = model.predict(X_scaled)[0]
probability = model.predict_proba(X_scaled)[0].max()
return {
"quality": int(prediction),
"confidence": round(float(probability), 4),
"label": "buen vino" if prediction >= 6 else "vino medio"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))2. El Dockerfile completo y comentado
Aquí está el Dockerfile completo. Analizaremos cada instrucción en detalle inmediatamente después.
# ============================================================
# Dockerfile para un modelo scikit-learn servido con FastAPI
# ============================================================
# Paso 1: Imagen base
FROM python:3.11-slim
# Paso 2: Metadatos de la imagen
LABEL maintainer="mlops-team@exemple.com"
LABEL version="1.0.0"
LABEL description="Wine Quality Classifier - Random Forest API"
# Paso 3: Variables de entorno del sistema
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Paso 4: Directorio de trabajo en el contenedor
WORKDIR /app
# Paso 5: Dependencias del sistema (si es necesario)
RUN apt-get update && apt-get install -y --no-install-recommends \
libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# Paso 6: Copiar E instalar las dependencias Python (capa en caché)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Paso 7: Copiar el código fuente
COPY src/ ./src/
# Paso 8: Copiar el modelo ML
COPY models/ ./models/
# Paso 9: Exponer el puerto de la API
EXPOSE 8000
# Paso 10: Usuario no root para seguridad
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
# Paso 11: Comando de inicio
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]3. Análisis detallado de cada instrucción
3.1 FROM python:3.11-slim
FROM define la imagen base. python:3.11-slim es la versión ligera de la imagen oficial de Python:
Para scikit-learn, slim es el buen compromiso: suficientemente pequeño, pero compatible con las extensiones C de numpy/scipy.
:latestFROM python:latest puede pasar de Python 3.11 a 3.12 de un día para otro y romper tu código. Especifica siempre la versión exacta: python:3.11-slim o incluso python:3.11.8-slim.Si tu modelo utiliza la GPU (TensorFlow, PyTorch), usa:
FROM nvidia/cuda:12.1-cudnn8-runtime-ubuntu22.043.2 Variables de entorno ENV
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1| Variable | Efecto | ¿Por qué activarla? |
|---|---|---|
PYTHONDONTWRITEBYTECODE=1 |
Sin archivos .pyc |
Reduce el tamaño de la imagen, evita archivos innecesarios |
PYTHONUNBUFFERED=1 |
Logs en tiempo real | Indispensable para ver los logs en docker logs inmediatamente |
PIP_NO_CACHE_DIR=1 |
Sin caché de pip en la imagen | Reduce el tamaño final de la imagen (puede ahorrar 100-200 MB) |
PIP_DISABLE_PIP_VERSION_CHECK=1 |
Sin verificación de actualización de pip | Acelera el build, evita advertencias innecesarias |
3.3 WORKDIR /app
WORKDIR define el directorio de trabajo en el contenedor. Todas las instrucciones siguientes (COPY, RUN, CMD) se ejecutan desde este directorio. Si el directorio no existe, Docker lo crea automáticamente.
/app como directorio de trabajo para las aplicaciones Python. Evita trabajar directamente en / o /root.Optimización de imágenes Docker ML
Curso MLOps Fundamentals — Capítulo 04 — Containerizar los modelos ML
- Comprender por qué el tamaño de las imágenes Docker ML es un desafío crítico en producción
- Dominar los multi-stage builds para separar construcción y ejecución
- Configurar un
.dockerignoreeficaz para proyectos ML - Aprovechar al máximo la caché de capas de Docker
- Aplicar las mejores prácticas para imágenes ML de producción
1. Por qué el tamaño de las imágenes ML es crítico
A diferencia de una aplicación web clásica (unas decenas de MB), una imagen Docker ML puede alcanzar fácilmente 2 a 8 GB. Este tamaño tiene consecuencias directas en tu flujo de trabajo MLOps:
🚬 Despliegue lento
Descargar una imagen de 5 GB en un clúster de Kubernetes tarda entre 10 y 20 minutos. En caso de autoescalado bajo carga, es inaceptable.
💸 Costo de almacenamiento
Almacenar 50 versiones de una imagen de 4 GB en AWS ECR = 200 GB × 0,10 $/GB/mes = 20 $/mes solo por el almacenamiento.
🔐 Superficie de ataque
Cada herramienta adicional (compiladores, utilidades del sistema) es una vulnerabilidad de seguridad potencial. Una imagen mínima es más segura.
2. Comparación: imágenes pesadas vs imágenes slim
| Enfoque | Imagen base | Tamaño típico | Tiempo de pull (100 Mbit/s) | Vulnerabilidades |
|---|---|---|---|---|
| 🔴 Imagen pesada (ingenua) | python:3.11 + todas las herramientas |
~2,1 GB | ~170 s | Muy numerosas |
| 🟡 Imagen slim (buena práctica) | python:3.11-slim |
~700 MB | ~56 s | Moderadas |
| 🟢 Multi-stage slim | Build: python:3.11-slimRun: python:3.11-slim |
~350 MB | ~28 s | Pocas |
| 🆕 Distroless | gcr.io/distroless/python3 |
~180 MB | ~14 s | Muy pocas |
| 🟢 Alpine (con precauciones) | python:3.11-alpine |
~100 MB | ~8 s | Mínimas |
Alpine utiliza
musl libc en lugar de glibc. Numpy, scikit-learn, pandas y PyTorch están compilados para glibc. El uso de Alpine obliga a compilar desde las fuentes, lo que tarda 30 a 60 minutos y puede fallar. Reserva Alpine para microservicios sin dependencias C. Para ML, prefiere python:3.11-slim.3. El .dockerignore: tu primera línea de defensa
Incluso antes del multi-stage build, el .dockerignore es la optimización más simple e inmediata. Impide que Docker envíe archivos innecesarios (o incluso peligrosos) al daemon de build.
Cuando ejecutas
docker build ., Docker envía todo el directorio actual al daemon de Docker (llamado «contexto de build»). Sin .dockerignore, esto incluye tu dataset de 10 GB, los notebooks Jupyter, los entornos virtuales de Python, los archivos de configuración secretos…# .dockerignore para un proyecto ML # Control de versiones .git/ .gitignore .github/ # Entornos virtuales de Python (¡crítico! puede ocupar cientos de MB) .venv/ venv/ env/ ENV/ __pycache__/ *.py[cod] *.pyo .pytest_cache/ .mypy_cache/ # Datos ML (¡nunca incluir en la imagen!) data/ datasets/ *.csv *.parquet *.arrow *.feather # Notebooks Jupyter (no necesarios en producción) *.ipynb .ipynb_checkpoints/ # Archivos de configuración local .env .env.local .env.development *.env # Documentación y tests (inútiles en producción) docs/ tests/ README.md CHANGELOG.md # Artefactos de build dist/ build/ *.egg-info/ htmlcov/ .coverage # IDE y editores .vscode/ .idea/ *.swp *.swo .DS_Store Thumbs.db # Logs y archivos temporales logs/ *.log tmp/ temp/ # Archivos MLflow y experimentos mlruns/ mlflow.db # Modelos de prueba (mantener solo el modelo de producción) models/experiments/ models/checkpoints/
Medir el impacto del .dockerignore
# Verificar el tamaño del contexto de build ANTES de .dockerignore docker build -t test-before . 2>&1 | head -5 # Sending build context to Docker daemon 4.521GB <-- ¡sin .dockerignore! # Verificar el tamaño del contexto de build DESPUÉS de .dockerignore docker build -t test-after . 2>&1 | head -5 # Sending build context to Docker daemon 12.34MB <-- con .dockerignore
4. Los multi-stage builds para ML
Un multi-stage build utiliza varias instrucciones FROM en un solo Dockerfile. Cada etapa produce una capa intermedia, y solo la última etapa se conserva en la imagen final. Esto permite:
4.1 Multi-stage build para un modelo scikit-learn
# ============================================================
# Multi-stage Dockerfile para Wine Classifier (producción)
# ============================================================
# ---- ETAPA 1: Builder ----
# Esta etapa instala todo, incluidas las herramientas de compilación
FROM python:3.11-slim AS builder
# Variables de entorno para el build
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /build
# Instalar las herramientas de build del sistema (NO estarán en la imagen final)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
build-essential \
libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# Copiar e instalar en un directorio local aislado (--prefix)
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ---- ETAPA 2: Imagen de producción (final) ----
# Imagen final ultraligera: NO contiene gcc, build-essential, etc.
FROM python:3.11-slim AS production
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Copiar solo las bibliotecas instaladas desde el builder
COPY --from=builder /install /usr/local
# Dependencias del sistema runtime (mínimas)
RUN apt-get update && apt-get install -y --no-install-recommends \
libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# Copiar el código fuente y el modelo
COPY src/ ./src/
COPY models/ ./models/
# Seguridad: usuario no root
RUN useradd --create-home --shell /bin/bash appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]Al eliminar
gcc, g++, build-essential y los archivos intermedios de compilación de la imagen final, generalmente ganas entre 200 y 600 MB según las dependencias C/C++ utilizadas.4.2 Multi-stage build con etapa de test
# ============================================================ # Multi-stage Dockerfile con etapa de test integrada # ============================================================ FROM python:3.11-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # ---- Etapa de test ---- FROM base AS test COPY requirements-dev.txt . RUN pip install --no-cache-dir -r requirements-dev.txt COPY src/ ./src/ COPY models/ ./models/ COPY tests/ ./tests/ RUN pytest tests/ -v --tb=short # ---- Etapa de producción ---- FROM base AS production COPY src/ ./src/ COPY models/ ./models/ RUN useradd --create-home appuser && chown -R appuser /app USER appuser EXPOSE 8000 CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]
Este artículo cubre los extractos más útiles — el curso completo MLOps Fundamentals (13 capítulos, 72 lecciones, ejercicios corregidos y proyecto final) te lleva hasta el final.
./acceder-al-curso-completo curso gratuito: Dominar Claude CodeFAQ
¿Cuánto tiempo se necesita para aprender MLOps Fundamentals?
¿Se necesitan requisitos previos?
¿Por dónde empezar concretamente?
📬 ¿Quieres recibir este tipo de guía cada semana? Suscríbete gratis — código real, cero palabrería.