Un dato incómodo que conviene tener presente al empezar esta lección: la mayoría de los modelos de machine learning que se entrenan en las empresas nunca llegan a producción. No porque el modelo sea malo. Porque nadie construyó el sistema que lo rodea.

AlpinaShop está justo en ese punto. En seis lecciones ha construido un recomendador heurístico en SQL, un clasificador de carritos, un clasificador de imágenes, un modelo de dos torres, ocho mil opiniones analizadas, veinte mil imágenes procesadas, dos mil cuatrocientas fichas generadas, un buscador semántico y un asistente con RAG.

Y ahora, las preguntas que nadie ha hecho todavía:

¿Quién ejecuta todo eso? Lucía lanza el proceso de sentimiento desde su notebook cuando se acuerda; Dani regenera fichas con un script en su portátil. Si mañana los dos están de vacaciones a la vez, nada se ejecuta y nadie se entera.

¿Cómo sabemos que el modelo nuevo es mejor? No lo sabemos. El modelo de dos torres se entrenó en marzo. Si se reentrena hoy con seis meses más de datos, nadie comparará el resultado con el que está en producción: se desplegará el nuevo porque es el nuevo.

¿Con qué datos se entrenó el modelo que hizo esta recomendación? Nadie lo sabe. No está escrito en ningún sitio.

¿Y si los datos cambian y un modelo se degrada? Nadie se entera, hasta que alguien de negocio diga "esto ya no funciona como antes"; y para entonces llevará meses funcionando mal.

Esas cuatro preguntas son MLOps. Y responderlas es lo que separa un experimento de un producto.

Contenido

  1. Por qué los modelos no llegan a producción
  2. Qué es MLOps y sus niveles de madurez
  3. Vertex AI Pipelines: componentes, artefactos y grafo
  4. El pipeline de recomendación de AlpinaShop
  5. Componente 1: extraer los datos
  6. Componente 2: validar la calidad
  7. Componente 3: entrenar
  8. Componente 4: evaluar contra producción
  9. La puerta de decisión: solo se despliega si mejora
  10. Componentes 5 y 6: registrar y desplegar
  11. Compilar y ejecutar el pipeline
  12. Ejecución programada y disparada por eventos
  13. Metadatos y linaje: lo que te salva en una auditoría
  14. Model Monitoring y el reentrenamiento
  15. Versionado y reproducibilidad
  16. Coste y sostenibilidad del ciclo
  17. El checklist de ML responsable

  1. Por qué los modelos no llegan a producción

La causa raíz es siempre la misma: el modelo es una parte pequeña del sistema. El código de modelado es una fracción mínima del total; todo lo demás es infraestructura que hay que construir y que casi nunca se presupuesta.

Los seis obstáculos concretos, con lo que ha pasado en AlpinaShop:

Obstáculo Manifestación en AlpinaShop
No hay proceso reproducible El modelo de marzo no se puede volver a obtener
La ejecución depende de personas Lucía lanza el proceso "cuando se acuerda"
No hay criterio de despliegue Se desplegaría el modelo nuevo sin comparar
No hay trazabilidad Nadie sabe qué datos entrenaron qué versión
No hay vigilancia La degradación se detecta por queja de negocio
No hay vuelta atrás Si el modelo nuevo va mal, no hay procedimiento

Fíjate en algo importante: ninguno de los seis es un problema de machine learning. Son problemas de ingeniería de software y de operaciones, y por eso la solución tampoco es de machine learning. Y hay una asimetría que conviene entender, porque explica por qué MLOps no es simplemente DevOps aplicado a modelos:

Aspecto Software tradicional Sistema de ML
Qué cambia El código El código y los datos
Qué se versiona Código Código, datos, modelo, hiperparámetros
Se degrada solo No : el mundo cambia y el modelo no
Prueba de aceptación Determinista: pasa o falla Estadística: "es mejor que el anterior"
Fallo típico Excepción visible Silencioso: predice peor sin errores

La última fila es la más peligrosa. Un servicio web caído se detecta en minutos. Un modelo que acierta un 15 % menos no produce ningún error: sigue respondiendo, sigue devolviendo predicciones bien formadas, y nadie lo nota.

  1. Qué es MLOps y sus niveles de madurez

MLOps es el conjunto de prácticas que llevan modelos a producción de forma fiable, reproducible y sostenible. Combina DevOps con las particularidades del ML: los datos son parte del artefacto y la calidad se mide estadísticamente. Tres niveles de madurez, y no hay que llegar al último para tener un sistema sano:

Nivel Cómo es Quién ejecuta Riesgos
0 — Manual Notebooks, pasos a mano, despliegue manual Una persona Irreproducible, dependiente, sin vigilancia
1 — Entrenamiento automatizado Pipeline que ejecuta el ciclo completo, programado Un orquestador Falta CI/CD del propio pipeline
2 — CI/CD completo El pipeline se construye, prueba y despliega solo al cambiar el código Un sistema de CI/CD Complejidad; requiere equipo

AlpinaShop está en el nivel 0 y en esta lección va al nivel 1. El nivel 2 requiere Cloud Build y disparadores desde el repositorio, que es materia de 06-01, y para una empresa de 40 personas puede ser más de lo que necesita. Un consejo que ahorra proyectos: el salto de 0 a 1 aporta la mayor parte del valor, porque un pipeline reproducible que se ejecuta solo, con puerta de decisión y linaje, resuelve las cuatro preguntas del inicio de la lección. El salto de 1 a 2 es refinamiento.

  1. Vertex AI Pipelines: componentes, artefactos y grafo

Vertex AI Pipelines ejecuta pipelines definidos con el SDK de Kubeflow Pipelines (KFP) sin que tengas que gestionar ningún clúster. Es serverless: describes el grafo, lo compilas, lo ejecutas y pagas por lo que consume. Cuatro conceptos bastan.

Un componente es un paso del pipeline: una función de Python empaquetada en un contenedor, con entradas y salidas declaradas. Un artefacto es algo que un componente produce o consume y que la plataforma rastrea —un dataset, un modelo, unas métricas—; los artefactos son la clave del linaje, porque cada uno sabe qué componente lo produjo y con qué entradas. Un parámetro es un valor simple que se pasa entre componentes o al pipeline entero. Y el grafo es dirigido y acíclico, con las dependencias inferidas solas: si B recibe la salida de A, se ejecuta después de A, sin declarar ningún orden.

flowchart TD
    A[Extraer datos<br/>de alpinashop_analitica] --> B[Validar calidad<br/>reglas de Dataplex]
    B -->|calidad OK| C[Entrenar modelo]
    B -->|calidad KO| Z[Detener pipeline]
    C --> D[Evaluar candidato]
    E[Modelo en produccion] --> D
    D --> F{Mejora el umbral?}
    F -->|si| G[Registrar version]
    F -->|no| H[Notificar y detener]
    G --> I[Desplegar / publicar tabla]
    I --> J[Registrar metadatos y linaje]

Frente a Cloud Composer y Workflows (04-06), que ya conoces, la diferencia de propósito es clara:

Aspecto Workflows / Composer Vertex AI Pipelines
Propósito Orquestación general Ciclo de vida de modelos
Rastreo de artefactos No Sí, nativo
Linaje de ML No
Integración con Model Registry Manual Nativa
Definición YAML / DAG de Airflow Python con KFP

