This repository contains the implementation of a research prototype for an unexpectedness-aware recommender system. The goal is to design a system that balances relevance and unexpectedness in recommendations to mitigate the filter bubble problem.
Modern recommender systems often over-optimize for historical relevance. As a result, users are repeatedly shown items similar to what they have already consumed, which leads to boredom, lack of discovery, and reduced long-term engagement.
This project explores the concept of unexpectedness-aware recommendation, where recommendations are ranked not only by relevance but also by how meaningfully they deviate from a user's expected preferences.
The final goal is to implement a Transformer-based sequential recommender architecture that incorporates:
- relevance
- unexpectedness
- temporal behavior
- rating intensity
- user preference stability
The current repository represents the initial experimental pipeline required before implementing the final model.
Dataset chosen:
MovieLens 32M
Source: https://grouplens.org/datasets/movielens/
Dataset characteristics:
- ~32 million interactions
- ~200k users
- ~87k movies
- timestamps for interactions
- explicit ratings (1–5)
The dataset was chosen for several reasons:
- It contains explicit ratings, which allows us to preserve rating intensity instead of converting interactions to binary feedback.
- It includes timestamps, which are necessary for modeling temporal behavior and stability.
- The dataset size is large enough to simulate real-world recommender system environments.
- It is widely used in recommender system research, making it suitable for benchmarking.
Current repository structure:
recsys/
│
├── configs/
├── data/
│ └── train_test_split.py
│
├── evaluation/
│ ├── precision.py
│ ├── recall.py
│ ├── ndcg.py
│ ├── serendipity.py
│ └── unexpectedness.py
│
├── experiments/
│ ├── test_popularity.py
│ ├── experiment_runner.py
│ └── preprocess_ml1m.py
│
├── models/
│ ├── popularity.py
│ ├── purs.py
│ └── purs_train.py
│
└── requirements.txt
A Python virtual environment was created using:
python -m venv .venv
Libraries were installed via pip.
The following libraries were installed:
numpy
pandas
scikit-learn
scipy
matplotlib
seaborn
tqdm
| Library | Purpose |
|---|---|
| NumPy | numerical computation |
| Pandas | dataset preprocessing |
| Scikit-Learn | machine learning utilities |
| SciPy | sparse matrices and scientific computation |
| Matplotlib | visualization |
| Seaborn | statistical visualization |
| tqdm | progress tracking |
Deep learning libraries such as PyTorch were intentionally not installed yet, since early experiments use classical recommender models.
Evaluation metrics were implemented before models to ensure proper comparison between algorithms.
Metrics implemented:
- Precision@K
- Recall@K
- NDCG@K
- Serendipity
- Unexpectedness
Traditional recommender systems optimize relevance only. However, our research focuses on unexpectedness and serendipity, so evaluation must reflect that.
Metric meanings:
| Metric | Purpose |
|---|---|
| Precision@K | fraction of recommended items that are relevant |
| Recall@K | fraction of relevant items retrieved |
| NDCG@K | ranking quality metric |
| Serendipity | relevant items that were not expected |
| Unexpectedness | distance from user's expected set |
These metrics will allow us to evaluate how well the system balances relevance vs novelty.
Before implementing the final architecture, we implemented baseline models.
This is essential for research validation.
Recommends globally popular items.
Logic:
count interactions per item
sort descending
recommend top K items
Purpose:
- simplest recommender baseline
- measures if the system is better than naive recommendations
This model is designed to optimize recommendation utility by combining relevance and unexpectedness.
Training objective:
score(user, positive_item) > score(user, negative_item)
Training samples consist of triplets:
(user, positive item, negative item)
PURS is the current primary baseline in this repository.
We implemented a temporal split:
Train = all interactions except last one
Test = last interaction per user
This simulates real-world recommendation scenarios where the model predicts the next item a user interacts with.
During early baseline implementation, several issues were identified.
Current implementation samples negative items randomly.
j = random item
However, this can produce:
j ∈ user's positive history
This creates incorrect training signals.
Correct approach:
sample j such that j ∉ user history
We are currently fixing this.
Current training loops over all user-item interactions.
This leads to extremely slow training for MovieLens 32M.
Example complexity:
users × interactions
This results in millions of updates.
This repository supports a research workflow for an unexpectedness-aware sequential recommender system, currently centered on MovieLens-1M and PURS as the active model path.
We fixed the core objective as balancing relevance and unexpectedness, because a pure relevance objective reinforces filter bubbles and hurts discovery.
We moved practical development to MovieLens-1M, because Kaggle free-tier iteration requires shorter experiment cycles and predictable memory/runtime.
We updated defaults to MovieLens-1M and PURS-only configuration, because reducing branching in early development lowers integration bugs and accelerates validation.
We implemented loaders that handle both ML-1M and ML-32M formats, because we need a stable migration path while still allowing scale-up later.
We added:
- k-core filtering (min user/item interactions)
- user/item contiguous reindexing
- chronological ordering
- temporal train/val/test splitting (last=test, second-last=val)
- log-scale time-gap buckets
We did this because sequential recommendation quality depends on correct temporal ordering and consistent ID space.
We save train/val/test splits, encoder, histories, and metadata to disk, because one-time preprocessing prevents repeated expensive work and guarantees reproducibility across model runs.
We created a standalone preprocessing script for ML-1M, because data preparation must be runnable independently from training for clean experiment management.
We removed BPR models/scripts and made runner/model exports PURS-only, because your immediate research track is unexpectedness-aware sequential modeling and BPR branches were adding maintenance overhead.
We renamed the training dataset abstraction from BPR-specific naming to pairwise naming, because the negative sampling pattern is reusable across methods and should not be tied to a removed baseline.
We updated Kaggle notebook framing and repository docs to PURS + ML-1M, because documentation must match executable code to avoid experiment drift.
What works now:
- ML-1M loading and environment-aware paths
- Sequential preprocessing and artifact generation
- PURS training/evaluation pipeline in runner
- Kaggle notebook flow aligned to PURS
What is intentionally not completed yet:
- SASRec baseline
- NOVA-inspired non-invasive gated attention model
- MeanShift-based unexpectedness lookup table for all user-item pairs
- Unified three-model comparison report
This section is the full roadmap to finish implementation of the project, from current code state to final three-model comparison.
-
Finalize immutable experiment rules: dataset split policy, sequence length, negative sampling, and target metrics. Why: if these rules change mid-way, model comparisons become invalid. Output: one frozen protocol document used by every run.
-
Freeze seeds and run settings for reproducibility. Why: we need to separate model improvements from randomness. Output: fixed seed policy and reproducible run template.
-
Finalize one-time preprocessing artifacts for ML-1M. Why: all models must train on exactly the same processed data. Output: train, val, test splits, encoder, sequence metadata, and history artifacts.
-
Add artifact versioning and manifest metadata. Why: we must always know which preprocessing settings produced each result. Output: artifact manifest with configuration hash, timestamp, and parameters.
-
Choose and lock the item embedding backbone (for distance computation). Why: unexpectedness distance is only meaningful if embedding space is consistent. Output: item embedding checkpoint and embedding export.
-
Run user-wise MeanShift on user history embeddings. Why: this defines each user's expected preference regions. Output: per-user cluster representation.
-
Precompute unexpectedness distances for user-item scoring. Why: on-the-fly clustering/distance is too slow for repeated training and evaluation. Output: lookup artifact optimized for dataloader access.
-
Build one shared sequential dataset contract for all models. Why: model comparison is fair only when inputs and sampling policy are identical. Output: common dataloader outputs: items, ratings, time gaps, unexpectedness, positives, negatives.
-
Enforce evaluation candidate protocol (1 positive + 99 negatives). Why: this is the paper-aligned ranking setup for all reported metrics. Output: single evaluation pipeline used by SASRec, PURS, and NOVA.
-
Implement SASRec model with the shared dataloader contract. Why: SASRec is the relevance-first sequential baseline needed to quantify gains from unexpectedness modeling. Output: trainable/evaluable SASRec module.
-
Validate SASRec metrics and stability. Why: a weak baseline creates misleading conclusions. Output: stable baseline runs with metric logs.
-
Match PURS scoring exactly to chosen utility definition. Why: baseline must reflect the intended unexpectedness-aware objective, not an approximate variant. Output: validated PURS implementation with clear equation-code mapping.
-
Verify PURS with the new shared pipeline. Why: all models must use identical data/eval mechanics. Output: PURS benchmark runs directly comparable to SASRec.
-
Build four-embedding gated fusion (item, rating, time gap, unexpectedness). Why: this is the core mechanism for multi-signal contextualization. Output: fused representation module.
-
Enforce non-invasive attention rule: Q/K from fused representation, V from pure item embedding. Why: this preserves item semantic integrity while still leveraging side information. Output: custom attention layer with architectural constraint guaranteed in code.
-
Integrate NOVA model into the shared trainer. Why: training logic must remain identical across models for fair comparison. Output: trainable/evaluable NOVA path in experiment runner.
-
Unify trainer, logger, checkpointing, and config handling for all three models. Why: mixed training code paths increase bugs and reduce reproducibility. Output: single experiment framework with model-specific hooks.
-
Add resume/restart and failure-safe checkpoints. Why: Kaggle sessions are time-limited and can terminate unexpectedly. Output: resilient run pipeline.
-
Run constrained hyperparameter search per model. Why: each architecture needs reasonable tuning before comparison. Output: best config per model under equal budget.
-
Run ablations for NOVA components. Why: we need to prove which design components actually contribute. Output: ablation table for gate, rating, time-gap, unexpectedness, and non-invasive attention constraint.
-
Repeat final runs with multiple seeds. Why: single-run improvements are not scientifically reliable. Output: mean and variance for each reported metric.
-
Perform targeted failure analysis (user/item segments). Why: this explains where the model helps and where it fails. Output: qualitative and quantitative error analysis section.
-
Generate final comparison tables for SASRec, PURS, NOVA. Why: this is the core evidence for the research claim. Output: publication-ready model comparison results.
-
Generate plots: training curves, metric trade-offs, and ablations. Why: visual evidence improves interpretability and paper clarity. Output: figure set aligned with final metrics.
-
Export reproducible artifacts and run cards. Why: every reported number must be traceable to a config and checkpoint. Output: experiment cards, artifact index, and final result bundle.
Implementation is complete when all conditions below are true:
- SASRec, PURS, and NOVA train and evaluate on the same pipeline.
- NOVA respects the non-invasive Q/K/V constraint in verified code.
- NDCG@10, Recall@10, and Unexpectedness@10 are reported for all models.
- Ablation and multi-seed stability results are available.
- Paper tables and figures are reproducible from saved artifacts.
- Freeze experiment contract and preprocessing manifest.
- Finalize unexpectedness lookup generation.
- Implement and validate SASRec baseline.
- Revalidate PURS under shared pipeline.
- Implement NOVA non-invasive model.
- Run benchmark, ablations, and multi-seed validation.
- Export final results for paper integration.
This order is intentional: data and protocol first, model implementation second, scientific validation and reporting last.