Navigation

Introduction to AI

Machine Learning

Deep Learning

Generative AI

Tools & Frameworks

General

Recommendation Systems

Recommendation systems are the most economically impactful deployed machine learning systems in existence. Netflix, YouTube, Spotify, Amazon, and TikTok attribute substantial revenue to their recommendation engines. Modern recommenders are complex multi-stage systems combining collaborative filtering, content-based features, and deep learning — built on top of subtle mathematical foundations that determine what billions of users see every day.

The Recommendation Problem

Given a set of users $\mathcal{U}$, items $\mathcal{I}$, and observed interactions $\mathcal{O} \subseteq \mathcal{U} \times \mathcal{I}$, predict the score or probability of each user-item pair $(u, i) \notin \mathcal{O}$.

Explicit feedback: ratings (1–5 stars), thumbs up/down. Sparse and noisy.

Implicit feedback: clicks, watches, purchases, dwell time. Dense but noisy — a watched video is not necessarily enjoyed.

Matrix Factorization

Matrix factorization (MF) decomposes the user-item interaction matrix $\mathbf{R} \in \mathbb{R}^{|\mathcal{U}| \times |\mathcal{I}|}$ into user and item embedding matrices:

$$\hat{r}_{ui} = \mathbf{p}_u^T \mathbf{q}_i + b_u + b_i + \mu$$

where $\mathbf{p}_u \in \mathbb{R}^d$ is the user embedding, $\mathbf{q}_i \in \mathbb{R}^d$ is the item embedding, $b_u, b_i$ are user/item biases, and $\mu$ is the global mean.

Weighted Matrix Factorization for Implicit Feedback

For implicit feedback, we observe binary preference ($r_{ui} = 1$ if interacted, 0 otherwise) with confidence weights (e.g., log of interaction count):

$$c_{ui} = 1 + \alpha \cdot n_{ui}$$

$$\mathcal{L}{\mathrm{WMF}} = \sum{u,i} c_{ui}(r_{ui} - \mathbf{p}_u^T \mathbf{q}_i)^2 + \lambda(|\mathbf{P}|_F^2 + |\mathbf{Q}|_F^2)$$

import torch
import torch.nn as nn


class MatrixFactorization(nn.Module):
    def __init__(self, num_users: int, num_items: int, embedding_dim: int = 64):
        super().__init__()
        self.user_emb = nn.Embedding(num_users, embedding_dim)
        self.item_emb = nn.Embedding(num_items, embedding_dim)
        self.user_bias = nn.Embedding(num_users, 1)
        self.item_bias = nn.Embedding(num_items, 1)
        self.global_bias = nn.Parameter(torch.zeros(1))

        nn.init.normal_(self.user_emb.weight, std=0.01)
        nn.init.normal_(self.item_emb.weight, std=0.01)

    def forward(self, user_ids, item_ids):
        p = self.user_emb(user_ids)       # (B, D)
        q = self.item_emb(item_ids)       # (B, D)
        b_u = self.user_bias(user_ids)    # (B, 1)
        b_i = self.item_bias(item_ids)    # (B, 1)
        dot = (p * q).sum(dim=1, keepdim=True)
        return (dot + b_u + b_i + self.global_bias).squeeze(1)

Bayesian Personalized Ranking (BPR)

For implicit feedback, optimizing pairwise rankings directly outperforms pointwise MSE. BPR (Rendle et al., 2009) maximizes the probability that interacted items rank above unobserved items:

$$\mathcal{L}{\mathrm{BPR}} = -\sum{(u, i, j) \in \mathcal{D}S} \log \sigma(\hat{r}{ui} - \hat{r}_{uj})$$

where $(u, i, j)$ means user $u$ interacted with item $i$ but not $j$. Negative items $j$ are sampled from unobserved interactions.

