Hybrid recommender for MovieLens-25M combining collaborative filtering (matrix factorization) with item-side content features (genres) via a two-tower architecture, ANN-based candidate retrieval with FAISS, and a lightweight MLP re-ranker on top. Includes an A/B simulation harness for measuring engagement lift across synthetic user sessions.
| Model | NDCG@10 | Recall@10 | Hit@10 |
|---|---|---|---|
| MF baseline | 0.333 | 0.185 | 0.472 |
| Two-tower + FAISS + re-rank | 0.380 | 0.218 | 0.541 |
| Relative lift vs MF | +14.1% | +17.8% | +14.6% |
Evaluated on the time-based leave-one-out test split (one held-out interaction per user, chronologically last). Training: BPR loss, 4 uniform negatives per positive, 10 epochs, Adam lr=1e-3.
Simulated two ranking policies on 500k+ synthetic user sessions where a user's latent preference vector drives click probability:
| Policy | CTR | Engagement (expected) |
|---|---|---|
| MF baseline | 0.091 | 0.094 |
| Two-tower hybrid | 0.108 | 0.112 |
| CTR lift | +18.7% |
Interactions (user, item) ──┐
▼
┌───── Two-Tower ──────┐
User tower: │ embed → MLP → normal │
Item tower: │ embed ⊕ feats → MLP │
└──────────┬───────────┘
│ dot product
▼
BPR loss with negatives
▼
Trained item embeddings
▼
FAISS index (IndexFlatIP)
▼
Top-k candidate retrieval
▼
Re-ranker MLP (user ⊕ item ⊕ elementwise product)
▼
Final top-10
src/
data.py MovieLens-25M loader with leave-one-out split
model.py MFBaseline, TwoTowerModel, ReRankerMLP
negative_sampling.py Uniform & popularity-based negative samplers
trainer.py BPR loss + training loop
metrics.py NDCG@k, Recall@k, Hit@k
retrieval.py FAISS wrapper (Flat/IVF) + NumPy fallback
reranker.py Listwise re-ranking over retrieval candidates
simulator.py A/B simulation harness
scripts/
train.py Train MF or two-tower model
simulate_ab.py A/B simulation comparing two policies
tests/ Unit tests for metrics and models
configs/default.yaml Default hyperparameters
pip install -r requirements.txt
# Smoke test on synthetic data (no download needed)
python scripts/train.py --synthetic --epochs 3
# Full run (requires MovieLens-25M in data/ml-25m/)
python scripts/train.py --model two-tower --epochs 10
# A/B simulation comparing MF vs two-tower
python scripts/simulate_ab.py --sessions 500000Download MovieLens-25M from
grouplens.org/datasets/movielens/25m/.
Extract to data/ml-25m/.
Matrix factorization captures collaborative signal well but ignores content features entirely — it has no way to use "this item is a sci-fi thriller" information. The item tower lets genre and metadata influence the embedding during training, which gives a measurable lift on cold-ish items (items with few ratings) and on tail queries where pure CF overfits.
The MLP re-ranker on top of FAISS retrieval catches cases the bilinear dot product misses — e.g. learned feature crosses like "this user likes long-horror-with-strong-female-lead" that embeddings alone approximate poorly.
- The lift over MF is real (+14% NDCG@10) but MF is already quite strong on a core-filtered MovieLens. Expect smaller relative lifts on datasets where MF plateaus.
- FAISS IndexFlatIP is exact; for catalogs >1M items switch to IndexIVFFlat —
faster recall with a tunable
nprobeaccuracy/speed knob. - The A/B simulation uses the two-tower's own latent space as ground truth, which biases toward the two-tower. Run with held-out preference vectors for unbiased comparison.