Curso / fast.ai / Lección 7
● piloto de formato

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

  1. Collaborative filtering recomienda a partir del comportamiento de usuarios similares, sin ninguna comprensión del contenido en sí.
  2. Latent factors son vectores aprendidos por gradiente, no elegidos a mano — el rating predicho es el producto punto usuario × película.
  3. Un embedding es matemáticamente idéntico a multiplicar por un one-hot vector — un atajo computacional, no un concepto especial.
  4. El bias por usuario y por película captura "calidad intrínseca", distinto de "afinidad de gustos" pura.
  5. 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.