def bpr_loss(model, user_ids, pos_item_ids, neg_item_ids):
    pos_scores = model(user_ids, pos_item_ids)
    neg_scores = model(user_ids, neg_item_ids)
    return -torch.log(torch.sigmoid(pos_scores - neg_scores)).mean()

Two-Tower Neural Models

Industrial recommenders use two-tower (dual-encoder) models: a user tower and an item tower, each encoding their respective features into a shared embedding space. The score is the dot product of the two embeddings:

$$\text{score}(u, i) = f_\theta(\mathbf{x}u) \cdot g\phi(\mathbf{x}_i)$$

class TwoTowerModel(nn.Module):
    def __init__(self, user_feature_dim: int, item_feature_dim: int, embedding_dim: int = 256):
        super().__init__()
        self.user_tower = nn.Sequential(
            nn.Linear(user_feature_dim, 512), nn.ReLU(), nn.LayerNorm(512),
            nn.Linear(512, embedding_dim),
        )
        self.item_tower = nn.Sequential(
            nn.Linear(item_feature_dim, 512), nn.ReLU(), nn.LayerNorm(512),
            nn.Linear(512, embedding_dim),
        )

    def forward(self, user_features, item_features):
        u_emb = self.user_tower(user_features)           # (B, D)
        i_emb = self.item_tower(item_features)           # (B, D)
        # L2 normalize for cosine similarity
        u_emb = torch.nn.functional.normalize(u_emb, dim=-1)
        i_emb = torch.nn.functional.normalize(i_emb, dim=-1)
        return (u_emb * i_emb).sum(dim=-1)              # dot product

    def get_user_embedding(self, user_features):
        return torch.nn.functional.normalize(self.user_tower(user_features), dim=-1)

    def get_item_embedding(self, item_features):
        return torch.nn.functional.normalize(self.item_tower(item_features), dim=-1)

In-batch negatives: during training, use all other items in the batch as negatives — efficient and effective when batch size is large:

def in_batch_softmax_loss(user_embs, item_embs, temperature: float = 0.05):
    """
    Contrastive loss treating all other items in batch as negatives.
    Diagonal entries are the positive pairs.
    """
    logits = (user_embs @ item_embs.T) / temperature   # (B, B)
    labels = torch.arange(len(user_embs), device=user_embs.device)
    return torch.nn.functional.cross_entropy(logits, labels)

Approximate Nearest Neighbor Retrieval

At inference time, the user tower computes one embedding and the system must find the top-K most similar items among millions. Exact search is $O(|\mathcal{I}| \cdot d)$ — too slow for real-time serving. ANN (Approximate Nearest Neighbor) indexes trade a small accuracy loss for orders-of-magnitude speedup:

  • FAISS (Facebook): hierarchical navigable small world (HNSW) graphs and inverted file (IVF) indexes — the standard for production retrieval
  • ScaNN (Google): anisotropic quantization optimized for maximum inner product search
import faiss
import numpy as np

# Build index offline (after computing item embeddings)
embedding_dim = 256
item_embeddings = np.random.randn(1_000_000, embedding_dim).astype(np.float32)

index = faiss.IndexHNSWFlat(embedding_dim, 32)   # HNSW with M=32 neighbors
index = faiss.IndexFlatIP(embedding_dim)          # Exact inner product (for small catalogs)
index.add(item_embeddings)

# Online retrieval
user_embedding = np.random.randn(1, embedding_dim).astype(np.float32)
scores, item_indices = index.search(user_embedding, k=100)

Deep Structured Semantic Models and Feature Crosses

Beyond dot products, deep cross networks (DCN-v2, DeepFM) explicitly model feature interactions for ranking:

