Fins aquí, AlpinaShop no ha escrit ni una sola línia de codi de modelatge. Amb SQL va posar en producció un recomanador heurístic, i amb AutoML va entrenar un classificador de cistelles i un altre d'imatges. Per a moltes empreses, això és suficient per sempre, i no hi ha cap vergonya a fer-ho així.

Però la Marta vol una cosa que cap de les dues vies no dóna. Vol un recomanador que entengui els clients i els productes, no que memoritzi parells. Que sàpiga que qui compra grampons, piolet i casc és un perfil diferent de qui compra motxilla de 25 litres i bastons, i que a un client nou amb dues compres li pugui suggerir alguna cosa sensata perquè s'assembla a altres clients que sí que tenen historial. I que funcioni amb productes que encara no apareixen a productos_juntos perquè van arribar el mes passat.

Això no és classificar ni predir un número: és aprendre una representació. No hi ha columna objectiu. No hi ha taula plana. I no hi ha opció sense codi.

Aquesta lliçó és la baixada al nivell del codi. No per convertir-te en investigador de deep learning, sinó perquè sàpigues quan cal, què és el mínim que s'ha d'entendre, i com s'executa tot això a Google Cloud sense muntar ni mantenir ni una sola màquina.

Contingut

  1. Quan cal baixar al codi
  2. TensorFlow i Keras: el mínim imprescindible
  3. Incrustacions: la idea que fa possible el recomanador
  4. L'arquitectura de dues torres
  5. El model d'AlpinaShop en codi
  6. tf.data: llegir dades sense ofegar la GPU
  7. TFRecord i la lectura des de Cloud Storage
  8. Entrenar a Vertex AI: empaquetar el codi
  9. CPU, GPU o TPU: com triar
  10. Entrenament distribuït, en el conceptual
  11. Punts de control, VM Spot i per què van junts
  12. TensorBoard gestionat i Vertex AI Experiments
  13. Desar i servir: SavedModel, Registry i endpoint
  14. Optimització per a inferència
  15. PyTorch, JAX i la regla d'or del cost

  1. Quan cal baixar al codi

No sempre. La majoria de les vegades, no. Els quatre casos en què sí:

Situació Per què AutoML no hi arriba
Arquitectura pròpia Dues torres, xarxes siameses, atenció sobre seqüències: no són al catàleg d'AutoML
Funció de pèrdua a mida Optimitzar marge en lloc d'encert, o penalitzar asimètricament els errors
Preprocessament específic Negatius mostrejats dinàmicament, augment de dades particular
Sortida no estàndard Un vector de 64 dimensions en lloc d'una classe o un número

El recomanador d'AlpinaShop cau en els quatre alhora, cosa que el converteix en un exemple honest. La sortida no és una classe: és un vector per client i un altre per producte. La pèrdua no és "has encertat o no": és "acosta els que van junts i separa els que no". I els exemples negatius —productes que el client no va comprar— cal generar-los, perquè no existeixen a les dades.

I el criteri invers, igual d'important: si el teu problema encaixa en una taula amb una columna objectiu, no baixis al codi. El que guanyes en control ho pagues en setmanes de feina i en manteniment perpetu.

  1. TensorFlow i Keras: el mínim imprescindible

TensorFlow és una biblioteca per construir i entrenar models numèrics. Keras és la seva API d'alt nivell, la que es fa servir avui per a gairebé tot. Quatre conceptes basten per seguir la lliçó.

Tensor. Un array multidimensional amb tipus. Un escalar és un tensor de rang 0, un vector de rang 1, una matriu de rang 2. Un lot de 32 vectors de 64 dimensions és un tensor de forma (32, 64).

import tensorflow as tf

t = tf.constant([[1.0, 2.0, 3.0],
                 [4.0, 5.0, 6.0]])
print(t.shape)   # (2, 3)  -> 2 files, 3 columnes
print(t.dtype)   # float32

Capa. Una transformació amb paràmetres aprenibles. Dense(64) multiplica l'entrada per una matriu de pesos i suma un biaix; Embedding(1000, 32) és una taula de 1.000 vectors de 32 dimensions que es busquen per índex.

Model. Una composició de capes. Dues maneres de construir-lo:

from tensorflow import keras
from tensorflow.keras import layers

# Sequencial: una capa darrere l altra. Simple i limitat.
model_seq = keras.Sequential([
    layers.Dense(64, activation="relu", input_shape=(20,)),
    layers.Dense(32, activation="relu"),
    layers.Dense(1,  activation="sigmoid"),
])

# Funcional: graf. Permet diverses entrades i diverses sortides.
entrada_a = keras.Input(shape=(20,), name="numericas")
entrada_b = keras.Input(shape=(1,),  name="categoria")
x = layers.Dense(64, activation="relu")(entrada_a)
y = layers.Flatten()(layers.Embedding(50, 8)(entrada_b))
sortida = layers.Dense(1, activation="sigmoid")(layers.Concatenate()([x, y]))
model_fun = keras.Model(inputs=[entrada_a, entrada_b], outputs=sortida)

La diferència importa per a aquesta lliçó. El model seqüencial només serveix per a piles lineals. El recomanador té dues entrades independents —client i producte— que es processen per camins separats i es combinen al final. Això només es pot expressar amb l'API funcional o subclassificant keras.Model.

Compilar i entrenar.

model_seq.compile(
    optimizer=keras.optimizers.Adam(learning_rate=1e-3),
    loss="binary_crossentropy",
    metrics=["AUC"],
)

historia = model_seq.fit(
    dades_entrenament,
    validation_data=dades_validacio,
    epochs=20,
    callbacks=[keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)],
)
  • L'optimitzador decideix com s'ajusten els pesos. Adam és l'elecció per defecte sensata.
  • La pèrdua és el que es minimitza. És la traducció matemàtica de "què significa equivocar-se", i és on es personalitza de debò un model.
  • Una època és una passada completa per les dades.
  • EarlyStopping atura quan la validació deixa de millorar i restaura els millors pesos. Sense restore_best_weights=True et quedes amb els pesos de l'última època, que són pitjors. És un error clàssic.

Amb això n'hi ha prou. No cal entendre el descens de gradient per seguir.

  1. Incrustacions: la idea que fa possible el recomanador

Una incrustació (embedding) és un vector de números que representa una entitat, après de manera que les entitats semblants quedin a prop en aquell espai.

