Hasta aquí, AlpinaShop no ha escrito una sola línea de código de modelado. Con SQL puso en producción un recomendador heurístico, y con AutoML entrenó un clasificador de carritos y otro de imágenes. Para muchas empresas, eso es suficiente para siempre, y no hay ninguna vergüenza en ello.

Pero Marta quiere algo que ninguna de las dos vías da. Quiere un recomendador que entienda a los clientes y a los productos, no que memorice pares. Que sepa que quien compra crampones, piolet y casco es un perfil distinto de quien compra mochila de 25 litros y bastones, y que a un cliente nuevo con dos compras le pueda sugerir algo sensato porque se parece a otros clientes que sí tienen historial. Y que funcione con productos que aún no aparecen en productos_juntos porque llegaron el mes pasado.

Eso no es clasificar ni predecir un número: es aprender una representación. No hay columna objetivo. No hay tabla plana. Y no hay opción sin código.

Esta lección es la bajada al nivel del código. No para convertirte en investigador de deep learning, sino para que sepas cuándo hace falta, qué es lo mínimo que hay que entender, y cómo se ejecuta todo eso en Google Cloud sin montar ni mantener una sola máquina.

Contenido

  1. Cuándo hace falta bajar al código
  2. TensorFlow y Keras: lo mínimo imprescindible
  3. Embeddings: la idea que hace posible el recomendador
  4. La arquitectura de dos torres
  5. El modelo de AlpinaShop en código
  6. tf.data: leer datos sin ahogar a la GPU
  7. TFRecord y la lectura desde Cloud Storage
  8. Entrenar en Vertex AI: empaquetar el código
  9. CPU, GPU o TPU: cómo elegir
  10. Entrenamiento distribuido, en lo conceptual
  11. Checkpoints, VMs Spot y por qué van juntos
  12. TensorBoard gestionado y Vertex AI Experiments
  13. Guardar y servir: SavedModel, Registry y endpoint
  14. Optimización para inferencia
  15. PyTorch, JAX y la regla de oro del coste

  1. Cuándo hace falta bajar al código

No siempre. La mayoría de las veces, no. Los cuatro casos en que sí:

Situación Por qué AutoML no llega
Arquitectura propia Dos torres, redes siamesas, atención sobre secuencias: no están en el catálogo de AutoML
Función de pérdida a medida Optimizar margen en lugar de acierto, o penalizar asimétricamente los errores
Preprocesado específico Negativos muestreados dinámicamente, aumentado de datos particular
Salida no estándar Un vector de 64 dimensiones en lugar de una clase o un número

El recomendador de AlpinaShop cae en los cuatro a la vez, lo que lo convierte en un ejemplo honesto. La salida no es una clase: es un vector por cliente y otro por producto. La pérdida no es "acertaste o no": es "acerca los que van juntos y separa los que no". Y los ejemplos negativos —productos que el cliente no compró— hay que generarlos, porque no existen en los datos.

Y el criterio inverso, igual de importante: si tu problema encaja en una tabla con una columna objetivo, no bajes al código. Lo que ganas en control lo pagas en semanas de trabajo y en mantenimiento perpetuo.

  1. TensorFlow y Keras: lo mínimo imprescindible

TensorFlow es una biblioteca para construir y entrenar modelos numéricos. Keras es su API de alto nivel, la que se usa hoy para casi todo. Cuatro conceptos bastan para seguir la lección.

Tensor. Un array multidimensional con tipo. Un escalar es un tensor de rango 0, un vector de rango 1, una matriz de rango 2. Un lote de 32 vectores de 64 dimensiones es 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 filas, 3 columnas
print(t.dtype)   # float32

Capa. Una transformación con parámetros aprendibles. Dense(64) multiplica la entrada por una matriz de pesos y suma un sesgo; Embedding(1000, 32) es una tabla de 1.000 vectores de 32 dimensiones que se buscan por índice.

Modelo. Una composición de capas. Dos formas de construirlo:

from tensorflow import keras
from tensorflow.keras import layers

# Secuencial: una capa detras de otra. Simple y limitado.
modelo_seq = keras.Sequential([
    layers.Dense(64, activation="relu", input_shape=(20,)),
    layers.Dense(32, activation="relu"),
    layers.Dense(1,  activation="sigmoid"),
])

# Funcional: grafo. Permite varias entradas y varias salidas.
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))
salida = layers.Dense(1, activation="sigmoid")(layers.Concatenate()([x, y]))
modelo_fun = keras.Model(inputs=[entrada_a, entrada_b], outputs=salida)

La diferencia importa para esta lección. El modelo secuencial solo sirve para pilas lineales. El recomendador tiene dos entradas independientes —cliente y producto— que se procesan por caminos separados y se combinan al final. Eso solo se puede expresar con la API funcional o subclasificando keras.Model.

Compilar y entrenar.

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

historia = modelo_seq.fit(
    datos_entrenamiento,
    validation_data=datos_validacion,
    epochs=20,
    callbacks=[keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)],
)
  • El optimizador decide cómo se ajustan los pesos. Adam es la elección por defecto sensata.
  • La pérdida es lo que se minimiza. Es la traducción matemática de "qué significa equivocarse", y es donde se personaliza de verdad un modelo.
  • Una época es una pasada completa por los datos.
  • EarlyStopping para cuando la validación deja de mejorar y restaura los mejores pesos. Sin restore_best_weights=True te quedas con los pesos de la última época, que son peores. Es un error clásico.

Con esto es suficiente. No hace falta entender el descenso de gradiente para seguir.

  1. Embeddings: la idea que hace posible el recomendador

Un embedding es un vector de números que representa una entidad, aprendido de forma que las entidades parecidas queden cerca en ese espacio.

