← Gym/Mini Recommender
00:00/ 60 min

🧭 Do not search for the first 15 minutes. When stuck: re-read the requirements → define I/O → choose the data structure → trace a small example by hand → write code.

Implement a Recommender that trains a latent-factor recommender on rating data with SGD, without sklearn. Where the other Tier 4 problems implement a single metric or tree, this one builds an entire recommender pipeline end to end: baseline → latent factors → early stopping → serving.

Shared spec

Ratings arrive as a list of (user, item, rating) triples, where rating is a float. The same (user, item) pair may appear several times — do not deduplicate. Each appearance is an independent training step.

python
model = Recommender(epochs=20, lr=0.05, reg=0.02, k=0, seed=0)
model.fit(ratings)
model.predict(user, item)   # float

Prediction formula

r^ui=μ+bu+bi+pu⋅qi\hat{r}_{ui} = \mu + b_u + b_i + p_u \cdot q_i

  • μ\mu — the global mean rating. Computed once at the start of fit and never changed. 0.0 when the training data is empty.
  • bu,bib_u, b_i — user and item biases. A user or item never seen is 0.0.
  • pu⋅qip_u \cdot q_i — the latent-factor dot product. When k == 0 this term is always 0 (L1). The latent vectors of a user or item never seen are also treated as zero (so the dot product is 0).

Training — stochastic gradient descent

Repeat epochs times. Each epoch walks ratings once in its original order (no shuffling). For each (u, i, r):

text
pred = mu + b_u + b_i + p_u·q_i     # predict with the pre-update values
err  = r - pred

b_u  ← b_u + lr·(err - reg·b_u)
b_i  ← b_i + lr·(err - reg·b_i)
p_u  ← p_u + lr·(err·q_i - reg·p_u)   # use the OLD q_i
q_i  ← q_i + lr·(err·p_u - reg·q_i)   # use the OLD p_u — simultaneous update

⚠️ pup_u and qiq_i update simultaneously. Do not feed the freshly updated pup_u into the qiq_i update — both updates must read the values as they stood at the start of this step.

Latent-factor initialization (only when k > 0, and reproducible from seed)

python
rng = np.random.default_rng(seed)

Create rng once. Walk ratings and, for each user as it first appears, call rng.normal(0, 0.1, size=k) in order to build pup_u; then walk ratings again and, for each item as it first appears, build qiq_i with the same call (all users first, then all items — swap the order and the same seed yields different vectors). This initialization happens once, before the training loop starts.

Level structure

LevelAdded requirement
1fit and predict — baseline (only μ+bu+bi\mu + b_u + b_i; k is ignored)
2Latent-factor SGD — add the pu⋅qip_u \cdot q_i term when k > 0
3val_ratings and patience — validation early stopping, restoring the best point
4recommend — top-k serving, excluding seen items, cold start

Each level must preserve the behavior of the previous ones. Calling fit without val_ratings must behave exactly as in L1 and L2 even after L3.

Level 1 · Baseline — biases only

At this level, ignore k and predict with only μ+bu+bi\mu + b_u + b_i.

python
Recommender(epochs: int, lr: float, reg: float, k: int, seed: int)
fit(ratings: list[tuple]) -> self
predict(user, item) -> float
  • Follow the shared spec's training rules (SGD, original order, no deduplication) exactly. The pu⋅qip_u \cdot q_i term is simply always 0 at this level.
  • Calling predict without calling fit returns 0.0 for everything.
  • An empty ratings list works without raising, and μ\mu is 0.0.
  • fit returns self.
  • Re-fitting the same model discards the previous state completely and starts over (no biases survive).