No compiten: se complementan. Workflows sigue orquestando el proceso nocturno de datos y dispara el pipeline de ML cuando toca.

  1. El pipeline de recomendación de AlpinaShop

El objetivo: reentrenar el recomendador two-tower de 05-03 cada semana, comparándolo con el modelo en producción y con la línea base heurística de 05-01, y desplegarlo solo si mejora.

from kfp import dsl
from kfp.dsl import component, Input, Output, Dataset, Model, Metrics

@dsl.pipeline(
    name="reco-alpinashop",
    description="Reentrenamiento semanal del recomendador de AlpinaShop",
    pipeline_root="gs://alpinashop-datalake/pipelines/reco",
)
def pipeline_reco(
    proyecto: str = "alpinashop-datos",
    region: str = "europe-west1",
    dias_historico: int = 730,
    umbral_mejora: float = 0.02,      # 2% de mejora relativa minima
    dim_embedding: int = 64,
    epocas: int = 25,
):
    extraer = extraer_datos(
        proyecto=proyecto, dias=dias_historico)

    validar = validar_calidad(
        datos=extraer.outputs["datos"], proyecto=proyecto)

    with dsl.If(validar.outputs["calidad_ok"] == True, name="calidad-correcta"):
        entrenar = entrenar_modelo(
            datos=extraer.outputs["datos"],
            dim=dim_embedding, epocas=epocas)

        evaluar = evaluar_contra_produccion(
            modelo_candidato=entrenar.outputs["modelo"],
            datos_test=extraer.outputs["datos_test"],
            proyecto=proyecto)

        with dsl.If(evaluar.outputs["mejora"] >= umbral_mejora, name="puerta"):
            registrar = registrar_modelo(
                modelo=entrenar.outputs["modelo"],
                metricas=evaluar.outputs["metricas"],
                proyecto=proyecto, region=region)

            publicar_recomendaciones(
                modelo_recurso=registrar.outputs["recurso"],
                proyecto=proyecto)

pipeline_root es el prefijo de Cloud Storage donde se guardan todos los artefactos de todas las ejecuciones: es lo que hace que un modelo entrenado hace tres meses siga existiendo y siga siendo recuperable. Y los parámetros están en la firma, no dentro del código: umbral_mejora=0.02 se puede cambiar al lanzar la ejecución sin tocar nada, que es lo que permite probar configuraciones sin editar el pipeline.

Los dos dsl.If anidados son las dos puertas del sistema: la de calidad de datos y la de mejora del modelo. Si cualquiera falla, el pipeline termina sin desplegar nada. Esa es la propiedad más importante de todo el diseño.

  1. Componente 1: extraer los datos

@component(base_image="python:3.11",
           packages_to_install=["google-cloud-bigquery==3.25.0",
                                "pandas==2.2.2", "pyarrow==17.0.0"])
def extraer_datos(proyecto: str, dias: int,
                  datos: Output[Dataset], datos_test: Output[Dataset]):
    from google.cloud import bigquery
    import pandas as pd

    consulta = f"""
    SELECT l.cliente_hash, l.sku, l.fecha_pedido,
           p.categoria, p.precio, p.puntuacion_media,
           h.pedidos_previos, h.ticket_medio_previo, h.dias_desde_ultimo
    FROM `{proyecto}.alpinashop_analitica.lineas_pedido` l
    JOIN `{proyecto}.alpinashop_analitica.productos` p USING (sku)
    LEFT JOIN `{proyecto}.alpinashop_analitica.hist_cliente_diario` h
      ON h.cliente_hash = l.cliente_hash
     AND h.fecha = DATE_SUB(l.fecha_pedido, INTERVAL 1 DAY)
    WHERE l.fecha_pedido >= DATE_SUB(CURRENT_DATE(), INTERVAL {dias} DAY)
      AND l.cliente_hash IS NOT NULL AND p.activo = TRUE
    """
    df = bigquery.Client(project=proyecto).query(consulta).to_dataframe()

    # Division TEMPORAL: las ultimas 4 semanas son el test
    corte = df["fecha_pedido"].max() - pd.Timedelta(days=28)
    df[df["fecha_pedido"] <  corte].to_parquet(datos.path)
    df[df["fecha_pedido"] >= corte].to_parquet(datos_test.path)

    datos.metadata["consulta"]    = consulta
    datos.metadata["fecha_corte"] = str(corte)
    datos.metadata["filas_train"] = int((df["fecha_pedido"] < corte).sum())

Cuatro decisiones que este componente hereda del módulo. El JOIN con hist_cliente_diario a fecha anterior es la prevención de fuga temporal de 05-02: el historial del cliente se toma del día anterior al pedido, no de hoy. La división es temporal, no aleatoria: las últimas cuatro semanas son el test, porque en un problema con estacionalidad la división aleatoria produce métricas falsas. p.activo = TRUE: no se entrena con productos descatalogados, la misma regla que en el recomendador heurístico de 05-01. Y los metadatos del artefacto: datos.metadata["consulta"] guarda la consulta exacta que produjo estos datos, que es la respuesta a "¿con qué datos se entrenó este modelo?" y no cuesta nada escribirla.

Y una nota sobre las versiones fijadas en packages_to_install: sin ellas, el componente que hoy funciona puede romperse dentro de tres meses porque una librería cambió. Misma disciplina que las etiquetas de imagen de 05-01.

  1. Componente 2: validar la calidad

Aquí es donde el módulo 4 vuelve, y no como decoración.

@component(base_image="python:3.11",
           packages_to_install=["pandas==2.2.2", "pyarrow==17.0.0"])
def validar_calidad(datos: Input[Dataset], proyecto: str,
                    informe: Output[Metrics]) -> bool:
    import pandas as pd
    df = pd.read_parquet(datos.path)

    reglas = {
        "volumen_minimo":     len(df) >= 50_000,
        "clientes_minimos":   df["cliente_hash"].nunique() >= 5_000,
        "productos_minimos":  df["sku"].nunique() >= 500,
        "sin_nulos_clave":    df[["cliente_hash", "sku"]].isna().sum().sum() == 0,
        "precios_positivos":  (df["precio"] > 0).all(),
        "sin_duplicados":     not df.duplicated(
                                  ["cliente_hash", "sku", "fecha_pedido"]).any(),
        "cobertura_historial": df["pedidos_previos"].notna().mean() >= 0.80,
    }

    for nombre, ok in reglas.items():
        informe.log_metric(nombre, 1.0 if ok else 0.0)
    informe.log_metric("filas", float(len(df)))

    fallidas = [n for n, ok in reglas.items() if not ok]
    if fallidas:
        print(f"REGLAS FALLIDAS: {fallidas}")
    return len(fallidas) == 0

Las siete reglas no son arbitrarias: son las dimensiones de calidad de Dataplex que definiste en 04-07, aplicadas al conjunto de entrenamiento. Volumen, unicidad, completitud, validez, consistencia. La misma disciplina, en otro punto del flujo.

