Tenemos los tres ingredientes versionados —código, entorno y datos—, pero la receta todavía se cocina a mano: alguien tiene que acordarse de ejecutar la limpieza, luego las features, luego el entrenamiento, luego la evaluación, en ese orden, y de repetir los pasos correctos cuando algo cambia. En esta lección convertimos ese ritual en un pipeline declarado: un fichero dvc.yaml donde cada etapa dice qué comando ejecuta, de qué depende y qué produce, y un solo comando —dvc repro— que lo reconstruye todo re-ejecutando únicamente lo que cambió. Es la pieza que cierra el módulo y la promesa que llevamos persiguiendo desde el diagnóstico: que cualquier persona del equipo pueda clonar el repositorio y obtener el mismo modelo que Laura, sin Laura.
Contenido
- Qué es un pipeline de entrenamiento y por qué encadenar a mano es frágil
- El pipeline de CineClick en
dvc.yaml params.yaml: los hiperparámetros como dependenciadvc repro: el grafo de dependencias en accióndvc dagydvc metrics diff: ver el pipeline y comparar ejecuciones- Cierre del módulo: qué hemos resuelto y qué sigue abierto
Qué es un pipeline de entrenamiento y por qué encadenar a mano es frágil
Un pipeline de entrenamiento es la secuencia de pasos que transforma datos crudos en un modelo evaluado: en nuestro caso, limpiar → construir features → entrenar → evaluar. La secuencia ya existe implícitamente en nuestro código desde 02-01; el problema es cómo se ejecuta. Hoy, el procedimiento es mental: Laura (o tú) lanza los pasos a mano. Y el encadenado manual falla de formas muy concretas:
- Orden y omisiones: nada impide entrenar con unas features viejas porque se te olvidó re-ejecutar el paso anterior tras cambiar la limpieza. El modelo sale, no da error, y es sutilmente incorrecto — la peor clase de bug.
- Re-ejecuciones innecesarias: la alternativa defensiva ("por si acaso, lo relanzo todo desde cero") desperdicia tiempo, y cuando la preparación de datos tarde horas, ese "por si acaso" será insostenible.
- Conocimiento no transferible: el orden correcto, los comandos exactos y sus condiciones viven en la cabeza de alguien o en un README que envejece. Es el problema nº 10 del diagnóstico con corbata nueva.
- Sin registro de qué produjo qué: aunque los datos estén versionados, nada ata esta versión del modelo a aquella ejecución con aquellos parámetros.
La solución es la misma que aplican make o los sistemas de build desde hace décadas: declarar los pasos con sus dependencias y salidas, y dejar que la herramienta calcule qué hay que ejecutar. DVC trae esto de serie, integrado con el versionado de datos de la lección anterior: las salidas de cada etapa quedan automáticamente bajo control de DVC.
El pipeline de CineClick en dvc.yaml
Primero, una pequeña adaptación del código: cada módulo del paquete gana una función main() ejecutable (python -m cineclick_churn.data, etc.) que lee su configuración y escribe sus salidas a disco — el patrón que dejamos preparado en 02-01. Los pasos se comunican por ficheros, no por variables en memoria: data.py escribe el CSV limpio, features.py lo lee y escribe la matriz de features, train.py la lee y escribe el modelo más el conjunto de test reservado, y evaluate.py lee ambos y escribe las métricas. Esa comunicación por ficheros es justo lo que permite a DVC saber quién depende de quién.
Con eso, el pipeline completo se declara en dvc.yaml, en la raíz del repo:
# dvc.yaml — el pipeline de entrenamiento de CineClick
stages:
preparar_datos:
cmd: python -m cineclick_churn.data
deps:
- data/raw/clientes_churn.csv
- src/cineclick_churn/data.py
outs:
- data/processed/clientes_limpio.csv
construir_features:
cmd: python -m cineclick_churn.features
deps:
- data/processed/clientes_limpio.csv
- src/cineclick_churn/features.py
outs:
- data/processed/features.csv
entrenar:
cmd: python -m cineclick_churn.train
deps:
- data/processed/features.csv
- src/cineclick_churn/train.py
params:
- entrenar.test_size
- entrenar.random_state
- entrenar.n_estimators
outs:
- models/modelo_churn.pkl
- data/processed/test.csv
evaluar:
cmd: python -m cineclick_churn.evaluate
deps:
- models/modelo_churn.pkl
- data/processed/test.csv
- src/cineclick_churn/evaluate.py
metrics:
- metrics.json:
cache: falseDesmenucemos la anatomía de una etapa (stage):
cmd: el comando que la ejecuta. Cualquier comando de shell vale; usamos los módulos del paquete, que es nuestro código testeado.deps: sus dependencias — ficheros de datos y también el código fuente del paso. Esto es clave: si alguien modificadata.py, DVC sabe que la limpieza (y todo lo que cuelga de ella) debe repetirse. El código es una dependencia más.params: dependencias de grano fino sobre valores delparams.yaml(siguiente apartado). Solo los valores listados afectan a la etapa.outs: lo que produce. DVC toma el control de estas salidas: las cachea por hash (como hacíadvc add, pero ahora automáticamente) y las añade a.gitignore. El modelomodelo_churn.pklpasa a estar versionado por DVC sin que hagamos nada más — su resguardo vive ahora endvc.lock, el fichero que DVC genera con los hashes exactos de cada ejecución y que sí se commitea.metrics: una salida especial — un pequeño JSON de métricas que sí queremos legible y comparable (por esocache: false: es diminuto y nos interesa verlo en Git, con sus difs).
La etapa evaluar escribe el metrics.json con las métricas honestas de 02-01:
Un detalle de diseño que merece pausa: entrenar produce dos salidas, el modelo y test.csv (la partición de test). ¿Por qué? Porque evaluar debe medir sobre exactamente las filas que el entrenamiento no vio; si cada etapa repitiera el split por su cuenta, un despiste en los parámetros podría evaluar sobre datos de entrenamiento. Materializar el test como fichero hace la frontera explícita y auditable.
params.yaml: los hiperparámetros como dependencia
En 02-01 pusimos los parámetros en configs/config.yaml. DVC introduce una distinción útil que adoptamos ahora: las rutas (dónde está cada cosa) ya están declaradas en el propio dvc.yaml vía deps/outs, mientras que los hiperparámetros (los valores que cambias cuando experimentas) se mudan a params.yaml, el fichero que DVC lee por convención:
# params.yaml — hiperparámetros del pipeline entrenar: test_size: 0.2 random_state: 42 n_estimators: 100
El código de train.py lo carga igual que cargaba el config (yaml.safe_load), así que el cambio es menor. La ganancia es conceptual y práctica a la vez: al declarar params: [entrenar.n_estimators, ...] en la etapa, cada hiperparámetro se convierte en una dependencia rastreada individualmente. DVC no vigila "el fichero params.yaml cambió" sino "el valor entrenar.n_estimators cambió" — y solo invalida las etapas que dependen de ese valor concreto. Editar un comentario del YAML no re-ejecuta nada.
dvc repro: el grafo de dependencias en acción
Todo declarado. El comando estrella:
En la primera ejecución, DVC recorre el grafo en orden y ejecuta las cuatro etapas, cacheando cada salida y anotando en dvc.lock los hashes de todas las dependencias y salidas de cada etapa. Ese dvc.lock se commitea junto a dvc.yaml y params.yaml: es la constancia exacta de "con estas entradas se produjeron estas salidas".
La magia aparece en la segunda ejecución. Supón que quieres probar un bosque más grande — editas params.yaml y cambias n_estimators: 100 por n_estimators: 300:
Stage 'preparar_datos' didn't change, skipping Stage 'construir_features' didn't change, skipping Running stage 'entrenar': > python -m cineclick_churn.train Running stage 'evaluar': > python -m cineclick_churn.evaluate Updating lock file 'dvc.lock'
DVC comparó hashes: los datos crudos no cambiaron, data.py y features.py tampoco, así que la limpieza y las features se saltan — sus salidas cacheadas siguen siendo válidas. Solo entrenar (cuyo parámetro cambió) y evaluar (que depende del modelo nuevo) se re-ejecutan. Con datasets grandes, esto es la diferencia entre iterar en minutos o en horas. Y funciona en todas las direcciones: si en vez del parámetro cambia data/raw/clientes_churn.csv (extracto nuevo vía dvc add... o directamente como dependencia), se re-ejecuta todo, porque todo cuelga de los datos; si solo cambia evaluate.py, se re-ejecuta únicamente la evaluación. Nunca de más, nunca de menos: el "¿qué tengo que relanzar?" ha dejado de ser una decisión humana.
dvc dag y dvc metrics diff: ver el pipeline y comparar ejecuciones
Para ver el grafo que DVC ha construido a partir de las dependencias declaradas:
+----------------+
| preparar_datos |
+----------------+
*
+--------------------+
| construir_features |
+--------------------+
*
+----------+
| entrenar |
+----------+
*
+---------+
| evaluar |
+---------+Que en versión bonita es este DAG (grafo dirigido acíclico — dirigido porque los datos fluyen en un sentido, acíclico porque ningún paso puede depender de sí mismo):
flowchart TD
RAW[("data/raw/<br/>clientes_churn.csv<br/>(DVC)")] --> P
P["preparar_datos<br/><small>data.py</small>"] --> |clientes_limpio.csv| F
F["construir_features<br/><small>features.py</small>"] --> |features.csv| T
PARAMS[["params.yaml<br/>n_estimators, test_size,<br/>random_state"]] --> T
T["entrenar<br/><small>train.py</small>"] --> |modelo_churn.pkl| E
T --> |test.csv| E
E["evaluar<br/><small>evaluate.py</small>"] --> M[["metrics.json"]]Y para el experimento de los 300 árboles, la pregunta de negocio es "¿mejoró?". Como metrics.json está declarado como métrica, DVC sabe compararlo entre el estado actual y el último commit:
Path Metric HEAD workspace Change metrics.json accuracy 0.86 0.8613 0.0013 metrics.json f1 0.5 0.512 0.012 metrics.json precision 0.61 0.618 0.008 metrics.json recall 0.43 0.44 0.01
Veredicto: triplicar los árboles rasca una mejora marginal en todas las métricas — probablemente no compensa el triple de tiempo de entrenamiento e inferencia, así que revertimos el parámetro (o commiteamos el cambio si decidimos que sí). Lo importante es el flujo: cambiar un valor, dvc repro, dvc metrics diff, decidir con datos. Ahora bien, seamos honestos sobre los límites: esto compara el workspace contra un commit, de uno en uno. Cuando quieras explorar veinte combinaciones de hiperparámetros, verlas en una tabla, ordenarlas por recall y recuperar cualquiera de ellas, este mecanismo se queda corto — esa comodidad es exactamente lo que aporta MLflow, y es la primera parada del módulo 3.
El cierre del círculo es el flujo completo de un compañero nuevo. Marc, en una máquina limpia:
git clone <repo> && cd cineclick-churn python -m venv .venv && source .venv/bin/activate pip install pip-tools && pip-sync requirements.lock && pip install -e . --no-deps dvc pull # trae los datos (y salidas cacheadas) del remote dvc repro # "Data and pipelines are up to date."
Ese mensaje final — todo al día, nada que re-ejecutar — es la reproducibilidad hecha frase: los hashes de datos, código y parámetros de la máquina de Marc coinciden exactamente con los del dvc.lock commiteado, luego su pipeline produciría el mismo modelo que el de Laura. Y si borra models/ y relanza dvc repro, lo comprueba empíricamente: mismas métricas, decimal a decimal. CineClick nunca había estado aquí.
Cierre del módulo: qué hemos resuelto y qué sigue abierto
Pasemos revista al diagnóstico de la lección 01-04, que era nuestra lista de tareas:
| # | Problema | Estado tras el módulo 2 |
|---|---|---|
| 1 | Rutas absolutas y personales | ✅ Resuelto (02-01): rutas relativas en configuración y dvc.yaml |
| 2 | Sin semilla aleatoria | ✅ Resuelto (02-01): random_state=42 en split y modelo, split estratificado |
| 3 | Datos sin versionar | ✅ Resuelto (02-03): DVC + remote; viaje en el tiempo con git checkout + dvc checkout |
| 8 | Dependencias implícitas | ✅ Resuelto (02-02): pyproject.toml + requirements.lock + .python-version |
| 9 | Preprocesado no reutilizable | ✅ Encarrilado (02-01): features.py importable; el servicio del módulo 4 lo consumirá |
| 10 | Proceso 100% manual | 🟡 Parcial (02-04): repetible con dvc repro, pero sigue lanzándose a mano y en local — programarlo y orquestarlo llega en 05-03 |
| 5 | Métrica única y engañosa | 🟡 Parcial (02-01): precision/recall/F1 en metrics.json; comparar muchos experimentos con comodidad → módulo 3 |
| 4 | Modelo sin versionar ni metadatos | 🟡 Parcial: el .pkl está en la caché de DVC atado a su commit, pero sin registro formal de versiones/estados → model registry, módulo 3 |
| 7 | Sin registro de experimentos | ❌ Abierto: dvc metrics diff compara de dos en dos; el historial cómodo y consultable → MLflow, módulo 3 |
| 6 | Sin tests | ❌ Abierto (salvo la semilla plantada en 02-01): tests de datos y modelo en CI → módulo 5 |
Cuatro problemas cerrados, tres bien encarrilados, y los tres restantes con dueño y fecha. Nada mal para un módulo.
Errores Comunes y Consejos
- Error: olvidar el código en las
deps. Sientrenarsolo declarafeatures.csvcomo dependencia, un cambio entrain.pyno re-ejecutará nada y seguirás sirviendo el modelo del código viejo. Regla: cada etapa depende de sus datos de entrada y de su código. - Error: pasos que se comunican por variables en memoria. Si un solo script hace features+entrenamiento "porque es más cómodo", DVC no puede saltarse la parte cara cuando solo cambia la barata. La granularidad del pipeline la marcan los ficheros intermedios: si quieres poder saltarte un paso, materializa su salida.
- Error: editar a mano una salida (
outs). Retocardata/processed/features.csvcon Excel dura hasta el siguientedvc repro, que lo regenerará desde sus dependencias. Todo cambio debe entrar por el principio del grafo: datos crudos, código o parámetros. - Error: no commitear
dvc.lock. Sin él, el resto del equipo no sabe qué hashes corresponden a la última ejecución buena, ydvc reprode Marc no puede verificar que está al día.dvc.yaml,dvc.lockyparams.yamlviajan juntos en el commit. - Consejo: mantén
dvc reprobarato. La disciplina de "¿mejoró?" condvc metrics diffsolo se practica si iterar cuesta minutos. Si un paso se vuelve lento, trocéalo (la parte cara y estable separada de la barata y cambiante) para que la caché trabaje a tu favor.
Ejercicios
Ejercicio 1
Para cada uno de estos cambios, indica qué etapas re-ejecutará el siguiente dvc repro (preparar_datos / construir_features / entrenar / evaluar) y por qué: (a) cambias entrenar.test_size de 0.2 a 0.25 en params.yaml; (b) el data engineer entrega el extracto de agosto y sustituyes data/raw/clientes_churn.csv; (c) corriges un docstring en evaluate.py; (d) añades una regla nueva de limpieza en data.py; (e) reordenas los comentarios de params.yaml sin tocar ningún valor.
Ejercicio 2
El equipo quiere probar limitar la profundidad de los árboles para acelerar la inferencia. Describe los cambios exactos necesarios para incorporar el hiperparámetro max_depth al pipeline (qué ficheros tocas y qué añades en cada uno), y el flujo completo para evaluar si max_depth: 10 merece la pena frente a la línea base.
Ejercicio 3
Un directivo escéptico pregunta: "¿y cómo me demostráis que el modelo que tenéis es reproducible, y no otra vez el 0.87 de Laura?". Diseña la demostración: la secuencia de comandos que ejecutarías delante de él en una máquina limpia, qué esperarías ver en cada paso, y cuál es el mensaje/resultado concreto que constituye la prueba.
Soluciones
Solución 1: (a) entrenar y evaluar — el parámetro es dependencia declarada de entrenar, y evaluar depende de sus salidas (modelo y test); las dos primeras etapas no dependen de él y se saltan. (b) Las cuatro — todo el grafo cuelga de los datos crudos. (c) Solo evaluar — evaluate.py es dependencia únicamente de esa etapa (sí, DVC re-ejecuta aunque el cambio sea un docstring: DVC compara hashes de fichero, no semántica; es el precio de la seguridad). (d) Las cuatro — data.py es dependencia de preparar_datos, su salida cambia (o puede cambiar), y la invalidación se propaga aguas abajo. (e) Ninguna — las etapas dependen de valores concretos del params.yaml (entrenar.n_estimators, etc.), no del fichero como texto, y ningún valor cambió.
Solución 2: cambios: (1) en params.yaml, añadir max_depth: 10 bajo entrenar:; (2) en src/cineclick_churn/train.py, leer el valor y pasarlo al RandomForestClassifier(..., max_depth=params["max_depth"]); (3) en dvc.yaml, añadir entrenar.max_depth a la lista params de la etapa entrenar. Flujo de evaluación: dvc repro (solo re-ejecuta entrenar+evaluar; nota: la primera vez re-entrena de todas formas porque train.py cambió), dvc metrics diff para comparar contra HEAD; si las métricas aguantan (recall similar) con árboles más pequeños, commit de params.yaml+train.py+dvc.yaml+dvc.lock y dvc push; si no, git checkout -- . y dvc checkout para descartar.
Solución 3: en una máquina limpia (o un contenedor): (1) git clone + creación del entorno con pip-sync requirements.lock + pip install -e . --no-deps — se espera instalación sin errores con versiones exactas; (2) dvc pull — descarga los datos exactos referenciados por el commit; (3) dvc repro — se espera "Data and pipelines are up to date", que ya es en sí la verificación de hashes; (4) para la prueba fuerte: borrar models/modelo_churn.pkl y metrics.json y relanzar dvc repro — el pipeline re-entrena desde cero; (5) mostrar metrics.json y compararlo con el commiteado (o dvc metrics diff, esperando cambios vacíos). La prueba concreta: las métricas regeneradas coinciden decimal a decimal con las commiteadas (accuracy 0.86, precision 0.61, recall 0.43, F1 0.50), en una máquina donde Laura no ha tocado nada. Eso es exactamente lo que el 0.87 original no podía ofrecer.
Conclusión
El módulo 2 termina donde prometía: el notebook churn_final_v3_DEFINITIVO.ipynb es ya un proyecto con paquete instalable y testeado, entorno fijado con lock, datos versionados con DVC y un pipeline declarado en dvc.yaml —preparar_datos → construir_features → entrenar → evaluar— que cualquiera reconstruye con git clone, dvc pull y dvc repro, obteniendo el mismo modelo y las mismas métricas honestas (precision 0.61, recall 0.43, F1 0.50) decimal a decimal. La reproducibilidad, el cimiento de todo lo demás, está puesta. Pero al repasar el diagnóstico hemos visto lo que falta, y el experimento de los 300 árboles lo ha hecho tangible: cambiar un parámetro y comparar una ejecución contra otra funciona; explorar decenas de combinaciones, verlas en una tabla ordenada por recall, saber qué se probó hace tres semanas y con qué resultado, y gestionar qué versión del modelo está "bendecida" para producción — eso pide herramientas dedicadas. En el módulo 3 se las damos a CineClick: MLflow para el tracking de experimentos, su model registry para versionar y promocionar modelos, y una mirada crítica a los feature stores. Laura va a poder responder por fin, con datos, a la pregunta que quedó flotando en el módulo 1: qué era exactamente "la v2", y si de verdad era peor.
Curso de MLOps
Módulo 1: Fundamentos de MLOps
- Qué es MLOps y por qué los modelos mueren en el notebook
- El ciclo de vida de un modelo de ML en producción
- Niveles de madurez MLOps y roles del equipo
- El proyecto del curso: del notebook a producción
Módulo 2: Del notebook al código reproducible
- Estructura de un proyecto de ML: del notebook al paquete
- Entornos reproducibles y gestión de dependencias
- Versionado de datos con DVC
- Pipelines de entrenamiento reproducibles
Módulo 3: Experimentos y registro de modelos
- Tracking de experimentos con MLflow
- Model registry: versionar y promocionar modelos
- Feature stores: cuándo y para qué
Módulo 4: Servir modelos en producción
- Patrones de despliegue: batch, online y streaming
- Un servicio de predicción con FastAPI
- Empaquetado con Docker
- Escalado y despliegue: Kubernetes y serverless
- Optimización de la inferencia: latencia y coste
Módulo 5: Automatización: CI/CD y orquestación
- CI para ML: tests de código, datos y modelos
- CD: automatizar el despliegue del modelo
- Orquestación de pipelines de ML
- Estrategias de release: shadow, canary y A/B
