La lección anterior terminó con un modelo ganador — recall 0.68 frente al 0.43 del base — que por ahora solo existe como "la run que salió bien" y un modelo_churn.pkl en una carpeta. Ese fichero es el último eslabón débil de la cadena: nadie puede saber, mirándolo, de qué run salió, si es la versión buena, ni quién decidió que lo fuera. En esta lección lo resolvemos con el model registry de MLflow: un catálogo con nombre, versiones numeradas y aliases (@champion, @challenger) que convierte "el pickle bendecido a mano" en un modelo versionado, trazable y cargable por nombre desde cualquier código — exactamente lo que necesitará el servicio de predicción del módulo 4.

Contenido

  1. El problema del modelo "bendecido" a mano
  2. Qué es un model registry
  3. Registrar el modelo ganador como churn-cineclick
  4. Versiones del modelo
  5. Aliases y tags: @champion, @challenger (y por qué las stages clásicas están deprecadas)
  6. Cargar el modelo por alias desde código
  7. El flujo de promoción: revisión humana, siempre
  8. Metadatos y lineage: documentar el modelo

El problema del modelo "bendecido" a mano

Pensemos en cómo funciona hoy CineClick, incluso con todo lo montado en las lecciones anteriores. El pipeline produce models/modelo_churn.pkl. Si mañana el servicio de predicción del módulo 4 necesita el modelo, ¿qué hace? ¿Copiar ese fichero? ¿De qué máquina, del último dvc repro de quién? Y cuando la búsqueda de hiperparámetros produce un candidato mejor, ¿cómo se "oficializa"? ¿Sobrescribiendo el pickle? Entonces perdemos el anterior. ¿Renombrando a modelo_churn_v2.pkl? Eso es literalmente cómo nació el misterio de la v2.

Los síntomas del modelo gestionado a mano:

  • Sin identidad: un .pkl no lleva dentro sus métricas, su run de origen ni sus datos de entrenamiento.
  • Sin versiones: o sobrescribes (pierdes historia) o renombras a mano (caos de sufijos: _v2, _final, _DEFINITIVO).
  • Sin estado: nada distingue "el modelo en producción" de "un experimento del martes". El estado vive en la cabeza de Laura.
  • Sin contrato de consumo: cada consumidor (un servicio, un batch, un notebook) apunta a una ruta de fichero distinta y se entera de los cambios por sorpresa.

DVC versiona el .pkl como output del pipeline, y eso resuelve la reproducibilidad (puedes volver a cualquier commit y regenerarlo). Pero no resuelve la gestión del ciclo de vida: qué versión es la oficial, quién la aprobó y cómo la piden los consumidores. Para eso existe el registry.

Qué es un model registry

Un model registry es un catálogo centralizado de modelos con tres ideas clave:

  • Registered model: una entrada con nombre estable — para nosotros, churn-cineclick. Representa el problema ("el modelo de churn de CineClick"), no un fichero concreto. Es el nombre por el que todo el mundo pedirá el modelo, para siempre.
  • Versión: cada vez que se registra un modelo bajo ese nombre, el registry le asigna un número incremental (1, 2, 3...). Cada versión apunta a los artifacts de la run de la que salió: el binario, la firma, el input_example.
  • Estado (aliases/tags): etiquetas que marcan qué versión juega qué papel — cuál es la campeona en producción, cuál es la aspirante en evaluación.
Pickle en carpeta Model registry
Identidad Ruta de fichero (models/modelo_churn.pkl) Nombre estable + número de versión
Historia Se pierde al sobrescribir (salvo arqueología en Git/DVC) Todas las versiones conviven, listadas
Origen (lineage) Desconocido Enlace directo a la run: params, métricas, commit, datos
Estado (¿cuál es el bueno?) Convención verbal / memoria de Laura Alias explícito y consultable (@champion)
Contrato de entrada Ninguno Firma (signature) validable
Cómo lo pide un consumidor Copiando un fichero de algún sitio URI estable: models:/churn-cineclick@champion
Auditoría (quién promocionó y cuándo) Imposible Registrada en el registry