Y merece detenerse en dos. cobertura_historial >= 0.80: si el LEFT JOIN con el historial falla para el 40 % de las filas —porque el proceso que construye hist_cliente_diario no se ejecutó—, el modelo entrenaría con la mayoría de sus características vacías, y sin esta regla ese fallo sería completamente invisible: el entrenamiento acabaría bien, las métricas serían algo peores, y nadie sabría por qué. Y sin_duplicados: un duplicado sesga el modelo hacia esos ejemplos y, si cae en train y en test a la vez, infla las métricas.

Que un fallo de calidad detenga el pipeline es la decisión de diseño más importante de esta lección. La alternativa —avisar y seguir— produce lo peor de los dos mundos: un modelo entrenado con datos malos, desplegado, con una alerta que nadie leyó. Mismo principio que detiene el Workflow nocturno de 04-06 cuando la comprobación de calidad falla.

  1. Componente 3: entrenar

El entrenamiento no se hace dentro del componente: el componente lanza un trabajo de Vertex AI y espera. Así se puede usar GPU y aislar recursos sin que el pipeline entero necesite una máquina grande.

@component(base_image="python:3.11",
           packages_to_install=["google-cloud-aiplatform==1.71.0"])
def entrenar_modelo(datos: Input[Dataset], dim: int, epocas: int,
                    modelo: Output[Model]):
    from google.cloud import aiplatform
    aiplatform.init(project="alpinashop-datos", location="europe-west1")

    trabajo = aiplatform.CustomContainerTrainingJob(
        display_name="reco-two-tower-pipeline",
        container_uri=("europe-west1-docker.pkg.dev/alpinashop-prod/"
                       "alpinashop/entrena-reco:1.0.3"),
    )
    trabajo.run(
        args=[f"--datos={datos.path}", f"--dim={dim}", f"--epocas={epocas}",
              f"--salida={modelo.path}"],
        replica_count=1, machine_type="n1-standard-8",
        accelerator_type="NVIDIA_TESLA_T4", accelerator_count=1,
        base_output_dir=modelo.path,
    )
    modelo.metadata.update({"framework": "tensorflow", "arquitectura": "two-tower",
                            "dim_embedding": dim, "epocas": epocas,
                            "imagen": "entrena-reco:1.0.3"})

La imagen está versionada (1.0.3, nunca latest), como en 05-01 y 05-03. Y los metadatos del modelo registran la arquitectura y los hiperparámetros, no por documentar sino porque son lo que se compara cuando un modelo va peor que otro.

  1. Componente 4: evaluar contra producción

Este componente es el corazón del pipeline y contiene la idea que más se olvida en los proyectos reales.

@component(base_image="python:3.11",
           packages_to_install=["google-cloud-aiplatform==1.71.0",
                                "pandas==2.2.2", "pyarrow==17.0.0"])
def evaluar_contra_produccion(modelo_candidato: Input[Model],
                              datos_test: Input[Dataset], proyecto: str,
                              metricas: Output[Metrics]) -> float:
    """Devuelve la mejora relativa del candidato sobre el modelo en produccion."""
    import pandas as pd
    df_test = pd.read_parquet(datos_test.path)

    r_cand = calcular_recall_at_k(modelo_candidato.path, df_test, k=10)
    r_prod = obtener_metrica_produccion(proyecto, "recall_at_10")
    r_base = calcular_recall_baseline_sql(proyecto, df_test, k=10)  # 05-01

    mejora = (r_cand - r_prod) / max(r_prod, 1e-9)

    metricas.log_metric("recall_at_10_candidato",  r_cand)
    metricas.log_metric("recall_at_10_produccion", r_prod)
    metricas.log_metric("recall_at_10_baseline",   r_base)
    metricas.log_metric("mejora_relativa",         mejora)
    metricas.log_metric("supera_baseline", 1.0 if r_cand > r_base else 0.0)

    # Si no supera ni la heuristica de 30 lineas de SQL, no hay nada que discutir
    return -1.0 if r_cand <= r_base else mejora

Las tres métricas y por qué son tres. Comparar el candidato solo contra el modelo en producción tiene un punto ciego: si el modelo en producción ya era peor que la heurística, un candidato ligeramente mejor que él sigue siendo peor que treinta líneas de SQL. Por eso la línea base de la decisión DA-003 se mide en cada ejecución, para siempre. No es un ritual: es el suelo por debajo del cual el sistema entero no merece existir. Y el return -1.0 cuando no la supera garantiza que la puerta de decisión no se abra bajo ninguna configuración de umbral.

Un aviso realista sobre las métricas offline, que ya apareció en 05-03: recall@10 mide sobre comportamiento pasado, y una mejora offline no garantiza una mejora de negocio. El pipeline decide qué modelo es candidato; la validación final sigue siendo un A/B test con métricas de negocio. Lo que el pipeline evita es que llegue a A/B test algo que ni siquiera mejora en laboratorio.

  1. La puerta de decisión: solo se despliega si mejora

with dsl.If(evaluar.outputs["mejora"] >= umbral_mejora, name="puerta"):
    registrar = registrar_modelo(...)
    publicar_recomendaciones(...)

Tres líneas de código que convierten un proceso de reentrenamiento en un sistema con criterio.

Por qué el umbral es 2 % y no 0 %. Porque una mejora del 0,3 % está dentro del ruido del muestreo, y desplegar por ruido significa cambiar el modelo cada semana sin ganancia real, con todo el coste de validación, riesgo y confusión que eso conlleva. El umbral debe ser mayor que la variabilidad natural de la métrica. Cómo estimarlo: entrenar el mismo modelo cinco veces cambiando solo la semilla aleatoria y medir la desviación de recall@10; si varía un ±1,5 % entre ejecuciones idénticas, un umbral del 2 % es ajustado y del 3 % prudente.

Qué pasa cuando la puerta no se abre. El pipeline termina correctamente. No es un fallo: es el sistema funcionando. El modelo en producción sigue, se registra la ejecución con sus métricas y se notifica al equipo. Si la puerta lleva cerrada ocho semanas, eso también es información valiosa: el modelo actual es bueno, o hace falta cambiar de enfoque en lugar de reentrenar lo mismo.

Las cuatro puertas de un sistema maduro, de las que AlpinaShop tiene las dos primeras: calidad de datos (¿son utilizables?, componente 2), mejora del modelo (¿es mejor que producción y que la heurística?, componente 4), sesgo por segmento (¿mejora para todos o solo en agregado?, ampliación recomendada) y A/B test (¿mejora el negocio?, fuera del pipeline).

La tercera merece una nota, porque conecta con 05-02: un modelo puede mejorar el recall@10 global y empeorar para los clientes que compran desde móvil o para los nuevos. Añadir una puerta que compruebe que ningún segmento relevante empeora más de un umbral es una de las mejores ampliaciones posibles de este pipeline.

  1. Componentes 5 y 6: registrar y desplegar

@component(base_image="python:3.11",
           packages_to_install=["google-cloud-aiplatform==1.71.0"])
def registrar_modelo(modelo: Input[Model], metricas: Input[Metrics],
                     proyecto: str, region: str, recurso: Output[str]):
    from google.cloud import aiplatform
    aiplatform.init(project=proyecto, location=region)

    previos = aiplatform.Model.list(filter='display_name="recomendador-alpinashop"')
    registrado = aiplatform.Model.upload(
        display_name="recomendador-alpinashop",
        artifact_uri=modelo.uri,
        serving_container_image_uri=(
            "europe-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-15:latest"),
        parent_model=previos[0].resource_name if previos else None,
        version_aliases=["candidato"],
        labels={"origen": "pipeline", "centro-coste": "analitica"},
    )
    recurso.value = registrado.resource_name