El problema que resol es veu millor amb l'alternativa. Per representar 2.400 productes sense incrustacions, la codificació one-hot fa servir un vector de 2.400 posicions, amb un 1 i 2.399 zeros. És enorme, és dispers, i sobretot no diu res: dues motxilles són tan lluny l'una de l'altra com una motxilla i un piolet, perquè tots els vectors són igual de diferents.

Una incrustació de 32 dimensions representa cada producte amb 32 números apresos. I apresos amb un criteri: si dos productes apareixen en contextos semblants, els seus vectors acaben assemblant-se.

Aspecte One-hot Incrustació
Dimensions per a 2.400 productes 2.400 32–128
Contingut Només identitat Similitud apresa
Productes nous Trivial però inútil Requereix reentrenar o fer servir atributs
Cost de memòria Alt i dispers Baix i dens

L'interessant és que la similitud emergeix sola, sense que ningú declari que un piolet s'assembla a uns grampons: surt de les dades de compra. I un cop tens vectors, comparar és trivial. El producte escalar o la similitud cosinus entre dos vectors és un número que mesura afinitat: si el vector del piolet és [0,21 −0,44 0,87 0,12] i el dels grampons [0,19 −0,40 0,91 0,09], el seu cosinus val gairebé 1 —apunten gairebé en la mateixa direcció—, mentre que davant del d'unes sandàlies de trekking surt negatiu.

La mida de la incrustació és un hiperparàmetre amb un compromís clar: poques dimensions no capturen matisos, massa memoritzen i sobreajusten. Per a 2.400 productes, entre 32 i 64 és un rang raonable de partida.

  1. L'arquitectura de dues torres

La intuïció, sense matemàtiques.

Imagina't dues funcions. La primera pren tot el que saps d'un client —el seu historial, les seves categories, el seu tiquet mitjà— i retorna un vector de 64 números. La segona pren tot el que saps d'un producte —la seva categoria, el seu preu, la seva marca, el seu identificador— i retorna un altre vector de 64 números en el mateix espai.

Si l'entrenament ha anat bé, el producte escalar entre el vector d'un client i el d'un producte és alt quan aquell client compraria aquell producte, i baix quan no.

flowchart TD
    A[Dades del client<br/>historial, categories, tiquet] --> B[Torre de client<br/>capes denses]
    C[Dades del producte<br/>id, categoria, preu, marca] --> D[Torre de producte<br/>capes denses]
    B --> E[Vector client<br/>64 dim]
    D --> F[Vector producte<br/>64 dim]
    E --> G[Producte escalar]
    F --> G
    G --> H[Puntuacio d afinitat]

Per què dues torres i no una sola xarxa que ho rebi tot junt. Per una raó purament operativa, i és la clau de tot el disseny: les dues torres es poden executar per separat.

Els 2.400 vectors de producte es calculen una vegada i es desen. Quan arriba un client, es calcula només el seu vector —una passada per la torre de client— i es busquen els productes més propers entre els ja precalculats. Això és una cerca de veïns propers, que amb índexs adequats és qüestió de mil·lisegons fins i tot amb milions d'elements.

L'alternativa —una sola xarxa que rebi el parell client-producte— obligaria a avaluar el model 2.400 vegades per cada client, una per producte. Amb dues torres, una vegada.

Els exemples negatius. Les dades d'AlpinaShop només contenen compres: parells client-producte positius. Perquè el model aprengui a distingir, necessita també exemples del que no es va comprar, i cal generar-los. La tècnica estàndard és el mostreig negatiu dins del lot: dins d'un lot de 512 parells reals, cada client s'aparella amb els productes dels altres 511 parells i aquests es tracten com a negatius. És eficient perquè no cal buscar res, i funciona sorprenentment bé.

Té un biaix conegut, i convé saber-ho: els productes populars apareixen més vegades als lots i per tant es penalitzen més com a negatius. Hi ha correccions per a això; per a una primera versió d'AlpinaShop, no compensa la complexitat.

  1. El model d'AlpinaShop en codi

El model complet, comentat. És el cor de la lliçó, així que llegeix-lo a poc a poc.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

DIM = 64          # dimensio de l espai compartit
N_CLIENTS = 45000
N_PRODUCTES = 2400
N_CATEGORIES = 18


def torre_client():
    """Converteix les dades d un client en un vector de DIM dimensions."""
    id_client   = keras.Input(shape=(), dtype=tf.int32,   name="cliente_idx")
    num_client  = keras.Input(shape=(4,), dtype=tf.float32, name="cliente_num")

    # Incrustacio: cada client te el seu propi vector apres
    emb = layers.Embedding(N_CLIENTS, DIM, name="emb_cliente")(id_client)

    # Senyals numerics ja normalitzats: pedidos_90d, ticket_medio,
    # dias_desde_ultimo, antiguedad_meses
    x = layers.Concatenate()([emb, num_client])
    x = layers.Dense(128, activation="relu")(x)
    x = layers.Dropout(0.2)(x)
    x = layers.Dense(DIM)(x)

    # Normalitzar a norma 1: el producte escalar passa a ser similitud cosinus
    sortida = layers.Lambda(lambda v: tf.math.l2_normalize(v, axis=1))(x)
    return keras.Model([id_client, num_client], sortida, name="torre_cliente")


def torre_producte():
    """Converteix les dades d un producte en un vector del mateix espai."""
    id_prod   = keras.Input(shape=(), dtype=tf.int32,   name="producto_idx")
    categoria = keras.Input(shape=(), dtype=tf.int32,   name="categoria_idx")
    num_prod  = keras.Input(shape=(3,), dtype=tf.float32, name="producto_num")

    emb_p = layers.Embedding(N_PRODUCTES,  DIM, name="emb_producto")(id_prod)
    emb_c = layers.Embedding(N_CATEGORIES, 16,  name="emb_categoria")(categoria)

    # precio_norm, ventas_30d_norm, puntuacion_media
    x = layers.Concatenate()([emb_p, emb_c, num_prod])
    x = layers.Dense(128, activation="relu")(x)
    x = layers.Dropout(0.2)(x)
    x = layers.Dense(DIM)(x)
    sortida = layers.Lambda(lambda v: tf.math.l2_normalize(v, axis=1))(x)
    return keras.Model([id_prod, categoria, num_prod], sortida, name="torre_producto")

