Un modelo con un 97 % de precisión que solo existe en tu sesión de Python vale exactamente cero para el negocio. El equipo de TecnoMarket lo tiene claro: el clasificador de reseñas de 04-03 solo aporta valor cuando la web puede consultarlo en vivo, y el clasificador de fotos de 05-03 solo ahorra trabajo cuando el sistema del almacén lo invoca con cada producto nuevo. Esta lección cierra el ciclo que empezó con aquel tímido model.save() de 02-05: aprenderás los formatos de guardado de Keras y PyTorch y cuándo usar cada uno, por qué el preprocesado debe guardarse junto al modelo, las opciones para servir predicciones (de un script batch a una API REST con FastAPI, con ejemplo completo), ONNX como puente entre frameworks, y cómo validar y vigilar un modelo antes y después de desplegarlo. Es la última milla: donde el deep learning se convierte en producto.

Contenido

  1. Guardar y cargar en Keras: .keras, SavedModel y solo pesos
  2. Guardar y cargar en PyTorch: state_dict y checkpoints
  3. El modelo no viaja solo: guardar el preprocesado
  4. Servir un modelo: opciones en escala
  5. Una API REST con FastAPI para las reseñas de TecnoMarket
  6. ONNX: el formato puente
  7. Validar antes de desplegar y vigilar después
  8. Checklist de despliegue de TecnoMarket

Guardar y cargar en Keras: .keras, SavedModel y solo pesos

En 02-05 hiciste model.save("tecnomarket-dl/models/mi_red.keras") y seguimos adelante. Profesionalicémoslo: Keras tiene tres formas de guardar, cada una con su caso de uso.

from tensorflow import keras

# 1. Formato .keras (el recomendado): TODO en un fichero
model.save("models/resenas_v3.keras")
modelo_cargado = keras.models.load_model("models/resenas_v3.keras")
# Guarda: arquitectura + pesos + estado del optimizador + compile()
# -> puedes seguir entrenando exactamente donde lo dejaste

# 2. SavedModel (formato de TensorFlow, un directorio completo)
model.export("models/resenas_v3_savedmodel")
# Guarda el grafo de computación autocontenido, SIN necesitar tu código Python
# -> es lo que consumen TF Serving y los conversores (TFLite)

# 3. Solo pesos
model.save_weights("models/resenas_v3.weights.h5")
modelo_nuevo = crear_modelo()               # necesitas RECONSTRUIR la arquitectura
modelo_nuevo.load_weights("models/resenas_v3.weights.h5")
Formato Qué guarda Cuándo usarlo
.keras Arquitectura + pesos + optimizador Uso general: pausar/reanudar, archivar, compartir con quien tenga Keras
SavedModel (export) Grafo autocontenido para inferencia Entregar a sistemas de producción TF (Serving, TFLite)
Solo pesos Únicamente los números aprendidos El código define la arquitectura (transfer learning como en 05-03, checkpoints ligeros)

Nota práctica: el ModelCheckpoint de 06-01 escribe en formato .keras; ya llevabas media lección aprendida.

Guardar y cargar en PyTorch: state_dict y checkpoints

PyTorch gira en torno al state_dict: un diccionario {nombre_de_parametro: tensor} con todos los pesos del modelo. La forma canónica de guardar es ese diccionario, no el objeto modelo:

import torch

# GUARDAR: la forma recomendada (solo el state_dict)
torch.save(model.state_dict(), "models/resenas_v3.pt")

# CARGAR: reconstruir la arquitectura y volcarle los pesos
model = ClasificadorResenas()                       # la clase de 06-02
model.load_state_dict(torch.load("models/resenas_v3.pt"))
model.eval()                                        # ¡a modo inferencia! (ver 06-02)

¿Por qué no guardar el modelo entero con torch.save(model, ...)? Puede hacerse, pero serializa referencias a tu código (clase, módulo, rutas): al cargarlo meses después con el proyecto reorganizado, se rompe. El state_dict son solo tensores con nombre: robusto y portable. Es el análogo del "solo pesos" de Keras — en PyTorch, la opción por defecto.