Un detalle práctico antes de empezar: el registry necesita un tracking server con base de datos como backend store — no funciona contra una simple carpeta mlruns/. El servidor que levantamos en la lección anterior ya cumple:

mlflow server \
  --backend-store-uri sqlite:///mlflow.db \
  --artifacts-destination ./mlartifacts \
  --host 0.0.0.0 --port 5000
export MLFLOW_TRACKING_URI=http://127.0.0.1:5000

Registrar el modelo ganador como churn-cineclick

Hay dos maneras de registrar un modelo. La primera: a posteriori, desde una run ya existente. Laura localiza en la UI la run ganadora de la búsqueda (rf-300-20-balanced) y registra su modelo:

import mlflow

# El run_id se copia de la UI (o se busca con mlflow.search_runs)
run_id = "f0d4a91c2e8b4f7a9c31d5e6b8a2f4c0"

# "runs:/<run_id>/modelo" apunta al artifact 'modelo' de esa run
version = mlflow.register_model(
    model_uri=f"runs:/{run_id}/modelo",
    name="churn-cineclick",
)
print(version.version)  # -> 1 (primera versión del registered model)

La segunda, más cómoda para el flujo habitual: en el momento de loguear, añadiendo registered_model_name a la llamada de log_model que ya teníamos en train.py:

mlflow.sklearn.log_model(
    modelo,
    artifact_path="modelo",
    signature=firma,
    input_example=X.head(5),
    registered_model_name="churn-cineclick",  # registra una versión nueva
)

¿Cuál usar? Depende de la política del equipo. Registrar desde train.py significa que cada dvc repro crea una versión — cómodo, pero llena el registry de versiones intermedias. Registrar a posteriori significa que solo los candidatos deliberadamente elegidos entran en el catálogo. CineClick adopta esta segunda política: al registry solo entran candidatos serios, elegidos por un humano tras mirar la tabla de runs; la exploración se queda en el tracking.

Siguiendo esa política, Laura registra dos versiones, y la numeración cuenta la historia del proyecto:

  • Versión 1: el modelo base del módulo 2 (run del dvc repro con 100 árboles) — accuracy 0.86, precision 0.61, recall 0.43. Es lo que hay "en producción" conceptualmente hoy.
  • Versión 2: el ganador de la búsqueda (n_estimators=300, max_depth=20, class_weight="balanced") — accuracy 0.842, precision 0.55, recall 0.68, F1 0.61.

Nótese la ironía feliz: CineClick vuelve a tener "una v2"... pero esta vez la v2 tiene run de origen, params, métricas, commit y datos enlazados. La diferencia entre las dos v2 es todo este módulo.

Versiones del modelo

Cada versión de un registered model es inmutable en lo esencial: apunta a los artifacts de su run y eso no cambia. Lo que sí se puede editar es su descripción, sus tags y sus aliases. Ideas que conviene fijar:

  • Las versiones son numeración automática e incremental: no eliges "v2.1"; el registry asigna 1, 2, 3... La semántica ("esta es la buena") no va en el número, va en los aliases.
  • Varias versiones conviven: la 1 no desaparece cuando llega la 2. Eso habilita el rollback instantáneo: si la 2 decepciona, el alias vuelve a la 1 sin reentrenar nada.
  • Una versión enlaza a su run: desde la UI del registry, un clic lleva a la run con todos sus params y métricas. El lineage es navegable, no arqueológico.

Aliases y tags: @champion y @challenger

¿Cómo se marca qué versión es "la oficial"? El MLflow moderno usa aliases: punteros con nombre que señalan exactamente a una versión, y que se pueden mover.