El problema que resuelve se ve mejor con la alternativa. Para representar 2.400 productos sin embeddings, la codificación one-hot usa un vector de 2.400 posiciones, con un 1 y 2.399 ceros. Es enorme, es disperso, y sobre todo no dice nada: dos mochilas están tan lejos entre sí como una mochila y un piolet, porque todos los vectores son igual de distintos.

Un embedding de 32 dimensiones representa cada producto con 32 números aprendidos. Y aprendidos con un criterio: si dos productos aparecen en contextos parecidos, sus vectores acaban pareciéndose.

Aspecto One-hot Embedding
Dimensiones para 2.400 productos 2.400 32–128
Contenido Solo identidad Similitud aprendida
Productos nuevos Trivial pero inútil Requiere reentrenar o usar atributos
Coste de memoria Alto y disperso Bajo y denso

Lo interesante es que la similitud emerge sola, sin que nadie declare que un piolet se parece a unos crampones: sale de los datos de compra. Y una vez tienes vectores, comparar es trivial. El producto escalar o la similitud coseno entre dos vectores es un número que mide afinidad: si el vector del piolet es [0,21 −0,44 0,87 0,12] y el de los crampones [0,19 −0,40 0,91 0,09], su coseno vale casi 1 —apuntan casi en la misma dirección—, mientras que frente al de unas sandalias de trekking sale negativo.

El tamaño del embedding es un hiperparámetro con un compromiso claro: pocas dimensiones no capturan matices, demasiadas memorizan y sobreajustan. Para 2.400 productos, entre 32 y 64 es un rango razonable de partida.

  1. La arquitectura de dos torres

La intuición, sin matemáticas.

Imagina dos funciones. La primera toma todo lo que sabes de un cliente —su historial, sus categorías, su ticket medio— y devuelve un vector de 64 números. La segunda toma todo lo que sabes de un producto —su categoría, su precio, su marca, su identificador— y devuelve otro vector de 64 números en el mismo espacio.

Si el entrenamiento ha ido bien, el producto escalar entre el vector de un cliente y el de un producto es alto cuando ese cliente compraría ese producto, y bajo cuando no.

flowchart TD
    A[Datos del cliente<br/>historial, categorias, ticket] --> B[Torre de cliente<br/>capas densas]
    C[Datos del producto<br/>id, categoria, precio, marca] --> D[Torre de producto<br/>capas densas]
    B --> E[Vector cliente<br/>64 dim]
    D --> F[Vector producto<br/>64 dim]
    E --> G[Producto escalar]
    F --> G
    G --> H[Puntuacion de afinidad]

Por qué dos torres y no una sola red que reciba todo junto. Por una razón puramente operativa, y es la clave de todo el diseño: las dos torres se pueden ejecutar por separado.

Los 2.400 vectores de producto se calculan una vez y se guardan. Cuando llega un cliente, se calcula solo su vector —una pasada por la torre de cliente— y se buscan los productos más cercanos entre los ya precalculados. Eso es una búsqueda de vecinos próximos, que con índices adecuados es cuestión de milisegundos incluso con millones de elementos.

La alternativa —una sola red que reciba el par cliente-producto— obligaría a evaluar el modelo 2.400 veces por cada cliente, una por producto. Con dos torres, una vez.

Los ejemplos negativos. Los datos de AlpinaShop solo contienen compras: pares cliente-producto positivos. Para que el modelo aprenda a distinguir, necesita también ejemplos de lo que no se compró, y hay que generarlos. La técnica estándar es el muestreo negativo dentro del lote: dentro de un lote de 512 pares reales, cada cliente se empareja con los productos de los otros 511 pares y esos se tratan como negativos. Es eficiente porque no hay que buscar nada, y funciona sorprendentemente bien.

Tiene un sesgo conocido, y conviene saberlo: los productos populares aparecen más veces en los lotes y por tanto se penalizan más como negativos. Hay correcciones para eso; para una primera versión de AlpinaShop, no compensa la complejidad.

  1. El modelo de AlpinaShop en código

El modelo completo, comentado. Es el corazón de la lección, así que léelo despacio.

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

DIM = 64          # dimension del espacio compartido
N_CLIENTES = 45000
N_PRODUCTOS = 2400
N_CATEGORIAS = 18


def torre_cliente():
    """Convierte los datos de un cliente en un vector de DIM dimensiones."""
    id_cliente   = keras.Input(shape=(), dtype=tf.int32,   name="cliente_idx")
    num_cliente  = keras.Input(shape=(4,), dtype=tf.float32, name="cliente_num")

    # Embedding: cada cliente tiene su propio vector aprendido
    emb = layers.Embedding(N_CLIENTES, DIM, name="emb_cliente")(id_cliente)

    # Senyales numericas ya normalizadas: pedidos_90d, ticket_medio,
    # dias_desde_ultimo, antiguedad_meses
    x = layers.Concatenate()([emb, num_cliente])
    x = layers.Dense(128, activation="relu")(x)
    x = layers.Dropout(0.2)(x)
    x = layers.Dense(DIM)(x)

    # Normalizar a norma 1: el producto escalar pasa a ser similitud coseno
    salida = layers.Lambda(lambda v: tf.math.l2_normalize(v, axis=1))(x)
    return keras.Model([id_cliente, num_cliente], salida, name="torre_cliente")


def torre_producto():
    """Convierte los datos de un producto en un vector del mismo espacio."""
    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_PRODUCTOS,  DIM, name="emb_producto")(id_prod)
    emb_c = layers.Embedding(N_CATEGORIAS, 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)
    salida = layers.Lambda(lambda v: tf.math.l2_normalize(v, axis=1))(x)
    return keras.Model([id_prod, categoria, num_prod], salida, name="torre_producto")