Tres decisions de disseny que expliquen la resta:

  1. Cada torre barreja incrustació i informació d'atributs. La incrustació pura memoritza l'entitat concreta; els atributs (preu, categoria) permeten dir alguna cosa raonable sobre un producte nou que amb prou feines s'ha venut. És la mitigació pràctica de l'arrencada en fred.
  2. Dropout(0.2) apaga aleatòriament un 20 % de les neurones a cada pas d'entrenament. És regularització: impedeix que el model depengui massa d'un senyal concret i redueix el sobreajust.
  3. La normalització L2 final fa que tots els vectors tinguin longitud 1. Així el producte escalar és exactament la similitud cosinus, acotada entre −1 i 1, cosa que estabilitza l'entrenament i fa les puntuacions comparables entre si.

El model complet, amb la pèrdua:

class Recomanador(keras.Model):
    def __init__(self, temperatura=0.05):
        super().__init__()
        self.client   = torre_client()
        self.producte = torre_producte()
        self.temperatura = temperatura
        self.metrica = keras.metrics.Mean(name="perdua")

    def train_step(self, lot):
        with tf.GradientTape() as cinta:
            v_cli = self.client([lot["cliente_idx"], lot["cliente_num"]])
            v_pro = self.producte([lot["producto_idx"],
                                   lot["categoria_idx"],
                                   lot["producto_num"]])

            # Matriu de similituds: cada client contra cada producte DEL LOT
            similituds = tf.matmul(v_cli, v_pro, transpose_b=True) / self.temperatura

            # La diagonal son els parells reals (positius); la resta, negatius
            etiquetes = tf.range(tf.shape(similituds)[0])
            perdua = tf.reduce_mean(
                tf.nn.sparse_softmax_cross_entropy_with_logits(
                    labels=etiquetes, logits=similituds))

        gradients = cinta.gradient(perdua, self.trainable_variables)
        self.optimizer.apply_gradients(zip(gradients, self.trainable_variables))
        self.metrica.update_state(perdua)
        return {"perdua": self.metrica.result()}

Com funciona aquesta pèrdua, en paraules. tf.matmul(v_cli, v_pro, transpose_b=True) produeix una matriu quadrada de la mida del lot: la cel·la (i, j) és l'afinitat del client i amb el producte j. Els parells reals són a la diagonal, perquè el lot està format per parells que sí que van passar. L'entropia creuada amb etiquetes 0, 1, 2, ... demana al model que, per a cada client, el producte correcte sigui el de puntuació més alta entre tots els del lot. Aquí hi ha el mostreig negatiu, sense codi addicional.

La temperatura (0,05) divideix les similituds abans del softmax. Els valors baixos fan la distribució més "punxeguda" i l'entrenament més exigent. És un hiperparàmetre sensible que convé ajustar.

Això és exactament el que AutoML no pot fer: una pèrdua a mida sobre una arquitectura a mida.

  1. tf.data: llegir dades sense ofegar la GPU

Un error car i freqüent: llogar una GPU i descobrir que està al 15 % d'ús perquè la lectura de dades no dóna l'abast. Es paga la GPU sencera i se n'aprofita una fracció.

tf.data construeix canalitzacions de dades que se solapen amb el còmput.

def construir_dataset(patro_fitxers, mida_lot=512, entrenament=True):
    fitxers = tf.data.Dataset.list_files(patro_fitxers, shuffle=entrenament)

    ds = fitxers.interleave(
        lambda f: tf.data.TFRecordDataset(f, compression_type="GZIP"),
        cycle_length=8,                        # 8 fitxers en paral·lel
        num_parallel_calls=tf.data.AUTOTUNE)

    if entrenament:
        ds = ds.shuffle(buffer_size=50000)     # barreja dins d una finestra

    ds = ds.map(parsejar_exemple, num_parallel_calls=tf.data.AUTOTUNE)
    ds = ds.batch(mida_lot, drop_remainder=True)
    ds = ds.prefetch(tf.data.AUTOTUNE)         # prepara el lot seguent
    return ds

Cada peça, i per què hi és:

Operació Què fa Per què importa
interleave Llegeix diversos fitxers alhora Un sol fitxer limita el cabal des de Cloud Storage
shuffle Barreja dins d'un búfer Sense això, els lots tindrien comandes consecutives i correlacionades
map + AUTOTUNE Analitza en paral·lel Ajusta els fils sol, segons la màquina
batch Agrupa en lots La GPU és eficient amb lots, no amb files soltes
prefetch Prepara el següent mentre entrena L'operació que més impacte té: solapa CPU i GPU
drop_remainder=True Descarta l'últim lot incomplet Necessari aquí: la pèrdua assumeix lots quadrats

Sobre shuffle: el búfer ha de ser gran. Amb 5.000 sobre un fitxer ordenat per data, els lots continuaran contenint comandes gairebé consecutives i el model veurà les dades amb un ordre artificial. Un búfer de 50.000 barrejat sobre fitxers ja mesclats és raonable.

Sobre la mida de lot: més gran significa millor ús de la GPU i, en aquest model concret, més negatius per exemple, cosa que sol millorar la qualitat. El límit és la memòria de la GPU. Començar en 512 i pujar mentre hi càpiga és una estratègia sensata.

  1. TFRecord i la lectura des de Cloud Storage

Les dades d'AlpinaShop són a BigQuery. Per entrenar cal exportar-les a un format que TensorFlow llegeixi de pressa, i aquest format és TFRecord: un contenidor binari de registres serialitzats.

Per què no CSV. El CSV s'analitza com a text, línia a línia, i amb milions de files això és el coll d'ampolla. TFRecord és binari, es llegeix seqüencialment i es comprimeix bé.

bq extract \
  --destination_format=CSV \
  --compression=GZIP \
  'alpinashop-datos:alpinashop_analitica.reco_entrenamiento' \
  'gs://alpinashop-datalake/reco/entrenamiento/parte-*.csv.gz'

BigQuery no exporta TFRecord directament, així que la conversió es fa amb un treball de Dataflow (04-02) o, per a volums moderats, amb un script. L'important és el particionat: molts fitxers de mida mitjana, no un de gegant.

Estratègia Resultat
1 fitxer de 20 GB Lectura seqüencial única, interleave inútil, impossible distribuir
20.000 fitxers d'1 MB Sobrecàrrega d'obertura, latència dominant
200 fitxers de 100 MB Equilibri recomanat

La regla habitual: fitxers d'entre 100 i 200 MB, i almenys tants fitxers com treballadors pensis fer servir, multiplicat per unes quantes vegades.

L'anàlisi de cada registre:

