A feed turns many weak signals into one useful next choice
Recommendation is a product loop with real math underneath: an implicit-feedback objective, a factorized retrieval model, a calibrated ranking score, and a way to measure whether the order was any good.
- Implicit feedback as a weighted matrix factorization problem
- Two-tower retrieval and the softmax that trains it
- A ranking score built from calibrated probabilities, not raw taps
- NDCG: measuring whether the order was worth it
Instagram does not need to read your mind. It needs a useful prediction about the next few seconds of your attention — and that prediction is an optimization problem with a loss function.
Open the app on a quiet Monday morning. You pause on a friend’s holiday photos, save a recipe, skip three videos about a sport you do not follow, and send a small studio-tour clip to a colleague. None of those actions is a full explanation of your taste. Together, they are enough to make the next screen less random.
signals → candidate set → ranked result → feedback
That loop sounds like product language. Underneath, it is a fitted model. Not a metaphor — an actual objective function, trained on your behavior and everyone else’s, re-scored every time you open the app. The rest of this piece stays in that layer: what the objective is, why it is shaped the way it is, and how anyone checks whether the resulting order was worth anything.
A feed is a prediction problem
Let be a person, a post, the moment the feed gets built. One number is being estimated:
Not “click.” Whatever the product decides counts as value — a save, a share, a full watch-through, a follow. Nobody observes directly. The system only sees a noisy trace of past behavior and has to generalize from a small, finite history to an enormous set of things it has never scored. Closing that gap is the whole job.
Two sub-problems fall out of it, solved by different parts of the system at very different scale:
- Retrieval — of everything that exists, what few thousand items even deserve a score?
- Ranking — of those few thousand, what order goes on screen?
huge catalog
↓ retrieval — cheap, approximate, high recall
small candidate set
↓ ranking — expensive, precise, high precision
feed
They fail differently, and the failure is visible in different ways. A weak retrieval step never gives a good post a chance — invisible, permanently. A weak ranking step buries a good post under ten worse ones — visible, but you can still scroll to it. Each stage has its own objective.
The feedback matrix has no negatives
If people rated posts one to five stars, this would be an ordinary recommender problem: build , factor it, done. Nobody rates a scroll. What exists instead is implicit feedback — watch time, saves, shares, comments, skips — all positive-leaning, with no honest “I dislike this.” A skip might mean dislike. It might mean busy right now.
Hu, Koren, and Volinsky’s 2008 fix splits one thing a star rating hides into two: whether you like the item, and how sure the system should be about that read.1 For each pair , let be a raw count — seconds watched, or a weighted tally of save/share/comment events.
is preference: a flat yes/no on whether any positive signal exists. is confidence: it grows with signal strength but never hits zero, because absence is weak evidence of disinterest, not proof of it. is tuned empirically (Hu et al. used ) and sets how much a strong signal outweighs a merely-present one.
Retrieval learns two low-rank vectors, , by minimizing:
The confidence weight is the whole point: the model pays a small penalty everywhere nothing happened, across billions of pairs, and a much larger one wherever something did. That is what “learn from what people actually did, without treating silence as rejection” looks like as an objective.
Retrieval: from a rating matrix to a two-tower model

That confidence-weighted objective is matrix factorization — it approximates a giant, mostly-empty matrix as . Once trained, finding candidates for a person stops being a scan of the catalog and becomes a nearest-neighbor search in . That is the only reason retrieval is cheap enough to run at feed scale.
Modern systems generalize this into a two-tower model: a network mapping recent behavior to a vector, a second network mapping a post’s content and metadata into the same space.2 The towers never see each other’s raw inputs — they only meet through a dot product:
That separation is the entire performance story. Every gets precomputed and indexed offline; serving a request is one ANN lookup for , not a forward pass over the catalog. Training uses a sampled softmax — one positive against a batch of in-batch negatives :
controls how hard the model separates the positive from the crowd. This is the same contrastive shape you find behind most embedding search, not just feeds — which is why retrieval and ranking keep showing up as the same two moves in completely different products.
Ranking: several probabilities, one order
Retrieval answers “what deserves a chance.” Ranking answers “what goes first,” by predicting a calibrated probability per action type and combining them:
ranges over action types — save, share, full watch, skip — each its own model or its own head on a shared network. The sigmoid is not the interesting part. Calibration is: if the model says “70% likely to be saved,” roughly 70% of those posts had better actually get saved, or the score is meaningless the moment it gets combined with a different action’s score. And it always gets combined:
The decay term is why a close friend’s five-minute-old post can beat a globally popular post from yesterday — grows, the exponential shrinks, freshness loses ground on a curve instead of falling off a cliff. The weights are where product intent actually lives:
def score(candidate, person, now, weights, decay_rate):
action_value = sum(
w * predict_probability(action, candidate, person)
for action, w in weights.items()
)
age = now - candidate.published_at
return action_value * math.exp(-decay_rate * age)
ranked_feed = sorted(
candidates,
key=lambda c: score(c, you, now, WEIGHTS, DECAY_RATE),
reverse=True,
)
WEIGHTS is not learned. A product team sets on purpose, because a save is a stronger claim on future value than a like. The model’s job is making each trustworthy. The weighting is a statement about what the product is for.