Tres decisiones de diseño que explican el resto:

  1. Cada torre mezcla embedding e información de atributos. El embedding puro memoriza la entidad concreta; los atributos (precio, categoría) permiten decir algo razonable sobre un producto nuevo que apenas se ha vendido. Es la mitigación práctica del arranque en frío.
  2. Dropout(0.2) apaga aleatoriamente un 20 % de las neuronas en cada paso de entrenamiento. Es regularización: impide que el modelo dependa demasiado de una señal concreta y reduce el sobreajuste.
  3. La normalización L2 final hace que todos los vectores tengan longitud 1. Así el producto escalar es exactamente la similitud coseno, acotada entre −1 y 1, lo que estabiliza el entrenamiento y hace las puntuaciones comparables entre sí.

El modelo completo, con la pérdida:

class Recomendador(keras.Model):
    def __init__(self, temperatura=0.05):
        super().__init__()
        self.cliente  = torre_cliente()
        self.producto = torre_producto()
        self.temperatura = temperatura
        self.metrica = keras.metrics.Mean(name="perdida")

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

            # Matriz de similitudes: cada cliente contra cada producto DEL LOTE
            similitudes = tf.matmul(v_cli, v_pro, transpose_b=True) / self.temperatura

            # La diagonal son los pares reales (positivos); el resto, negativos
            etiquetas = tf.range(tf.shape(similitudes)[0])
            perdida = tf.reduce_mean(
                tf.nn.sparse_softmax_cross_entropy_with_logits(
                    labels=etiquetas, logits=similitudes))

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

Cómo funciona esta pérdida, en palabras. tf.matmul(v_cli, v_pro, transpose_b=True) produce una matriz cuadrada del tamaño del lote: la celda (i, j) es la afinidad del cliente i con el producto j. Los pares reales están en la diagonal, porque el lote está formado por pares que sí ocurrieron. La entropía cruzada con etiquetas 0, 1, 2, ... le pide al modelo que, para cada cliente, el producto correcto sea el de mayor puntuación entre todos los del lote. Ahí está el muestreo negativo, sin código adicional.

La temperatura (0,05) divide las similitudes antes del softmax. Valores bajos hacen la distribución más "picuda" y el entrenamiento más exigente. Es un hiperparámetro sensible que conviene ajustar.

Esto es exactamente lo que AutoML no puede hacer: una pérdida a medida sobre una arquitectura a medida.

  1. tf.data: leer datos sin ahogar a la GPU

Un error caro y frecuente: alquilar una GPU y descubrir que está al 15 % de uso porque la lectura de datos no le da abasto. Se paga la GPU entera y se aprovecha una fracción.

tf.data construye canalizaciones de datos que se solapan con el cómputo.

def construir_dataset(patron_ficheros, tamano_lote=512, entrenamiento=True):
    ficheros = tf.data.Dataset.list_files(patron_ficheros, shuffle=entrenamiento)

    ds = ficheros.interleave(
        lambda f: tf.data.TFRecordDataset(f, compression_type="GZIP"),
        cycle_length=8,                        # 8 ficheros en paralelo
        num_parallel_calls=tf.data.AUTOTUNE)

    if entrenamiento:
        ds = ds.shuffle(buffer_size=50000)     # baraja dentro de una ventana

    ds = ds.map(parsear_ejemplo, num_parallel_calls=tf.data.AUTOTUNE)
    ds = ds.batch(tamano_lote, drop_remainder=True)
    ds = ds.prefetch(tf.data.AUTOTUNE)         # prepara el lote siguiente
    return ds

Cada pieza, y por qué está:

Operación Qué hace Por qué importa
interleave Lee varios ficheros a la vez Un solo fichero limita el caudal desde Cloud Storage
shuffle Baraja dentro de un búfer Sin esto, los lotes tendrían pedidos consecutivos y correlacionados
map + AUTOTUNE Parsea en paralelo Ajusta los hilos solo, según la máquina
batch Agrupa en lotes La GPU es eficiente con lotes, no con filas sueltas
prefetch Prepara el siguiente mientras entrena La operación que más impacto tiene: solapa CPU y GPU
drop_remainder=True Descarta el último lote incompleto Necesario aquí: la pérdida asume lotes cuadrados

Sobre shuffle: el búfer debe ser grande. Con 5.000 sobre un fichero ordenado por fecha, los lotes seguirán conteniendo pedidos casi consecutivos y el modelo verá los datos con un orden artificial. Un búfer de 50.000 barajado sobre ficheros ya mezclados es razonable.

Sobre el tamaño de lote: más grande significa mejor uso de la GPU y, en este modelo concreto, más negativos por ejemplo, lo que suele mejorar la calidad. El límite es la memoria de la GPU. Empezar en 512 y subir mientras quepa es una estrategia sensata.

  1. TFRecord y la lectura desde Cloud Storage

Los datos de AlpinaShop están en BigQuery. Para entrenar hay que exportarlos a un formato que TensorFlow lea rápido, y ese formato es TFRecord: un contenedor binario de registros serializados.

Por qué no CSV. El CSV se parsea como texto, línea a línea, y con millones de filas eso es el cuello de botella. TFRecord es binario, se lee secuencialmente y se comprime bien.

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 directamente, así que la conversión se hace con un trabajo de Dataflow (04-02) o, para volúmenes moderados, con un script. Lo importante es el particionado: muchos ficheros de tamaño medio, no uno gigante.

Estrategia Resultado
1 fichero de 20 GB Lectura secuencial única, interleave inútil, imposible distribuir
20.000 ficheros de 1 MB Sobrecarga de apertura, latencia dominante
200 ficheros de 100 MB Equilibrio recomendado