parent_model hace que sea una nueva versión del modelo existente y no un modelo distinto: es lo que mantiene el historial y permite comparar versiones y revertir. Y el alias es candidato, no produccion: el pipeline registra, y la promoción a producción es un paso explícito —automático tras el A/B test o manual—. Separar registro de promoción es lo que permite tener un modelo listo sin que esté sirviendo.

Y el despliegue, que en AlpinaShop no es un endpoint, por la decisión DA-002:

@component(base_image="python:3.11",
           packages_to_install=["google-cloud-bigquery==3.25.0",
                                "google-cloud-aiplatform==1.71.0"])
def publicar_recomendaciones(modelo_recurso: str, proyecto: str):
    """Predice por lotes y publica la tabla que consume la tienda."""
    from google.cloud import aiplatform, bigquery
    aiplatform.init(project=proyecto, location="europe-west1")

    aiplatform.Model(modelo_recurso).batch_predict(
        job_display_name="reco-batch-semanal",
        bigquery_source=f"bq://{proyecto}.alpinashop_analitica.v_clientes_activos",
        bigquery_destination_prefix=f"bq://{proyecto}.alpinashop_analitica",
        machine_type="n1-standard-4", sync=True)

    bigquery.Client(project=proyecto).query(f"""
      CREATE OR REPLACE TABLE `{proyecto}.alpinashop_analitica.reco_publicada` AS
      SELECT cliente_hash, sku, afinidad, posicion,
             CURRENT_TIMESTAMP() AS actualizada_en,
             '{modelo_recurso}'  AS modelo_origen
      FROM `{proyecto}.alpinashop_analitica.reco_candidata`
      WHERE posicion <= 20
    """).result()

La columna modelo_origen en la tabla publicada es pequeña y vale oro: cada recomendación que ve un cliente lleva grabado qué versión del modelo la generó. Es lo que permite responder, meses después, "¿qué modelo hizo esta recomendación?".

  1. Compilar y ejecutar el pipeline

from kfp import compiler
from google.cloud import aiplatform

compiler.Compiler().compile(pipeline_func=pipeline_reco,
                            package_path="reco_pipeline.yaml")

aiplatform.init(project="alpinashop-datos", location="europe-west1")
aiplatform.PipelineJob(
    display_name="reco-semanal-2026-08-05",
    template_path="reco_pipeline.yaml",
    pipeline_root="gs://alpinashop-datalake/pipelines/reco",
    parameter_values={"dias_historico": 730, "umbral_mejora": 0.02},
    enable_caching=True,
).submit(service_account="[email protected]")

El YAML compilado es el artefacto versionable. Va a Git junto con el código Python: es la definición exacta del pipeline y permite reproducir una ejecución de hace meses.

enable_caching=True es una función excelente y una trampa a partes iguales. Si un componente se ejecuta con exactamente las mismas entradas que una vez anterior, Vertex AI reutiliza el resultado sin volver a ejecutarlo, lo que en desarrollo ahorra muchísimo tiempo y dinero. Pero cuidado: si el componente de extracción tiene los mismos parámetros, la caché puede devolver datos antiguos aunque BigQuery tenga datos nuevos. La defensa es incluir un parámetro que cambie —la fecha de ejecución— en los componentes que leen datos frescos.

service_account también importa: el pipeline se ejecuta con una cuenta dedicada de permisos mínimos —leer las vistas necesarias, escribir en el bucket de artefactos, gestionar modelos— y ninguno más. Es 03-04 aplicado a un proceso automático.

  1. Ejecución programada y disparada por eventos

Tres formas de lanzar el pipeline, y AlpinaShop usa las tres:

Programada. Vertex AI Pipelines tiene un programador propio, que se crea con gcloud ai pipeline-jobs schedules create --cron="0 3 * * 1" --pipeline-job-file=reco_pipeline.yaml --max-concurrent-run-count=1. Ese último parámetro importa: evita que una ejecución que se alargue se solape con la siguiente, algo que produce condiciones de carrera al escribir en la misma tabla.

Desde Workflows, que es lo coherente con la decisión de 04-06. El proceso nocturno de datos ya existe; el pipeline de ML se encadena después de que los datos estén listos y validados:

- lanzar_pipeline_reco:
    call: http.post
    args:
      url: ${"https://" + region + "-aiplatform.googleapis.com/v1/projects/"
            + proyecto + "/locations/" + region + "/pipelineJobs"}
      auth: {type: OAuth2}
      body:
        displayName: ${"reco-" + text.substring(time.format(sys.now()), 0, 10)}
        templateUri: "gs://alpinashop-datalake/pipelines/reco_pipeline.yaml"
        runtimeConfig:
          gcsOutputDirectory: "gs://alpinashop-datalake/pipelines/reco"
          parameterValues: {dias_historico: 730, umbral_mejora: 0.02}
    result: respuesta

Esta es la integración que importa y es la razón de encadenarlo así: el pipeline de ML no se ejecuta si los datos de la noche fallaron. Sin ese encadenamiento, un lunes en el que el proceso de datos hubiera fallado, el pipeline entrenaría con datos incompletos —y aunque la puerta de calidad probablemente lo detendría, es mejor no llegar a ese punto.

Por eventos. El disparo desde el repositorio al cambiar el código del pipeline es el nivel 2 de madurez y corresponde a Cloud Build (06-01): un push a la rama principal reconstruye la imagen de entrenamiento, recompila el YAML, ejecuta el pipeline en desarrollo y, si pasa, lo promueve. Aquí queda anticipado.

  1. Metadatos y linaje: lo que te salva en una auditoría

Vertex ML Metadata registra automáticamente cada ejecución, cada artefacto y cada relación entre ellos. No hay que hacer nada especial: usar componentes de KFP con entradas y salidas tipadas ya lo produce.

Las cinco preguntas que responde, y cuándo las necesitas:

Pregunta Cuándo la haces Qué la responde
¿Con qué datos se entrenó esta versión? Auditoría, incidencia Linaje del artefacto Dataset
¿Qué versión hizo esta predicción? Reclamación de cliente modelo_origen en la tabla publicada
¿Qué cambió entre la v7 y la v8? El modelo empeoró Metadatos y métricas de ambas versiones
¿Qué modelos usan este dato? Petición de supresión (RGPD) Linaje inverso
¿Se puede reproducir el modelo de marzo? Verificación YAML + parámetros + artefactos

La cuarta es la que sorprende a todo el mundo. Un cliente ejerce su derecho de supresión y sus datos se borran de pedidos. Pero ese cliente contribuyó al entrenamiento de tres modelos, y sus datos están incorporados en los pesos. El linaje permite saber qué modelos lo incluyeron y decidir con criterio jurídico qué hacer —normalmente, asegurar que el próximo reentrenamiento no lo incluya y documentar el razonamiento—. Sin linaje, la pregunta es directamente irrespondible.