Una nota histórica que evita confusiones al leer documentación antigua: MLflow tenía un sistema de stages (Staging, Production, Archived) por el que las versiones "transicionaban". Ese sistema está deprecado desde MLflow 2.9: era rígido (solo esos estados fijos) y ambiguo (¿pueden dos versiones estar en Production?). Los aliases lo sustituyen con ventaja: defines los nombres que tu proceso necesite y cada alias apunta a exactamente una versión. Si un tutorial te enseña transition_model_version_stage, es material antiguo.

La convención que adopta CineClick (y que es ya un estándar de facto):

  • @champion: la versión aprobada para producción. Solo hay una.
  • @challenger: la versión candidata a destronarla, en evaluación.

Asignarlos, con la API cliente:

from mlflow import MlflowClient

client = MlflowClient()

# La versión 1 (modelo base) es la campeona actual: es lo que hay funcionando
client.set_registered_model_alias("churn-cineclick", "champion", version=1)

# La versión 2 (balanced) entra como aspirante
client.set_registered_model_alias("churn-cineclick", "challenger", version=2)

Los tags complementan a los aliases: mientras un alias es un puntero único ("la campeona"), un tag es información descriptiva que puede repetirse entre versiones (validado_qa: "true", dataset: "julio-2026"). Regla mental: alias = rol, tag = atributo.

client.set_model_version_tag("churn-cineclick", version=2, key="validado_subgrupos", value="pendiente")

Cargar el modelo por alias desde código

Aquí está el pago de todo el esfuerzo. Cualquier consumidor puede pedir el modelo por su URI de registry, sin saber nada de rutas, runs ni versiones:

import mlflow

mlflow.set_tracking_uri("http://127.0.0.1:5000")

# "Dame la versión que tenga el alias champion, sea cual sea"
modelo = mlflow.sklearn.load_model("models:/churn-cineclick@champion")

# También se puede fijar una versión concreta (útil para depurar o auditar):
modelo_v2 = mlflow.sklearn.load_model("models:/churn-cineclick/2")

Dos observaciones importantes:

  • El consumidor que carga @champion no cambia ni una línea cuando se promociona una versión nueva: el alias se mueve en el registry y la siguiente carga trae la nueva campeona. El despliegue del modelo se desacopla del despliegue del código.
  • Esta URI — models:/churn-cineclick@champion — es exactamente la que usará el servicio FastAPI del módulo 4 para cargar el modelo al arrancar. La estamos dejando fijada aquí como el contrato entre entrenamiento y serving.
graph LR
    R["Run ganadora<br/>(tracking, 03-01)"] -->|register_model| RM["Registered model<br/>churn-cineclick"]
    RM --> V1["Version 1<br/>recall 0.43"]
    RM --> V2["Version 2<br/>recall 0.68"]
    V2 -->|alias| CH["@champion"]
    CH -->|"models:/churn-cineclick@champion"| C1["Servicio de prediccion<br/>(modulo 4)"]
    CH --> C2["Scoring batch"]
    V1 -.->|rollback posible| CH

El flujo de promoción: revisión humana, siempre

¿Y cómo pasa la versión 2 de @challenger a @champion? Esta es la parte del registry que no es técnica, y es deliberado: promocionar un modelo es una decisión con consecuencias de negocio, y la toma una persona. El sistema calcula, compara y recomienda; la decisión de promocionar es siempre humana. (Y en un sector regulado, además, auditable: quién aprobó qué y cuándo.)

El flujo que fija CineClick:

  1. Candidato registrado como versión nueva con alias @challenger, con su descripción y lineage completos.
  2. Revisión de métricas globales contra la campeona, sobre el mismo conjunto de test (garantizado aquí por random_state=42 y el split estratificado del pipeline). v2 vs. v1: recall 0.68 vs. 0.43, F1 0.61 vs. 0.50, a cambio de precision 0.55 vs. 0.61.
  3. Validación en subgrupos: las métricas globales pueden esconder degradaciones locales. Laura corta las métricas por plan, por tramos de antiguedad_meses y por metodo_pago, y comprueba que la mejora de recall no se concentra en un segmento mientras otro empeora. (Esto también es una primera línea de defensa de equidad: que el modelo no funcione notablemente peor para un colectivo concreto.)
  4. Criterio de negocio explícito: acordado con el equipo de retención — "aceptamos bajar precision hasta 0.50 si el recall supera 0.60, porque el coste de una llamada innecesaria es muy inferior al de una baja no detectada". La v2 cumple con margen.
  5. Decisión y movimiento del alias, con la revisión documentada:
