Recommender Systems

Recommender systems predict which items a user will find relevant — products, videos, songs, articles, jobs — from past interactions and from user and item attributes. They are among the highest-impact production ML systems, and they look different from textbook classification: the data is a huge, extremely sparse user-item matrix, feedback is mostly implicit (clicks, views, purchases), and the output is a ranked list, not a label.

This page covers content-based and collaborative filtering, matrix factorization, two-tower neural retrieval, the multi-stage serving architecture, ranking metrics, and the pitfalls (cold start, popularity bias, feedback loops).



1. Content-Based vs Collaborative Filtering

Content-based filtering recommends items similar to those the user liked, using item features (text, category, embeddings). It handles new items well and is explainable, but tends to recommend more of the same.

Collaborative filtering (CF) uses only the interaction matrix: "users who liked what you liked also liked X." Memory-based CF computes user-user or item-item similarities (cosine, Jaccard) directly; item-item CF powers many "customers also bought" widgets because item similarities are stable and cacheable. Model-based CF learns latent factors. CF captures taste patterns that features miss but cannot handle items or users with no interactions (cold start).

Hybrid systems combine both, which in modern practice means feeding IDs and content features into one neural model.


2. Matrix Factorization

Matrix Factorization: R ≈ U · Vᵀ ? 5 ? ? 3 ? 4 ? ? 1 ? ? ? ? 5 ? ? 4 ? 2 ? ? 5 ? 3 ? ? ? ? 1 R: users × items (sparse) ≈ U: user factors · Vᵀ: item factors k latent factors (here k = 2) r̂ᵤᵢ = uᵤ · vᵢ Fit on known cells; predict the '?'s. Loss: Σ over observed (r − uᵤ·vᵢ − bᵤ − bᵢ − μ)² + λ(‖uᵤ‖² + ‖vᵢ‖²)

Matrix factorization represents each user u and item i as a k-dimensional vector so that their dot product approximates the observed interaction. With biases, r̂ui = μ + bu + bi + uu·vi. It is fit by SGD or Alternating Least Squares (fix V, solve for U in closed form, swap — parallelizes well, the approach in Spark MLlib).

For implicit feedback there are no negative labels: an unclicked item may be disliked or merely unseen. Implicit ALS (Hu, Koren & Volinsky) treats every cell as a 0/1 preference weighted by a confidence cui = 1 + α·count; BPR (Bayesian Personalized Ranking) instead optimizes that observed items rank above sampled unobserved ones.


3. Neural Recommenders and Two-Tower Retrieval

Two-Tower Retrieval Model User featuresid, history, context User towerMLP / transformer Item featuresid, text, category Item towerMLP / text encoder Dot productscore = u · v Item embeddings are precomputed and put in an ANN index; serving = one user-tower pass + kNN lookup. Trained with in-batch negatives (sampled softmax); correct for popularity bias in sampling.

The two-tower (dual encoder) model generalizes MF: each tower is a network that can consume IDs plus arbitrary features, so new items get sensible embeddings from their content. Because scoring is a dot product, item embeddings can be indexed with approximate nearest neighbor search (FAISS, ScaNN, pgvector) and retrieval over millions of items takes milliseconds.

Other neural families: sequential recommenders (GRU4Rec, SASRec, BERT4Rec) treat a session as a token sequence and predict the next item with self-attention; wide-and-deep / DCN / DLRM ranking models learn feature crosses over large sparse categorical inputs; graph neural networks (PinSage, LightGCN) propagate signal over the user-item bipartite graph.


4. Production Architecture: Retrieve, Rank, Re-rank

Multi-Stage Recommendation Funnel Catalogmillions Retrievalthousands Rankinghundreds Re-ranktens Showntop-N Retrieval: two-tower / item-item / MF via ANN. Ranking: GBM or deep model on rich features. Re-rank: diversity, freshness, business rules, de-duplication.

No single model can score every item for every request with rich features, so production systems are funnels. Candidate generation pulls a few hundred to a few thousand items from several sources (two-tower ANN, item-item co-occurrence, trending, recently viewed). A heavier ranking model — often gradient boosting or a deep network — scores those candidates with user, item, context and cross features, predicting click, watch time or purchase probability. A re-ranking layer applies diversity (e.g. MMR), freshness, fairness, and business rules.