ESQUEMA = {
    "cliente_idx":   tf.io.FixedLenFeature([],  tf.int64),
    "producto_idx":  tf.io.FixedLenFeature([],  tf.int64),
    "categoria_idx": tf.io.FixedLenFeature([],  tf.int64),
    "cliente_num":   tf.io.FixedLenFeature([4], tf.float32),
    "producto_num":  tf.io.FixedLenFeature([3], tf.float32),
}

def parsejar_exemple(registre):
    ex = tf.io.parse_single_example(registre, ESQUEMA)
    return {
        "cliente_idx":   tf.cast(ex["cliente_idx"],   tf.int32),
        "producto_idx":  tf.cast(ex["producto_idx"],  tf.int32),
        "categoria_idx": tf.cast(ex["categoria_idx"], tf.int32),
        "cliente_num":   ex["cliente_num"],
        "producto_num":  ex["producto_num"],
    }

Un detall operatiu que causa molts disgustos: el bucket de les dades i la màquina d'entrenament han de ser a la mateixa regió, europe-west1. Llegir des d'una altra regió afegeix latència, cost de sortida de dades, i en un entrenament llarg això es multiplica per milions de lectures.

  1. Entrenar a Vertex AI: empaquetar el codi

El codi d'entrenament ha d'arribar a la màquina d'alguna manera. Dues vies, ja vistes a 05-01, ara amb detall.

Via A: aplicació d'entrenament en contenidor precompilat. Empaquetes el teu script en un .tar.gz i Vertex AI l'executa sobre una imatge oficial de TensorFlow. Ràpid de muntar, poc control sobre les dependències.

Via B: contenidor propi. Construeixes la imatge i la puges a Artifact Registry. AlpinaShop ja té el registre muntat, així que és l'opció coherent.

FROM europe-docker.pkg.dev/vertex-ai/training/tf-gpu.2-15.py310:latest

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY entrenador/ ./entrenador/

ENTRYPOINT ["python", "-m", "entrenador.principal"]

Punts importants del Dockerfile. La imatge base oficial ja porta TensorFlow amb suport GPU, CUDA i els controladors correctament aparellats: muntar això a mà és una font inesgotable de problemes de versions. I requirements.txt ha de portar versions fixades (pandas==2.2.1, no pandas), perquè si no la imatge que construeixis d'aquí a tres mesos no serà la mateixa.

# Construir i publicar
gcloud builds submit \
  --tag=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/entrena-reco:1.0.3 \
  --project=alpinashop-cicd

# Llancar l entrenament
gcloud ai custom-jobs create \
  --project=alpinashop-datos --region=europe-west1 \
  --display-name=reco-two-tower-v3 \
  --worker-pool-spec="machine-type=n1-standard-8,accelerator-type=NVIDIA_TESLA_T4,accelerator-count=1,replica-count=1,container-image-uri=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/entrena-reco:1.0.3" \
  --args="--datos=gs://alpinashop-datalake/reco/tfrecord/,--salida=gs://alpinashop-datalake/modelos/reco/v3,--epocas=25,--lote=512,--dim=64"

Es fa servir Cloud Build per construir la imatge. La integració completa amb CI/CD, els disparadors i el pipeline de desplegament són matèria de 06-01; aquí n'hi ha prou amb la comanda.

L'script ha de llegir una variable d'entorn que Vertex AI injecta i que molta gent ignora: AIP_MODEL_DIR, la ruta de Cloud Storage on la plataforma espera trobar el model. Si hi deses en lloc de fer-ho en una ruta pròpia, el registre posterior és directe i la represa funciona sola.

import os
directori_sortida = os.environ.get("AIP_MODEL_DIR", args.salida)

Les seves germanes AIP_TENSORBOARD_LOG_DIR i AIP_CHECKPOINT_DIR compleixen el mateix paper per als registres i els punts de control.

  1. CPU, GPU o TPU: com triar

Accelerador Quan convé Exemple a AlpinaShop Cost relatiu
CPU Models petits, poca profunditat, tabular Prototip del recomanador amb dades d'un mes 1×
GPU T4 Xarxes mitjanes, incrustacions, visió lleugera El recomanador complet 3–5×
GPU L4 / A100 Xarxes grans, visió pesada, ajust de models grans No aplica avui 10–40×
TPU Models molt grans amb operacions matricials massives No aplica avui Variable

Els multiplicadors són ordres de magnitud orientatius i canvien amb el temps i la regió: consulta sempre el tarificador oficial.

El criteri pràctic, en tres regles:

  1. Prototipa sempre en CPU amb una mostra petita. Si el codi té una fallada, és molt més barat descobrir-la en una màquina de 0,20 €/hora.
  2. Puja a GPU quan el temps d'època sigui el coll d'ampolla, i només si has comprovat que la GPU s'aprofita. Una GPU al 15 % d'ús són diners llençats i sol significar que el problema és a tf.data, no a l'accelerador.
  3. TPU només amb models que ho justifiquin. Requereix adaptar el codi, té restriccions de formes i operacions, i el seu avantatge apareix en models amb moltíssim còmput matricial. Per a un recomanador de 2.400 productes i 45.000 clients, una T4 sobra.

Per comprovar l'aprofitament s'activa el perfilador de TensorBoard durant uns quants lots (profile_batch=(20, 40) al callback de l'apartat 12). Mostra el temps dedicat a entrada de dades davant del còmput: si el primer domina, la GPU està esperant i la solució és prefetch, interleave i fitxers més ben particionats, no una màquina més gran.

  1. Entrenament distribuït, en el conceptual

Quan un model no hi cap o triga massa en una màquina, es reparteix. Dues estratègies:

Paral·lelisme de dades (l'habitual). Cada màquina té una còpia completa del model i processa un tros diferent del lot. Els gradients es promitgen entre totes i els pesos se sincronitzen. Escala bé mentre el cost de comunicació no domini.

Paral·lelisme de model. El model es parteix entre màquines perquè no hi cap en una. Necessari en models de llenguatge enormes; per a AlpinaShop, completament aliè.

A TensorFlow s'expressa amb una estratègia, i la resta del codi amb prou feines canvia:

estrategia = tf.distribute.MultiWorkerMirroredStrategy()

with estrategia.scope():
    model = Recomanador(temperatura=0.05)
    model.compile(optimizer=keras.optimizers.Adam(1e-3))

model.fit(construir_dataset(patro, mida_lot=512), epochs=25)

Tot el que es creï dins d'estrategia.scope() es replica automàticament. A Vertex AI, n'hi ha prou amb declarar més rèpliques al worker-pool-spec; la plataforma configura la comunicació entre nodes.

L'avís realista: distribuir té sobrecàrrega. Amb quatre màquines no s'entrena quatre vegades més ràpid, i amb models petits pot ser fins i tot més lent que amb una de sola, perquè la comunicació de gradients costa més que el còmput. Distribueix només quan hagis mesurat que una màquina no basta. Per al recomanador d'AlpinaShop, una T4 entrena en menys d'una hora: distribuir seria complexitat sense benefici.

  1. Punts de control, VM Spot i per què van junts

Un punt de control (checkpoint) és una foto de l'estat de l'entrenament —pesos i estat de l'optimitzador— desada periòdicament.

ruta_ckpt = os.path.join(directori_sortida, "checkpoints", "ckpt-{epoch:03d}")

callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath=ruta_ckpt,
        save_weights_only=True,
        save_freq="epoch",
    ),
    keras.callbacks.BackupAndRestore(
        backup_dir=os.path.join(directori_sortida, "backup")
    ),
]