La regla habitual: ficheros de entre 100 y 200 MB, y al menos tantos ficheros como trabajadores vayas a usar, multiplicado por unas cuantas veces.

El parseo de cada registro:

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 parsear_ejemplo(registro):
    ej = tf.io.parse_single_example(registro, ESQUEMA)
    return {
        "cliente_idx":   tf.cast(ej["cliente_idx"],   tf.int32),
        "producto_idx":  tf.cast(ej["producto_idx"],  tf.int32),
        "categoria_idx": tf.cast(ej["categoria_idx"], tf.int32),
        "cliente_num":   ej["cliente_num"],
        "producto_num":  ej["producto_num"],
    }

Un detalle operativo que causa muchos disgustos: el bucket de los datos y la máquina de entrenamiento deben estar en la misma región, europe-west1. Leer desde otra región añade latencia, coste de salida de datos, y en un entrenamiento largo eso se multiplica por millones de lecturas.

  1. Entrenar en Vertex AI: empaquetar el código

El código de entrenamiento tiene que llegar a la máquina de alguna manera. Dos vías, ya vistas en 05-01, ahora con detalle.

Vía A: aplicación de entrenamiento en contenedor precompilado. Empaquetas tu script en un .tar.gz y Vertex AI lo ejecuta sobre una imagen oficial de TensorFlow. Rápido de montar, poco control sobre las dependencias.

Vía B: contenedor propio. Construyes la imagen y la subes a Artifact Registry. AlpinaShop ya tiene el registro montado, así que es la opción coherente.

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"]

Puntos importantes del Dockerfile. La imagen base oficial ya trae TensorFlow con soporte GPU, CUDA y los controladores correctamente emparejados: montar eso a mano es una fuente inagotable de problemas de versiones. Y requirements.txt debe llevar versiones fijadas (pandas==2.2.1, no pandas), porque si no la imagen que construyas dentro de tres meses no será la misma.

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

# Lanzar el entrenamiento
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"

Se usa Cloud Build para construir la imagen. La integración completa con CI/CD, los disparadores y el pipeline de despliegue son materia de 06-01; aquí basta con el comando.

El script debe leer una variable de entorno que Vertex AI inyecta y que mucha gente ignora: AIP_MODEL_DIR, la ruta de Cloud Storage donde la plataforma espera encontrar el modelo. Si guardas ahí en lugar de en una ruta propia, el registro posterior es directo y la reanudación funciona sola.

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

Sus hermanas AIP_TENSORBOARD_LOG_DIR y AIP_CHECKPOINT_DIR cumplen el mismo papel para los registros y los checkpoints.

  1. CPU, GPU o TPU: cómo elegir

Acelerador Cuándo conviene Ejemplo en AlpinaShop Coste relativo
CPU Modelos pequeños, poca profundidad, tabular Prototipo del recomendador con datos de un mes 1×
GPU T4 Redes medianas, embeddings, visión ligera El recomendador completo 3–5×
GPU L4 / A100 Redes grandes, visión pesada, ajuste de modelos grandes No aplica hoy 10–40×
TPU Modelos muy grandes con operaciones matriciales masivas No aplica hoy Variable

Los multiplicadores son órdenes de magnitud orientativos y cambian con el tiempo y la región: consulta siempre el tarificador oficial.

El criterio práctico, en tres reglas:

  1. Prototipa siempre en CPU con una muestra pequeña. Si el código tiene un fallo, es mucho más barato descubrirlo en una máquina de 0,20 €/hora.
  2. Sube a GPU cuando el tiempo de época sea el cuello de botella, y solo si has comprobado que la GPU se aprovecha. Una GPU al 15 % de uso es dinero tirado y suele significar que el problema está en tf.data, no en el acelerador.
  3. TPU solo con modelos que lo justifiquen. Requiere adaptar el código, tiene restricciones de formas y operaciones, y su ventaja aparece en modelos con muchísimo cómputo matricial. Para un recomendador de 2.400 productos y 45.000 clientes, una T4 sobra.

Para comprobar el aprovechamiento se activa el perfilador de TensorBoard durante unos cuantos lotes (profile_batch=(20, 40) en el callback del apartado 12). Muestra el tiempo dedicado a entrada de datos frente a cómputo: si el primero domina, la GPU está esperando y la solución es prefetch, interleave y ficheros mejor particionados, no una máquina más grande.

  1. Entrenamiento distribuido, en lo conceptual

Cuando un modelo no cabe o tarda demasiado en una máquina, se reparte. Dos estrategias:

Paralelismo de datos (el habitual). Cada máquina tiene una copia completa del modelo y procesa un trozo distinto del lote. Los gradientes se promedian entre todas y los pesos se sincronizan. Escala bien mientras el coste de comunicación no domine.

Paralelismo de modelo. El modelo se parte entre máquinas porque no cabe en una. Necesario en modelos de lenguaje enormes; para AlpinaShop, completamente ajeno.

En TensorFlow se expresa con una estrategia, y el resto del código apenas cambia:

estrategia = tf.distribute.MultiWorkerMirroredStrategy()

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

modelo.fit(construir_dataset(patron, tamano_lote=512), epochs=25)

Todo lo que se cree dentro de estrategia.scope() se replica automáticamente. En Vertex AI, basta con declarar más réplicas en el worker-pool-spec; la plataforma configura la comunicación entre nodos.

El aviso realista: distribuir tiene sobrecarga. Con cuatro máquinas no se entrena cuatro veces más rápido, y con modelos pequeños puede ser incluso más lento que con una sola, porque la comunicación de gradientes cuesta más que el cómputo. Distribuye solo cuando hayas medido que una máquina no basta. Para el recomendador de AlpinaShop, una T4 entrena en menos de una hora: distribuir sería complejidad sin beneficio.

  1. Checkpoints, VMs Spot y por qué van juntos

