Introducción
Los sistemas de recomendación son una pieza clave en plataformas de streaming, e‑commerce y redes sociales. Singular Value Decomposition (SVD) es uno de los algoritmos más robustos para collaborative filtering porque permite reducir la dimensionalidad de una matriz de usuarios‑productos y capturar relaciones latentes.
En este artículo encontrarás:
- Fundamentos teóricos de SVD aplicados a recomendación.
- Implementación completa en Python (NumPy, SciPy y
surprise). - Comparativa con algoritmos alternativos (ALS, NMF, Deep Learning).
- Mejores prácticas de pre‑procesado, evaluación, optimización y despliegue.
- Guía de troubleshooting, seguridad y escalabilidad.
1. Fundamentos de SVD para Sistemas de Recomendación
Partimos de una matriz R de dimensiones m × n (usuarios × ítems) donde cada celda contiene una valoración (por ejemplo, de 1 a 5). La descomposición SVD se expresa como:
R ≈ U Σ Vᵀ
donde:
U (m × k)– vectores latentes de usuarios.Σ (k × k)– valores singulares (importancia de cada factor).Vᵀ (k × n)– vectores latentes de ítems.
Al truncar a k factores (usualmente entre 20 y 200) reducimos el ruido y obtenemos predicciones mediante:
Ŕ = U Σ Vᵀ
Las entradas de Ŕ son estimaciones de la valoración que un usuario daría a un ítem que aún no ha evaluado.
2. Preparación de los Datos
Los datos reales son extremadamente escasos. Algunas tareas esenciales:
- Filtrado de usuarios/ítems con muy pocas interacciones.
- Normalización. Restar la media del usuario o del ítem para centrar la distribución.
- División de conjuntos.
train_test_split(ej. 80/20) usandosurprise.model_selection.
Ejemplo de pre‑procesado con pandas:
import pandas as pd
# Cargar dataset (formato: userId, movieId, rating, timestamp)
ratings = pd.read_csv('ml‑100k/u.data', sep='\t',
names=['user','item','rating','ts'])
# Filtrar usuarios con < 20 ratings y items con < 20 ratings
user_counts = ratings['user'].value_counts()
item_counts = ratings['item'].value_counts()
ratings = ratings[ratings['user'].isin(user_counts[user_counts>=20].index)]
ratings = ratings[ratings['item'].isin(item_counts[item_counts>=20].index)]
# Centering por usuario
ratings['rating_centered'] = ratings.groupby('user')['rating'].transform(lambda x: x - x.mean())
3. Implementación Básica de SVD con NumPy / SciPy
Esta versión es útil para comprender el algoritmo, aunque no es la más eficiente para matrices muy grandes.
import numpy as np
from scipy.sparse.linalg import svds
# Construir matriz de usuarios‑items (rellena ceros donde no hay rating)
m = ratings['user'].nunique()
n = ratings['item'].nunique()
R = np.zeros((m, n))
for row in ratings.itertuples():
R[row.user-1, row.item-1] = row.rating_centered
# Número de factores latentes
k = 50
# svds devuelve los k valores singulares más grandes
U, sigma, VT = svds(R, k=k)
Sigma = np.diag(sigma)
# Predicción
pred = np.dot(np.dot(U, Sigma), VT)
# Des‑centering (añadir la media del usuario)
user_means = ratings.groupby('user')['rating'].mean().values
pred += user_means[:, np.newaxis]
Para obtener las 10 mejores recomendaciones para el usuario 5:
user_id = 5 - 1 # índice 0‑based
user_pred = pred[user_id]
# Excluir ítems ya valorados
already_rated = ratings[ratings['user']==5]['item'].values - 1
user_pred[already_rated] = -np.inf
top_items = np.argsort(user_pred)[::-1][:10] + 1 # volver a 1‑based
print('Recomendaciones para usuario 5:', top_items)
4. Implementación con surprise (Recomendado para Producción)
La librería Surprise abstrae la mayor parte del pipeline y permite entrenar SVD con regularización y bias de forma eficiente.
from surprise import Dataset, Reader, SVD, accuracy
from surprise.model_selection import train_test_split
# Formato: user, item, rating
reader = Reader(line_format='user item rating', sep='\t', rating_scale=(1, 5))
data = Dataset.load_from_df(ratings[['user','item','rating']], reader)
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)
# Entrenamiento con bias y regularización
svd = SVD(n_factors=100, n_epochs=30, biased=True, lr_all=0.005, reg_all=0.02)
svd.fit(trainset)
# Predicciones y métricas
predictions = svd.test(testset)
rmse = accuracy.rmse(predictions)
mae = accuracy.mae(predictions)
print(f'RMSE: {rmse:.4f} | MAE: {mae:.4f}')
# Función de recomendación
from collections import defaultdict
def get_top_n(predictions, n=10):
top_n = defaultdict(list)
for uid, iid, true_r, est, _ in predictions:
top_n[uid].append((iid, est))
# Ordenar y cortar
for uid, user_ratings in top_n.items():
user_ratings.sort(key=lambda x: x[1], reverse=True)
top_n[uid] = user_ratings[:n]
return top_n
top10 = get_top_n(predictions, n=10)
print('Top‑10 para el usuario 5:', top10[5])
Ventajas de surprise.SVD:
- Optimiza automáticamente los hiperparámetros mediante
GridSearchCV. - Implementación en Cython, mucho más rápida que NumPy puro.
- Manejo interno de bias de usuarios e ítems.
5. Comparativa: SVD vs ALS vs NMF vs Deep Learning
SVD (Factorización de Matriz)
- Ventajas: Interpretabilidad, buen desempeño en datasets medianos, bajo coste computacional para
k<200. - Desventajas: No escala bien a millones de usuarios sin paralelismo.
ALS (Alternating Least Squares)
- Ventajas: Fácil de paralelizar (Spark MLlib), maneja datos implícitos.
- Desventajas: Requiere más iteraciones y memoria que SVD.
NMF (Non‑Negative Matrix Factorization)
- Ventajas: Factores no negativos → interpretables como “preferencias positivas”.
- Desventajas: Convergencia más lenta y menos robusta a valores atípicos.
Deep Learning (Autoencoders, Neural CF)
- Ventajas: Captura relaciones no lineales, fácilmente extensible con side‑information.
- Desventajas: Necesita gran cantidad de datos, GPU y mayor tiempo de entrenamiento.
6. Métricas de Evaluación y Validación
Además de RMSE y MAE, en entornos de recomendación se usan métricas de ranking:
- Precision@K / Recall@K: Cuántos de los K ítems recomendados son relevantes.
- MAP (Mean Average Precision).
- NDCG (Normalized Discounted Cumulative Gain): Penaliza ítems relevantes en posiciones bajas.
Ejemplo de cálculo de Precision@10 con surprise:
from sklearn.metrics import precision_score
def precision_at_k(predictions, k=10):
user_est_true = defaultdict(list)
for uid, iid, true_r, est, _ in predictions:
user_est_true[uid].append((est, true_r))
precisions = []
for uid, ratings in user_est_true.items():
ratings.sort(key=lambda x: x[0], reverse=True)
top_k = ratings[:k]
# Consideramos rating >=4 como relevante
relevant = sum(1 for _, r in top_k if r >= 4)
precisions.append(relevant / k)
return np.mean(precisions)
print('Precision@10:', precision_at_k(predictions, k=10))
7. Buenas Prácticas, Optimización y Escalabilidad
Regularización y Bias
Siempre incluya bias_user y bias_item (p.e., surprise.SVD(biased=True)) para evitar sobre‑ajuste.
Selección de k
Realice una búsqueda de hiperparámetros (GridSearchCV de Surprise) con valores típicos: 20, 50, 100, 150.
Paralelismo
Para datasets > 10⁶ interacciones, considere:
- Implementar SVD en Spark (
spark.ml.recommendation.ALS) y usar la varianteimplicitPrefssi trabaja con clicks. - Utilizar
faissoAnnoypara búsqueda aproximada de vecinos cuando se necesita generar recomendaciones en tiempo real.
Persistencia del Modelo
Guarde los factores U, Σ y V en un almacén columnar (Parquet) o en Redis para consultas de baja latencia.
8. Seguridad, Privacidad y Consideraciones Éticas
- Anonimización: Elimine cualquier dato identificable antes de entrenar (hash de IDs).
- Protección contra ataques de inferencia: Limite la exposición de puntuaciones predichas mediante rate‑limiting y differential privacy si el modelo se expone como API.
- Bias de género/raza: Evalúe métricas de equidad (por ejemplo, disparity‑ratio) y ajuste los factores o añada regularización de fairness.
9. Despliegue en Producción
Un flujo típico:
- Ingesta de eventos (Kafka) → tabla de interacciones.
- Batch nightly: re‑entrenamiento del modelo SVD con Spark o con un script Python programado (Airflow).
- Persistencia de factores en un servicio de vectores (RedisVector, Milvus).
- API REST (FastAPI) que recibe
user_idy devuelve top‑N ítems usandonp.dotsobre los factores cargados.
Ejemplo de endpoint rápido con FastAPI:
from fastapi import FastAPI
import numpy as np, redis
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, db=0)
U = np.load('U.npy') # shape (users, k)
V = np.load('V.npy') # shape (items, k)
@app.get('/recommend/{user_id}')
def recommend(user_id: int, n: int = 10):
user_vec = U[user_id]
scores = V @ user_vec # dot product
top_idx = np.argpartition(-scores, n)[:n]
return {'user': user_id, 'recommendations': top_idx.tolist()}
10. Futuro y Tendencias
Si bien SVD sigue siendo la referencia por su simplicidad y rendimiento, la integración con:
- Graph Neural Networks (GNN) para capturar relaciones de usuarios‑usuarios.
- Large Language Models (LLM) como “retrieval‑augmented recommendation”.
- Federated Learning para entrenar modelos sin centralizar datos sensibles.
Estas combinaciones prometen superar los límites de precisión de los enfoques puramente lineales.