BackupAndRestore és el que fa la feina interessant: si el procés mor i es reinicia, reprèn a l'època on era en lloc de començar de zero. No cal escriure lògica de represa.

I aquí ve la connexió amb els diners. Les VM Spot costen una fracció de les normals —el descompte típic és molt gran, encara que variable— a canvi que Google les pugui reclamar amb un preavís mínim. Sense punts de control, una interrupció a l'hora i mitja d'entrenament significa perdre-ho tot. Amb punts de control per època, es perden com a màxim uns minuts.

gcloud ai custom-jobs create \
  --project=alpinashop-datos --region=europe-west1 \
  --display-name=reco-two-tower-spot \
  --worker-pool-spec="machine-type=n1-standard-8,accelerator-type=NVIDIA_TESLA_T4,accelerator-count=1,replica-count=1,container-image-uri=...:1.0.3" \
  --config=config-spot.yaml
# config-spot.yaml
scheduling:
  strategy: SPOT
  restartJobOnWorkerRestart: true
Estratègia Cost relatiu Risc Quan utilitzar-la
Estàndard 100 % Cap Entrenament amb termini estricte
Spot + punts de control Fracció de l'anterior Interrupcions absorbides Experimentació i reentrenaments periòdics
Spot sense punts de control Barat Alt: es perd tot Mai

Per a AlpinaShop, els reentrenaments de la qual són nocturns i sense urgència, Spot amb punts de control és l'elecció òbvia.

  1. TensorBoard gestionat i Vertex AI Experiments

Entrenar a cegues és la manera més ràpida de perdre temps. Vertex AI TensorBoard és la versió gestionada de TensorBoard: els registres van a Cloud Storage i es visualitzen sense muntar un servidor.

callbacks.append(
    keras.callbacks.TensorBoard(
        log_dir=os.environ["AIP_TENSORBOARD_LOG_DIR"],
        update_freq="epoch",
        profile_batch=(20, 40),
    )
)

Què mirar, per ordre d'utilitat: la pèrdua d'entrenament i de validació al mateix gràfic —si la primera baixa i la segona puja, hi ha sobreajust i cal aturar-se—; si la pèrdua no baixa gens, la taxa d'aprenentatge sol ser la culpable (massa alta oscil·la, massa baixa no avança); i el perfilador, per confirmar que la GPU treballa.

Vertex AI Experiments afegeix la capa que falta: comparar execucions entre si i registrar amb quins paràmetres es va fer cadascuna.

from google.cloud import aiplatform

aiplatform.init(project="alpinashop-datos", location="europe-west1",
                experiment="recomendador-alpinashop")

with aiplatform.start_run(run_name="two-tower-dim64-temp005") as execucio:
    execucio.log_params({
        "dim": 64, "temperatura": 0.05, "lote": 512,
        "learning_rate": 1e-3, "epocas": 25, "dropout": 0.2,
    })
    historia = model.fit(...)
    execucio.log_metrics({
        "perdua_final":       float(historia.history["perdua"][-1]),
        "recall_at_10":        0.243,
        "recall_at_10_baseline": 0.198,   # el SQL de 05-01
    })

Fixa't en l'última mètrica: cada execució registra també el resultat de la línia base. Així, la comparació amb el recomanador heurístic de la decisió DA-003 no és una discussió de memòria, és una columna en una taula.

Un consell que estalvia setmanes: el run_name ha de descriure la configuració. two-tower-dim64-temp005 d'aquí a tres mesos continua significant alguna cosa; prova7 no.

  1. Desar i servir: SavedModel, Registry i endpoint

El format de serialització de TensorFlow és SavedModel: un directori amb el graf, els pesos i les signatures d'entrada i sortida.

Per al recomanador, el que se serveix no és el model complet: és la torre de client. Els vectors de producte es calculen una vegada i es desen.

# 1) Vectors de producte: es calculen una vegada, es desen a BigQuery
vectors = model.producte.predict(dataset_cataleg)   # (2400, 64)

# 2) Nomes la torre de client es desa com a servei
model.client.save(os.path.join(directori_sortida, "torre_cliente"))

I la cerca per similitud es resol a BigQuery, sense infraestructura addicional:

CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.reco_two_tower` AS
SELECT
  c.cliente_hash,
  p.sku,
  ROUND(( SELECT SUM(cv * pv)
          FROM UNNEST(c.vector) cv WITH OFFSET i
          JOIN UNNEST(p.vector) pv WITH OFFSET j ON i = j ), 4) AS afinidad
FROM `alpinashop-datos.alpinashop_analitica.vectores_cliente`  c
CROSS JOIN `alpinashop-datos.alpinashop_analitica.vectores_producto` p
QUALIFY ROW_NUMBER() OVER (PARTITION BY c.cliente_hash ORDER BY afinidad DESC) <= 20;

Registre i desplegament, si es volgués servir en línia:

gcloud ai models upload \
  --project=alpinashop-datos --region=europe-west1 \
  --display-name=reco-torre-cliente \
  --artifact-uri=gs://alpinashop-datalake/modelos/reco/v3/torre_cliente \
  --container-image-uri=europe-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-15:latest \
  --version-aliases=candidato

El contenidor de TensorFlow Serving precompilat sap llegir un SavedModel i exposar-lo per HTTP i gRPC sense que escriguis res de servidor.

L'alternativa: servir a Cloud Run. S'empaqueta el model en una imatge amb un petit servidor i es desplega com a servei. Avantatges: escala a zero —no pagues si ningú no truca—, és la mateixa tecnologia que ja farà servir el catàleg per la decisió DA-001, i l'equip la coneixerà. Inconvenients: t'ocupes tu del servidor, del versionatge i del monitoratge del model. La comparació completa és a 07-02; aquí n'hi ha prou amb saber que existeix i que per a càrregues intermitents sol guanyar.

I la decisió que ja va prendre AlpinaShop a 05-01 continua dempeus: per lots cada nit, taula precalculada. La torre de client només caldria en línia si s'hagués de recomanar en funció del que el client està fent en aquesta sessió, i avui no és el cas.

  1. Optimització per a inferència

Un model entrenat no està optimitzat per respondre ràpid. Tres tècniques, de menor a major esforç:

Quantització. Convertir els pesos de 32 bits en coma flotant a 16 bits o a enters de 8 bits: el model ocupa entre la meitat i la quarta part i respon més ràpid, a canvi d'una pèrdua de precisió que sol ser petita i que cal mesurar, no suposar.

Poda. Eliminar pesos propers a zero. Redueix mida, però el benefici en velocitat depèn del maquinari. Optimització del graf. Fusionar operacions i eliminar nodes innecessaris; TensorFlow Serving en fa una part sol.

Tècnica Reducció de mida Guany de latència Risc de qualitat
Quantització a 16 bits ~50 % Moderada Molt baix
Quantització a 8 bits ~75 % Alta Mitjà: cal mesurar
Poda / graf Variable Baixa sense maquinari específic Baix o mitjà

Sobre la latència: la mètrica que importa és el percentil 95 (p95), no la mitjana. Si 95 de cada 100 peticions responen en 40 ms i 5 triguen 800 ms, la mitjana diu 78 ms i sona bé, però aquestes 5 són clients amb la pàgina bloquejada. Els SLO es defineixen sobre percentils, i això es tracta a fons a 07-06.

I l'observació que tanca l'apartat: per al recomanador d'AlpinaShop, amb predicció per lots nocturna, res d'això no cal. Optimitzar la inferència d'un model que s'executa una vegada al dia sobre 45.000 clients és optimitzar el que no fa mal.

  1. PyTorch, JAX i la regla d'or del cost

Vertex AI és agnòstic respecte al framework. L'entrenament personalitzat executa contenidors, i dins d'un contenidor hi cap el que sigui.

Framework Contenidors precompilats Comentari
TensorFlow / Keras Entrenament i servei Màxima integració amb TF Serving i TFX
PyTorch Entrenament i servei (TorchServe) Molt estès; suport de primera classe
JAX Entrenament Fort en TPU i recerca
scikit-learn, XGBoost Entrenament i servei Per a tabular, sovint la millor opció real

L'elecció de framework es fa per què sap l'equip i quin codi es pot reutilitzar, no per preferències de la plataforma. I una observació honesta que tanca el cercle del mòdul: per a problemes tabulars, XGBoost sol batre una xarxa neuronal amb molt menys esforç. Les xarxes profundes brillen amb dades no estructurades —imatges, text, seqüències— i amb problemes de representació com aquest recomanador.

La regla d'or del cost, sense adorns: no deixis GPU enceses. Un treball d'entrenament personalitzat s'apaga sol en acabar, i per això és segur. Els perills són uns altres dos:

  • Una instància de Workbench amb GPU creada per provar i oblidada. Costa més de deu vegades una sense GPU.
  • Un endpoint amb GPU desplegat per a una demostració. Factura per hora, sense trànsit, indefinidament.
gcloud workbench instances list --project=alpinashop-datos \
  --format="table(name, state, gceSetup.machineType, gceSetup.acceleratorConfigs)"
gcloud ai endpoints list --region=europe-west1 --project=alpinashop-datos

I les mesures preventives: idle-timeout-seconds a tot notebook, alerta de pressupost sobre l'etiqueta centro-coste:analitica, i una revisió mensual al calendari. La gestió de costos a fons arriba a 07-05.

Errors Habituals i Consells

GPU al 15 % d'ús. El problema gairebé mai no és la GPU: és la lectura de dades. Revisa prefetch, interleave i el particionat dels TFRecord abans de demanar una màquina més gran.

EarlyStopping sense restore_best_weights=True. Et quedes amb els pesos de l'última època, que per definició són pitjors que els de la millor.

Búfer de shuffle massa petit. Si les dades estan ordenades per data, un búfer curt deixa els lots correlacionats i el model aprèn l'ordre.

Dades en una regió i entrenament en una altra. Latència, cost de sortida i un entrenament innecessàriament lent. Tot a europe-west1.

Distribuir sense haver mesurat. Amb models petits, quatre màquines poden ser més lentes que una per la comunicació de gradients.

Utilitzar latest a la imatge del contenidor. Trenca la reproductibilitat, igual que a 05-01.

Oblidar els exemples negatius. Un model entrenat només amb positius no aprèn a discriminar: aprèn a dir "sí" a tot.

Servir el model complet quan n'hi havia prou amb una torre. Multiplica el cost d'inferència pel nombre de productes.

Consell: prototipa amb un 1 % de les dades en CPU. Si el codi funciona amb 50.000 files, funcionarà amb 5 milions. Descobrir una fallada de forma de tensor en una GPU de pagament és car i evitable.

Consell: registra sempre la línia base al mateix experiment. Que cada execució porti al costat el número del SQL de 05-01 converteix "crec que ha millorat" en un fet.

Consell: fixa totes les llavors (tf.random.set_seed, numpy, python). Un entrenament irreproduïble és impossible de depurar.

Exercicis

Exercici 1

En Dani llança l'entrenament del recomanador en una màquina n1-standard-8 amb una GPU T4. Cada època triga 47 minuts. En mirar el perfilador de TensorBoard, veu que la GPU està ocupada el 18 % del temps. Diagnostica el problema, proposa quatre mesures per ordre d'impacte i estima quina millora es pot esperar.

Exercici 2

La Marta pregunta si el recomanador two-tower s'ha de servir en un endpoint de Vertex AI amb GPU, en un endpoint amb CPU, a Cloud Run o per lots. Analitza les quatre opcions per al cas d'AlpinaShop i justifica una recomanació. Inclou què canviaria la decisió.

Exercici 3

Després d'entrenar, el recall@10 del model és 0,243 davant del 0,198 de la línia base SQL. Es desplega? Argumenta-ho i defineix quina prova faries abans de decidir.

Solucions

Solució 1

Diagnòstic: coll d'ampolla a la canalització de dades. Una GPU al 18 % significa que passa el 82 % del temps esperant dades. L'entrenament no està limitat per còmput, sinó per entrada. Pagar una GPU perquè esperi és el malbaratament més comú d'aquesta lliçó.

Quatre mesures, per impacte esperat:

1. Afegir prefetch(tf.data.AUTOTUNE) al final de la canalització. És la mesura de més impacte i la de menys esforç. Sense ella, el cicle és estrictament seqüencial: la CPU prepara un lot, la GPU el processa, la CPU prepara el següent. Amb prefetch, la CPU prepara el lot n+1 mentre la GPU processa el n. Només amb això, l'ús pot pujar a un 40-50 %.

2. Revisar el particionat dels TFRecord. Si les dades són en un sol fitxer de 20 GB, interleave no hi pot fer res: hi ha una única lectura seqüencial. Reparticionar a uns 200 fitxers de ~100 MB i llegir amb cycle_length=8 multiplica el cabal des de Cloud Storage. Impacte alt, esforç mitjà (un treball de Dataflow).

3. Comprovar la regió del bucket. Si el bucket és multiregió o és fora d'europe-west1, cada lectura afegeix latència. Ha de ser regional i coincidir amb la màquina.

4. Moure el preprocessament pesat fora del map. Si el map fa transformacions costoses per exemple —càlculs, normalitzacions complexes—, això consumeix CPU a cada època. Precalcular-les en generar els TFRecord les executa una vegada en lloc de vint-i-cinc.

I una mesura complementària: n1-standard-8 són 8 vCPU per alimentar una T4. Si després de les quatre mesures la CPU continua saturada, pujar a n1-standard-16 és raonable —i surt rendible, perquè el cost de les vCPU és petit comparat amb el de tenir la GPU aturada.

Millora esperada: amb les mesures 1 i 2, un ús de GPU del 70-85 % és assolible, cosa que portaria l'època de 47 minuts a un rang de 10-15. La comprovació és tornar a mirar el perfilador, no suposar-ho.

Verificació: abans de tocar res, mesurar el cabal de la canalització sense model —iterar 200 lots del Dataset cronometrant i dividir exemples entre segons—. Si la lectura sola ja va lenta, el model no hi té res a veure i el diagnòstic queda confirmat.

Solució 2

Les quatre opcions:

Opció Cost Latència Complexitat Valoració
Endpoint amb GPU Molt alt (24×7) Molt baixa Baixa Descartada
Endpoint amb CPU Alt (24×7) Baixa Baixa Descartada avui
Cloud Run Baix, escala a zero Baixa amb arrencada en fred Mitjana Reserva
Predicció per lots Molt baix Alta (irrellevant) Baixa Recomanada

Descartar la GPU és immediat. La torre de client és una xarxa diminuta: tres capes denses sobre una incrustació. En CPU respon en pocs mil·lisegons. Una GPU per a això és com fer servir un camió per portar una carta.

Descartar l'endpoint 24×7 pel mateix que a 05-01: un endpoint factura per hora de node encara que no rebi trànsit. Amb recomanacions que no canvien d'un minut a un altre, és pagar disponibilitat que ningú no utilitza.

Cloud Run és l'opció intermèdia raonable i mereix consideració seriosa: escala a zero, així que sense trànsit no costa res; és la mateixa tecnologia del catàleg per DA-001, així que l'equip la coneixerà; i l'arrencada en fred es mitiga amb una instància mínima. El seu inconvenient és que cal gestionar el servidor, el versionatge i el monitoratge del model a mà, sense la integració amb Model Registry ni amb Model Monitoring.

Recomanació: predicció per lots nocturna, coherent amb la decisió DA-002.

El raonament en tres punts. Primer, les recomanacions no depenen de la sessió en curs: els vectors de client canvien quan canvia el seu historial, és a dir, quan fa una comanda, no quan mou el ratolí. Segon, 45.000 clients × 20 recomanacions són 900.000 files, una taula trivial que Firestore o Memorystore (02-06) serveixen en microsegons i que a més no depèn que cap model estigui viu. Tercer, la disponibilitat millora: si l'endpoint caigués, el web es quedaria sense recomanacions; amb una taula precalculada, no hi ha res que pugui caure.

Què canviaria la decisió. Tres escenaris concrets:

  1. Recomanar dins de la sessió. Si es volgués reaccionar al que el client està mirant ara —"fa cinc minuts que mira grampons"—, el vector de client s'hauria de calcular en calent i caldria servei en línia. Seria el moment de Cloud Run, no d'un endpoint dedicat.
  2. Clients anònims. El 60 % del trànsit no està identificat i no té vector precalculat. Per a ells avui es fa servir el recomanador heurístic per producte, que no necessita client. Si es volgués personalitzar per a anònims segons el comportament de sessió, tornaríem al cas anterior.
  3. Catàleg molt més gran. Amb 2.400 productes, precalcular és trivial. Amb 500.000, la taula client × producte deixaria de ser còmoda i entraria en joc Vector Search (05-06) per a la cerca de veïns propers.

Solució 3

No amb aquestes dades. Encara no.

Què diu el número. Una millora de 0,198 a 0,243 en recall@10 és un 22,7 % relatiu, que no és menyspreable. Però hi ha tres raons per no fer el salt directament a producció:

1. És una mètrica offline. El recall@10 mesura si el model hauria encertat sobre el que els clients van comprar en el passat, quan el web no recomanava res. Un recomanador canvia el comportament que intenta predir: si suggereix un producte, la probabilitat que es compri puja pel fet de suggerir-lo. Les mètriques offline sistemàticament sub o sobreestimen l'efecte real, i no se sap en quina direcció fins que es prova.

2. No mesura el que importa al negoci. El que decideix és el valor de la comanda, no el nombre d'encerts. Un recomanador que encerta molt suggerint accessoris de 4 € pot generar menys marge que un que encerta menys amb productes de 90 €. Cal mirar també les mètriques de negoci: valor mitjà de la comanda, articles per comanda, taxa de clic a la recomanació, i la taxa de devolució (recomanar malament augmenta devolucions).

3. Hi ha un interval de confiança que ningú no ha calculat. Quants clients hi ha al conjunt de test? Si en són 2.000, la diferència entre 0,198 i 0,243 pot estar dins del soroll. Sense barres d'error, dos números no són una comparació.

La prova que cal fer: un A/B test.

Element Definició
Grup A (control) 50 % del trànsit, recomanador heurístic SQL (DA-003)
Grup B (tractament) 50 % del trànsit, model two-tower
Mètrica principal Valor mitjà de la comanda
Mètriques secundàries Taxa de clic a la recomanació, articles per comanda, conversió
Mètrica de guàrdia Taxa de devolució (que no empitjori)
Durada Almenys 2-3 setmanes completes
Assignació Per client, estable, no per sessió

Quatre detalls que fan vàlida la prova:

  • Durada de setmanes completes. El comportament de compra varia molt entre cap de setmana i dies laborables. Una prova de tres dies mesura el dia, no el model.
  • Assignació estable per client. Si un client veu un recomanador el dilluns i un altre el dimarts, l'experiència és incoherent i el mesurament es contamina.
  • Mètrica de guàrdia. Si la taxa de devolució puja significativament, s'atura la prova encara que les vendes pugin: recomanar malament genera cost logístic i insatisfacció.
  • Mida de mostra calculada per endavant. Cal saber abans de començar quantes comandes calen per detectar la millora esperada. Si amb el trànsit d'AlpinaShop calguessin tres mesos per detectar un 2 %, val més saber-ho abans de muntar l'experiment.

I el criteri de decisió, escrit abans de mirar els resultats, que és la part que més s'incompleix: es desplega el model si el valor mitjà de la comanda millora de forma estadísticament significativa i la taxa de devolució no empitjora més d'un punt. Fixar-lo després de veure les dades és la manera més elegant d'enganyar-se.

Un escenari que cal acceptar per endavant: és perfectament possible que la prova no mostri diferència. Si passa, la decisió correcta és quedar-se amb el SQL, perquè és més simple, més barat, més explicable i no requereix reentrenament. Estar disposat a llençar setmanes de feina quan les dades no acompanyen és el que distingeix un equip madur.

Conclusió

Has baixat al codi, i saps quan val la pena: arquitectura pròpia, pèrdua a mida, preprocessament específic o sortida no estàndard. El recomanador d'AlpinaShop cau en els quatre casos, i per això justifica l'esforç. Si el teu problema cap en una taula amb una columna objectiu, la resposta continua sent AutoML o BigQuery ML.

Tens el mínim de TensorFlow i Keras per seguir: tensors, capes, la diferència entre el model seqüencial i el funcional —que aquí importa perquè hi ha dues entrades independents—, i la compilació amb optimitzador, pèrdua i mètriques, amb EarlyStopping restaurant els millors pesos.

Entens què és una incrustació i per què canvia les regles: 32 números apresos en lloc de 2.400 posicions buides, amb la similitud emergint sola de les dades de compra. I has construït l'arquitectura de dues torres, entenent que la raó de partir-la en dues no és teòrica sinó operativa: els vectors de producte es calculen una vegada, el de client es calcula al moment, i la cerca és de veïns propers i no de 2.400 avaluacions del model. Has vist el mostreig negatiu dins del lot, que converteix una matriu de similituds en exemples negatius gratis.

Saps llegir dades sense ofegar la GPU amb tf.data —interleave, shuffle amb búfer gran, map paral·lel, batch i sobretot prefetch—, i per què TFRecord particionat en fitxers de 100 a 200 MB a la mateixa regió que la màquina és la diferència entre una GPU al 18 % i una al 80 %.

Has empaquetat l'entrenament com a contenidor propi amb versió fixada, llançat amb gcloud ai custom-jobs create, i tens el criteri de CPU, GPU o TPU: prototipar sempre en CPU, pujar a GPU només quan l'època sigui el coll d'ampolla i la GPU s'aprofiti de debò, i oblidar-te de les TPU fins que un model les justifiqui. Saps què és el paral·lelisme de dades i per què distribuir sense mesurar pot sortir més lent.

Has unit punts de control i VM Spot en la combinació que fa l'experimentació assequible: BackupAndRestore reprèn on es va quedar, i la interrupció d'una Spot deixa de ser un desastre. Segueixes l'entrenament amb TensorBoard gestionat i compares configuracions amb Vertex AI Experiments, registrant sempre la línia base al costat perquè la comparació sigui un fet i no un record.

I saps desar i servir: SavedModel, pujada al Model Registry, TensorFlow Serving precompilat o Cloud Run com a alternativa (07-02), amb l'observació que aquí només se serveix una de les dues torres. Coneixes la quantització i per què la latència es mesura en p95 i no en mitjana. I tens la regla d'or gravada: cap GPU encesa sense feina.

Però fixa't en el que ha costat. Un contenidor, una canalització de dades, una arquitectura, una pèrdua a mida, un entrenament, un experiment, un A/B test de tres setmanes, i encara la possibilitat honesta que el SQL de trenta línies guanyi. És el preu del control, i de vegades val la pena.

De vegades, no. Perquè AlpinaShop té dues preguntes més pendents: 8.000 ressenyes de clients en text lliure que ningú no ha llegit, i els 60 GB d'imatges que AutoML va classificar per categoria però de les quals no s'ha extret res més. Per a totes dues existeixen models ja entrenats, per gent amb més dades i més recursos dels que AlpinaShop tindrà mai, disponibles amb una crida a una API i sense ni un minut d'entrenament.

A 05-04, l'API de llenguatge natural, apliquem el principi contrari al d'aquesta lliçó: no entrenis el que ja està entrenat. Veuràs què ofereix exactament l'API de Natural Language —sentiment, entitats, classificació, sintaxi—, entendràs d'una vegada què signifiquen score i magnitude, que és on tothom s'equivoca, processaràs les 8.000 ressenyes des de Python amb control de quotes i de cost, creuaràs el sentiment amb les devolucions a BigQuery per respondre si els productes pitjor valorats es retornen més, i veuràs amb honestedat on falla —ironia, negacions i l'argot de la muntanya— i quan compensa demanar-ho a Gemini o entrenar un classificador propi.

Curs de Google Cloud Platform (GCP)

Mòdul 1: Introducció a Google Cloud Platform

Mòdul 2: Serveis principals de GCP

Mòdul 3: Xarxes i seguretat

Mòdul 4: Dades i anàlisi

Mòdul 5: Aprenentatge automàtic i IA

Mòdul 6: DevOps i monitoratge

Mòdul 7: Temes avançats de GCP

Mòdul 8: Projecte final

© Copyright 2026. Tots els drets reservats