Consultarlo es directo: aiplatform.PipelineJob.list(filter='display_name:"reco-*"', order_by="create_time desc") enumera todas las ejecuciones con su estado, y PipelineJob.get(...) con task_details devuelve los artefactos de una ejecución concreta con sus metadatos.

Y la práctica complementaria: escribir la tarjeta de modelo (model card) como artefacto del propio pipeline, con cuatro apartados —para qué sirve, con qué datos se entrenó, qué métricas tiene, qué limitaciones conocidas— generados automáticamente a partir de los metadatos. Documentación que no se queda obsoleta porque se regenera en cada ejecución.

  1. Model Monitoring y el reentrenamiento

El pipeline resuelve el "cómo reentrenar". Falta el "cuándo".

Tres estrategias, y la mejor es la combinación:

Estrategia Cómo funciona Ventaja Riesgo
Periódica Cada semana, pase lo que pase Simple y predecible Reentrena sin necesidad
Por deriva Cuando Model Monitoring alerta Eficiente Puede tardar en detectar
Por degradación Cuando la métrica de negocio baja Directamente relevante Ya hubo daño

Para AlpinaShop: periódica semanal como base, con reentrenamiento adicional si la deriva o la métrica de negocio lo piden. Cuesta poco (apartado 16) y la puerta de decisión garantiza que no se despliegue nada que no mejore. Los dos fenómenos que vigilar, ya vistos en 05-01. Deriva de datos: cambia la distribución de las entradas, y en AlpinaShop es estacional y esperada —en octubre entra material de invierno donde en julio había sandalias de trekking—, así que una alerta de deriva no es una avería. Deriva de concepto: cambia la relación entre entradas y resultado —un competidor baja precios y cambia el comportamiento de compra—, y es más difícil de detectar y más grave.

Como AlpinaShop sirve por lotes y no tiene endpoint, la monitorización se hace sobre los datos, comparando la distribución semanal contra la del entrenamiento:

CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.v_deriva_semanal` AS
WITH actual AS (
  SELECT p.categoria, COUNT(*) / SUM(COUNT(*)) OVER () AS pct
  FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido` l
  JOIN `alpinashop-datos.alpinashop_analitica.productos` p USING (sku)
  WHERE l.fecha_pedido >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
  GROUP BY p.categoria
)
SELECT a.categoria,
       ROUND(a.pct, 4)              AS pct_actual,
       ROUND(r.pct, 4)              AS pct_entrenamiento,
       ROUND(ABS(a.pct - r.pct), 4) AS diferencia,
       IF(ABS(a.pct - r.pct) > 0.10, 'ALERTA', 'OK') AS estado
FROM actual a
FULL OUTER JOIN `alpinashop-datos.alpinashop_analitica.dist_entrenamiento` r
  USING (categoria)
ORDER BY diferencia DESC;

Y la advertencia de 05-01, vigente: un umbral demasiado sensible produce ruido, el ruido produce indiferencia, y la indiferencia produce que nadie mire la alerta que sí importaba.

  1. Versionado y reproducibilidad

Reproducir un modelo exige que cuatro cosas estén versionadas. Casi todos los equipos versionan la primera y ninguna de las otras tres.

Qué Cómo se versiona Dónde vive
Código Git Cloud Source Repositories (06-02) o GitHub
Datos Consulta + fecha de corte en metadatos Metadatos del artefacto
Modelo Versiones del Model Registry Vertex AI
Entorno Imagen de contenedor con etiqueta Artifact Registry

Los datos son la parte difícil. Una tabla de BigQuery cambia constantemente: no se puede "hacer commit" de ella. Tres soluciones, de menor a mayor coste: guardar la consulta y la fecha de corte en los metadatos del artefacto (lo que hace el componente 1), barato y suficiente si las tablas son append-only; usar los viajes en el tiempo de BigQuery (FOR SYSTEM_TIME AS OF), que consultan el estado de una tabla en un instante pasado dentro de la ventana de retención; y materializar una instantánea del conjunto en Cloud Storage, que es lo que el pipeline_root hace de facto al guardar el Parquet.

La lista de comprobación de reproducibilidad, que conviene verificar una vez y no volver a preocuparse:

  • [ ] El YAML compilado está en Git con su etiqueta de versión.
  • [ ] La imagen de entrenamiento tiene etiqueta fija, nunca latest.
  • [ ] Los packages_to_install llevan versión fijada.
  • [ ] La consulta de extracción está en los metadatos del artefacto.
  • [ ] Las semillas aleatorias están fijadas y registradas.
  • [ ] Los hiperparámetros están en metadatos, no en el código.
  • [ ] Los artefactos de cada ejecución persisten en pipeline_root.

  1. Coste y sostenibilidad del ciclo

Aquí se cierra el argumento que empezó en 05-01 con la decisión DA-002, y ahora con números.

Opción A: endpoint en línea 24×7. Dos nodos n1-standard-4 encendidos todo el mes para servir recomendaciones que cambian una vez por semana, facturando 24 horas al día haya tráfico o no: del orden de cientos de euros al mes.

Opción B: predicción por lotes semanal. Un trabajo que se ejecuta los lunes, procesa 45.000 clientes en unos minutos y muere, más el entrenamiento semanal en una T4 y la ejecución del pipeline: del orden de unas pocas decenas de euros al mes.

Un orden de magnitud de diferencia, y sin ninguna pérdida funcional: las recomendaciones se sirven desde una tabla precalculada en Firestore o Memorystore (02-06), con latencia de microsegundos y sin depender de que ningún modelo esté vivo. Verifica los precios vigentes en el tarificador oficial; lo que no cambia es la proporción.

Las cinco medidas de sostenibilidad del ciclo: predicción por lotes en lugar de endpoint siempre que el resultado no dependa de la sesión en curso; VMs Spot con checkpoints para el entrenamiento (05-03), porque los reentrenamientos semanales no tienen urgencia; caché del pipeline en desarrollo, con la precaución del apartado 11; frecuencia de reentrenamiento proporcional al ritmo de cambio, ya que reentrenar a diario un modelo cuyos patrones cambian en meses es gastar por gastar; y retención de artefactos, con una política de ciclo de vida en Cloud Storage (02-02) que pase a Nearline a los 30 días y borre a los 180, porque los Parquet y los checkpoints de cada ejecución se acumulan en pipeline_root sin límite.

Y la observación de fondo, más allá de la factura: el coste computacional tiene un coste energético. Reentrenar innecesariamente, dejar GPUs encendidas o servir 24×7 lo que se puede precalcular no es solo caro: es consumo sin propósito.

  1. El checklist de ML responsable

Este apartado recoge todo lo que el módulo ha ido dejando por el camino. Es el checklist que hay que completar antes de que un modelo tome decisiones que afecten a personas.

Datos

  • [ ] ¿Cuál es la base legal y está la finalidad cubierta por la información dada al recoger los datos?
  • [ ] ¿Se usa el mínimo dato necesario, entrenando sobre vistas seudonimizadas (04-07) y no sobre tablas con datos personales directos?
  • [ ] ¿Hay plazo de conservación del conjunto de entrenamiento y se puede atender una supresión? ¿El linaje dice qué modelos incluyeron los datos de una persona?
  • [ ] ¿Los datos son representativos de la población sobre la que se aplicará el modelo?