Checkpoint completo: pausar y reanudar entrenamientos

Para reanudar un entrenamiento no basta con los pesos: Adam (02-04) mantiene estadísticas internas por parámetro que también hay que restaurar. El patrón profesional:

# Guardar un checkpoint completo al final de cada época
checkpoint = {
    "epoch": epoch,
    "model_state": model.state_dict(),
    "optimizer_state": optimizer.state_dict(),   # ¡el estado de Adam también!
    "val_loss": val_loss,
}
torch.save(checkpoint, "models/checkpoint_ultimo.pt")

# Reanudar días después, exactamente donde se quedó
ckpt = torch.load("models/checkpoint_ultimo.pt")
model.load_state_dict(ckpt["model_state"])
optimizer.load_state_dict(ckpt["optimizer_state"])
epoca_inicial = ckpt["epoch"] + 1
Necesidad Keras PyTorch
Archivar/compartir modelo listo .keras state_dict + código de la clase
Reanudar entrenamiento .keras (lleva el optimizador) checkpoint dict (modelo + optimizador + época)
Entregar a producción SavedModel state_dict (o exportar a ONNX/TorchScript)

El modelo no viaja solo: guardar el preprocesado

Aquí está el error que más despliegues rompe, y es conceptual, no técnico. Tu modelo aprendió sobre datos transformados: si en producción recibe datos crudos o transformados de otra manera, sus predicciones serán basura sin dar ningún error. Recuerda dos casos del curso:

  • En 04-04, el predictor de demanda entrenó con ventas escaladas por un scaler ajustado sobre el train (con cuidado de no fugar información). Si en producción le pasas euros crudos donde esperaba valores en [0, 1], predirá disparates con total seguridad en sí mismo.
  • En 04-03, el clasificador de reseñas usa una capa TextVectorization cuyo vocabulario se aprendió del corpus de entrenamiento. Otro vocabulario = otros índices = otra "lengua": la red recibiría galimatías.

La regla de oro: el par (preprocesado, modelo) es LA unidad desplegable. Nunca viajan por separado.

# Opción A (Keras): meter el preprocesado DENTRO del modelo antes de guardar
# (TextVectorization es una capa: puede formar parte del grafo)
modelo_completo = keras.Sequential([
    keras.Input(shape=(1,), dtype="string"),   # ¡entra TEXTO crudo!
    vectorizador,                              # la TextVectorization ya adaptada (04-03)
    modelo_entrenado,                          # la red que espera índices
])
modelo_completo.save("models/resenas_completo.keras")
# En producción: modelo.predict(["El cargador llego roto"])  -> directo del texto

# Opción B (general, válida también para PyTorch): guardar el transformador aparte
import joblib
joblib.dump(scaler, "models/demanda_scaler.joblib")   # el scaler de 04-04
# ...y en producción, SIEMPRE en pareja:
scaler = joblib.load("models/demanda_scaler.joblib")
x = scaler.transform(datos_crudos)                    # mismo scaler, mismos parámetros
prediccion = model.predict(x)

La opción A es la más segura (imposible desincronizar); la B es la más flexible. Elijas cual elijas: mismos ficheros versionados juntos, con el mismo nombre de versión.

Servir un modelo: opciones en escala

"Desplegar" no significa siempre "montar un servidor". Hay una escala de opciones, de menor a mayor complejidad, y lo profesional es elegir la mínima que cubra la necesidad:

Opción Cómo funciona Caso de TecnoMarket
Script batch Un script se ejecuta periódicamente, predice sobre lo acumulado y guarda resultados Predicción de demanda semanal (04-04): no hace falta tiempo real
API REST Un servidor expone el modelo por HTTP; otras aplicaciones piden predicciones al momento Clasificar cada reseña al publicarse (04-03)
Servidor de modelos (TF Serving / TorchServe) Software especializado: versiones conviviendo, lotes automáticos, alto rendimiento Cuando las peticiones/segundo desborden la API artesanal
Edge (TFLite) El modelo, comprimido, corre EN el dispositivo, sin red El clasificador de fotos en la app del almacén (plan de 06-03)