Un checkpoint es una foto del estado del entrenamiento —pesos y estado del optimizador— guardada periódicamente.

ruta_ckpt = os.path.join(directorio_salida, "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(directorio_salida, "backup")
    ),
]

BackupAndRestore es el que hace el trabajo interesante: si el proceso muere y se reinicia, retoma en la época donde estaba en lugar de empezar de cero. No hay que escribir lógica de reanudación.

Y aquí viene la conexión con el dinero. Las VMs Spot cuestan una fracción de las normales —el descuento típico es muy grande, aunque variable— a cambio de que Google pueda reclamarlas con un preaviso mínimo. Sin checkpoints, una interrupción a la hora y media de entrenamiento significa perderlo todo. Con checkpoints por época, se pierden como mucho unos minutos.

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
Estrategia Coste relativo Riesgo Cuándo usarla
Estándar 100 % Ninguno Entrenamiento con plazo estricto
Spot + checkpoints Fracción del anterior Interrupciones absorbidas Experimentación y reentrenamientos periódicos
Spot sin checkpoints Barato Alto: se pierde todo Nunca

Para AlpinaShop, cuyos reentrenamientos son nocturnos y sin urgencia, Spot con checkpoints es la elección obvia.

  1. TensorBoard gestionado y Vertex AI Experiments

Entrenar a ciegas es la forma más rápida de perder tiempo. Vertex AI TensorBoard es la versión gestionada de TensorBoard: los logs van a Cloud Storage y se visualizan sin montar un servidor.

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

Qué mirar, en orden de utilidad: la pérdida de entrenamiento y de validación en el mismo gráfico —si la primera baja y la segunda sube, hay sobreajuste y hay que parar—; si la pérdida no baja nada, la tasa de aprendizaje suele ser la culpable (demasiado alta oscila, demasiado baja no avanza); y el perfilador, para confirmar que la GPU trabaja.

Vertex AI Experiments añade la capa que falta: comparar ejecuciones entre sí y registrar con qué parámetros se hizo cada una.

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 ejecucion:
    ejecucion.log_params({
        "dim": 64, "temperatura": 0.05, "lote": 512,
        "learning_rate": 1e-3, "epocas": 25, "dropout": 0.2,
    })
    historia = modelo.fit(...)
    ejecucion.log_metrics({
        "perdida_final":       float(historia.history["perdida"][-1]),
        "recall_at_10":        0.243,
        "recall_at_10_baseline": 0.198,   # el SQL de 05-01
    })

Fíjate en la última métrica: cada ejecución registra también el resultado de la línea base. Así, la comparación con el recomendador heurístico de la decisión DA-003 no es una discusión de memoria, es una columna en una tabla.

Un consejo que ahorra semanas: el run_name debe describir la configuración. two-tower-dim64-temp005 dentro de tres meses sigue significando algo; prueba7 no.

  1. Guardar y servir: SavedModel, Registry y endpoint

El formato de serialización de TensorFlow es SavedModel: un directorio con el grafo, los pesos y las firmas de entrada y salida.

Para el recomendador, lo que se sirve no es el modelo completo: es la torre de cliente. Los vectores de producto se calculan una vez y se guardan.

# 1) Vectores de producto: se calculan una vez, se guardan en BigQuery
vectores = modelo.producto.predict(dataset_catalogo)   # (2400, 64)

# 2) Solo la torre de cliente se guarda como servicio
modelo.cliente.save(os.path.join(directorio_salida, "torre_cliente"))

Y la búsqueda de similitud se resuelve en BigQuery, sin infraestructura adicional:

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;

Registro y despliegue, si se quisiera servir en línea:

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 contenedor de TensorFlow Serving precompilado sabe leer un SavedModel y exponerlo por HTTP y gRPC sin que escribas nada de servidor.

La alternativa: servir en Cloud Run. Se empaqueta el modelo en una imagen con un pequeño servidor y se despliega como servicio. Ventajas: escala a cero —no pagas si nadie llama—, es la misma tecnología que ya va a usar el catálogo por la decisión DA-001, y el equipo la conocerá. Inconvenientes: te ocupas tú del servidor, del versionado y de la monitorización del modelo. La comparación completa está en 07-02; aquí basta saber que existe y que para cargas intermitentes suele ganar.

Y la decisión que ya tomó AlpinaShop en 05-01 sigue en pie: por lotes cada noche, tabla precalculada. La torre de cliente solo se necesitaría en línea si hubiera que recomendar en función de lo que el cliente está haciendo en esta sesión, y hoy no es el caso.

  1. Optimización para inferencia

Un modelo entrenado no está optimizado para responder rápido. Tres técnicas, de menor a mayor esfuerzo:

Cuantización. Convertir los pesos de 32 bits en coma flotante a 16 bits o a enteros de 8 bits: el modelo ocupa entre la mitad y la cuarta parte y responde más rápido, a cambio de una pérdida de precisión que suele ser pequeña y que hay que medir, no suponer.

Poda. Eliminar pesos cercanos a cero. Reduce tamaño, pero el beneficio en velocidad depende del hardware. Optimización del grafo. Fusionar operaciones y eliminar nodos innecesarios; TensorFlow Serving hace parte de esto solo.

Técnica Reducción de tamaño Ganancia de latencia Riesgo de calidad
Cuantización a 16 bits ~50 % Moderada Muy bajo
Cuantización a 8 bits ~75 % Alta Medio: hay que medir
Poda / grafo Variable Baja sin hardware específico Bajo o medio

Sobre la latencia: la métrica que importa es el percentil 95 (p95), no la media. Si 95 de cada 100 peticiones responden en 40 ms y 5 tardan 800 ms, la media dice 78 ms y suena bien, pero esas 5 son clientes con la página bloqueada. Los SLO se definen sobre percentiles, y eso se trata a fondo en 07-06.

