MLOps — Módulo 7: CI/CD/CT para ML — testear datos y modelos, no solo código
Séptimo módulo del Curso de MLOps: el CI/CD del curso de Deploy extendido para ML con CT (continuous training). Validación de datos con pandera, validación de modelo comparando el candidato contra producción antes de promover, y un pipeline nuevo dato/código → entrena → valida → registra → despliega.
☰ Índice del contenido
En el curso de Deploy montaste CI/CD para tu agente: un robot que en cada PR corre lint, tests y evals (deploy-08) y después vuelve esos checks obligatorios (deploy-09). Todo eso sigue valiendo aquí — no lo vamos a repetir. Pero el CI/CD de software da por sentado algo que en ML es falso: que el sistema solo cambia cuando cambia el código. Un modelo se degrada aunque nadie toque una línea, porque los datos cambian. Este módulo agrega las dos verificaciones que el CI de software no tiene — validar datos y validar el modelo — y una tercera letra a la sigla: CT.
La tesis del módulo
En software, el código es lo único que cambia; en ML, cambian el código, los datos y el modelo — y los tres pueden romper el sistema. El CI de tu curso de Deploy vigila el código. Para ML hay que sumarle dos guardianes que el software no necesita: un test que valida que los datos entran sanos, y un test que valida que el modelo nuevo es de verdad mejor que el que ya está en producción.
La tercera letra: CT
CT — continuous training, entrenamiento continuo. Es la práctica de reentrenar el modelo de forma automática cuando llega un disparador: datos nuevos, una caída de métrica, o simplemente el calendario. Donde el CD lleva código a producción sin intervención, el CT lleva modelos: dispara el pipeline del Módulo 5, entrena con datos frescos, valida, y promueve solo si el candidato aprueba.
La sigla completa del ciclo de ML queda así:
| Letra | Qué automatiza | Qué mueve a producción |
|---|---|---|
| CI | Integrar y verificar cada cambio | Confianza en el código |
| CD | Empaquetar y desplegar | Código / servicio |
| CT | Reentrenar cuando hace falta | Modelos |
Lo nuevo #1: validación de datos
El código puede estar perfecto y el pipeline entrenar basura, porque la basura entró por los datos. Validación de datos es verificar, antes de entrenar o de servir, que los datos cumplen un contrato: el schema (qué columnas y de qué tipo), los rangos (una edad no es 400, una probabilidad no es 1.5) y las distribuciones (que la proporción de una categoría no se disparó de un día para otro). Herramientas reales: pandera (contratos sobre DataFrames, liviano) y Great Expectations (más completo y pesado).
Con pandera defines el contrato como código y falla ruidosamente si los datos lo violan:
# validacion_datos.py — contrato de datos con pandera
import pandera.pandas as pa # desde pandera 0.24: el módulo por backend
schema = pa.DataFrameSchema(
{
"cliente_id": pa.Column(int, nullable=False, unique=True),
"edad": pa.Column(int, pa.Check.in_range(18, 100)),
"gasto_promedio_30d": pa.Column(float, pa.Check.ge(0), nullable=False),
"usa_app": pa.Column(int, pa.Check.isin([0, 1])),
# La proporción de churn no debería saltar bruscamente: si el
# promedio se sale del rango histórico, algo pasó aguas arriba.
"target": pa.Column(int, pa.Check.isin([0, 1])),
},
checks=pa.Check(
lambda df: 0.02 <= df["target"].mean() <= 0.30,
error="Tasa de churn fuera del rango histórico esperado",
),
)
def validar(df):
"""Lanza SchemaErrors si los datos violan el contrato. lazy=True
junta TODOS los errores en vez de morir en el primero."""
return schema.validate(df, lazy=True)
Ese validar(df) va como primer paso del pipeline de entrenamiento: si los datos no cumplen el contrato, el pipeline para antes de entrenar en vez de producir un modelo entrenado sobre basura.
Lo nuevo #2: validación del modelo
Pasar los tests no significa que el modelo sirva. Validación de modelo es verificar que el candidato cumple un piso de calidad y, sobre todo, que es mejor que el modelo que ya está en producción antes de reemplazarlo. Un modelo nuevo que rinde peor que el actual no se promueve — por más que haya entrenado sin errores.
# validacion_modelo.py — el candidato tiene que ganarle a producción
import sys
import mlflow
from sklearn.metrics import roc_auc_score
UMBRAL_MINIMO = 0.80 # piso absoluto de calidad
MARGEN = 0.005 # cuánto tiene que mejorar para valer el cambio
def validar_candidato(candidato, modelo_prod, X_test, y_test) -> bool:
auc_nuevo = roc_auc_score(y_test, candidato.predict_proba(X_test)[:, 1])
# 1) Piso absoluto.
if auc_nuevo < UMBRAL_MINIMO:
print(f"RECHAZADO: AUC {auc_nuevo:.3f} < mínimo {UMBRAL_MINIMO}")
return False
# 2) Comparar contra producción sobre EL MISMO set de test.
if modelo_prod is not None:
auc_prod = roc_auc_score(y_test, modelo_prod.predict_proba(X_test)[:, 1])
if auc_nuevo < auc_prod + MARGEN:
print(f"RECHAZADO: {auc_nuevo:.3f} no supera prod {auc_prod:.3f}")
return False
print(f"APROBADO: AUC {auc_nuevo:.3f}, promover a producción")
return True
if __name__ == "__main__":
# En un job de CI: exit 1 bloquea la promoción (como cualquier test rojo).
ok = validar_candidato(...) # cargando modelos del registry, Módulo 4
sys.exit(0 if ok else 1)
El detalle que importa: se comparan sobre el mismo set de test. Comparar el AUC del candidato contra el AUC que producción sacó hace tres meses en otro dataset no dice nada. Mismo examen, mismas preguntas, y recién ahí decides quién pasa.
El flujo automatizado completo
Este es el pipeline CI/CD/CT: se dispara con dato o código nuevo, y encadena los dos guardianes nuevos entre el entrenamiento y el despliegue.
START (dato nuevo / código nuevo / cron)
│
▼
┌──────────────┐
│ pipeline │ M5: datos → features → train
└──────────────┘
│
▼
┌──────────────┐
│ valida datos │ pandera / GE: ¿schema y rangos OK?
└──────────────┘
│
▼
┌──────────────┐
│ valida modelo│ ¿pasa el piso Y le gana a prod?
└──────────────┘
│
▼
┌──────────────────────┐
│ ¿aprobó? │
│ si ──► registra │ M4: registry
│ no ──► BLOQUEA │
└──────────────────────┘
│
▼
┌──────────────┐
│ despliega │ M8: serving
└──────────────┘
│
▼
END
En GitHub Actions, cada uno de esos guardianes es un step que puede fallar el job — igual que el lint → test → eval → build de deploy-08, encadenado con needs y con los checks vueltos obligatorios como en deploy-09:
# .github/workflows/ct.yml — reentrenar y validar (fragmento)
name: CT
on:
schedule:
- cron: "0 6 * * 1" # lunes 6:00: reentrena con datos frescos
workflow_dispatch: {} # o a mano cuando haga falta
jobs:
entrenar-y-validar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt
- name: Validar datos (pandera)
# si los datos violan el contrato, el step falla y corta el job.
run: python -m validacion_datos
- name: Entrenar candidato
run: python pipeline.py
- name: Validar modelo vs producción
# exit 1 aquí impide registrar y desplegar un modelo peor.
run: python validacion_modelo.py
env:
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
CI/CD de software vs de ML: qué se agrega
| CI/CD de software | CI/CD/CT de ML | |
|---|---|---|
| Qué cambia | Solo el código | Código, datos y modelo |
| Se testea | Lint, tests, build | + validación de datos y de modelo |
| Disparadores | Push, PR | + datos nuevos, drift, cron (CT) |
| Criterio de “pasa” | Tests en verde | + candidato le gana a producción |
| Qué se despliega | El artefacto de código | El modelo aprobado |
| Qué se versiona | Código | + datos (M2) y modelo (M4) |
Lo de la izquierda no desaparece: es la base que ya montaste en Deploy. Lo de la derecha es lo que ML le suma encima.
Cuidado con promover por métrica sola. Que el AUC suba 0.001 no siempre justifica cambiar el modelo en producción: un cambio implica riesgo operacional, revalidar, y a veces sesgos nuevos que la métrica global esconde. El
MARGENdel código existe para eso — pide una mejora que valga el cambio, no cualquier decimal.
Cuidado con el CT en piloto automático total. Reentrenar solo está bien; promover solo, sin ningún ojo humano, es peligroso en dominios sensibles (crédito, salud, riesgo). Muchos equipos serios dejan el entrenamiento y la validación automáticos, pero ponen un aprobador humano antes del
deployfinal. Automatiza el trabajo, no la responsabilidad.
Errores comunes
- Testear solo el código. Si tu CI corre pytest pero nunca mira los datos, un cambio silencioso aguas arriba (una columna que cambió de unidad) pasa todos los tests y envenena el modelo. Valida los datos como validas el código.
- Comparar el candidato contra un número viejo. El AUC de producción de hace meses en otro dataset no es comparación. Mismo set de test para los dos, o la decisión no significa nada.
- CT sin validación. Reentrenar automático y promover automático sin el guardián de modelo es una máquina de desplegar regresiones. El CT sin gate es peor que no tener CT.
- Confundir “entrenó sin errores” con “es mejor”. Un pipeline verde solo dice que corrió. Que el modelo sirva lo dice la validación contra producción, no la ausencia de excepciones.
- Repetir el CI/CD de software desde cero. No reinventes lint, secrets y branch protection: eso ya lo montaste en Deploy. Aquí solo agregas las dos validaciones propias de ML.
La frase para llevarse
“El CI de software pregunta ‘¿el código sigue funcionando?’. El de ML agrega dos preguntas que el software nunca se hace: ‘¿los datos entraron sanos?’ y ‘¿este modelo le gana al que ya está en producción?’.”
Qué viene
Módulo 8, cierre del bloque intermedio: serving de modelos. Ya tienes un modelo validado y registrado que aprobó todos los guardianes. Falta lo obvio y lo difícil: ponerlo a responder de verdad — en batch para procesar millones de filas de noche, o en un endpoint online que contesta en milisegundos.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.