DEV Community

Cover image for Guilded-Guild: recommending the next item with SASRec in PyTorch
Divyakush Punjabi
Divyakush Punjabi

Posted on

Guilded-Guild: recommending the next item with SASRec in PyTorch

Recommending the next thing, not a similar thing

Classical recommenders treat your history as a bag of items: you liked these ten movies, here are ten more that people with similar bags also liked. That works, but it throws away the single most informative signal you have — order. What you watched last says more about what you want next than what you watched a year ago.

Guilded-Guild is a recommendation engine built around that idea. It models the sequence of a user's interactions with a self-attention transformer, and serves the result end to end through a Flask API and a React interface. It covers two domains: movies, trained on MovieLens 20M, and music, pulled live from Spotify and iTunes catalogs.

Why SASRec instead of matrix factorization

The model is SASRec — Self-Attentive Sequential Recommendation — implemented from scratch in PyTorch, with a Neural Collaborative Filtering (NCF) model kept alongside as a baseline for honest comparison.

Matrix factorization has two well-known weaknesses that SASRec directly targets:

  • Cold-start. A brand-new interaction sequence is hard to place in a factorized user/item space.
  • No sense of sequence. Factorization sees an unordered set; it can't learn that "watched a trilogy in order, last night" implies something specific about tonight.

Self-attention fixes the second problem by construction: for each position in a user's history, the model attends over all previous items and learns which past interactions matter for predicting the next one. It's the same mechanism that powers language models, pointed at behavior sequences instead of tokens.

Inside the model

The core lives in flask_app/sasrec_model.py, and if you've built a transformer block before, it'll look familiar — with a few choices that matter for recommendation specifically:

  • Item + positional embeddings with padding-aware masking. Variable-length histories get padded to a fixed length, and the mask ensures those padding slots never leak into the attention computation.
  • Multi-head self-attention blocks using pre-LayerNorm residual connections — normalizing before the sublayer rather than after, which trains more stably at depth.
  • Point-wise feed-forward networks implemented as Conv1d with kernel size 1, plus dropout for regularization.

Training is handled by train_sasrec.py; evaluation by evaluate_sasrec.py. The model comes out around 25M parameters.

The numbers, and how to reproduce them

Evaluated on a held-out temporal split of MovieLens 20M using the leave-last-out protocol — hold back each user's most recent interaction and see if the model ranks it correctly:

Metric Score
AUC-ROC 98.47%
Hit-Rate @ 10 98.23%
Parameters ~25M

The reason I trust these numbers, and the reason I'll put them in writing, is that the split is temporal rather than random. Random splits leak the future into the training set and inflate sequential-model scores; a temporal split doesn't. And they're reproducible — evaluate_sasrec.py runs the whole evaluation on the provided data split.

Serving it like a product, not a notebook

A model in a Jupyter notebook isn't a recommender; it's a research artifact. Guilded-Guild is wired for use:

  • A Flask REST API exposes /api/recommend/movies and /api/recommend/music, with CORS configured for the SPA.
  • The frontend is React 18 + TypeScript + Vite, using shadcn/ui, Radix, and Tailwind — the same stack I'd reach for on any production front end.
  • An enrichment layer turns raw item IDs into something a human wants to look at: TMDB posters and trailers, with a 100% fetch-success rate on the catalog, plus Spotify metadata for the music side.

That enrichment step is easy to underrate. A recommender that returns movie_id: 4993 is correct and useless; one that returns a poster, a title, and a trailer is a product. The dataset itself is substantial — MovieLens 20M is 27,278 movies, 20M+ ratings, and 138,493 users.

What I learned building it

  • Order is a feature, and most systems ignore it. Switching from a set-based to a sequence-based model is the single biggest lever if your domain has any temporal structure — and almost all of them do.
  • Keep an honest baseline. Having NCF next to SASRec meant every claim about the transformer was measured against something real, not against nothing.
  • Evaluate the way the world works. Temporal splits are less flattering than random ones, and that's exactly why they're the ones worth reporting.

The full model code, training and evaluation scripts, API, and React frontend are in the repository, including the exact commands to reproduce the metrics above.


www.divyakush.com · GitHub · LinkedIn

Top comments (0)