Y la observación que cierra el apartado: para el recomendador de AlpinaShop, con predicción por lotes nocturna, nada de esto hace falta. Optimizar la inferencia de un modelo que se ejecuta una vez al día sobre 45.000 clientes es optimizar lo que no duele.

  1. PyTorch, JAX y la regla de oro del coste

Vertex AI es agnóstico respecto al framework. El entrenamiento personalizado ejecuta contenedores, y dentro de un contenedor cabe lo que sea.

Framework Contenedores precompilados Comentario
TensorFlow / Keras Entrenamiento y servicio Máxima integración con TF Serving y TFX
PyTorch Entrenamiento y servicio (TorchServe) Muy extendido; soporte de primera clase
JAX Entrenamiento Fuerte en TPU e investigación
scikit-learn, XGBoost Entrenamiento y servicio Para tabular, a menudo la mejor opción real

La elección de framework se hace por qué sabe el equipo y qué código se puede reutilizar, no por preferencias de la plataforma. Y una observación honesta que cierra el círculo del módulo: para problemas tabulares, XGBoost suele batir a una red neuronal con mucho menos esfuerzo. Las redes profundas brillan con datos no estructurados —imágenes, texto, secuencias— y con problemas de representación como este recomendador.

La regla de oro del coste, sin adornos: no dejes GPUs encendidas. Un trabajo de entrenamiento personalizado se apaga solo al terminar, y por eso es seguro. Los peligros son otros dos:

  • Una instancia de Workbench con GPU creada para probar y olvidada. Cuesta más de diez veces una sin GPU.
  • Un endpoint con GPU desplegado para una demo. Factura por hora, sin tráfico, indefinidamente.
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

Y las medidas preventivas: idle-timeout-seconds en todo notebook, alerta de presupuesto sobre la etiqueta centro-coste:analitica, y una revisión mensual en el calendario. La gestión de costes a fondo llega en 07-05.

Errores Comunes y Consejos

GPU al 15 % de uso. El problema casi nunca es la GPU: es la lectura de datos. Revisa prefetch, interleave y el particionado de los TFRecord antes de pedir una máquina mayor.

EarlyStopping sin restore_best_weights=True. Te quedas con los pesos de la última época, que por definición son peores que los de la mejor.

Búfer de shuffle demasiado pequeño. Si los datos están ordenados por fecha, un búfer corto deja los lotes correlacionados y el modelo aprende el orden.

Datos en una región y entrenamiento en otra. Latencia, coste de salida y un entrenamiento innecesariamente lento. Todo en europe-west1.

Distribuir sin haber medido. Con modelos pequeños, cuatro máquinas pueden ser más lentas que una por la comunicación de gradientes.

Usar latest en la imagen del contenedor. Rompe la reproducibilidad, igual que en 05-01.

Olvidar los ejemplos negativos. Un modelo entrenado solo con positivos no aprende a discriminar: aprende a decir "sí" a todo.

Servir el modelo completo cuando bastaba una torre. Multiplica el coste de inferencia por el número de productos.

Consejo: prototipa con un 1 % de los datos en CPU. Si el código funciona con 50.000 filas, funcionará con 5 millones. Descubrir un fallo de forma de tensor en una GPU de pago es caro y evitable.

Consejo: registra siempre la línea base en el mismo experimento. Que cada ejecución lleve al lado el número del SQL de 05-01 convierte "creo que ha mejorado" en un hecho.

Consejo: fija todas las semillas (tf.random.set_seed, numpy, python). Un entrenamiento irreproducible es imposible de depurar.

Ejercicios

Ejercicio 1

Dani lanza el entrenamiento del recomendador en una máquina n1-standard-8 con una GPU T4. Cada época tarda 47 minutos. Al mirar el perfilador de TensorBoard, ve que la GPU está ocupada el 18 % del tiempo. Diagnostica el problema, propón cuatro medidas en orden de impacto y estima qué mejora cabe esperar.

Ejercicio 2

Marta pregunta si el recomendador two-tower debe servirse en un endpoint de Vertex AI con GPU, en un endpoint con CPU, en Cloud Run o por lotes. Analiza las cuatro opciones para el caso de AlpinaShop y justifica una recomendación. Incluye qué cambiaría la decisión.

Ejercicio 3

Tras entrenar, el recall@10 del modelo es 0,243 frente a 0,198 de la línea base SQL. ¿Se despliega? Argumenta y define qué prueba harías antes de decidir.

Soluciones

Solución 1

Diagnóstico: cuello de botella en la canalización de datos. Una GPU al 18 % significa que pasa el 82 % del tiempo esperando datos. El entrenamiento no está limitado por cómputo, sino por entrada. Pagar una GPU para que espere es el desperdicio más común de esta lección.

Cuatro medidas, por impacto esperado:

1. Añadir prefetch(tf.data.AUTOTUNE) al final de la canalización. Es la medida de mayor impacto y la de menor esfuerzo. Sin ella, el ciclo es estrictamente secuencial: la CPU prepara un lote, la GPU lo procesa, la CPU prepara el siguiente. Con prefetch, la CPU prepara el lote n+1 mientras la GPU procesa el n. Solo con esto, el uso puede subir a un 40-50 %.

2. Revisar el particionado de los TFRecord. Si los datos están en un solo fichero de 20 GB, interleave no puede hacer nada: hay una única lectura secuencial. Reparticionar a unos 200 ficheros de ~100 MB y leer con cycle_length=8 multiplica el caudal desde Cloud Storage. Impacto alto, esfuerzo medio (un trabajo de Dataflow).

