Un model amb un 97 % de precisió que només existeix a la teva sessió de Python val exactament zero per al negoci. L'equip de TecnoMarket ho té clar: el classificador de ressenyes de 04-03 només aporta valor quan la web el pot consultar en viu, i el classificador de fotos de 05-03 només estalvia feina quan el sistema del magatzem l'invoca amb cada producte nou. Aquesta lliçó tanca el cicle que va començar amb aquell tímid model.save() de 02-05: aprendràs els formats de desat de Keras i PyTorch i quan fer servir cadascun, per què el preprocessament s'ha de desar al costat del model, les opcions per servir prediccions (d'un script batch a una API REST amb FastAPI, amb exemple complet), ONNX com a pont entre frameworks, i com validar i vigilar un model abans i després de desplegar-lo. És la darrera milla: on el deep learning es converteix en producte.

Contingut

  1. Desar i carregar en Keras: .keras, SavedModel i només pesos
  2. Desar i carregar en PyTorch: state_dict i checkpoints
  3. El model no viatja sol: desar el preprocessament
  4. Servir un model: opcions en escala
  5. Una API REST amb FastAPI per a les ressenyes de TecnoMarket
  6. ONNX: el format pont
  7. Validar abans de desplegar i vigilar després
  8. Checklist de desplegament de TecnoMarket

Desar i carregar en Keras: .keras, SavedModel i només pesos

A 02-05 vas fer model.save("tecnomarket-dl/models/mnist_dens_v1.keras") i vam seguir endavant. Professionalitzem-ho: Keras té tres maneres de desar, cadascuna amb el seu cas d'ús.

from tensorflow import keras

# 1. Format .keras (el recomanat): TOT en un sol fitxer
model.save("models/ressenyes_v3.keras")
model_carregat = keras.models.load_model("models/ressenyes_v3.keras")
# Desa: arquitectura + pesos + estat de l'optimitzador + compile()
# -> pots continuar entrenant exactament on ho vas deixar

# 2. SavedModel (format de TensorFlow, un directori complet)
model.export("models/ressenyes_v3_savedmodel")
# Desa el graf de computació autocontingut, SENSE necessitar el teu codi Python
# -> és el que consumeixen TF Serving i els conversors (TFLite)

# 3. Només pesos
model.save_weights("models/ressenyes_v3.weights.h5")
model_nou = crear_model()                   # necessites RECONSTRUIR l'arquitectura
model_nou.load_weights("models/ressenyes_v3.weights.h5")
Format Què desa Quan fer-lo servir
.keras Arquitectura + pesos + optimitzador Ús general: pausar/reprendre, arxivar, compartir amb qui tingui Keras
SavedModel (export) Graf autocontingut per a inferència Lliurar a sistemes de producció TF (Serving, TFLite)
Només pesos Únicament els números apresos El codi defineix l'arquitectura (transfer learning com a 05-03, checkpoints lleugers)

Nota pràctica: el ModelCheckpoint de 06-01 escriu en format .keras; ja portaves mitja lliçó apresa.

Desar i carregar en PyTorch: state_dict i checkpoints

PyTorch gira al voltant del state_dict: un diccionari {nom_de_parametre: tensor} amb tots els pesos del model. La manera canònica de desar és aquest diccionari, no l'objecte model:

import torch

# DESAR: la manera recomanada (només el state_dict)
torch.save(model.state_dict(), "models/ressenyes_v3.pt")

# CARREGAR: reconstruir l'arquitectura i abocar-hi els pesos
model = ClassificadorRessenyes()                    # la classe de 06-02
model.load_state_dict(torch.load("models/ressenyes_v3.pt"))
model.eval()                                        # a mode inferència! (vegeu 06-02)