Contextual bandits or reinforcement learning are used where exploration and long-term engagement matter more than immediate click probability.


5. Python: Implicit-Feedback Matrix Factorization in PyTorch (BPR)

import numpy as np, torch, torch.nn as nn

# Toy implicit data: (user, item) interactions
rng = np.random.default_rng(0)
n_users, n_items, k = 500, 300, 32
true_u, true_v = rng.normal(size=(n_users, 8)), rng.normal(size=(n_items, 8))
scores = true_u @ true_v.T
pairs = np.argwhere(scores > np.quantile(scores, 0.95))       # top 5% are "interactions"
rng.shuffle(pairs)
test_pairs, train_pairs = pairs[:1000], pairs[1000:]
seen = {(u, i) for u, i in train_pairs}

class MF(nn.Module):
    def __init__(self):
        super().__init__()
        self.U = nn.Embedding(n_users, k); self.V = nn.Embedding(n_items, k)
        nn.init.normal_(self.U.weight, std=0.1); nn.init.normal_(self.V.weight, std=0.1)
    def forward(self, u, i): return (self.U(u) * self.V(i)).sum(-1)

model = MF(); opt = torch.optim.Adam(model.parameters(), lr=1e-2, weight_decay=1e-5)
tp = torch.tensor(train_pairs)
for epoch in range(30):
    perm = tp[torch.randperm(len(tp))]
    for b in perm.split(1024):
        u, pos = b[:, 0], b[:, 1]
        neg = torch.randint(0, n_items, (len(b),))               # sampled negatives
        loss = -nn.functional.logsigmoid(model(u, pos) - model(u, neg)).mean()   # BPR
        opt.zero_grad(); loss.backward(); opt.step()

# Recall@10 on held-out interactions, excluding already-seen items
with torch.no_grad():
    all_scores = model.U.weight @ model.V.weight.T
    for u, i in train_pairs: all_scores[u, i] = -1e9
    top10 = all_scores.topk(10, dim=1).indices.numpy()
hits = np.mean([i in top10[u] for u, i in test_pairs])
print(f"Recall@10 (hit rate): {hits:.3f}")

For production-scale implicit MF, libraries such as implicit (ALS/BPR on sparse matrices) or Spark MLlib's ALS are faster and better tested.


6. Evaluation


7. Common Pitfalls


8. Worked Example: One MF Update

A two-factor example makes the biased MF formula and one SGD update concrete. Let the global mean be μ = 3.5, user bias bu = 0.2, item bias bi = −0.3, user vector u = [1.0, 0.5] and item vector v = [0.8, 0.4].

In practice an L2 term λ(||u||2 + ||v||2 + bu2 + bi2) is added, which shrinks factors of users and items with few interactions toward zero so their predictions fall back on the biases. The main hyperparameters are the factor count k (tens to a few hundred), λ, the learning rate or number of ALS sweeps, and for implicit ALS the confidence scale α. Tune them against a ranking metric on a time-based split, not RMSE on a random split.

9. Feedback Loops and Online Evaluation

The Recommendation Feedback Loop Model scores candidates Exposure what gets shown Interactions clicks, watches, buys Training data logged with propensities Items never shown cannot be clicked, so naive retraining reinforces past choices (popularity bias).

A recommender is trained on data that its predecessor generated. Items it never showed have no clicks, so they look unpopular, so they are shown even less. Offline metrics computed on that logged data share the same blind spot. Several practices break the loop:

10. Cold-Start Playbook

SituationApproach
New item, has contentFeature-based tower or content embedding (text, image via CLIP-style models) places it near similar items immediately.
New item, little contentGuaranteed exploration impressions, then fold its early interactions into the model quickly (frequent incremental retraining).
New userPopularity by context (locale, device, referrer), onboarding preference questions, then session-based models that adapt within the first few clicks.
Returning user, changed tasteRecency-weighted features and sequential models over recent history rather than a static user vector.

Cold start is less a separate algorithm than a reason to keep content features in the model at all, even when pure ID-based collaborative filtering scores slightly better offline on warm users.

11. Further Reading