Sesgo y equidad

  • [ ] ¿Se ha medido el rendimiento por segmento —dispositivo, canal, geografía, antigüedad— y no solo en agregado? ¿Hay algún grupo para el que funcione significativamente peor?
  • [ ] ¿Existe un bucle de retroalimentación en el que el modelo fabrique la realidad que predice (05-02, apartado 15)?
  • [ ] ¿La métrica de evaluación refleja lo que importa al negocio y a las personas afectadas?

Explicabilidad

  • [ ] ¿Se puede explicar por qué el modelo tomó una decisión concreta, con atribuciones locales, y en términos comprensibles para quien recibe la explicación?
  • [ ] ¿La importancia global de características tiene sentido, o hay alguna con peso sospechosamente alto que revele una fuga?

Supervisión humana

  • [ ] ¿Hay una persona en el bucle para las decisiones con impacto significativo, y un canal de reclamación con plazo de respuesta?
  • [ ] ¿Se puede desactivar el modelo rápidamente y volver al proceso anterior?
  • [ ] ¿Alguien tiene asignada la responsabilidad de vigilarlo, con nombre y apellidos?

Documentación

  • [ ] ¿Existe una tarjeta de modelo con propósito, datos, métricas y limitaciones conocidas?
  • [ ] ¿Está registrado quién aprobó el despliegue y sobre qué evidencia, y permite el linaje reconstruir qué datos entrenaron qué versión?

Cumplimiento

  • [ ] ¿Se trata de una decisión automatizada del artículo 22 del RGPD? Si hay duda, hay que preguntar.
  • [ ] ¿Cuál es la clasificación bajo el AI Act, qué obligaciones conlleva, y sigue el uso actual correspondiendo a esa clasificación?
  • [ ] ¿Hace falta una evaluación de impacto (EIPD)? ¿Se cumplen las obligaciones de transparencia?
  • [ ] ¿Lo ha revisado compliance o el DPO?

Recomendación expresa y final del módulo. Este checklist es una ayuda para estructurar el trabajo técnico y no sustituye a un dictamen jurídico. La clasificación de un sistema bajo el AI Act, la determinación de si un tratamiento constituye decisión automatizada bajo el artículo 22 del RGPD, la necesidad de una evaluación de impacto y el alcance de las obligaciones de transparencia deben ser determinados por un profesional de compliance o por el delegado de protección de datos antes de poner el sistema en producción. Todos los datos y escenarios de este curso son ficticios.

Y la regla que resume el módulo entero: si al recorrer este checklist hay más de dos casillas que no puedes marcar con seguridad, el modelo no está listo para producción, por bueno que sea su recall@10.

Errores Comunes y Consejos

No tener puerta de decisión. Desplegar el modelo nuevo porque es el nuevo es el error más frecuente y el más caro. Tres líneas de dsl.If lo evitan.

No medir contra la línea base. Si el candidato no supera treinta líneas de SQL, no hay sistema de ML que justificar.

Umbral de mejora en 0 %. Desplegar por ruido estadístico produce cambios semanales sin ganancia real. El umbral debe superar la variabilidad natural de la métrica.

Confiar en la caché del pipeline sin pensar. Puede devolver datos antiguos de una ejecución previa. Incluye la fecha como parámetro en los componentes que leen datos frescos.

Que un fallo de calidad no detenga el pipeline. Avisar y seguir produce lo peor: modelo malo desplegado con una alerta que nadie leyó.

Usar latest en imágenes o no fijar versiones de librerías, que rompe la reproducibilidad; y ejecutar el pipeline con permisos amplios en lugar de una cuenta de servicio dedicada de permisos mínimos.

Confundir mejora offline con mejora de negocio. El pipeline decide qué es candidato; el A/B test decide qué va a producción.

Reentrenar más de lo que cambia el fenómeno. Diario cuando los patrones cambian en meses es gasto sin retorno.

Consejo: registra las tres métricas siempre —candidato, producción y línea base—: convierte cualquier discusión sobre si el modelo mejora en una consulta. Pon el nombre de una persona responsable de cada modelo, porque un modelo sin dueño es un modelo que nadie mira. Y genera la tarjeta de modelo como artefacto del pipeline: la documentación que se regenera sola no se queda obsoleta.

Ejercicios

Ejercicio 1

El pipeline lleva seis semanas ejecutándose. En las seis, la puerta de decisión no se ha abierto: ningún candidato ha superado el umbral del 2 %. Dani propone bajar el umbral al 0,5 % "para que al menos se actualice". Analiza la propuesta y di qué harías.

Ejercicio 2

Diseña la puerta de sesgo por segmento que falta en el pipeline. Indica qué segmentos comprobarías, qué criterio aplicarías y cómo lo implementarías como componente.

Ejercicio 3

Un cliente reclama: dice que la web le recomendó unos crampones incompatibles con sus botas, los compró, y no le sirven. Pide explicaciones. Enumera qué información necesitas, qué debería estar registrado, y qué cambiarías en el sistema.

Soluciones

Solución 1

La propuesta de Dani es incorrecta, pero la pregunta que hay detrás es buena.

Por qué bajar el umbral es un error. El umbral del 2 % se fijó porque es mayor que la variabilidad natural de la métrica. Con un 0,5 %, se desplegarían modelos cuya "mejora" es indistinguible del ruido de la semilla aleatoria. Se cambiaría de modelo cada semana, cada cambio requeriría validación, y el sistema iría dando bandazos sin ninguna ganancia. Y el objetivo del pipeline no es actualizar el modelo: es que el mejor modelo esté en producción. Que la puerta no se abra significa que el modelo actual sigue siendo el mejor, que es exactamente el resultado deseado.

Pero seis semanas seguidas sí es información, y hay que investigarla. Cuatro hipótesis, en orden de probabilidad. (1) El modelo actual ya es bueno y el enfoque ha llegado a su techo, lo más probable: reentrenar la misma arquitectura con datos ligeramente distintos no produce saltos, y la respuesta no es cambiar el umbral sino cambiar de enfoque —añadir características, otra arquitectura, señales de sesión— o aceptar que el modelo está bien y bajar la frecuencia a mensual. (2) No hay datos nuevos suficientes: con 730 días de historia, una semana más son un 0,14 % adicional. (3) Hay un problema en el pipeline: ¿está la caché devolviendo siempre los mismos datos? ¿coge el componente de extracción la misma ventana? Es la primera comprobación técnica. (4) La métrica no captura la mejora: recall@10 puede estar saturado, y quizá el modelo mejora en algo que no ve —diversidad, cobertura del catálogo, resultados para clientes nuevos—.

-- Evolucion de las tres metricas a lo largo de las ejecuciones
SELECT
  fecha_ejecucion,
  ROUND(recall_candidato, 4)   AS candidato,
  ROUND(recall_produccion, 4)  AS produccion,
  ROUND(recall_baseline, 4)    AS baseline,
  ROUND(mejora_relativa, 4)    AS mejora,
  filas_entrenamiento
FROM `alpinashop-datos.alpinashop_analitica.historico_pipeline_reco`
ORDER BY fecha_ejecucion DESC LIMIT 12;