Per què no desar el model sencer amb torch.save(model, ...)? Es pot fer, però serialitza referències al teu codi (classe, mòdul, rutes): en carregar-lo mesos després amb el projecte reorganitzat, es trenca. El state_dict són només tensors amb nom: robust i portable. És l'anàleg del "només pesos" de Keras — a PyTorch, l'opció per defecte.

Checkpoint complet: pausar i reprendre entrenaments

Per reprendre un entrenament no n'hi ha prou amb els pesos: Adam (02-04) manté estadístiques internes per paràmetre que també cal restaurar. El patró professional:

# Desar un checkpoint complet al final de cada època
checkpoint = {
    "epoch": epoch,
    "model_state": model.state_dict(),
    "optimizer_state": optimizer.state_dict(),   # l'estat d'Adam també!
    "val_loss": val_loss,
}
torch.save(checkpoint, "models/checkpoint_darrer.pt")

# Reprendre dies després, exactament on s'havia quedat
ckpt = torch.load("models/checkpoint_darrer.pt")
model.load_state_dict(ckpt["model_state"])
optimizer.load_state_dict(ckpt["optimizer_state"])
epoca_inicial = ckpt["epoch"] + 1
Necessitat Keras PyTorch
Arxivar/compartir un model a punt .keras state_dict + codi de la classe
Reprendre un entrenament .keras (porta l'optimitzador) checkpoint dict (model + optimitzador + època)
Lliurar a producció SavedModel state_dict (o exportar a ONNX/TorchScript)

El model no viatja sol: desar el preprocessament

Aquí hi ha l'error que més desplegaments trenca, i és conceptual, no tècnic. El teu model va aprendre sobre dades transformades: si en producció rep dades brutes o transformades d'una altra manera, les seves prediccions seran escombraries sense donar cap error. Recorda dos casos del curs:

  • A 04-04, el predictor de demanda va entrenar amb vendes escalades per un scaler ajustat sobre el train (amb cura de no fugar informació). Si en producció li passes euros crus on esperava valors en [0, 1], predirà disbarats amb total seguretat en si mateix.
  • A 04-03, el classificador de ressenyes fa servir una capa TextVectorization el vocabulari de la qual es va aprendre del corpus d'entrenament. Un altre vocabulari = altres índexs = una altra "llengua": la xarxa rebria un galimaties.

La regla d'or: el parell (preprocessament, model) és LA unitat desplegable. Mai no viatgen per separat.

# Opció A (Keras): posar el preprocessament DINS del model abans de desar
# (TextVectorization és una capa: pot formar part del graf)
model_complet = keras.Sequential([
    keras.Input(shape=(1,), dtype="string"),   # hi entra TEXT cru!
    vectoritzador,                             # la TextVectorization ja adaptada (04-03)
    model_entrenat,                            # la xarxa que espera índexs
])
model_complet.save("models/ressenyes_complet.keras")
# En producció: model.predict(["El carregador va arribar trencat"])  -> directe del text

# Opció B (general, vàlida també per a PyTorch): desar el transformador a part
import joblib
joblib.dump(scaler, "models/demanda_scaler.joblib")   # el scaler de 04-04
# ...i en producció, SEMPRE en parella:
scaler = joblib.load("models/demanda_scaler.joblib")
x = scaler.transform(dades_brutes)                    # mateix scaler, mateixos paràmetres
prediccio = model.predict(x)

L'opció A és la més segura (impossible desincronitzar); la B és la més flexible. Triïs la que triïs: mateixos fitxers versionats junts, amb el mateix nom de versió.

Servir un model: opcions en escala

"Desplegar" no vol dir sempre "muntar un servidor". Hi ha una escala d'opcions, de menys a més complexitat, i el que és professional és triar la mínima que cobreixi la necessitat:

Opció Com funciona Cas de TecnoMarket
Script batch Un script s'executa periòdicament, prediu sobre el que s'ha acumulat i desa els resultats Predicció de demanda setmanal (04-04): no cal temps real
API REST Un servidor exposa el model per HTTP; altres aplicacions demanen prediccions al moment Classificar cada ressenya en publicar-se (04-03)
Servidor de models (TF Serving / TorchServe) Programari especialitzat: versions convivint, lots automàtics, alt rendiment Quan les peticions/segon desbordin l'API artesanal
Edge (TFLite) El model, comprimit, corre AL dispositiu, sense xarxa El classificador de fotos a l'app del magatzem (pla de 06-03)

Un script batch mínim (de vegades la millor arquitectura és la més avorrida):

# predir_demanda_setmanal.py -- es programa perquè s'executi cada dilluns
import joblib
from tensorflow import keras

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

vendes = carregar_vendes_darreres_setmanes()      # de la base de dades
pred = model.predict(scaler.transform(vendes))
desar_prediccions(scaler_invers(pred))            # a la taula que llegeix compres

TF Serving i TorchServe queden anotats al teu radar: són l'evolució natural quan l'escala apreti, i consumeixen precisament els formats "de producció" de la primera part (SavedModel i state_dict empaquetat).

Una API REST amb FastAPI per a les ressenyes de TecnoMarket

Servirem el classificador de ressenyes en viu. FastAPI és un framework web de Python modern i minimalista, ideal per embolcallar un model. Instal·lació: pip install fastapi uvicorn.

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

app = FastAPI(title="TecnoMarket - Analisi de ressenyes")

# 1. Carregar el model UN cop, en arrencar el servidor (no a cada peticio)
#    Es el model "complet" amb TextVectorization a dins (opcio A d'abans)
model = keras.models.load_model("models/ressenyes_complet.keras")
LLINDAR_REVISIO = 0.65   # el llindar de confianca del pipeline de 03-04

class Ressenya(BaseModel):      # 2. contracte d'entrada: JSON amb un camp "text"
    text: str

@app.post("/classificar")       # 3. endpoint: POST /classificar
def classificar(ressenya: Ressenya):
    prob = float(model.predict([ressenya.text], verbose=0)[0][0])  # 4. inferencia
    sentiment = "positiva" if prob >= 0.5 else "negativa"
    confianca = max(prob, 1 - prob)
    return {                    # 5. resposta JSON per a la web/CRM
        "sentiment": sentiment,
        "confianca": round(confianca, 3),
        "revisio_humana": confianca < LLINDAR_REVISIO,   # cua de revisio (03-04)
        "versio_model": "ressenyes_v3",
    }

Explicació de les decisions:

  1. Carregar en arrencar: carregar el model triga segons; una predicció, mil·lisegons. Carregar-lo a cada petició mataria l'API. La càrrega va a nivell de mòdul.
  2. Pydantic (BaseModel) defineix i valida l'entrada: si arriba un JSON sense text, FastAPI retorna un error clar automàticament, sense que el model vegi res.
  3. L'endpoint és POST perquè envia dades al cos de la petició.
  4. Gràcies a l'opció A (vectoritzador a dins), l'API rep text cru: zero risc de desincronitzar preprocessaments.
  5. La resposta inclou la versió del model (traçabilitat) i el camp revisio_humana: el mateix patró de llindar de confiança + cua de revisió que vam dissenyar a 03-04, ara en producció.

Arrencar i provar:

uvicorn api_ressenyes:app --port 8000
# Documentacio interactiva automatica: http://localhost:8000/docs

curl -X POST http://localhost:8000/classificar \
     -H "Content-Type: application/json" \
     -d '{"text": "El carregador va arribar trencat i ningu respon"}'
# {"sentiment":"negativa","confianca":0.94,"revisio_humana":false,"versio_model":"ressenyes_v3"}

Amb això, qualsevol sistema de TecnoMarket (la web, el CRM d'atenció al client) consumeix el model amb una simple petició HTTP, sense saber res de deep learning.

ONNX: el format pont

A 06-03 vam deixar anotat ONNX (Open Neural Network Exchange): un format estàndard d'intercanvi de models. La idea: entrenes on vulguis, exportes a .onnx, i executes amb ONNX Runtime gairebé a qualsevol lloc (Python, C#, Java, mòbil...), sense instal·lar el framework d'origen.

# Exportar el model PyTorch de ressenyes (06-02) a ONNX
import torch

model.eval()
exemple = torch.randn(1, 200)                      # entrada d'exemple (defineix shapes)
torch.onnx.export(model, exemple, "models/ressenyes.onnx",
                  input_names=["ressenya"], output_names=["logit"])

# Executar-lo SENSE PyTorch, nomes amb onnxruntime (pip install onnxruntime)
import onnxruntime as ort
import numpy as np

sessio = ort.InferenceSession("models/ressenyes.onnx")
sortida = sessio.run(None, {"ressenya": np.random.rand(1, 200).astype("float32")})
print(sortida[0])   # el logit, identic al que donaria el model original

Quan interessa: lliurar un model a un equip que treballa en un altre llenguatge o framework, unificar la inferència de models d'orígens diferents (just el cas de TecnoMarket després de la decisió de 06-03: Keras en producció, PyTorch en exploració), o aprofitar les optimitzacions d'inferència d'ONNX Runtime. Bones pràctiques mínimes: després d'exportar, comparar les sortides del model original i de l'ONNX sobre un lot de prova (han de coincidir llevat de decimals) — i recorda que el preprocessament extern continua viatjant a part, també aquí.

Validar abans de desplegar i vigilar després

Desplegar no és un acte de fe; és un procés amb dos guardes:

Abans: validació sobre un conjunt congelat. L'equip manté un conjunt de test fix i versionat (ressenyes reals etiquetades a mà, mai no usades per entrenar — la disciplina anti-fuga de 04-04 elevada a norma d'equip). Tot candidat a producció, sigui Keras o PyTorch (la regla acordada a 06-03), ha de:

  1. Igualar o superar en aquest conjunt les mètriques del model actual en producció.
  2. Comprovar-se també en els segments crítics (ressenyes curtes, productes nous): un model millor "de mitjana" pot ser pitjor just on més fa mal.
  3. Passar una prova de fum de la unitat desplegable completa: aixecar l'API amb l'artefacte final i verificar prediccions conegudes d'extrem a extrem.

Després: monitorar la deriva (drift). El món canvia i les dades de producció s'allunyen a poc a poc de les d'entrenament: TecnoMarket llança categories noves, el vocabulari de les ressenyes evoluciona ("no em funciona l'aparellament BLE" no existia al corpus original). El rendiment decau en silenci: no hi ha excepció ni log d'error, només prediccions cada cop pitjors. Vigilància conceptual mínima:

  • Registrar cada predicció amb la seva confiança i la versió del model (per això l'API la retorna).
  • Vigilar senyals indirectes: % de casos enviats a revisió humana pujant, distribució de confiances desplaçant-se, i les correccions dels revisors humans com a mostreig continu de la veritat.
  • Definir el llindar d'acció per endavant: "si X empitjora més de Y durant Z setmanes, es reentrena amb dades recents" — i el reentrenament torna a passar pel conjunt congelat. El cicle es tanca.

Checklist de desplegament de TecnoMarket

La llista que l'equip repassa abans de cada posada en producció:

  1. Model desat en el format correcte (.keras / state_dict) amb nom de versió (ressenyes_v3), no model_final_BO.
  2. Preprocessament desat i versionat al costat del model (dins del graf o com a artefacte parella).
  3. Mètriques sobre el conjunt congelat ≥ model actual, inclosos els segments crítics.
  4. Llavors, config i versions de llibreries registrades (la reproduïbilitat de 06-04): aquest model es pot reconstruir des de zero.
  5. Prova de fum de la unitat desplegable: API aixecada en local, prediccions conegudes verificades d'extrem a extrem.
  6. La resposta de l'API inclou versió del model i senyal de confiança; els casos dubtosos van a la cua de revisió humana (03-04).
  7. Monitoratge actiu: logging de prediccions i alertes de deriva definides abans del llançament, no després del primer ensurt.
  8. Pla de tornada enrere: la versió anterior (ressenyes_v2) queda arxivada i a punt per restaurar en minuts.

Errors Comuns i Consells

  • Desplegar el model sense el seu preprocessament (o amb una altra versió d'aquest): la fallada silenciosa número u. No hi ha error, només prediccions absurdes amb confiança alta. La unitat desplegable és el parell, sempre.
  • torch.save(model, ...) de l'objecte sencer: funciona avui, es trenca quan refactoritzes el projecte. State_dict + codi de la classe, com a norma.
  • Oblidar model.eval() en carregar en PyTorch per a inferència: amb el dropout actiu, cada petició a la teva API donaria una predicció diferent per a la mateixa ressenya (ho vas veure a 06-02; en producció és un bug desconcertant).
  • Carregar el model dins de l'endpoint: cada petició trigaria segons i l'API s'arrossegaria. Carregar un cop en arrencar; predir moltes vegades.
  • Validar sobre un test que canvia: si cada candidat es mesura contra dades diferents, les comparacions no signifiquen res. Conjunt congelat i versionat.
  • Confiar que "si no hi ha errors, funciona": la degradació per deriva no llança excepcions. Sense monitoratge, el teu model pot portar mesos equivocant-se quan algú se n'adoni.
  • Consell: després de cada save, fes el cicle complet en un intèrpret net: carregar, predir sobre 3 exemples coneguts, comparar amb els valors esperats. Trenta segons que detecten el 90 % dels problemes de serialització.

Exercicis

Exercici 1: triar format

Per a cada situació, indica el mecanisme de desat adequat i per què: (a) l'entrenament PyTorch del mòdul 7 durarà tres nits a Colab i s'ha de poder reprendre cada matí; (b) cal lliurar el classificador Keras de fotos a l'equip de la plataforma per servir-lo amb TF Serving; (c) vols arxivar el classificador de ressenyes Keras per reprendre'l (fins i tot reentrenar-lo) d'aquí a sis mesos; (d) l'equip backend, que programa en C# i no vol instal·lar PyTorch, necessita executar el model de frau.

Exercici 2: el desplegament que prediu disbarats

El predictor de demanda de 04-04 es desplega com a script batch. En proves donava un error mitjà del 8 %; en producció prediu quantitats absurdes (milers d'unitats d'un producte que en ven desenes), sense cap error als logs. L'script carrega demanda_v2.keras correctament i la base de dades lliura bé les vendes. Quina és la causa més probable, per què no apareix cap error, i quins dos canvis faries per arreglar-ho i perquè no torni a passar?

Exercici 3: ampliar l'API

Amplia api_ressenyes.py amb un endpoint POST /classificar_lot que rebi una llista de ressenyes (fins a 100) i retorni una llista de resultats, aprofitant que el model prediu més eficientment per lots que d'una en una. Afegeix també un endpoint GET /salut que retorni {"estat": "ok", "versio_model": "ressenyes_v3"} i explica per a què serveix en producció.

Solucions

Solució 1:

  • (a) Checkpoint complet PyTorch (dict amb model_state, optimizer_state i època) desat a Drive cada època: reprendre exigeix l'estat d'Adam, no només pesos.
  • (b) SavedModel (model.export(...)): és el format autocontingut que TF Serving consumeix directament.
  • (c) .keras: un sol fitxer amb arquitectura, pesos i optimitzador; d'aquí a sis mesos, load_model i continuar (reentrenament inclòs).
  • (d) ONNX: exportar amb torch.onnx.export i lliurar el .onnx; el backend l'executa amb ONNX Runtime per a C#, sense PyTorch. (Verificant abans que les sortides coincideixen amb l'original.)

Solució 2: Causa més probable: l'script aplica malament (o no aplica) el scaler — en fa servir un de reajustat sobre altres dades, una versió desincronitzada, o passa vendes crues al model que esperava valors escalats; també hi encaixa oblidar la transformació inversa de la sortida. No hi ha error perquè les shapes i els dtypes són vàlids: el model opera amb números "legals" però a l'escala equivocada — la fallada silenciosa clàssica. Arranjaments: (1) desplegar el parell com a unitat — carregar demanda_scaler_v2.joblib versionat al costat del model, mateix sufix de versió, i aplicar transform a l'entrada i la inversa a la sortida; (2) afegir una prova de fum al checklist: després de cada desplegament, predir sobre 3 casos històrics coneguts i comparar amb els valors esperats abans d'escriure res a la taula de compres (més una validació de rangs: si la predicció surt del rang històric raonable, alerta i no publicar).

Solució 3:

from typing import List
from pydantic import BaseModel, Field

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

@app.post("/classificar_lot")
def classificar_lot(lot: LotRessenyes):
    probs = model.predict(lot.textos, verbose=0)[:, 0]   # UNA passada per lots
    resultats = []
    for text, prob in zip(lot.textos, probs):
        prob = float(prob)
        confianca = max(prob, 1 - prob)
        resultats.append({
            "sentiment": "positiva" if prob >= 0.5 else "negativa",
            "confianca": round(confianca, 3),
            "revisio_humana": confianca < LLINDAR_REVISIO,
        })
    return {"versio_model": "ressenyes_v3", "resultats": resultats}

@app.get("/salut")
def salut():
    return {"estat": "ok", "versio_model": "ressenyes_v3"}

La clau d'eficiència: una sola crida a model.predict amb les 100 ressenyes (el paral·lelisme per lots de tot el curs), no 100 crides. L'endpoint /salut (health check) permet a la infraestructura comprovar cada pocs segons que l'API és viva i quina versió serveix: si deixa de respondre, es reinicia o s'avisa automàticament — i durant un desplegament confirma que la versió nova és realment en marxa.

Conclusió

Es tanca el cercle que va obrir el model.save() de 02-05. Ara saps desar amb criteri (.keras/SavedModel/pesos en Keras; state_dict i checkpoints en PyTorch), saps que la unitat desplegable és el parell preprocessament + model (el scaler de 04-04 i el vocabulari de 04-03 viatgen sempre amb la seva xarxa), saps triar la manera de servir segons la necessitat (batch → FastAPI → servidors de models → edge), tens ONNX com a pont entre mons, i entens que desplegar exigeix validar contra un conjunt congelat abans i vigilar la deriva després — tot condensat al checklist de TecnoMarket.

I amb això, el mòdul 6 queda complet: coneixes TensorFlow per dins, parles PyTorch, saps triar entre ells, treballes amb entorn i disciplina professionals, i portes models fins a producció. Tècniques (mòduls 2-5) i eines (mòdul 6): la caixa és plena. Al mòdul 7 la buidarem sencera: construirem de principi a fi els projectes complets de TecnoMarket — classificació d'imatges, generació de text, detecció d'anomalies, una GAN i el fine-tuning d'un model preentrenat. Deixa de ser un curs; comença a ser un portafolis.

Curs de Deep Learning

Mòdul 1: Introducció al Deep Learning

Mòdul 2: Fonaments de Xarxes Neuronals

Mòdul 3: Xarxes Neuronals Convolucionals (CNN)

Mòdul 4: Xarxes Neuronals Recurrents (RNN)

Mòdul 5: Tècniques Avançades en Deep Learning

Mòdul 6: Eines i Frameworks

Mòdul 7: Projectes Pràctics

Mòdul 8: Consideracions Ètiques i Futur del Deep Learning

© Copyright 2026. Tots els drets reservats