3. Comprobar la región del bucket. Si el bucket es multirregión o está fuera de europe-west1, cada lectura añade latencia. Debe ser regional y coincidir con la máquina.

4. Mover el preprocesado pesado fuera del map. Si el map hace transformaciones costosas por ejemplo —cálculos, normalizaciones complejas—, eso consume CPU en cada época. Precalcularlas al generar los TFRecord las ejecuta una vez en lugar de veinticinco.

Y una medida complementaria: n1-standard-8 son 8 vCPU para alimentar una T4. Si tras las cuatro medidas la CPU sigue saturada, subir a n1-standard-16 es razonable —y sale rentable, porque el coste de las vCPU es pequeño comparado con el de tener la GPU parada.

Mejora esperada: con las medidas 1 y 2, un uso de GPU del 70-85 % es alcanzable, lo que llevaría la época de 47 minutos a un rango de 10-15. La comprobación es volver a mirar el perfilador, no suponerlo.

Verificación: antes de tocar nada, medir el caudal de la canalización sin modelo —iterar 200 lotes del Dataset cronometrando y dividir ejemplos entre segundos—. Si la lectura sola ya va lenta, el modelo no tiene nada que ver y el diagnóstico queda confirmado.

Solución 2

Las cuatro opciones:

Opción Coste Latencia Complejidad Valoración
Endpoint con GPU Muy alto (24×7) Muy baja Baja Descartada
Endpoint con CPU Alto (24×7) Baja Baja Descartada hoy
Cloud Run Bajo, escala a cero Baja con arranque en frío Media Reserva
Predicción por lotes Muy bajo Alta (irrelevante) Baja Recomendada

Descartar la GPU es inmediato. La torre de cliente es una red diminuta: tres capas densas sobre un embedding. En CPU responde en pocos milisegundos. Una GPU para eso es como usar un camión para llevar una carta.

Descartar el endpoint 24×7 por lo mismo que en 05-01: un endpoint factura por hora de nodo aunque no reciba tráfico. Con recomendaciones que no cambian de un minuto a otro, es pagar disponibilidad que nadie usa.

Cloud Run es la opción intermedia razonable y merece consideración seria: escala a cero, así que sin tráfico no cuesta nada; es la misma tecnología del catálogo por DA-001, así que el equipo la va a conocer; y el arranque en frío se mitiga con una instancia mínima. Su inconveniente es que hay que gestionar el servidor, el versionado y la monitorización del modelo a mano, sin la integración con Model Registry ni con Model Monitoring.

Recomendación: predicción por lotes nocturna, coherente con la decisión DA-002.

El razonamiento en tres puntos. Primero, las recomendaciones no dependen de la sesión en curso: los vectores de cliente cambian cuando cambia su historial, es decir, cuando hace un pedido, no cuando mueve el ratón. Segundo, 45.000 clientes × 20 recomendaciones son 900.000 filas, una tabla trivial que Firestore o Memorystore (02-06) sirven en microsegundos y que además no depende de que ningún modelo esté vivo. Tercero, la disponibilidad mejora: si el endpoint cayera, la web se quedaría sin recomendaciones; con una tabla precalculada, no hay nada que pueda caer.

Qué cambiaría la decisión. Tres escenarios concretos:

  1. Recomendar dentro de la sesión. Si se quisiera reaccionar a lo que el cliente está mirando ahora —"lleva cinco minutos viendo crampones"—, el vector de cliente tendría que calcularse en caliente y haría falta servicio en línea. Sería el momento de Cloud Run, no de un endpoint dedicado.
  2. Clientes anónimos. El 60 % del tráfico no está identificado y no tiene vector precalculado. Para ellos hoy se usa el recomendador heurístico por producto, que no necesita cliente. Si se quisiera personalizar a anónimos por comportamiento de sesión, volveríamos al caso anterior.
  3. Catálogo mucho más grande. Con 2.400 productos, precalcular es trivial. Con 500.000, la tabla cliente × producto dejaría de ser cómoda y entraría en juego Vector Search (05-06) para la búsqueda de vecinos próximos.

Solución 3

No con esos datos. Todavía no.

Qué dice el número. Una mejora de 0,198 a 0,243 en recall@10 es un 22,7 % relativo, que no es despreciable. Pero hay tres razones para no dar el salto directamente a producción:

1. Es una métrica offline. El recall@10 mide si el modelo habría acertado sobre lo que los clientes compraron en el pasado, cuando la web no recomendaba nada. Un recomendador cambia el comportamiento que intenta predecir: si sugiere un producto, la probabilidad de que se compre sube por el hecho de sugerirlo. Las métricas offline sistemáticamente sub o sobreestiman el efecto real, y no se sabe en qué dirección hasta que se prueba.

2. No mide lo que importa al negocio. Lo que decide es el valor del pedido, no el número de aciertos. Un recomendador que acierta mucho sugiriendo accesorios de 4 € puede generar menos margen que uno que acierta menos con productos de 90 €. Hay que mirar también las métricas de negocio: valor medio del pedido, artículos por pedido, tasa de clic en la recomendación, y la tasa de devolución (recomendar mal aumenta devoluciones).

3. Hay un intervalo de confianza que nadie ha calculado. ¿Cuántos clientes hay en el conjunto de test? Si son 2.000, la diferencia entre 0,198 y 0,243 puede estar dentro del ruido. Sin barras de error, dos números no son una comparación.

La prueba que hay que hacer: un A/B test.

Elemento Definición
Grupo A (control) 50 % del tráfico, recomendador heurístico SQL (DA-003)
Grupo B (tratamiento) 50 % del tráfico, modelo two-tower
Métrica principal Valor medio del pedido
Métricas secundarias Tasa de clic en recomendación, artículos por pedido, conversión
Métrica de guardia Tasa de devolución (que no empeore)
Duración Al menos 2-3 semanas completas
Asignación Por cliente, estable, no por sesión

Cuatro detalles que hacen válida la prueba:

  • Duración de semanas completas. El comportamiento de compra varía mucho entre fin de semana y días laborables. Una prueba de tres días mide el día, no el modelo.
  • Asignación estable por cliente. Si un cliente ve un recomendador el lunes y otro el martes, la experiencia es incoherente y la medición se contamina.
  • Métrica de guardia. Si la tasa de devolución sube significativamente, se para la prueba aunque las ventas suban: recomendar mal genera coste logístico e insatisfacción.
  • Tamaño de muestra calculado de antemano. Hay que saber antes de empezar cuántos pedidos hacen falta para detectar la mejora esperada. Si con el tráfico de AlpinaShop harían falta tres meses para detectar un 2 %, más vale saberlo antes de montar el experimento.

Y el criterio de decisión, escrito antes de mirar los resultados, que es la parte que más se incumple: se despliega el modelo si el valor medio del pedido mejora de forma estadísticamente significativa y la tasa de devolución no empeora más de un punto. Fijarlo después de ver los datos es la forma más elegante de engañarse.

Un escenario que hay que aceptar de antemano: es perfectamente posible que la prueba no muestre diferencia. Si ocurre, la decisión correcta es quedarse con el SQL, porque es más simple, más barato, más explicable y no requiere reentrenamiento. Estar dispuesto a tirar semanas de trabajo cuando los datos no acompañan es lo que distingue a un equipo maduro.

Conclusión

Has bajado al código, y sabes cuándo merece la pena: arquitectura propia, pérdida a medida, preprocesado específico o salida no estándar. El recomendador de AlpinaShop cae en los cuatro casos, y por eso justifica el esfuerzo. Si tu problema cabe en una tabla con una columna objetivo, la respuesta sigue siendo AutoML o BigQuery ML.

Tienes lo mínimo de TensorFlow y Keras para seguir: tensores, capas, la diferencia entre el modelo secuencial y el funcional —que aquí importa porque hay dos entradas independientes—, y la compilación con optimizador, pérdida y métricas, con EarlyStopping restaurando los mejores pesos.

Entiendes qué es un embedding y por qué cambia las reglas: 32 números aprendidos en lugar de 2.400 posiciones vacías, con la similitud emergiendo sola de los datos de compra. Y has construido la arquitectura de dos torres, entendiendo que la razón de partirla en dos no es teórica sino operativa: los vectores de producto se calculan una vez, el de cliente se calcula en el momento, y la búsqueda es de vecinos próximos y no de 2.400 evaluaciones del modelo. Has visto el muestreo negativo dentro del lote, que convierte una matriz de similitudes en ejemplos negativos gratis.

Sabes leer datos sin ahogar a la GPU con tf.data —interleave, shuffle con búfer grande, map paralelo, batch y sobre todo prefetch—, y por qué TFRecord particionado en ficheros de 100 a 200 MB en la misma región que la máquina es la diferencia entre una GPU al 18 % y una al 80 %.

Has empaquetado el entrenamiento como contenedor propio con versión fijada, lanzado con gcloud ai custom-jobs create, y tienes el criterio de CPU, GPU o TPU: prototipar siempre en CPU, subir a GPU solo cuando la época sea el cuello de botella y la GPU se aproveche de verdad, y olvidarte de las TPU hasta que un modelo las justifique. Sabes qué es el paralelismo de datos y por qué distribuir sin medir puede salir más lento.

Has unido checkpoints y VMs Spot en la combinación que hace la experimentación asequible: BackupAndRestore retoma donde se quedó, y la interrupción de una Spot deja de ser un desastre. Sigues el entrenamiento con TensorBoard gestionado y comparas configuraciones con Vertex AI Experiments, registrando siempre la línea base al lado para que la comparación sea un hecho y no un recuerdo.

Y sabes guardar y servir: SavedModel, subida al Model Registry, TensorFlow Serving precompilado o Cloud Run como alternativa (07-02), con la observación de que aquí solo se sirve una de las dos torres. Conoces la cuantización y por qué la latencia se mide en p95 y no en media. Y tienes la regla de oro grabada: ninguna GPU encendida sin trabajo.

Pero fíjate en lo que ha costado. Un contenedor, una canalización de datos, una arquitectura, una pérdida a medida, un entrenamiento, un experimento, un A/B test de tres semanas, y todavía la posibilidad honesta de que el SQL de treinta líneas gane. Es el precio del control, y a veces vale la pena.

A veces, no. Porque AlpinaShop tiene otras dos preguntas pendientes: 8.000 opiniones de clientes en texto libre que nadie ha leído, y las 60 GB de imágenes que AutoML clasificó por categoría pero de las que no se ha extraído nada más. Para ambas existen modelos ya entrenados, por gente con más datos y más recursos de los que AlpinaShop tendrá jamás, disponibles con una llamada a una API y sin un solo minuto de entrenamiento.

En 05-04, la API de lenguaje natural, aplicamos el principio contrario al de esta lección: no entrenes lo que ya está entrenado. Verás qué ofrece exactamente la API de Natural Language —sentimiento, entidades, clasificación, sintaxis—, entenderás de una vez qué significan score y magnitude, que es donde todo el mundo se equivoca, procesarás las 8.000 opiniones desde Python con control de cuotas y de coste, cruzarás el sentimiento con las devoluciones en BigQuery para responder si los productos peor valorados se devuelven más, y verás con honestidad dónde falla —ironía, negaciones y la jerga de la montaña— y cuándo compensa pedírselo a Gemini o entrenar un clasificador propio.

Curso de Google Cloud Platform (GCP)

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

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados