Serialización, inferencia, APIs, monitoreo, drift y fundamentos de MLOps.
Author
Wilder Ramírez Delgado
Published
September 11, 2026
Despliegue de modelos y MLOps
Serie Deep Learning · del perceptrón a los transformers.
En este artículo cerramos el ciclo: serializamos una CNN, servimos inferencia con un contrato tipo API, detectamos data drift y model drift, y situamos el reentrenamiento dentro de MLOps, no como un fit suelto.
1. Introducción
Entrenar no es desplegar. Un accuracy de notebook no llega al usuario: hay que congelar pesos, exponer una entrada/salida estable, registrar lo que se predijo y decidir cuándo el modelo dejó de valer.
MLOps es la unión de Machine Learning y operaciones: versionar datos, código y modelos; automatizar entrenamiento y publicación; observar el servicio. No es «subir un .h5 a un VPS».
Pregunta: ¿qué artefactos y qué alarmas hacen falta para que una CNN de la sesión 6 sobreviva fuera del laboratorio?
SERIALIZAR → INFERIR → API → CICLO DE VIDA → MONITOREO → DRIFT → REENTRENAMIENTO → MLOPS.
2. Serialización de modelos
Serializar es escribir el grafo y los pesos en un artefacto reproducible. En Keras 3 el formato nativo es una carpeta/archivo .keras. Alternativas: SavedModel (TensorFlow Serving), ONNX (interop), TFLite (móvil).
Reglas que evitan sustos:
Guarde después de elegir el checkpoint (mejor val_loss), no solo el último fit.
Versionar el artefacto (mnist_cnn_2026-09-11.keras), no sobrescribir modelo.keras.
Guardar junto el preproceso (escala [0,1], tamaño 28×28, orden de canales). Un modelo correcto con un preproceso distinto es un modelo distinto.
Idea clave
El artefacto no es solo pesos. Es pesos + contrato de entrada. Si el contrato se rompe, el servicio «funciona» y predice basura.
La inferencia de producción no reentrena. Carga el artefacto, aplica el mismo preproceso y devuelve la distribución (no solo el argmax) para poder rechazar predicciones inciertas.
modelo_prod = tf.keras.models.load_model(ruta)print(modelo_prod.evaluate(x_te[:2000], y_te[:2000], verbose=0))def preprocesar(lote_uint8):"""Contrato: uint8 (N,28,28) → float32 (N,28,28,1) en [0,1].""" x = np.asarray(lote_uint8, dtype="float32") /255.0if x.ndim ==3: x = x[..., None]return xdef inferir(lote_uint8, umbral=0.45): x = preprocesar(lote_uint8) probs = modelo_prod.predict(x, verbose=0) clases = np.argmax(probs, axis=1) conf = probs[np.arange(len(clases)), clases]return [ {"clase": int(c),"confianza": float(k),"rechazar": bool(k < umbral),"probs": [float(p) for p in row], }for c, k, row inzip(clases, conf, probs) ]muestra = (x_te[:3] *255).astype("uint8").squeeze()print(json.dumps(inferir(muestra), indent=2)[:500], "...")
En un servicio real, inferir se envuelve en HTTP (FastAPI, Flask, TensorFlow Serving, Vertex, SageMaker). El contrato JSON es más importante que el framework:
Esta sesión simula el servicio en proceso: misma función, sin abrir un puerto. Así el artículo corre en cualquier máquina del curso.
Checklist del endpoint:
validar forma y rango antes de llamar a TF,
timeout y tamaño máximo de lote,
no loguear imágenes crudas si hay PII,
versionar /v1/prediccion cuando cambie el contrato.
class ServicioModelo:def__init__(self, path, umbral=0.45):self.modelo = tf.keras.models.load_model(path)self.umbral = umbralself.n_consultas =0self.confianzas = []def predecir(self, payload): inst = np.asarray(payload["instances"], dtype="uint8")if inst.ndim ==3:passelif inst.ndim ==2: inst = inst[None, ...]else:raiseValueError("instances debe ser (28,28) o (N,28,28)") out = inferir(inst, umbral=self.umbral)self.n_consultas +=len(out)self.confianzas.extend(r["confianza"] for r in out)return {"model": "fashion_cnn_v1", "predictions": out}servicio = ServicioModelo(ruta)resp = servicio.predecir({"instances": (x_te[0] *255).astype("uint8").squeeze()})print(resp["predictions"][0]["clase"], resp["predictions"][0]["confianza"])print("consultas:", servicio.n_consultas)
9 0.6872240304946899
consultas: 1
5. Ciclo de vida del modelo
dato → entrenamiento → evaluación → registro → despliegue → monitoreo → reentrenamiento
↑ |
+------------------------- disparador --------------------+
Cada flecha tiene dueño y criterio. Si el reentrenamiento es «cuando alguien se acuerde», no hay ciclo: hay artesanía.
Versiones: v1 en producción, v2 candidata. Se promociona si gana en un test fijo y no empeora un slice crítico (por ejemplo la clase Shirt).
6. Monitoreo, data drift y model drift
Data drift: cambia (p(x)). Las fotos llegan más oscuras, o llega una prenda nueva.
Concept drift / model drift: cambia (p(y x)). La misma silueta ahora se etiqueta distinto (temporada, normativa).
Drift de predicción: cambia la distribución de () o de la confianza, síntoma de cualquiera de los dos.
Un test de Kolmogorov–Smirnov sobre un estadístico de la imagen (brillo medio) detecta un corrimiento univariado. No detecta un cambio sutil de estilo. Es una alarma, no un diagnóstico.
brillo_ref = x_tr[:4000].mean(axis=(1, 2, 3))brillo_prod_ok = x_te[:2000].mean(axis=(1, 2, 3))brillo_prod_drift = np.clip(x_te[:2000] *0.55+0.05, 0, 1).mean(axis=(1, 2, 3))ks_ok = ks_2samp(brillo_ref, brillo_prod_ok)ks_dr = ks_2samp(brillo_ref, brillo_prod_drift)print("KS producción ≈ train: stat=%.3f p=%.4g"% (ks_ok.statistic, ks_ok.pvalue))print("KS producción oscura: stat=%.3f p=%.4g"% (ks_dr.statistic, ks_dr.pvalue))# Model drift simulado: el servicio sigue con v1, las etiquetas «reales» se sesganpred = modelo_prod.predict(x_te[:2000], verbose=0).argmax(1)acc_ahora =float((pred == y_te[:2000]).mean())y_desplazado = y_te[:2000].copy()y_desplazado[y_desplazado ==6] =0# Shirt se relabeléa como T-shirtacc_concepto =float((pred == y_desplazado).mean())print(f"Accuracy vs etiqueta original: {acc_ahora:.4f}")print(f"Accuracy vs concepto desplazado (Shirt→T-shirt): {acc_concepto:.4f}")fig, ax = plt.subplots(figsize=(6, 3))ax.hist(brillo_ref, bins=30, alpha=0.5, label="Referencia train", density=True)ax.hist(brillo_prod_drift, bins=30, alpha=0.5, label="Producción oscura", density=True)ax.set_title("Data drift de un estadístico (brillo)")ax.legend()plt.tight_layout()plt.show()
KS producción ≈ train: stat=0.014 p=0.9483
KS producción oscura: stat=0.317 p=0
Accuracy vs etiqueta original: 0.8490
Accuracy vs concepto desplazado (Shirt→T-shirt): 0.8080
Idea clave
Una alarma de drift no reentrena sola. Dispara una revisión: ¿cambió el sensor, la etiqueta, o el umbral de negocio?
Reto técnico
Fije un umbral de KS y un mínimo de 500 consultas. Escriba la regla if ...: crear_ticket() sin llamar a fit dentro del if.
7. Reentrenamiento periódico
Criterios honestos:
venció la ventana (cada lunes, cada 10k etiquetas nuevas),
cayó un KPI de negocio (devoluciones, clics), no solo el accuracy interno,
el slice crítico empeoró.
El reentrenamiento usa dato nuevo + dato de ancla (para no olvidar clases raras) y un test que no se reutiliza como validación del ajuste. El artefacto nuevo entra como candidata, no como reemplazo silencioso.
8. Fundamentos de MLOps (mapa)
Pieza
Pregunta que responde
Control de versiones (git + DVC/Lake)
¿Qué dato y qué código produjeron v1?
Registro de modelos
¿Cuál está en prod y con qué métricas?
CI/CD
¿Un push reentrena y testea o solo lint?
Serving
¿Quién atiende POST /prediccion?
Observabilidad
¿Latencia, tasa de rechazo, drift?
Gobernanza
¿Quién puede promover v2?
Este artículo implementa tres ladrillos (artefacto, contrato de inferencia, alarma KS). El resto es organización. Sin esos tres, el resto es diapositiva.
9. Amenazas a la validez
El «servicio» vive en el kernel: no mide red, autenticación ni carga.
KS en brillo no es un monitor de visión real.
El relabel Shirt→T-shirt es un proxy grosero de concept drift.
Tres épocas y 8 000 imágenes: el .keras es didáctico.
10. Ejercicios (sin soluciones completas)
Añada al JSON de respuesta un model_version leído de un metadata.json al lado del .keras.
Simule 200 consultas, calcule la tasa de rechazar y grafíquela por lote de 20.
Implemente un segundo estadístico (varianza espacial) y combine alarmas con AND/OR. ¿Qué cambia?
Escriba el contrato de promoción v1 → v2 en cinco viñetas (métrica, slice, rollback).
Sin escribir Flask, liste tres cabeceras HTTP que pediría en producción (auth, request-id, timeout).
💬 ¿Te sirvió?Deja en los comentarios una duda o un caso donde aplicarías esto —respondo todos. Sígueme para no perderte el próximo artículo de la serie ycomparte con alguien que esté aprendiendo deep learning.👉 El código completo está disponible para ejecutar directamente.