Un script batch mínimo (a veces la mejor arquitectura es la más aburrida):

# predecir_demanda_semanal.py -- se programa para ejecutarse cada lunes
import joblib
from tensorflow import keras

model = keras.models.load_model("models/demanda_v2.keras")
scaler = joblib.load("models/demanda_scaler_v2.joblib")   # SIEMPRE en pareja

ventas = cargar_ventas_ultimas_semanas()          # de la base de datos
pred = model.predict(scaler.transform(ventas))
guardar_predicciones(scaler_inverso(pred))        # a la tabla que lee compras

TF Serving y TorchServe quedan mencionados en tu radar: son la evolución natural cuando la escala apriete, y consumen precisamente los formatos "de producción" de la primera parte (SavedModel y state_dict empaquetado).

Una API REST con FastAPI para las reseñas de TecnoMarket

Vamos a servir el clasificador de reseñas en vivo. FastAPI es un framework web de Python moderno y minimalista, ideal para envolver un modelo. Instalación: pip install fastapi uvicorn.

# api_resenas.py
from fastapi import FastAPI
from pydantic import BaseModel
from tensorflow import keras

app = FastAPI(title="TecnoMarket - Analisis de resenas")

# 1. Cargar el modelo UNA vez, al arrancar el servidor (no en cada peticion)
#    Es el modelo "completo" con TextVectorization dentro (opcion A de antes)
model = keras.models.load_model("models/resenas_completo.keras")
UMBRAL_REVISION = 0.65   # el umbral de confianza del pipeline de 03-04

class Resena(BaseModel):        # 2. contrato de entrada: JSON con un campo "texto"
    texto: str

@app.post("/clasificar")        # 3. endpoint: POST /clasificar
def clasificar(resena: Resena):
    prob = float(model.predict([resena.texto], verbose=0)[0][0])  # 4. inferencia
    sentimiento = "positiva" if prob >= 0.5 else "negativa"
    confianza = max(prob, 1 - prob)
    return {                    # 5. respuesta JSON para la web/CRM
        "sentimiento": sentimiento,
        "confianza": round(confianza, 3),
        "revision_humana": confianza < UMBRAL_REVISION,   # cola de revision (03-04)
        "version_modelo": "resenas_v3",
    }

Explicación de las decisiones:

  1. Cargar al arrancar: cargar el modelo tarda segundos; una predicción, milisegundos. Cargarlo en cada petición mataría la API. La carga va a nivel de módulo.
  2. Pydantic (BaseModel) define y valida la entrada: si llega un JSON sin texto, FastAPI devuelve un error claro automáticamente, sin que el modelo vea nada.
  3. El endpoint es POST porque envía datos en el cuerpo de la petición.
  4. Gracias a la opción A (vectorizador dentro), la API recibe texto crudo: cero riesgo de desincronizar preprocesados.
  5. La respuesta incluye la versión del modelo (trazabilidad) y el campo revision_humana: el mismo patrón de umbral de confianza + cola de revisión que diseñamos en 03-04, ahora en producción.

Arrancar y probar:

uvicorn api_resenas:app --port 8000
# Documentacion interactiva automatica: http://localhost:8000/docs

curl -X POST http://localhost:8000/clasificar \
     -H "Content-Type: application/json" \
     -d '{"texto": "El cargador llego roto y nadie responde"}'
# {"sentimiento":"negativa","confianza":0.94,"revision_humana":false,"version_modelo":"resenas_v3"}

Con esto, cualquier sistema de TecnoMarket (la web, el CRM de atención al cliente) consume el modelo con una simple petición HTTP, sin saber nada de deep learning.

