- Explain the difference between explicit and implicit feedback and how each changes the training objective.
- Derive and apply the SGD updates for biased matrix factorization, and implement it in NumPy.
- Design a retrieval-then-ranking architecture with a two-tower model, in-batch negatives and an approximate nearest-neighbor index.
- Choose cold-start strategies for new users and new items, including content features and exploration.
- Calculate precision@k, recall@k and NDCG@k by hand, and plan an online A/B test with guardrail metrics.
- Identify feedback loops, popularity and position bias, and filter bubbles, and apply mitigations.
Before this: Linear Algebra for AI, Optimization for Machine Learning, Model Evaluation and Metrics
Recommender systems decide what each person sees next: the next video, product, song, article, job posting or ad. They are among the most commercially important uses of machine learning, because in large catalogs most items are discovered through recommendations rather than search. They are also an unusual ML problem: the model's outputs change the data it will be trained on next, so engineering and ethics are inseparable.
The picture in your head
Picture a giant spreadsheet with one row per user and one column per item. A cell holds a rating, a purchase or a click if the user interacted with that item; almost every cell is empty (a typical user touches far less than 1% of the catalog). Recommendation is the art of filling in the blanks: guessing which empty cells in your row would be bright if you tried those items. Two ways to guess: "people whose rows look like yours liked this" (collaborative filtering) and "this item looks like the items you liked" (content-based). Industrial systems combine both and add a second stage that carefully re-ranks a shortlist.
Feedback: explicit versus implicit
- Explicit feedback is a deliberate judgment: 1–5 stars, thumbs up or down. It is clear in meaning but scarce, and biased toward people who rate and items they chose to consume.
- Implicit feedback is behavior: clicks, plays, watch time, purchases, add-to-cart, skips. It is abundant but ambiguous. A play might be a misclick; a missing interaction usually means "never saw it," not "dislikes it." There are no true negatives, only positives and unknowns.
Most production systems run on implicit feedback. Hu, Koren and Volinsky (2008) formalized a standard treatment: convert raw counts into a binary preference if else , and a confidence that grows with how much the user engaged, then fit factors to the preferences weighted by confidence. Unobserved cells are treated as weak negatives rather than ignored.
Collaborative filtering
Collaborative filtering (CF) uses only the interaction matrix, no item descriptions.
Neighborhood methods. User-based CF finds users similar to you (by cosine similarity or Pearson correlation of their rows) and recommends what they liked. Item-based CF computes item–item similarity from co-interaction ("people who bought this also bought…"), then scores each candidate by its similarity to items in your history. Item-based CF became an industry staple because item similarities are more stable than user similarities and can be precomputed.
Matrix factorization (MF). Assume each user and item can be described by hidden factors, vectors , with much smaller than the number of users or items (often tens to a few hundred). For movies, one factor might loosely capture "action versus drama," another "mainstream versus arthouse," though factors are learned, not named. The predicted rating is
where is the global mean rating, the user's bias (a generous or harsh rater) and the item's bias (broadly liked or not). With the set of observed (user, item) pairs, we minimize regularized squared error:
with the regularization strength. Note the sum covers only observed ratings; filling empty cells with zeros and running a classic SVD would teach the model that unseen means disliked. This formulation, with biases, was popularized by the Netflix Prize era and described by Koren, Bell and Volinsky (2009).
Two standard optimizers:
- Stochastic gradient descent (SGD). For each observed rating compute the error and update, with learning rate : , , , (using the old values on the right-hand sides).
- Alternating least squares (ALS). Fix all item vectors and the problem becomes an independent ridge regression per user, solvable in closed form; then fix users and solve for items; alternate. ALS parallelizes well and is the usual choice for the implicit-feedback formulation, where the sum runs over all cells.
Worked example: one SGD step
Let , , , , , , the true rating , and .
- Dot product: .
- Prediction: .
- Error: .
- User bias: .
- Item bias: .
- User vector: .
- Item vector: .
- New prediction: .
One step moved the prediction from 3.92 toward 5. Training repeats this over all observed ratings for many epochs.
The following NumPy implementation trains on a toy matrix with two obvious taste groups and fills in the missing cells:
import numpy as np
def train_mf(R, k=2, epochs=200, lr=0.02, lam=0.1, seed=0):
"""R: (n_users, n_items) ratings with np.nan for missing entries."""
rng = np.random.default_rng(seed)
n_u, n_i = R.shape
P, Q = 0.1 * rng.standard_normal((n_u, k)), 0.1 * rng.standard_normal((n_i, k))
bu, bi = np.zeros(n_u), np.zeros(n_i)
obs = np.argwhere(~np.isnan(R)) # observed (user, item) pairs only
mu = np.nanmean(R)
for _ in range(epochs):
rng.shuffle(obs)
for u, i in obs:
e = R[u, i] - (mu + bu[u] + bi[i] + P[u] @ Q[i])
bu[u] += lr * (e - lam * bu[u])
bi[i] += lr * (e - lam * bi[i])
P[u], Q[i] = (P[u] + lr * (e * Q[i] - lam * P[u]),
Q[i] + lr * (e * P[u] - lam * Q[i]))
return np.clip(mu + bu[:, None] + bi[None, :] + P @ Q.T, 1, 5)
nan = np.nan
R = np.array([[5, 4, nan, 1, 1],
[4, nan, 5, 1, 2],
[1, 1, 2, 5, nan],
[nan, 2, 1, 4, 5]], dtype=float)
print(np.round(train_mf(R), 1))
Users 0 and 1 like items 0–2 and dislike 3–4; users 2 and 3 are the opposite. The model predicts about 5.0 for user 0 on item 2, about 3.4 for user 1 on item 1, about 4.9 for user 2 on item 4 and about 2.0 for user 3 on item 0 (exact values depend on the random seed), which is what a person would guess from the pattern.
Content-based recommendation
Content-based methods describe items by their attributes (genre, text, price, brand, image embeddings, audio features) and build a profile of each user from the items they engaged with, for example the average of those items' feature vectors. Candidates are ranked by similarity to the profile. Strengths: they can recommend a brand-new item on day one and explain themselves ("because you watched…"). Weaknesses: they over-specialize (more of the same), and they need good item features. Pretrained text, image and audio encoders from the previous lessons now supply those features almost for free.
Industrial architecture: retrieval, then ranking
With hundreds of millions of items you cannot score every item with an expensive model for every request within a ~100 ms budget. Production systems therefore run a funnel, described publicly in YouTube's two-stage design (Covington, Adams and Sargin, 2016):
- Candidate generation (retrieval). Cheaply select a few hundred to a few thousand plausible items from the whole catalog, usually from several sources merged together: embedding retrieval, item-to-item CF, trending items, items from followed creators.
- Ranking. Score each candidate with a rich model that uses many features (user history, item features, context such as time and device, cross features) and predicts one or more outcomes (click, watch time, purchase, satisfaction surveys). Gradient-boosted trees and deep networks are both common.
- Re-ranking and business rules. Enforce diversity, freshness, deduplication, eligibility, fairness and policy constraints, then return the final list.
Two-tower retrieval
The standard retrieval model is the two-tower (dual-encoder) network: a user tower maps user features and history to an embedding , an item tower maps item features to , and relevance is their dot product . Because the towers do not interact until the final dot product, all item embeddings can be precomputed and stored in an approximate nearest-neighbor (ANN) index (the same vector-search technology used for RAG); at request time you compute one user embedding and retrieve the top items in milliseconds. Matrix factorization is the special case where each tower is just an embedding lookup.
Training typically uses observed (user, positive item) pairs and in-batch negatives: the other items in the mini-batch serve as negatives, and a softmax over the batch is trained with cross-entropy. Popular items appear as negatives more often, which biases the model against them, so practical systems apply a correction such as subtracting the log of each item's sampling probability from its logit (the approach described by Yi et al., 2019).
import torch, torch.nn as nn, torch.nn.functional as F
class Tower(nn.Module):
def __init__(self, n_ids, dim=64):
super().__init__()
self.emb = nn.Embedding(n_ids, dim)
self.mlp = nn.Sequential(nn.Linear(dim, dim), nn.ReLU(), nn.Linear(dim, dim))
def forward(self, ids):
return F.normalize(self.mlp(self.emb(ids)), dim=-1) # unit-length embeddings
user_tower, item_tower = Tower(n_ids=10_000), Tower(n_ids=50_000)
opt = torch.optim.Adam(list(user_tower.parameters()) + list(item_tower.parameters()), lr=1e-3)
def train_step(user_ids, pos_item_ids, temperature=0.05):
u = user_tower(user_ids) # (B, d)
v = item_tower(pos_item_ids) # (B, d)
logits = u @ v.T / temperature # (B, B): row b's positive is column b
labels = torch.arange(len(user_ids)) # the other B-1 items are in-batch negatives
loss = F.cross_entropy(logits, labels)
opt.zero_grad(); loss.backward(); opt.step()
return loss.item()
print(train_step(torch.randint(0, 10_000, (256,)), torch.randint(0, 50_000, (256,))))
Real towers take many features (IDs plus categorical and text features, recent-history sequences), not only an ID, and duplicates of the same item within a batch should be masked.
Cold start
New users and new items have no interactions, so pure CF cannot place them. Standard remedies:
- New items: content features in the item tower (so a new item gets a sensible embedding from its title, image and category), plus deliberate exploration traffic to collect early feedback.
- New users: onboarding questions, context (location, device, referral source), popularity within a segment, and fast session-based models that adapt within the first few clicks.
- Bandits: treat recommendation as explore–exploit (for example Thompson sampling or upper-confidence-bound methods), giving uncertain items some exposure to learn their value.
Offline evaluation
Hold out interactions, ideally the latest ones for each user (a time-based split, since random splits leak the future), and check whether the model ranks them highly. For a ranked list of length and a user with a set of relevant held-out items:
- Precision@k = (relevant items in the top ) ÷ .
- Recall@k = (relevant items in the top ) ÷ (total relevant items for that user).
- NDCG@k (normalized discounted cumulative gain, Järvelin and Kekäläinen, 2002) rewards placing the most relevant items near the top. With graded relevance at rank ,
where IDCG@k is the DCG of the ideal ordering, so NDCG lies in . (Some libraries use the linear gain instead of ; state which you use.)
Worked example (precision and recall). A user has 4 relevant held-out items. The top 5 recommendations contain relevant items at ranks 1, 3 and 5. Precision@5 ; recall@5 .
Worked example (NDCG). The top 5 items have graded relevance and these are all the relevant items for the user.
| Rank | rel | Gain | Discount | Contribution |
|---|---|---|---|---|
| 1 | 3 | 7 | 1.000 | 7.000 |
| 2 | 2 | 3 | 0.631 | 1.893 |
| 3 | 0 | 0 | 0.500 | 0.000 |
| 4 | 1 | 1 | 0.431 | 0.431 |
| 5 | 2 | 3 | 0.387 | 1.161 |
DCG@5 . The ideal order is : . NDCG@5 . The list is nearly ideal; it loses a little because a relevance-2 item sits at rank 5 behind a non-relevant item.
import numpy as np
def dcg(rels):
rels = np.asarray(rels, dtype=float)
return float(np.sum((2 ** rels - 1) / np.log2(np.arange(2, len(rels) + 2))))
def ndcg_at_k(ranked_rels, all_rels, k):
ideal = sorted(all_rels, reverse=True)[:k]
return dcg(ranked_rels[:k]) / dcg(ideal) if dcg(ideal) > 0 else 0.0
print(round(ndcg_at_k([3, 2, 0, 1, 2], all_rels=[3, 2, 2, 1], k=5), 3)) # 0.969
Also track coverage (what fraction of the catalog ever gets recommended), diversity and novelty; a model can score well on NDCG by recommending only blockbusters.
Online A/B testing
Offline metrics are computed on logs produced by the old system, so they only partly predict live behavior; the final judge is a randomized online controlled experiment (A/B test). Users are randomly assigned to control (current system) or treatment (new model), and you compare a pre-registered primary metric (for example, long-term engagement or purchases per user) plus guardrail metrics (latency, complaints, unsubscribes, revenue). Good practice, as laid out by Kohavi, Tang and Xu (2020): randomize by user, not by request; compute the sample size needed for the effect you care about before starting; run for full weekly cycles; beware novelty effects; check that the observed traffic split matches the intended one (a sample ratio mismatch signals a bug); and do not stop the test the moment results look significant ("peeking" inflates false positives unless you use sequential-testing methods).
Feedback loops and filter bubbles
A recommender trains on interactions that it caused. Items it shows get clicks; items it hides get none, so the next model is even more confident they are bad. This feedback loop produces popularity bias (the rich get richer), position bias (top slots get clicked because they are on top) and, for users, filter bubbles, where exposure narrows toward what they already consume. Optimizing short-term clicks can also reward sensational or low-quality content. Mitigations include logging the propensity (probability) with which each item was shown and using inverse-propensity weighting during training and evaluation, reserving a small amount of randomized exploration traffic, explicit diversity and freshness constraints in re-ranking, optimizing longer-term or survey-based satisfaction targets, and giving users controls ("not interested," topic preferences). Regulators also care: the EU Digital Services Act requires online platforms to set out the main parameters of their recommender systems in their terms and conditions (Article 27), and requires very large online platforms and search engines to offer at least one recommender option not based on profiling (Article 38).
Common pitfalls
- Random train/test splits that let the model see the future; split by time.
- Treating missing as negative in explicit-rating MF, or ignoring negatives entirely with implicit data.
- Evaluating with sampled negatives (ranking the true item among 100 random items) and reporting the result as if it were full-catalog ranking; sampled metrics can reorder models.
- Popularity baseline neglected. Always compare against "most popular" and "recently popular"; they are surprisingly hard to beat.
- Training–serving skew: features computed differently offline and online, or the retrieval index built from stale embeddings.
- Optimizing the wrong objective: click-through rate rewards clickbait; choose targets that reflect user value.
In practice
Teams start with strong baselines (popularity, item-to-item CF, ALS on implicit feedback), then add a two-tower retrieval model with content features to handle cold start and a feature-rich ranker trained on multiple objectives. They evaluate offline with time-split recall@k and NDCG@k plus coverage and diversity, ship only through A/B tests with guardrails, and keep a small exploration budget to fight feedback loops. Infrastructure matters as much as models: a feature store for consistent online and offline features, an ANN index refreshed as embeddings change, logging of what was shown (not only what was clicked) with propensities, and dashboards that slice metrics by new versus returning users and by item popularity tier. Increasingly, sequence models (transformers over a user's recent interaction history) and LLM-generated item features are added to the same funnel rather than replacing it.
Key takeaways
- Recommendation fills in a very sparse user-item matrix, using collaborative signals, item content, or both.
- Matrix factorization with biases fits only observed entries for explicit ratings; implicit feedback needs a confidence-weighted treatment of unobserved items.
- Production systems run a funnel: cheap multi-source retrieval (often two-tower plus ANN), a rich ranker, then re-ranking for diversity and policy.
- Offline metrics such as recall@k and NDCG@k must use time-based splits and be complemented by coverage and diversity; A/B tests are the final judge.
- Recommenders train on data they caused, so log exposure propensities, keep exploration traffic and optimize for long-term user value to limit feedback loops.
Terms in this lesson
- Collaborative filtering
- Recommending items using patterns in the interactions of many users, without relying on item content.
- Matrix factorization
- Approximating the user-item interaction matrix as the product of low-dimensional user and item factor matrices.
- Implicit feedback
- Behavioral signals such as clicks, plays or purchases that indicate preference indirectly and lack explicit negatives.
- Two-tower model
- A retrieval model with separate user and item encoders whose embeddings are compared by a dot product, allowing item embeddings to be precomputed and indexed.
- In-batch negatives
- A training technique that treats the other positive items in a mini-batch as negatives for each user.
- Cold start
- The difficulty of recommending for new users or new items that have little or no interaction history.
- NDCG@k
- Discounted cumulative gain of the top-k list divided by that of the ideal ordering, rewarding relevant items placed near the top.
- A/B test
- A randomized controlled experiment that compares a control and a treatment experience on live users using pre-specified metrics.
- Feedback loop
- The cycle in which a recommender's own outputs shape the interaction data used to train its next version.
- Filter bubble
- A narrowing of the content a user is exposed to caused by personalization repeatedly favoring what they already consume.
Check your understanding
- 1.
A user has 8 relevant held-out items. The top-10 list contains 4 of them. What are precision@10 and recall@10?
- 2.
Why can a two-tower model retrieve candidates from hundreds of millions of items within milliseconds?
- 3.
In implicit-feedback data, what does a missing user-item interaction most often mean?
- 4.
A marketplace adds thousands of new listings per hour, each with a title, photos and a category, and they have no interactions yet. Which change best helps them get recommended?
- 5.
A ranked list of two items has binary relevance (0, 1), and the single relevant item is the only relevant one for the user. Using gain 2^rel - 1 and discount 1/log2(rank + 1), what is NDCG@2?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Perform one SGD step of biased matrix factorization with , , , , , observed rating , learning rate and no regularization (). Give the updated parameters and the new prediction.
Show solution
Prediction: . Error .
Updates (with , using old values on the right):
New prediction: . The step moved the prediction from 4.0 toward the true rating 2. Without regularization, repeated steps can overfit users and items with few ratings, which is why is used in practice.
- Exercise 2Core
A model returns three items with graded relevance (0, 2, 1); these are all the relevant items for the user. Compute DCG@3, IDCG@3 and NDCG@3 using gain . Then say what NDCG@3 would be if the order were (2, 1, 0).
Show solution
Discounts for ranks 1-3: 1.000, 0.631, 0.500.
DCG@3 = .
Ideal order (2, 1, 0): IDCG@3 = .
NDCG@3 = 0.659. Putting a non-relevant item in the top slot costs about a third of the achievable gain. With the ideal order (2, 1, 0), DCG equals IDCG, so NDCG@3 = 1.0.
- Exercise 3Core
Write Python functions
precision_at_kandrecall_at_kfor one user, and a function that averages them over users givenrecs(dict user -> ranked list of item ids) andtruth(dict user -> set of relevant held-out items). Skip users with no relevant items. Test with recs = {'a': [1,2,3,4], 'b': [5,6,7,8]}, truth = {'a': {2,4,9}, 'b': {8}}, k = 4.Show solution
def precision_at_k(ranked, relevant, k): return sum(1 for i in ranked[:k] if i in relevant) / k def recall_at_k(ranked, relevant, k): return sum(1 for i in ranked[:k] if i in relevant) / len(relevant) def mean_metrics(recs, truth, k): users = [u for u in recs if truth.get(u)] # skip users with no relevant items p = sum(precision_at_k(recs[u], truth[u], k) for u in users) / len(users) r = sum(recall_at_k(recs[u], truth[u], k) for u in users) / len(users) return p, r recs = {'a': [1, 2, 3, 4], 'b': [5, 6, 7, 8]} truth = {'a': {2, 4, 9}, 'b': {8}} print(mean_metrics(recs, truth, k=4)) # (0.375, 0.8333...)User a: 2 hits (items 2 and 4), P@4 = 2/4 = 0.5, R@4 = 2/3 = 0.667. User b: 1 hit (item 8), P@4 = 0.25, R@4 = 1.0. Means: precision (0.5 + 0.25)/2 = 0.375, recall (0.667 + 1.0)/2 = 0.833. This is a macro (per-user) average, so light users count as much as heavy users; report which averaging you use.
- Exercise 4Challenge
Your team wants to A/B test a new ranking model. The primary metric is purchase conversion per user, currently 5.0%, and the business cares about detecting a 5% relative lift (to 5.25%). (a) Estimate the users needed per arm for a two-sided test at with 80% power. (b) After three days the dashboard shows p = 0.03 and someone wants to ship. List the problems with stopping now and the checks you would run. (c) Name one way the new model could win the test yet harm the product over months, and how you would detect it.
Show solution
(a) For two proportions, with , , , , :
; variance term ; 122,000 users per arm (Lehr's rule of thumb, , gives about 121,600). Small relative lifts on low base rates require large samples.
(b) Problems: the test was designed for a fixed sample, and checking repeatedly then stopping when p < 0.05 ('peeking') inflates the false-positive rate well above 5% unless a sequential method was planned; three days is not a full weekly cycle; early novelty effects can fade. Checks: confirm the planned sample size has been reached; run a sample-ratio-mismatch test on the traffic split; check guardrails (latency, revenue per user, returns, complaints, unsubscribes); look at segments (new vs returning users, platforms) for bugs; and confirm the metric is computed per user, matching the randomization unit.
(c) Example: the model pushes heavily discounted or clickbait items, raising short-term conversion while lowering margin, catalog diversity and long-term satisfaction, and starving new sellers of exposure (a feedback loop). Detect it with long-running holdout groups, guardrail metrics for margin, return rate and repeat purchase at 30-90 days, catalog coverage and exposure concentration (for example, the share of impressions going to the top 1% of items), and periodic user satisfaction surveys.
Further reading
- paperKoren, Bell & Volinsky, Matrix Factorization Techniques for Recommender Systems (IEEE Computer, 2009)
- paperHu, Koren & Volinsky, Collaborative Filtering for Implicit Feedback Datasets (ICDM 2008)
- paperCovington, Adams & Sargin, Deep Neural Networks for YouTube Recommendations (RecSys 2016)
- paperYi et al., Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations (RecSys 2019)
- courseGoogle for Developers, Recommendation Systems course
- bookKohavi, Tang & Xu, Trustworthy Online Controlled Experiments (2020)
Go further
Sources
Fact-checked and last reviewed 2026-11-05