Abres Netflix un martes a la noche. Todavía no sabes qué quieres ver, pero en menos de dos segundos la pantalla ya está llena de recomendaciones personalizadas para ti. No son aleatorias. No son las mismas que ve tu vecino. Son películas y series que, de alguna manera, el sistema sabe que te van a interesar. Y todo esto pasa en tiempo real, eligiendo entre un catálogo de millones de títulos.
La pregunta obvia es: cómo hace Netflix para comparar tus gustos contra millones de películas en milisegundos? No revisa una por una. No tiene un equipo de personas eligiendo manualmente qué mostrarte. La respuesta está en vectores. El sistema convierte tanto a ti como a cada película en vectores numéricos, y después usa matemática para encontrar los más cercanos. Suena abstracto, pero es elegantemente simple una vez que entiendes cómo funciona.
En este post te voy a explicar paso a paso cómo funcionan los modelos Two-Tower, la arquitectura que está detrás de los sistemas de recomendación de Netflix, YouTube, Spotify y prácticamente cualquier plataforma que te recomienda contenido. Vamos a ir desde el problema fundamental hasta la implementación con código real.
El problema de la escala
Piensa en los números por un segundo. Una plataforma como Netflix tiene alrededor de 1 millón de usuarios activos en un país grande, y un catálogo de 100,000 títulos entre películas y series. Si quisieras calcular qué tan compatible es cada usuario con cada título, tendrías que hacer 1,000,000 x 100,000 = 100 mil millones de comparaciones. Por cada usuario. En tiempo real. Eso no es difícil, es directamente imposible con un enfoque ingenuo.
Y acá es donde entra la idea clave: en vez de comparar usuarios contra películas directamente, convertimos ambos en embedding
embedding
Representación numérica de un objeto (usuario, película, palabra) como un vector en un espacio de múltiples dimensiones, donde objetos similares quedan cerca entre sí.
, es decir, vectores numéricos en un espacio de muchas dimensiones. Una vez que todo es un vector, encontrar las mejores recomendaciones se reduce a buscar los vectores más cercanos. Y para eso existen algoritmos increíblemente rápidos que pueden buscar entre millones de vectores en milisegundos.
Embeddings
Un embedding es simplemente una lista de números que representa algo. Puede representar un usuario, una película, una canción, un producto, cualquier cosa. La magia está en que estos números no son aleatorios: se aprenden durante el entrenamiento de un modelo de machine learning, de tal forma que objetos similares terminan teniendo vectores similares.
Imagina que cada película se convierte en un vector de 128 números. Cada uno de esos 128 números captura algún aspecto de la película: quizás uno tiene que ver con qué tan de acción es, otro con qué tan romántica, otro con la complejidad de la trama, otro con el tono visual. No sabemos exactamente qué representa cada dimensión (el modelo lo aprende solo), pero el resultado es que películas similares terminan con vectores parecidos. Lo mismo pasa con los usuarios: cada usuario se convierte en un vector de 128 números que representa sus gustos y preferencias.
Cuando un usuario y una película tienen vectores cercanos en este espacio de 128 dimensiones, significa que hay alta compatibilidad. El usuario probablemente va a disfrutar esa película. Cuando los vectores están lejos, probablemente no le va a interesar.
En código, crear una capa de embedding es sorprendentemente simple. Lo único que necesitas es decirle cuántos objetos vas a representar y en cuántas dimensiones:
python
import tensorflow as tf
# Crear una capa de embedding para 10,000 peliculas
# Cada pelicula se representa como un vector de 128 dimensiones
embedding_layer = tf.keras.layers.Embedding(
input_dim=10000, # numero de peliculas
output_dim=128 # dimensiones del embedding
)
# Obtener el embedding de la pelicula con ID 42
movie_id = tf.constant([42])
movie_embedding = embedding_layer(movie_id)
print(f"Forma del embedding: {movie_embedding.shape}") # (1, 128)
Esa capa de embedding empieza con valores aleatorios, pero durante el entrenamiento del modelo esos valores se van ajustando hasta que capturan las relaciones reales entre los objetos. Es como si el modelo aprendiera a "posicionar" cada película en el lugar correcto del espacio.
Entendiendo la similitud con tus propios ojos
Antes de meternos en la arquitectura del modelo, quiero que entiendas intuitivamente qué significa que dos vectores sean "similares". Para eso, vamos a simplificar las cosas y pensar en solo 2 dimensiones en vez de 128. Imagina un plano donde el eje X representa qué tanto le gusta la acción a un usuario, y el eje Y representa qué tanto le gusta el romance.
Tenemos tres usuarios:
Usuario A: [0.85, 0.15]. Le encanta la acción y casi no ve romance.
Usuario B: [0.95, 0.4]. También prefiere la acción, pero ve más romance que A.
Usuario C: [0.15, 0.85]. Ama el romance y casi no ve acción.
Si pones estos tres puntos en un gráfico, es obvio que A y B están cerquita el uno del otro. Tienen gustos parecidos. C, en cambio, está en la esquina opuesta del gráfico. Si le recomiendas a A las películas que le gustan a B, probablemente aciertes. Si le recomiendas las películas de C, probablemente falle.
Ahora viene el salto mental importante: lo que acabamos de hacer en 2 dimensiones, el modelo lo hace en 128 dimensiones. En vez de solo "acción" y "romance", tienes 128 ejes que capturan aspectos mucho más sutiles: el ritmo de la película, la complejidad narrativa, el estilo de cinematografía, el tipo de humor, la época en que está ambientada, y decenas de características más que ni siquiera tienen un nombre humano pero que el modelo aprendió que son relevantes.
No necesitas visualizar 128 dimensiones
No puedes visualizar 128 dimensiones, y está bien. Nadie puede. Pero la computadora calcula distancias en ese espacio exactamente igual de fácil que en 2D. La fórmula es la misma, solo tiene más términos. Así que cada vez que veas "vectores cercanos" en este post, piensa en el ejemplo 2D que acabamos de ver, pero con más ejes.
Two-Tower Model
Ahora sí, la arquitectura estrella. El modelo Two-Tower (dos torres) tiene exactamente la estructura que sugiere su nombre: dos redes neuronales separadas que trabajan en paralelo. Una torre procesa información del usuario y produce un vector (el embedding del usuario). La otra torre procesa información del ítem (película, canción, producto) y produce otro vector (el embedding del ítem). Ambos vectores tienen la misma cantidad de dimensiones, por ejemplo 128.
La belleza de esta arquitectura es que las dos torres son independientes. Una vez entrenado el modelo, puedes calcular el embedding de cada película una sola vez y guardarlo. Cuando un usuario entra a la app, solo necesitas calcular su embedding (una pasada por la torre de usuario) y después buscar las películas con embeddings más cercanos. No necesitas pasar cada par usuario-película por el modelo completo.
El entrenamiento funciona con pares positivos y negativos. Un par positivo es un usuario que efectivamente vio y le gustó una película. Un par negativo es un usuario con una película que no le interesó (o una película aleatoria que nunca vio). Durante el entrenamiento, el modelo ajusta los pesos de ambas torres para que los pares positivos tengan embeddings cercanos y los pares negativos tengan embeddings lejanos.
Piénsalo así: cada vez que le muestras al modelo un par positivo (usuario que vio Inception y le puso 5 estrellas), el modelo "empuja" los vectores del usuario y de Inception para que estén más cerca. Cada vez que le muestras un par negativo (el mismo usuario y una película random de romance que nunca vio), el modelo "empuja" esos vectores para que estén más lejos. Después de millones de estos ajustes, los vectores terminan en posiciones que reflejan las preferencias reales.
Viéndolo en 2D
Para que quede claro cómo el entrenamiento transforma el espacio, veamos un antes y después simplificado en 2 dimensiones.
Antes del entrenamiento, los embeddings de las películas están esparcidos de forma aleatoria por el espacio. No hay ningún patrón. Una película de ciencia ficción puede estar al lado de una comedia romántica, y un thriller psicológico puede estar en la otra punta del espacio. Los vectores no significan nada todavía.
Después del entrenamiento, el panorama cambia completamente. Las películas de acción se agrupan en una zona, las comedias románticas en otra, los documentales en otra. Y lo más importante: el vector del usuario aparece cerca del cluster de películas que le gustan. Si a un usuario le encantan las películas de ciencia ficción, su vector va a estar en la zona de ciencia ficción del espacio. Las recomendaciones se convierten simplemente en "encontrar las películas más cercanas al usuario en este espacio".
Ahora veamos cómo se implementa el modelo Two-Tower en TensorFlow. La torre del usuario toma el ID del usuario, lo convierte en un embedding, y lo pasa por capas densas. La torre del ítem hace exactamente lo mismo con el ID de la película. Al final, se calcula la similitud entre ambos vectores con un producto punto:
Cada torre tiene su propia capa de embedding seguida de capas densas que refinan la representación. La capa final de cada torre produce un vector de 128 dimensiones. Después, el Dot layer calcula el producto punto normalizado entre ambos vectores, que nos da un score de similitud entre -1 y 1. El modelo se entrena durante múltiples epoch
epoch
Una pasada completa por todo el conjunto de datos de entrenamiento. Si tienes 1 millón de pares usuario-película, un epoch significa que el modelo vio cada uno de esos pares una vez.
para que aprenda a asignar scores altos a pares usuario-película compatibles y scores bajos a pares incompatibles.
Similitud de vectores
Ya vimos que la clave de todo esto es medir qué tan "cerca" están dos vectores. Pero hay varias formas de definir "cercanía" entre vectores, y cada una tiene sus particularidades. Las tres métricas más usadas en sistemas de recomendación son el dot product
dot product
Operación matemática que multiplica los elementos correspondientes de dos vectores y suma los resultados. Mide tanto la dirección como la magnitud de los vectores.
(producto punto), la cosine similarity
cosine similarity
Métrica que mide el ángulo entre dos vectores, ignorando su magnitud. Un valor de 1 significa que apuntan en la misma dirección, 0 que son perpendiculares, y -1 que apuntan en direcciones opuestas.
(similitud coseno), y la distancia euclidiana.
El dot product es la operación más simple: multiplicas los elementos correspondientes de ambos vectores y sumas todo. Si tienes dos vectores de 4 dimensiones, a = [0.9, 0.1, 0.8, 0.3] y b = [0.8, 0.2, 0.7, 0.4], el dot product es (0.9*0.8) + (0.1*0.2) + (0.8*0.7) + (0.3*0.4) = 0.72 + 0.02 + 0.56 + 0.12 = 1.42. Cuanto más alto el número, más similares son los vectores. La ventaja del dot product es que es extremadamente rápido de calcular. La desventaja es que es sensible a la magnitud de los vectores: un vector con valores grandes siempre va a tener un dot product alto, independientemente de la dirección.
La cosine similarity resuelve ese problema. En vez de solo multiplicar y sumar, divide el resultado por el producto de las magnitudes de ambos vectores. El resultado siempre está entre -1 y 1. Un valor de 1 significa que los vectores apuntan en exactamente la misma dirección (máxima similitud), 0 significa que son perpendiculares (sin relación), y -1 significa que apuntan en direcciones opuestas. Es la métrica más popular en sistemas de recomendación porque solo le importa la dirección del vector, no su longitud.
La distancia euclidiana es la distancia "en línea recta" entre dos puntos. Es la fórmula de distancia que aprendiste en el colegio, pero extendida a N dimensiones. Cuanto menor sea la distancia, más similares son los vectores. Se usa menos en sistemas de recomendación que la cosine similarity, pero es útil en otros contextos como clustering.
Veamos las tres métricas calculadas en código con vectores concretos:
python
import numpy as np
a = np.array([0.9, 0.1, 0.8, 0.3])
b = np.array([0.8, 0.2, 0.7, 0.4])
# Dot Product
dot = np.dot(a, b)
print(f"Dot Product: {dot:.4f}") # 1.33
# Cosine Similarity
cos_sim = dot / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"Cosine Similarity: {cos_sim:.4f}") # 0.9887
# Distancia Euclidiana
euclidean = np.linalg.norm(a - b)
print(f"Distancia Euclidiana: {euclidean:.4f}") # 0.2000
Fíjate que la cosine similarity es 0.9887, muy cerca de 1. Eso nos dice que estos dos vectores apuntan casi en la misma dirección, o sea que los objetos que representan son muy similares. La distancia euclidiana es 0.2, que es baja, confirmando lo mismo. En la práctica, la mayoría de los sistemas de recomendación basados en Two-Tower usan cosine similarity o dot product con vectores normalizados (que es lo mismo).
Por qué brute force no escala
Ya tenemos los embeddings. Cada usuario es un vector de 128 dimensiones y cada película es un vector de 128 dimensiones. Ahora necesitamos encontrar las 10 películas más cercanas al usuario. La forma más obvia es calcular la similitud entre el vector del usuario y el vector de cada película del catálogo. Eso es brute force
brute force
Enfoque que prueba todas las combinaciones posibles para encontrar la solución. Simple de implementar pero extremadamente lento cuando el número de opciones es grande.
.
El problema es la complejidad. Si tienes 1 millón de películas y cada vector tiene 128 dimensiones, calcular la similitud contra todas requiere 1,000,000 x 128 = 128 millones de operaciones de punto flotante. Para un solo usuario. Si tu plataforma recibe 1,000 consultas por segundo, estamos hablando de 128 mil millones de operaciones por segundo solo para recomendaciones. Eso no es viable ni siquiera con hardware moderno, al menos no de forma eficiente en costos.
La solución es no buscar entre todos los vectores, sino usar algoritmos deANN
ANN
Approximate Nearest Neighbors (Vecinos Más Cercanos Aproximados). Familia de algoritmos que encuentran los vectores más cercanos sin revisar todos, sacrificando un poco de precisión a cambio de mucha velocidad.
(Approximate Nearest Neighbors) que encuentran los vectores más cercanos sin necesidad de compararlos todos. En vez de O(n*d) donde n es el número de ítems y d las dimensiones, estos algoritmos logran complejidades sub-lineales como O(log n * d) o incluso mejores.
Approximate Nearest Neighbors
La idea central de ANN es simple: sacrificas un poquito de precisión a cambio de una cantidad enorme de velocidad. En vez de garantizar que encuentras los 10 vecinos más cercanos exactos, encuentras 10 vecinos que son "casi" los más cercanos. En la práctica, la diferencia de calidad es mínima (los resultados coinciden con brute force más del 95% de las veces), pero la diferencia de velocidad es de órdenes de magnitud. Pasas de segundos a milisegundos.
Hay varios algoritmos de ANN, pero los dos más relevantes para sistemas de recomendación son LSH (Locality-Sensitive Hashing) y HNSW (Hierarchical Navigable Small World). Veamos cada uno.
LSH (Locality-Sensitive Hashing)
LSH es una técnica que usa funciones hash especiales para agrupar vectores similares en el mismo "balde" (bucket). A diferencia de las funciones hash tradicionales que intentan distribuir valores uniformemente, las funciones hash de LSH están diseñadas para que vectores cercanos en el espacio original terminen con el mismo hash.
Piensa en una biblioteca gigante. En vez de revisar libro por libro, primero organizas los libros en estantes por tema: ciencia ficción en el estante A, romance en el B, historia en el C. Cuando alguien te pide una recomendación, no recorres toda la biblioteca; solo buscas en el estante que corresponde.
LSH hace exactamente eso con vectores. Toma el espacio de embeddings y lo divide en regiones (buckets) usando funciones hash especiales. La magia está en que estas funciones asignan vectores cercanos al mismo bucket con alta probabilidad. Cuando necesitas buscar los vecinos de un vector, solo comparas contra los que cayeron en su mismo bucket.
El resultado: si tu bucket tiene 1,000 vectores en vez de 1,000,000 en total, la búsqueda es 1,000 veces más rápida. Pierdes un poco de precisión (un vecino real podría haber caído en otro bucket), pero la ganancia en velocidad es enorme.
Veamos una implementación conceptual de LSH en Python:
python
import numpy as np
def random_hash_function(dim, num_planes=8):
"""Genera planos aleatorios para LSH"""
planes = np.random.randn(num_planes, dim)
def hash_vector(v):
projections = planes @ v
# Cada proyeccion se convierte en 0 o 1
return tuple((projections > 0).astype(int))
return hash_vector
# Crear funcion hash para vectores de 128 dimensiones
hasher = random_hash_function(dim=128)
# Vectores similares obtienen el mismo hash (con alta probabilidad)
v1 = np.random.randn(128)
v2 = v1 + np.random.randn(128) * 0.1 # muy similar a v1
v3 = np.random.randn(128) # completamente diferente
print(f"Hash v1: {hasher(v1)}")
print(f"Hash v2: {hasher(v2)}") # probablemente igual a v1
print(f"Hash v3: {hasher(v3)}") # probablemente diferente
En este ejemplo, v1 y v2 son vectores muy similares (v2 es v1 con un poquito de ruido), así que con alta probabilidad van a obtener el mismo hash. v3 es completamente aleatorio, así que su hash va a ser diferente. Cuando necesites buscar los vecinos de v1, solo tienes que comparar contra los vectores que tienen el mismo hash, ignorando el resto.
HNSW (Hierarchical Navigable Small World)
HNSW es probablemente el algoritmo de ANN más popular en producción hoy en día. La idea es construir un grafo de múltiples capas donde cada nodo es un vector y las conexiones representan proximidad. Las capas superiores son "autopistas" con pocos nodos y conexiones largas que te permiten saltar rápidamente a la zona correcta del espacio. Las capas inferiores tienen más nodos y conexiones cortas para hacer la búsqueda fina.
La búsqueda funciona de arriba hacia abajo. Empiezas en la capa más alta, encuentras el nodo más cercano a tu query saltando de nodo en nodo de forma greedy (siempre moviéndote hacia el vecino más cercano al target). Cuando ya no puedes mejorar en esa capa, bajas a la capa siguiente y repites el proceso con más nodos disponibles. Cuando llegas a la capa más baja, tienes un resultado muy preciso habiendo visitado solo una fracción pequeña de todos los nodos.
Piensa en HNSW como buscar una dirección en una ciudad: primero tomas la autopista para llegar a la zona correcta (capa alta), después tomas avenidas para acercarte al barrio (capa media), y finalmente caminas por las calles locales hasta llegar a la puerta exacta (capa baja). Es mucho más rápido que recorrer cada calle de la ciudad.
FAISS en la práctica
FAISS (Facebook AI Similarity Search) es una librería de código abierto creada por Meta que permite indexar y buscar entre millones (o miles de millones) de vectores de forma eficiente. Es la herramienta más popular para búsqueda de vecinos cercanos en producción y soporta tanto búsqueda exacta como aproximada.
La idea es simple: generas los embeddings de todas tus películas (usando la torre de ítems del modelo Two-Tower), los agregas a un índice FAISS, y después buscas los más cercanos a cualquier query vector. FAISS se encarga de la estructura de datos interna, la optimización de memoria, y la paralelización en GPU si la necesitas.
python
import faiss
import numpy as np
# Generar embeddings simulados (1 millon de peliculas, 128 dimensiones)
dimension = 128
num_items = 1_000_000
item_embeddings = np.random.randn(num_items, dimension).astype("float32")
# Crear indice FAISS (IndexFlatIP = Inner Product, ideal para Two-Tower)
index = faiss.IndexFlatIP(dimension)
# Para millones de vectores, usar un indice aproximado:
# index = faiss.IndexIVFFlat(faiss.IndexFlatIP(dimension), dimension, 1000)
# Agregar todos los embeddings al indice
faiss.normalize_L2(item_embeddings) # normalizar para cosine similarity
index.add(item_embeddings)
print(f"Vectores indexados: {index.ntotal}") # 1,000,000
# Buscar las 10 peliculas mas similares a un usuario
user_embedding = np.random.randn(1, dimension).astype("float32")
faiss.normalize_L2(user_embedding)
distances, indices = index.search(user_embedding, k=10)
print(f"Top 10 peliculas mas similares: {indices[0]}")
print(f"Scores de similitud: {distances[0]}")
En este ejemplo usamos IndexFlatIP que hace búsqueda exacta con inner product. Para un millón de vectores esto todavía es manejable, pero cuando tienes decenas de millones, vas a querer usar un índice aproximado como IndexIVFFlat que divide el espacio en clusters y solo busca en los clusters más relevantes. La diferencia de velocidad es dramática: de cientos de milisegundos a menos de uno.
ScaNN en la práctica
ScaNN (Scalable Nearest Neighbors) es la librería de búsqueda de vectores de Google. Está especialmente optimizada para inner product, que es exactamente la operación que usa el modelo Two-Tower para calcular similitud. ScaNN se integra directamente con TensorFlow Recommenders (TFRS), lo que lo hace particularmente conveniente si ya estás usando TensorFlow para entrenar tu modelo.
La ventaja principal de ScaNN es su enfoque de quantización anisotrópica: en vez de tratar todas las dimensiones del vector por igual, prioriza las dimensiones que más contribuyen al inner product. Esto le permite comprimir los vectores de forma más inteligente sin perder tanta precisión.
python
import tensorflow as tf
import tensorflow_recommenders as tfrs
import numpy as np
# Embeddings pre-calculados del modelo Two-Tower
item_embeddings = np.random.randn(100000, 128).astype("float32")
item_ids = [f"movie_{i}" for i in range(100000)]
# Crear indice ScaNN optimizado para busqueda rapida
scann_index = tfrs.layers.factorized_top_k.ScaNN(
model=tf.keras.Sequential([
tf.keras.layers.InputLayer(input_shape=(128,)),
]),
k=10,
num_leaves=1000,
num_leaves_to_search=100,
)
# Indexar todos los embeddings
scann_index.index_from_dataset(
tf.data.Dataset.from_tensor_slices(item_embeddings).batch(1000)
)
# Buscar recomendaciones para un usuario
user_embedding = np.random.randn(1, 128).astype("float32")
scores, movie_indices = scann_index(tf.constant(user_embedding))
print(f"Top 10 recomendaciones: {movie_indices.numpy()[0]}")
Fíjate que ScaNN tiene dos parámetros clave: num_leaves (en cuántos clusters divide el espacio) y num_leaves_to_search (cuántos de esos clusters revisa durante la búsqueda). Con 1,000 clusters y revisando solo 100, estamos buscando en el 10% del espacio pero encontrando resultados que coinciden con brute force más del 95% de las veces. Ese es el tradeoff fundamental de ANN: un poquito de precisión a cambio de una enorme ganancia de velocidad.
FAISS vs ScaNN: cual usar?
FAISS es la mejor opcion cuando necesitas flexibilidad: soporta multiples tipos de indices (flat, IVF, HNSW, PQ), funciona con cualquier framework de ML, y tiene una comunidad enorme. Ideal si no usas TensorFlow o si necesitas control fino sobre el indice.
ScaNN es la mejor opcion cuando ya trabajas con TensorFlow: se integra directamente con el ecosistema (TensorFlow Recommenders), esta optimizado para inner product (la operacion del Two-Tower), y ofrece excelente rendimiento con configuracion minima.
En resumen: si tu stack es TensorFlow, usa ScaNN. Si necesitas algo mas general o ya usas PyTorch, usa FAISS.
El pipeline completo
Ahora que vimos cada pieza individual, armemos el rompecabezas completo. Un sistema de recomendación basado en Two-Tower tiene dos fases bien diferenciadas: la fase offline (que pasa en background, sin urgencia de tiempo) y la fase online (que pasa en tiempo real cuando el usuario abre la app).
En la fase offline, primero entrenas el modelo Two-Tower con datos históricos de interacciones usuario-ítem. Una vez entrenado, usas la torre de ítems para generar el embedding de cada película del catálogo. Estos embeddings se guardan en un índice de búsqueda vectorial (FAISS, ScaNN, o similar). Este proceso puede tardar horas, pero no importa porque se ejecuta una vez al día o cuando hay cambios significativos en el catálogo.
En la fase online, cuando un usuario abre la app, su información pasa por la torre de usuarios para generar su embedding en tiempo real. Ese embedding se usa como query contra el índice vectorial, que devuelve las K películas con embeddings más cercanos en milisegundos. Esas son las recomendaciones. Todo el proceso, desde que el usuario abre la app hasta que ve sus recomendaciones, toma menos de 100 milisegundos.
En resumen
Los modelos Two-Tower son la base de los sistemas de recomendación modernos. No son magia. Son vectores, distancias, y estructuras de datos inteligentes. La idea central es elegantemente simple: convierte usuarios e ítems en vectores en el mismo espacio, y después encuentra los más cercanos. El modelo aprende qué significa "cercano" a partir de datos reales de interacciones.
Lo que hace poderosa a esta arquitectura es la separación en dos torres independientes. Una vez entrenado el modelo, puedes pre-calcular todos los embeddings de ítems y guardarlos en un índice. En tiempo real, solo necesitas calcular el embedding del usuario y hacer una búsqueda rápida. Eso es lo que permite escalar a millones de usuarios y millones de ítems sin que exploten los costos de infraestructura.
Si quieres experimentar con esto por tu cuenta, escribimos un tutorial paso a paso donde construyes un modelo Two-Tower completo con TensorFlow Recommenders y el dataset de MovieLens. Código real, datos reales, resultados reales. En una tarde puedes tener tu propio sistema de recomendación funcionando.