# La revisora (Laura, con el visto bueno de retención) mueve el alias
client.set_registered_model_alias("churn-cineclick", "champion", version=2)
client.delete_registered_model_alias("churn-cineclick", "challenger")
client.set_model_version_tag("churn-cineclick", 2, "aprobado_por", "laura")
client.set_model_version_tag("churn-cineclick", 2, "fecha_promocion", "2026-07-07")
  1. Rollback preparado: si tras el despliegue la v2 decepciona, set_registered_model_alias("churn-cineclick", "champion", version=1) y el siguiente consumidor que cargue el alias vuelve al modelo anterior. Sin reentrenar, sin buscar ficheros.

En el módulo 5 automatizaremos partes de este flujo (tests que verifican los criterios como gates de CD), y en el 5.4 veremos cómo exponer al challenger a tráfico real (shadow/canary) antes de decidir. Pero la estructura — candidato, evidencia, criterios, decisión humana, rollback — queda fijada aquí.

Metadatos y lineage: documentar el modelo

Una versión sin contexto es otro pickle con número. La última milla es anotar en el registry todo lo que un futuro lector (Marc dentro de seis meses, un auditor, la propia Laura) necesitará:

client.update_model_version(
    name="churn-cineclick",
    version=2,
    description=(
        "RandomForest con class_weight='balanced', n_estimators=300, max_depth=20. "
        "Sube recall de 0.43 a 0.68 (precision 0.61 -> 0.55, F1 0.50 -> 0.61). "
        "Elegido en la busqueda de hiperparametros del 2026-07-07 (12 runs). "
        "Criterio: maximizar recall con precision >= 0.50 (acuerdo con retencion)."
    ),
)
client.set_model_version_tag("churn-cineclick", 2, "git_commit", "a1b2c3d")
client.set_model_version_tag("churn-cineclick", 2, "datos_dvc_md5", "af12bc90...")

Buena parte del lineage ya viene "gratis" porque la versión enlaza a su run, y la run (lección anterior) lleva el commit de Git y el hash de datos DVC. La descripción y los tags de la versión lo hacen visible sin excavar: la ficha del modelo se lee en la UI del registry en treinta segundos. La firma (signature) que registramos con el modelo completa la ficha: cualquiera puede ver qué columnas y tipos espera la v2 antes de intentar usarla. En el módulo 6 formalizaremos esta documentación (model cards, gobernanza); la disciplina de anotar cada versión empieza aquí.

Errores Comunes y Consejos

  • Usar las stages deprecadas (Staging/Production) porque el tutorial que encontraste es de 2022. Funcionarán una temporada con avisos, pero estás aprendiendo un sistema en extinción. Aliases y tags son el presente.
  • Registrar cada run automáticamente "por si acaso": un registry con 80 versiones sin criterio es tan opaco como una carpeta con 80 pickles. Decide una política de entrada (en CineClick: solo candidatos revisados por un humano) y cúmplela.
  • Mover el alias @champion sin dejar rastro de por qué: el movimiento queda registrado, pero el motivo no, salvo que lo escribas (descripción/tags). La pregunta del auditor no es "¿cuándo cambió?" sino "¿por qué y con qué evidencia?".
  • Cargar por versión fija en producción (models:/churn-cineclick/2) y olvidarlo: te pierdes el desacoplamiento que da el alias, y las promociones dejan de llegar al servicio. Versión fija para depurar y auditar; alias para consumir.
  • Comparar challenger y champion sobre tests distintos: si cada uno se evaluó con otro split u otra versión de los datos, la comparación es ruido. Mismo test set, mismos datos (el hash DVC de las runs permite verificarlo).
  • Olvidar que el registry necesita servidor con base de datos: contra una carpeta mlruns/ sin backend store, register_model falla. Si trabajas solo, mlflow server --backend-store-uri sqlite:///mlflow.db es suficiente.

Ejercicios

  1. Política de rollback. Tres días después de promocionar la v2, el equipo de retención reporta que llegan demasiadas llamadas a clientes de plan premium que no iban a darse de baja. Escribe (a) el código para devolver @champion a la versión 1, (b) los tags que dejarías en la versión 2 para documentar lo ocurrido, y (c) qué análisis del flujo de promoción debería haber detectado el problema antes.

  2. URIs de modelo. Indica qué carga cada una de estas URIs y en qué situación usarías cada una: models:/churn-cineclick@champion, models:/churn-cineclick/1, runs:/f0d4a91c.../modelo.

  3. Diseño de aliases. CineClick quiere, además de champion y challenger, mantener identificada la última versión que pasó la validación de subgrupos aunque no sea aún candidata. ¿Lo modelarías con un alias o con un tag? Justifica la elección y escribe el código.

Soluciones

  1. (a) Rollback:
from mlflow import MlflowClient
client = MlflowClient()
client.set_registered_model_alias("churn-cineclick", "champion", version=1)

(b) Documentación en la v2:

client.set_model_version_tag("churn-cineclick", 2, "estado", "retirada")
client.set_model_version_tag("churn-cineclick", 2, "motivo_retirada",
                             "exceso de falsos positivos en plan premium (2026-07-10)")

(c) El paso 3 del flujo (validación en subgrupos): la precision por segmento plan=premium habría mostrado la concentración de falsos positivos antes de promocionar. Lección: las métricas globales (precision 0.55) pueden ser aceptables mientras un subgrupo concreto está muy por debajo.

  1. models:/churn-cineclick@champion carga la versión que tenga el alias en ese momento — es la URI de los consumidores de producción, porque sigue las promociones sin cambiar código. models:/churn-cineclick/1 carga exactamente la versión 1 aunque el alias se haya movido — útil para auditar, comparar o reproducir un comportamiento pasado. runs:/f0d4a91c.../modelo carga el artifact directamente desde la run, sin pasar por el registry — útil durante la exploración, antes de que el modelo tenga entidad de candidato.

  2. Con un alias (por ejemplo @validado): el requisito es señalar una versión concreta ("la última que pasó la validación"), que es exactamente la semántica de un alias — puntero único que se mueve. Un tag validado=true se acumularía en todas las versiones que alguna vez pasaron la validación, y habría que buscar cuál es la más reciente. Ambos pueden convivir: el tag como atributo histórico, el alias como puntero a la actual.

client.set_registered_model_alias("churn-cineclick", "validado", version=2)

Conclusión

El modelo de churn ya no es un fichero suelto: es el registered model churn-cineclick, con la versión 1 (el base, recall 0.43) y la versión 2 (el balanced, recall 0.68) conviviendo en el catálogo, la 2 promocionada a @champion tras un flujo de revisión con criterios explícitos y decisión humana, y cualquier consumidor puede cargarla con models:/churn-cineclick@champion — la URI que el módulo 4 usará como contrato. Versiones, estado, lineage y rollback: el problema del "modelo bendecido" del diagnóstico queda cerrado. Pero al preparar el terreno para servirlo asoma un riesgo más sutil: el modelo espera features (las que construye features.py), y en producción alguien tendrá que calcularlas de nuevo, en otro código y en otro momento. Si ese cálculo no es idéntico al del entrenamiento, el modelo campeón dará predicciones corruptas sin que nada falle visiblemente. De ese problema — y de la herramienta que la industria ha inventado para él, el feature store, con su correspondiente mirada crítica — trata la última lección del módulo.

© Copyright 2026. Todos los derechos reservados