Qué haría, en tres pasos. Primero, mantener el umbral en 2 %: no se toca un criterio de calidad para conseguir el resultado que uno quiere ver, y eso es exactamente lo que el umbral existe para impedir. Segundo, investigar con la consulta anterior: si el recall_candidato es prácticamente idéntico semana tras semana, es la hipótesis 2 o 3; si oscila sin tendencia, la 1. Y tercero, actuar según el diagnóstico: si el enfoque tocó techo, bajar la frecuencia a mensual (ahorro directo) y abrir una línea de trabajo con características nuevas; si es un problema de pipeline, arreglarlo; si es la métrica, añadir métricas secundarias de diversidad y cobertura y considerar un A/B test aunque la mejora offline sea pequeña.

Y una observación cultural que vale más que la técnica: que un equipo tenga un criterio objetivo y lo respete cuando el resultado no le gusta es la señal más fiable de madurez. El umbral solo sirve si se mantiene también cuando incomoda.

Solución 2

Segmentos a comprobar en AlpinaShop:

Segmento Valores Por qué importa
Antigüedad del cliente Nuevo (<3 pedidos) / recurrente El arranque en frío afecta a los nuevos
Dispositivo Móvil / escritorio Bucle de retroalimentación (05-02)
Geografía Península / Baleares / Canarias Catálogo y logística distintos
Categoría de compra Textil / material técnico / calzado Volúmenes muy distintos
Volumen de gasto Alto / medio / bajo Que no mejore solo para los que más compran

Criterio de la puerta, con tres condiciones: que ningún segmento con volumen relevante empeore más de un 5 % relativo respecto al modelo en producción; que la mejora agregada del 2 % se mantenga; y que solo se evalúen segmentos con al menos 500 clientes en el test, porque por debajo la métrica es ruido.

@component(base_image="python:3.11",
           packages_to_install=["pandas==2.2.2", "pyarrow==17.0.0"])
def validar_segmentos(modelo_candidato: Input[Model], datos_test: Input[Dataset],
                      metricas_segmento: Output[Metrics],
                      tolerancia: float = -0.05,
                      minimo_clientes: int = 500) -> bool:
    import pandas as pd
    df = pd.read_parquet(datos_test.path)
    problemas = []

    for columna in ["segmento_antiguedad", "dispositivo", "zona", "categoria_top"]:
        for valor, grupo in df.groupby(columna):
            n = grupo["cliente_hash"].nunique()
            if n < minimo_clientes:
                continue
            r_cand = calcular_recall_at_k(modelo_candidato.path, grupo, k=10)
            r_prod = obtener_metrica_produccion_segmento(columna, valor)
            delta  = (r_cand - r_prod) / max(r_prod, 1e-9)
            metricas_segmento.log_metric(f"{columna}={valor}", delta)
            if delta < tolerancia:
                problemas.append(f"{columna}={valor}: {delta:+.1%} (n={n})")

    if problemas:
        print("SEGMENTOS QUE EMPEORAN:", problemas)
    return len(problemas) == 0

Dónde va en el pipeline: entre la evaluación agregada y la puerta de decisión, anidando un segundo dsl.If sobre segmentos.output == True dentro del dsl.If de la mejora agregada, de forma que la puerta exija ambas condiciones.

Tres consideraciones importantes. El mínimo de 500 clientes es imprescindible: sin él, un segmento con 12 clientes produciría oscilaciones enormes que bloquearían despliegues válidos, y una puerta que bloquea siempre acaba desactivada. La tolerancia del −5 % no es cero por una razón: exigir que ningún segmento empeore nada es imposible en la práctica, y lo que se persigue es evitar degradaciones significativas, no fluctuaciones. Y los resultados se registran aunque no bloqueen: metricas_segmento guarda el delta de todos los segmentos evaluados, y con el tiempo esa serie revela tendencias —si "móvil" lleva ocho semanas por debajo del agregado sin llegar a bloquear, hay un problema estructural que ninguna ejecución individual habría destapado—.

Y una advertencia de honestidad: esta puerta detecta que un modelo empeora para un grupo respecto al anterior, pero no detecta que funcione mal para ese grupo desde siempre. Para eso hay que mirar los valores absolutos por segmento, no solo los deltas. Ambas cosas van en el checklist del apartado 17.

Solución 3

Qué información necesitas para responder: qué recomendación exacta se le mostró, cuándo y en qué contexto (ficha, carrito, correo); qué versión del modelo la generó; qué datos entrenaron esa versión; por qué el modelo puntuó alto ese producto para ese cliente (atribución local); qué información de compatibilidad existía en la ficha en ese momento; y qué texto acompañaba a la recomendación en la web.

Qué debería estar registrado. La mayor parte ya lo está si se siguió la lección:

Dato Dónde está ¿Existe?
Recomendación mostrada reco_publicada con actualizada_en
Versión del modelo Columna modelo_origen Sí, apartado 10
Datos de entrenamiento Metadatos del artefacto Dataset Sí, apartado 5
Métricas de esa versión Model Registry
Ficha del producto en esa fecha Historial de cambios del catálogo Probablemente no
Texto mostrado junto a la recomendación Configuración del front Probablemente no

Las dos que faltan son justamente las que más importan aquí, y eso ya es un hallazgo del análisis.

Y ahora la parte incómoda, que es donde está la lección real: aunque todo estuviera registrado, la respuesta técnica sería "el modelo sugirió ese producto porque clientes con un perfil de compra similar compraron ambos". Eso es cierto, es explicable, y no resuelve nada para el cliente, porque el problema no es que la recomendación fuera estadísticamente rara: es que el sistema no sabe nada de compatibilidad técnica. Un recomendador aprende asociaciones de compra y no sabe que un crampón automático necesita una bota con inserto en la puntera. Nunca lo aprendió porque nadie se lo enseñó, y esa información no está en lineas_pedido: está en la ficha técnica, y ningún componente del pipeline la mira.

Qué cambiaría en el sistema, en cuatro medidas. La primera, un filtro duro de compatibilidad aplicado antes que ningún modelo, que es la que resuelve el problema de verdad:

CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.reco_publicada` AS
SELECT r.*
FROM `alpinashop-datos.alpinashop_analitica.reco_candidata` r
LEFT JOIN `alpinashop-datos.alpinashop_analitica.incompatibilidades` i
  ON i.sku_a = r.sku
WHERE r.posicion <= 20
  AND NOT EXISTS (
    -- No recomendar nada incompatible con lo que el cliente ya tiene
    SELECT 1
    FROM `alpinashop-datos.alpinashop_analitica.v_productos_cliente` c
    JOIN `alpinashop-datos.alpinashop_analitica.incompatibilidades` x
      ON x.sku_a = c.sku AND x.sku_b = r.sku
    WHERE c.cliente_hash = r.cliente_hash
  );

Es la misma lección que el stock > 0 de 05-01, en otra dimensión. Hay reglas de negocio que no deben aprenderse de los datos: se aplican como filtro. Un modelo, por bueno que sea, no debe ser la última línea de defensa frente a una recomendación técnicamente peligrosa.

La segunda, avisos de compatibilidad en la interfaz: los productos con requisitos técnicos —crampones, fijaciones, sistemas de anclaje— llevan un aviso visible junto a la recomendación ("Requiere bota con inserto delantero. Comprueba la compatibilidad."). Es barato y evita la mayoría de estos casos. La tercera, lenguaje honesto: "otros clientes también compraron" describe lo que el sistema hace de verdad, mientras que "te recomendamos" o "compatible con tu compra" sugieren un juicio técnico que el sistema no está haciendo, y la formulación importa jurídicamente, no solo estéticamente. Y la cuarta, una puerta adicional en el pipeline: ningún despliegue si la tabla de incompatibilidades no está actualizada o si el filtro no se aplica, comprobado con un caso de prueba conocido.

La respuesta al cliente: reconocer el problema sin escudarse en el sistema, aceptar la devolución aunque el producto se haya usado para probarlo, explicar en lenguaje llano que la sugerencia se basaba en patrones de compra y no en una verificación de compatibilidad, e informar de que se ha corregido. Y registrar el caso, porque un cliente que reclama representa a muchos que no lo hicieron.

Y la reflexión que cierra el módulo: este caso no es un fallo del modelo, que hizo exactamente lo que se le pidió —encontrar productos que gente parecida compra junta—. El fallo es del sistema alrededor del modelo, que no incorporó una regla de negocio evidente. Y eso es MLOps: no la parte de entrenar, sino toda la parte que impide que un modelo correcto produzca un resultado incorrecto.

Conclusión

El módulo 5 se cierra aquí, y se cierra con el sistema completo.

Sabes por qué la mayoría de los modelos no llegan a producción, y ninguna de las seis razones es de machine learning: falta de reproducibilidad, dependencia de personas, ausencia de criterio de despliegue, falta de trazabilidad, ausencia de vigilancia e imposibilidad de volver atrás. Todos son problemas de ingeniería y de operaciones. Y conoces la asimetría que hace que MLOps no sea DevOps con otro nombre: en un sistema de ML cambian el código y los datos, la degradación ocurre sola porque el mundo se mueve, la prueba de aceptación es estadística y no determinista, y el fallo típico es silencioso: un modelo que acierta un 15 % menos no lanza ninguna excepción.

Conoces los tres niveles de madurez y que el salto del 0 al 1 —de notebooks y ejecuciones manuales a un pipeline reproducible que se ejecuta solo— aporta la mayor parte del valor. El nivel 2, con CI/CD completo, llega con Cloud Build en 06-01.

Has construido el pipeline real de AlpinaShop con Vertex AI Pipelines y el SDK de KFP: componentes, artefactos, parámetros y un grafo cuyas dependencias se infieren solas. Extraer los datos con la división temporal y el JOIN histórico que evita la fuga de 05-02. Validar la calidad reutilizando las reglas de Dataplex de 04-07, con la decisión de diseño más importante de la lección: que un fallo de calidad detiene el pipeline en lugar de avisar y seguir. Entrenar lanzando un trabajo de Vertex AI con imagen versionada. Evaluar contra el modelo en producción y contra la línea base heurística, siempre las tres métricas, porque si el candidato no supera treinta líneas de SQL no hay sistema que justificar. Y la puerta de decisión: tres líneas de dsl.If que convierten un proceso de reentrenamiento en un sistema con criterio, con un umbral mayor que la variabilidad natural de la métrica y con la disposición a respetarlo cuando el resultado no gusta.

Sabes registrar como nueva versión con parent_model y alias candidato —separando registro de promoción—, y publicar por lotes la tabla que consume la tienda, con la columna modelo_origen que hace trazable cada recomendación que ve un cliente. Compilas el pipeline a un YAML que va a Git, lo ejecutas con una cuenta de servicio de permisos mínimos, conoces la trampa de la caché, y lo encadenas desde Workflows para que no se ejecute si el proceso nocturno de datos falló.

Tienes los metadatos y el linaje respondiendo a las cinco preguntas de una auditoría, incluida la que sorprende a todo el mundo: qué modelos incluyeron los datos de un cliente que ejerce su derecho de supresión. Sabes cuándo reentrenar combinando periodicidad, deriva y degradación, con la advertencia de que una alerta de deriva estacional no es una avería y de que el ruido produce indiferencia. Tienes las cuatro cosas que hay que versionar —código, datos, modelo y entorno— con su lista de comprobación. Y tienes el cálculo que cierra el argumento del módulo: un endpoint 24×7 para servir recomendaciones que cambian una vez por semana cuesta un orden de magnitud más que la predicción por lotes, sin ninguna ventaja funcional.

Por último, el checklist de ML responsable: datos con base legal y mínimo necesario, sesgo medido por segmento y no solo en agregado, explicabilidad comprensible para quien la recibe, supervisión humana con canal de reclamación y capacidad de desactivar, documentación con tarjeta de modelo y linaje, y cumplimiento con RGPD y AI Act revisado por compliance antes de producción, no después. Con la regla que resume el módulo: si hay más de dos casillas que no puedes marcar con seguridad, el modelo no está listo, por bueno que sea su recall@10.

Y mira dónde está AlpinaShop ahora. Tiene una plataforma de datos gobernada del módulo 4, modelos que predicen, clasifican, generan y recomiendan, y un pipeline que reentrena solo, valida solo, decide solo y despliega solo cuando mejora, con trazabilidad completa y con puertas que impiden que un dato malo o un modelo peor lleguen a producción.

Pero ahora levanta la vista del machine learning y mira el resto del sistema, porque la comparación es demoledora. El recomendador tiene más disciplina de ingeniería que la tienda.

El catálogo web en Flask que Dani mantiene se despliega copiando ficheros a mano. La infraestructura del módulo 3 —la VPC, el balanceador, las reglas de Cloud Armor, los certificados— se creó con comandos de gcloud que están en el historial del terminal de Marta y en ningún otro sitio: si hubiera que reconstruir el entorno en otra región, nadie sabría exactamente cómo. No hay pruebas automáticas: se sube y se comprueba mirando la web. No hay entornos separados de verdad: alpinashop-dev existe, pero lo que se prueba allí no es lo que se despliega en alpinashop-prod. Y cuando algo va mal en producción, la forma de enterarse es que un cliente escriba un correo. Es exactamente el nivel 0 de madurez que acabas de superar para los modelos, aplicado a todo lo demás.

En el módulo 6, DevOps y monitoreo, AlpinaShop lleva al resto de la plataforma la disciplina que acaba de aplicar al ciclo del modelo. Empezaremos por 06-01, Cloud Build, la integración continua que construye, prueba y publica cada cambio de forma automática y reproducible —y que, de paso, es la pieza que faltaba para llevar el pipeline de esta lección del nivel 1 al nivel 2—. Después llegarán Cloud Source Repositories y la gestión del código, Cloud Functions para las piezas por eventos que este módulo ha ido dejando pendientes —el análisis de cada imagen nueva del topic imagenes-subidas, entre otras—, Cloud Monitoring para saber qué está pasando sin abrir la consola, Deployment Manager y Terraform para que la infraestructura deje de vivir en el historial de un terminal y pase a estar escrita, versionada y revisable como cualquier otro código, y Cloud Logging y Trace para que cuando algo falle se pueda averiguar por qué en minutos y no en tardes.

Hay aplicación, hay datos y hay modelos. Todos funcionan, y todos dependen de que dos personas se acuerden de ejecutarlos. Ha llegado el momento de automatizar la entrega, escribir la infraestructura como código y enterarse de los problemas antes que los clientes.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados