fast.ai · Practical Deep Learning for Coders · Lección 7
Collaborative filtering: recomendar sin entender el contenido
El capítulo que explica por qué Netflix te recomienda una película sin que ningún algoritmo "entienda" de qué trata — y que de paso da la definición más clara y concreta de qué es un embedding que vas a encontrar en todo el curso.
1. El problema: recomendar sin saber nada del contenido
Collaborative filtering resuelve esto: dado un historial de usuarios calificando items (películas, productos), predecir qué le va a gustar a un usuario a partir del comportamiento de usuarios parecidos — sin ninguna información explícita sobre el contenido en sí. El dataset de ejemplo es MovieLens (100.000 ratings: usuario, película, rating, timestamp).
2. Latent factors y el producto punto
La idea central: aunque nunca le preguntamos a nadie "¿cuánto te gusta la ciencia ficción?", tiene que
existir un conjunto subyacente de conceptos (género, época, tono) que explica por qué a alguien le gustan
ciertas películas. Esos son los latent factors — no se eligen a mano, se aprenden. El
libro ilustra la mecánica con un ejemplo manual antes de entrenar nada: representa una película de space
opera como vector [0.98, 0.9, -0.9] (muy sci-fi, muy acción, muy no-vieja) y a un usuario
fan del género como [0.9, 0.8, -0.6]. El rating predicho es el producto punto
de ambos vectores: (usuario * pelicula).sum(). En la práctica no sabemos esos valores de
antemano — la solución es dejar que gradient descent los aprenda: inicializar ambas matrices al azar,
calcular el producto punto como predicción, medir el error con MSE, y ajustar ambas matrices para
minimizarlo.
3. Qué es un embedding, exactamente
El libro es preciso sobre algo que suele quedar como magia: "buscar por índice" (indexar
user_factors[3]) es matemáticamente idéntico a multiplicar la matriz de
factores por un vector one-hot con un 1 en la posición 3:
one_hot_3 = one_hot(3, n_users).float()
user_factors.t() @ one_hot_3 # == user_factors[3]
Un embedding es justamente ese atajo computacional: en vez de hacer la multiplicación
matricial completa (cara e innecesaria), la capa Embedding indexa directamente, pero calcula
su derivada como si hubiera hecho la multiplicación por one-hot. No es un concepto nuevo ni un
truco misterioso — es álgebra lineal estándar con un atajo de eficiencia, la misma definición que aplica
igual a embeddings de palabras, de usuarios, o de tokens en un LLM.
4. El código: DotProduct con bias
class DotProduct(Module):
def __init__(self, n_users, n_movies, n_factors):
self.user_factors = Embedding(n_users, n_factors)
self.movie_factors = Embedding(n_movies, n_factors)
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])
return (users * movies).sum(dim=1)
Esto captura afinidad temática, pero no distingue una película objetivamente mala de una que simplemente no calza con los gustos de un usuario particular. Por eso se agrega bias por usuario y por película (un escalar extra, embedding de tamaño 1) que captura "¿le gusta a la gente en general?" — algo distinto de "¿calza con este usuario?":
class DotProductBias(Module):
def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)):
self.user_factors = Embedding(n_users, n_factors)
self.user_bias = Embedding(n_users, 1)
self.movie_factors = Embedding(n_movies, n_factors)
self.movie_bias = Embedding(n_movies, 1)
self.y_range = y_range
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])
res = (users * movies).sum(dim=1, keepdim=True)
res += self.user_bias(x[:,0]) + self.movie_bias(x[:,1])
return sigmoid_range(res, *self.y_range)
5. Cold start y weight decay
El cold start es el problema sin solución mágica: ¿qué recomendar a un usuario o item nuevo, sin historial? Opciones reales: asignar el promedio de embeddings existentes (riesgoso — la combinación "promedio" puede no representar a nadie real), elegir un usuario que represente un gusto típico, o —mejor— predecir el embedding inicial con un modelo tabular a partir de metadata del signup (edad, género favorito declarado) — así es como Netflix o Pandora justifican preguntar tus gustos al registrarte.
Weight decay (L2) evita que DotProductBias sobreajuste: se suma a la loss el
cuadrado de todos los pesos, penalizando valores grandes (loss + wd * (parameters**2).sum()).
En la práctica equivale a restar wd * 2 * parameters del gradiente en cada paso — menos peso
en los embeddings da funciones de pérdida menos "afiladas", que generalizan mejor.
6. Interpretar embeddings con PCA
Los bias son fáciles de leer directamente (movie_bias.argsort() da las películas peores/mejores
de forma "intrínseca", más allá de gustos particulares). Pero la matriz de embeddings (50 dimensiones) es
imposible de inspeccionar a mano. Con PCA se proyectan esas 50 dimensiones a 2-3
componentes principales, y al graficarlas aparecen agrupamientos que el modelo descubrió solo, sin que
nadie se los dijera explícitamente — un eje que separa cine de culto de cine comercial, por ejemplo,
emergente de puros ratings numéricos.
🔧 El mismo truco de interpretación que ya usaste
Proyectar embeddings de 50 dimensiones a 2-3 con PCA para ver qué "concepto" aprendió el modelo sin que nadie se lo dijera es la misma lógica de interpretabilidad detrás del buscador semántico con FAISS del Capítulo 5 de HF: en ambos casos, un espacio vectorial de alta dimensión aprendido por gradiente termina codificando estructura semántica real (similitud de películas, similitud de texto) que no fue programada a mano, solo emergió de optimizar una tarea numérica simple.
7. Resumen
- Collaborative filtering recomienda a partir del comportamiento de usuarios similares, sin ninguna comprensión del contenido en sí.
- Latent factors son vectores aprendidos por gradiente, no elegidos a mano — el rating predicho es el producto punto usuario × película.
- Un embedding es matemáticamente idéntico a multiplicar por un one-hot vector — un atajo computacional, no un concepto especial.
- El bias por usuario y por película captura "calidad intrínseca", distinto de "afinidad de gustos" pura.
- Cold start no tiene solución perfecta; weight decay evita que los embeddings sobreajusten; PCA permite ver qué conceptos aprendió el modelo sin supervisión explícita.