class DeepCrossNetwork(nn.Module):
    """DCN-v2: Cross layers + Deep layers for click-through rate prediction."""

    def __init__(self, input_dim: int, num_cross_layers: int = 3, deep_dims=(256, 128, 64)):
        super().__init__()
        # Cross layers: learn explicit feature interactions
        self.cross_weights = nn.ParameterList([
            nn.Parameter(torch.randn(input_dim, input_dim)) for _ in range(num_cross_layers)
        ])
        self.cross_biases = nn.ParameterList([
            nn.Parameter(torch.zeros(input_dim)) for _ in range(num_cross_layers)
        ])
        # Deep tower: implicit high-order interactions
        layers = []
        dims = [input_dim] + list(deep_dims)
        for i in range(len(dims) - 1):
            layers += [nn.Linear(dims[i], dims[i+1]), nn.ReLU()]
        self.deep = nn.Sequential(*layers)
        # Final prediction
        self.output = nn.Linear(input_dim + deep_dims[-1], 1)

    def forward(self, x):
        # Cross network
        x_cross = x
        for W, b in zip(self.cross_weights, self.cross_biases):
            x_cross = x @ (W * x_cross).T + b + x_cross  # simplified cross layer

        # Deep network
        x_deep = self.deep(x)

        return torch.sigmoid(self.output(torch.cat([x_cross, x_deep], dim=-1)))

Session-Based Recommendation with Transformers

When user history is sparse or cold-start, session-based models recommend based on the current session’s sequence of interactions. BERT4Rec and SASRec apply masked/causal self-attention to item sequences:

from transformers import BertConfig, BertModel


class BERT4Rec(nn.Module):
    def __init__(self, num_items: int, max_seq_len: int = 50, d_model: int = 256):
        super().__init__()
        self.item_emb = nn.Embedding(num_items + 1, d_model, padding_idx=0)
        config = BertConfig(
            hidden_size=d_model, num_attention_heads=4,
            num_hidden_layers=2, intermediate_size=1024,
            max_position_embeddings=max_seq_len,
        )
        self.bert = BertModel(config)
        self.head = nn.Linear(d_model, num_items)

    def forward(self, item_seq, masked_positions=None):
        embs = self.item_emb(item_seq)
        outputs = self.bert(inputs_embeds=embs)
        logits = self.head(outputs.last_hidden_state)
        return logits

Multi-Objective Ranking

Commercial recommenders optimize multiple objectives simultaneously (clicks, watch time, shares, revenue) using weighted multi-task heads or Pareto-constrained optimization. YouTube’s ranking system (Zhao et al., 2019) uses a multi-gate mixture-of-experts architecture where different expert networks specialize in different objectives.

A simple multi-objective loss:

def multi_objective_loss(
    click_logit, watch_time_logit, share_logit,
    click_label, watch_time_label, share_label,
    weights=(1.0, 0.5, 0.3),
):
    click_loss = torch.nn.functional.binary_cross_entropy_with_logits(click_logit, click_label)
    watch_loss = torch.nn.functional.mse_loss(watch_time_logit, watch_time_label)
    share_loss = torch.nn.functional.binary_cross_entropy_with_logits(share_logit, share_label)
    return weights[0] * click_loss + weights[1] * watch_loss + weights[2] * share_loss

Industrial System Architecture

A production recommender has multiple stages:

User Request

Candidate Generation (millions → thousands)
  Two-tower ANN retrieval

Filtering (business rules, already-seen, safety)

Ranking (thousands → tens)
  Deep cross network / transformer ranker

Post-ranking / Re-ranking (diversity, freshness)

Final Recommendations Served

Summary

Modern recommendation systems combine principled mathematical foundations with industrial-scale engineering:

  • Matrix factorization with BPR loss provides a strong baseline for collaborative filtering on implicit feedback
  • Two-tower models with in-batch negatives scale to millions of items through ANN retrieval at serving time
  • Deep cross networks explicitly model feature interactions for click-through rate and conversion prediction
  • BERT4Rec and SASRec use transformer self-attention over item sequences for session-based cold-start recommendations
  • Multi-objective ranking balances competing signals (engagement, quality, revenue) through multi-task learning
  • Production systems are multi-stage pipelines: generation → filtering → ranking → re-ranking, each using models of increasing complexity and smaller candidate sets