🧭 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.
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.
model = Recommender(epochs=20, lr=0.05, reg=0.02, k=0, seed=0)
model.fit(ratings)
model.predict(user, item) # float
fit and never
changed. 0.0 when the training data is empty.0.0.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).Repeat epochs times. Each epoch walks ratings once in its original order (no
shuffling). For each (u, i, r):
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
⚠️ and update simultaneously. Do not feed the freshly updated into the update — both updates must read the values as they stood at the start of this step.
k > 0, and reproducible from seed)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 ; then walk ratings again and,
for each item as it first appears, build 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 | Added requirement |
|---|---|
| 1 | fit and predict — baseline (only ; k is ignored) |
| 2 | Latent-factor SGD — add the term when k > 0 |
| 3 | val_ratings and patience — validation early stopping, restoring the best point |
| 4 | recommend — 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.
At this level, ignore k and predict with only .
Recommender(epochs: int, lr: float, reg: float, k: int, seed: int)
fit(ratings: list[tuple]) -> self
predict(user, item) -> float
0 at this level.predict without calling fit returns 0.0 for everything.ratings list works without raising, and is 0.0.fit returns self.