A worked example
Three candidates for the Monday-morning feed, model outputs already computed:
| Post | age (h) | |||
|---|---|---|---|---|
| A — friend’s photos | 0.10 | 0.05 | 0.20 | 1 |
| B — recipe | 0.55 | 0.10 | 0.05 | 18 |
| C — studio-tour clip | 0.20 | 0.60 | 0.10 | 3 |
Weights , , , decay :
Order: C, B, A. The share-worthy clip beats the older, more save-able recipe. The recipe still beats the friend’s photos at sixteen hours older. And post A scores negative — the predicted skip outweighs the thin save and share signal.
Sit with that last one. A post from someone you follow closely is not automatically ranked first. The model does not know about the relationship. It knows about the predicted action, and in this toy example none of the three predicted actions are enthusiastic about post A.
Checking whether the order was any good
None of this means anything without a way to check it. NDCG — normalized discounted cumulative gain — rewards relevant items for showing up early and punishes them for showing up late, on a curve rather than a cliff:
is the true relevance of whatever landed at position . is the DCG of the best possible ordering of the same set — dividing by it rescales everything to , which is what makes NDCG comparable across people with wildly different amounts of engagement.
import math
def dcg(relevances: list[float]) -> float:
return sum(
(2 ** rel - 1) / math.log2(position + 2)
for position, rel in enumerate(relevances)
)
def ndcg_at_k(ranked_relevances: list[float], k: int) -> float:
actual = dcg(ranked_relevances[:k])
ideal = dcg(sorted(ranked_relevances, reverse=True)[:k])
return actual / ideal if ideal > 0 else 0.0
# C scored highest and was shared; B was saved; A was skipped.
observed_relevance_in_ranked_order = [2, 1, 0] # C, B, A
print(ndcg_at_k(observed_relevance_in_ranked_order, k=3))
Precision@ and recall@ ride alongside it: precision tells you if the feed is wasting attention, recall tells you if retrieval quietly dropped a good candidate before ranking ever saw it.
Why the loop needs an escape hatch
A system trained only to maximize predicted engagement will narrow. More of the same creator, the same format, the same three interests — that is not a bug, it is the objective working as designed. Nothing in rewards uncertainty. Production systems counter this with a small, deliberate exploration bonus, often nothing fancier than an upper-confidence-bound term added before ranking:
counts how many similar items this person has already seen, counts total impressions. The bonus shrinks as evidence piles up — exploration is front-loaded, and fades as the model gets confident. sets how much the product is willing to spend on curiosity.
The rest of the escape hatch has no math in it: an unfollow, a mute, a search bar, an explore tab. People need ways to correct the system that do not route back through its own reward function. A recommendation system should widen what you find, not turn your past into the ceiling on your future feed.
The next piece starts where this one cannot: what does a product do before any of these matrices have a single entry in them? The cold start problem is not an AI miracle. It is a first-use design decision.
Footnotes
-
Y. Hu, Y. Koren, and C. Volinsky, “Collaborative Filtering for Implicit Feedback Datasets,” IEEE International Conference on Data Mining, 2008. The preference/confidence split comes straight from that paper and is still the standard way to reason about implicit engagement data. ↩
-
X. Yi, J. Yang, L. Hong, et al., “Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations,” ACM RecSys, 2019. The two-tower / dual-encoder retrieval architecture and the sampled-softmax objective referenced above. ↩