ONNX: el formato puente

En 06-03 dejamos anotado ONNX (Open Neural Network Exchange): un formato estándar de intercambio de modelos. La idea: entrenas donde quieras, exportas a .onnx, y ejecutas con ONNX Runtime en casi cualquier sitio (Python, C#, Java, móvil...), sin instalar el framework de origen.

# Exportar el modelo PyTorch de resenas (06-02) a ONNX
import torch

model.eval()
ejemplo = torch.randn(1, 200)                      # entrada de ejemplo (define shapes)
torch.onnx.export(model, ejemplo, "models/resenas.onnx",
                  input_names=["resena"], output_names=["logit"])

# Ejecutarlo SIN PyTorch, solo con onnxruntime (pip install onnxruntime)
import onnxruntime as ort
import numpy as np

sesion = ort.InferenceSession("models/resenas.onnx")
salida = sesion.run(None, {"resena": np.random.rand(1, 200).astype("float32")})
print(salida[0])   # el logit, identico al que daria el modelo original

Cuándo interesa: entregar un modelo a un equipo que trabaja en otro lenguaje o framework, unificar la inferencia de modelos de orígenes distintos (justo el caso de TecnoMarket tras la decisión de 06-03: Keras en producción, PyTorch en exploración), o aprovechar las optimizaciones de inferencia de ONNX Runtime. Buenas prácticas mínimas: tras exportar, comparar las salidas del modelo original y el ONNX sobre un lote de prueba (deben coincidir salvo decimales) — y recuerda que el preprocesado externo sigue viajando aparte, también aquí.

Validar antes de desplegar y vigilar después

Desplegar no es un acto de fe; es un proceso con dos guardias:

Antes: validación sobre un conjunto congelado. El equipo mantiene un conjunto de test fijo y versionado (reseñas reales etiquetadas a mano, nunca usadas para entrenar — la disciplina anti-fuga de 04-04 elevada a norma de equipo). Todo candidato a producción, sea Keras o PyTorch (la regla acordada en 06-03), debe:

  1. Igualar o superar en ese conjunto las métricas del modelo actual en producción.
  2. Comprobarse también en los segmentos críticos (reseñas cortas, productos nuevos): un modelo mejor "en media" puede ser peor justo donde más duele.
  3. Pasar una prueba de humo de la unidad desplegable completa: levantar la API con el artefacto final y verificar predicciones conocidas de extremo a extremo.

Después: monitorizar la deriva (drift). El mundo cambia y los datos de producción se alejan poco a poco de los de entrenamiento: TecnoMarket lanza categorías nuevas, el vocabulario de las reseñas evoluciona ("no me funciona el emparejamiento BLE" no existía en el corpus original). El rendimiento decae en silencio: no hay excepción ni log de error, solo predicciones cada vez peores. Vigilancia conceptual mínima:

  • Registrar cada predicción con su confianza y la versión del modelo (por eso la API la devuelve).
  • Vigilar señales indirectas: % de casos enviados a revisión humana subiendo, distribución de confianzas desplazándose, y las correcciones de los revisores humanos como muestreo continuo de la verdad.
  • Definir el umbral de acción de antemano: "si X empeora más de Y durante Z semanas, se reentrena con datos recientes" — y el reentrenamiento vuelve a pasar por el conjunto congelado. El ciclo se cierra.

Checklist de despliegue de TecnoMarket

La lista que el equipo repasa antes de cada puesta en producción:

  1. Modelo guardado en formato correcto (.keras / state_dict) con nombre de versión (resenas_v3), no modelo_final_BUENO.
  2. Preprocesado guardado y versionado junto al modelo (dentro del grafo o como artefacto pareja).
  3. Métricas sobre el conjunto congelado ≥ modelo actual, incluidos los segmentos críticos.
  4. Semillas, config y versiones de librerías registradas (la reproducibilidad de 06-04): se puede reconstruir este modelo desde cero.
  5. Prueba de humo de la unidad desplegable: API levantada en local, predicciones conocidas verificadas de extremo a extremo.
  6. La respuesta de la API incluye versión del modelo y señal de confianza; los casos dudosos van a la cola de revisión humana (03-04).
  7. Monitorización activa: logging de predicciones y alertas de deriva definidas antes del lanzamiento, no después del primer susto.
  8. Plan de vuelta atrás: la versión anterior (resenas_v2) queda archivada y lista para restaurar en minutos.

Errores Comunes y Consejos

  • Desplegar el modelo sin su preprocesado (o con otra versión de este): el fallo silencioso número uno. No hay error, solo predicciones absurdas con confianza alta. La unidad desplegable es el par, siempre.
  • torch.save(model, ...) del objeto entero: funciona hoy, se rompe cuando refactorizas el proyecto. State_dict + código de la clase, como norma.
  • Olvidar model.eval() al cargar en PyTorch para inferencia: con dropout activo, cada petición a tu API daría una predicción distinta para la misma reseña (lo viste en 06-02; en producción es un bug desconcertante).
  • Cargar el modelo dentro del endpoint: cada petición tardaría segundos y la API se arrastraría. Cargar una vez al arrancar; predecir muchas.
  • Validar sobre un test que cambia: si cada candidato se mide contra datos distintos, las comparaciones no significan nada. Conjunto congelado y versionado.
  • Confiar en que "si no hay errores, funciona": la degradación por deriva no lanza excepciones. Sin monitorización, tu modelo puede llevar meses equivocándose cuando alguien lo note.
  • Consejo: tras cada save, haz el ciclo completo en un intérprete limpio: cargar, predecir sobre 3 ejemplos conocidos, comparar con los valores esperados. Treinta segundos que detectan el 90 % de los problemas de serialización.

Ejercicios

Ejercicio 1: elegir formato

Para cada situación, indica el mecanismo de guardado adecuado y por qué: (a) el entrenamiento PyTorch del módulo 7 durará tres noches en Colab y debe poder reanudarse cada mañana; (b) hay que entregar el clasificador Keras de fotos al equipo de la plataforma para servirlo con TF Serving; (c) quieres archivar el clasificador de reseñas Keras para retomarlo (incluso reentrenarlo) dentro de seis meses; (d) el equipo backend, que programa en C# y no quiere instalar PyTorch, necesita ejecutar el modelo de fraude.

Ejercicio 2: el despliegue que predice disparates

El predictor de demanda de 04-04 se despliega como script batch. En pruebas daba error medio del 8 %; en producción predice cantidades absurdas (miles de unidades de un producto que vende decenas), sin ningún error en los logs. El script carga demanda_v2.keras correctamente y la base de datos entrega bien las ventas. ¿Cuál es la causa más probable, por qué no aparece ningún error, y qué dos cambios harías para arreglarlo y para que no vuelva a pasar?

Ejercicio 3: ampliar la API

Amplía api_resenas.py con un endpoint POST /clasificar_lote que reciba una lista de reseñas (hasta 100) y devuelva una lista de resultados, aprovechando que el modelo predice más eficientemente por lotes que de una en una. Añade también un endpoint GET /salud que devuelva {"estado": "ok", "version_modelo": "resenas_v3"} y explica para qué sirve en producción.

Soluciones

Solución 1:

  • (a) Checkpoint completo PyTorch (dict con model_state, optimizer_state y época) guardado en Drive cada época: reanudar exige el estado de Adam, no solo pesos.
  • (b) SavedModel (model.export(...)): es el formato autocontenido que TF Serving consume directamente.
  • (c) .keras: un solo fichero con arquitectura, pesos y optimizador; en seis meses, load_model y seguir (reentrenamiento incluido).
  • (d) ONNX: exportar con torch.onnx.export y entregar el .onnx; el backend lo ejecuta con ONNX Runtime para C#, sin PyTorch. (Verificando antes que las salidas coinciden con el original.)

Solución 2: Causa más probable: el script aplica mal (o no aplica) el scaler — usa uno reajustado sobre otros datos, una versión desincronizada, o pasa ventas crudas al modelo que esperaba valores escalados; también encaja olvidar la transformación inversa de la salida. No hay error porque las shapes y dtypes son válidos: el modelo opera con números "legales" pero en la escala equivocada — el fallo silencioso clásico. Arreglos: (1) desplegar el par como unidad — cargar demanda_scaler_v2.joblib versionado junto al modelo, mismo sufijo de versión, y aplicar transform a la entrada e inversa a la salida; (2) añadir una prueba de humo al checklist: tras cada despliegue, predecir sobre 3 casos históricos conocidos y comparar con los valores esperados antes de escribir nada en la tabla de compras (más una validación de rangos: si la predicción sale del rango histórico razonable, alerta y no publicar).

Solución 3:

from typing import List
from pydantic import BaseModel, Field

class LoteResenas(BaseModel):
    textos: List[str] = Field(..., max_length=100)   # valida el limite de 100

@app.post("/clasificar_lote")
def clasificar_lote(lote: LoteResenas):
    probs = model.predict(lote.textos, verbose=0)[:, 0]   # UNA pasada por lotes
    resultados = []
    for texto, prob in zip(lote.textos, probs):
        prob = float(prob)
        confianza = max(prob, 1 - prob)
        resultados.append({
            "sentimiento": "positiva" if prob >= 0.5 else "negativa",
            "confianza": round(confianza, 3),
            "revision_humana": confianza < UMBRAL_REVISION,
        })
    return {"version_modelo": "resenas_v3", "resultados": resultados}

@app.get("/salud")
def salud():
    return {"estado": "ok", "version_modelo": "resenas_v3"}

La clave de eficiencia: una sola llamada a model.predict con las 100 reseñas (el paralelismo por lotes de todo el curso), no 100 llamadas. El endpoint /salud (health check) permite a la infraestructura comprobar cada pocos segundos que la API vive y qué versión sirve: si deja de responder, se reinicia o se avisa automáticamente — y durante un despliegue confirma que la versión nueva está realmente en el aire.

Conclusión

Se cierra el círculo que abrió el model.save() de 02-05. Ahora sabes guardar con criterio (.keras/SavedModel/pesos en Keras; state_dict y checkpoints en PyTorch), sabes que la unidad desplegable es el par preprocesado + modelo (el scaler de 04-04 y el vocabulario de 04-03 viajan siempre con su red), sabes elegir la forma de servir según la necesidad (batch → FastAPI → servidores de modelos → edge), tienes a ONNX como puente entre mundos, y entiendes que desplegar exige validar contra un conjunto congelado antes y vigilar la deriva después — todo condensado en el checklist de TecnoMarket.

Y con esto, el módulo 6 queda completo: conoces TensorFlow por dentro, hablas PyTorch, sabes elegir entre ellos, trabajas con entorno y disciplina profesionales, y llevas modelos hasta producción. Técnicas (módulos 2-5) y herramientas (módulo 6): la caja está llena. En el módulo 7 la vaciaremos entera: construiremos de principio a fin los proyectos completos de TecnoMarket — clasificación de imágenes, generación de texto, detección de anomalías, una GAN y el fine-tuning de un modelo preentrenado. Deja de ser un curso; empieza a ser un portafolio.

Curso de Deep Learning

Módulo 1: Introducción a Deep Learning

Módulo 2: Fundamentos de Redes Neuronales

Módulo 3: Redes Neuronales Convolucionales (CNN)

Módulo 4: Redes Neuronales Recurrentes (RNN)

Módulo 5: Técnicas Avanzadas en Deep Learning

Módulo 6: Herramientas y Frameworks

Módulo 7: Proyectos Prácticos

Módulo 8: Consideraciones Éticas y Futuro del Deep Learning

© Copyright 2026. Todos los derechos reservados