Train a recommender
Rate a few shows. The model learns a vector for every show and for you from everyone's ratings, then ranks the shows you have not rated.
- Stand-up special
- Baking contest
- Office sitcom
- Nature documentary
- True crime series
- War documentary
- Courtroom drama
- Heist thriller
- Monster horror
- Space war epic
- Animated musical
- Fairy-tale adventure
Challenges
0 of 2 complete
- With a single rating, make your top pick a show from the bottom half of the list a brand-new viewer gets. (not yet)
- Set the penalty to 0, rate exactly two shows, and make the model predict more than 7 stars (or less than -1) for a show you have not rated. (not yet)
Try this
- Before rating anything, note your top pick: with no ratings you get the same list as every new viewer.
- Rate one show 5 stars and watch your arrow appear. Which shows rose, and where are they on the map?
- Press Train again from a new random start: the map turns or flips, your predictions stay put.
- Set the penalty to 0 and rate two shows that sit close together on the map, one 5 stars and one 1 star.
A feed that learns from itself
Each day, 200 new viewers are each shown one show they have not seen, and rate it. Every night the model retrains on all the ratings it has.
Challenges
0 of 2 complete
- Get the new release recommended by the model's own choice, not a random pick, to at least 20 viewers over the three days. (not yet)
- Explore so much that viewers rate what they are shown lower, on average, than with exploring off. (not yet)
Try this
- With exploring off, check the new release’s row, then press New luck a few times.
- Choose Day 1 to see what every brand-new viewer is shown.
- Raise exploring step by step and watch the new release and the average stars.
Matrix factorization by gradient descent in NumPyPython with NumPy
import numpy as np
shows = ["Sitcom", "Stand-up", "Baking", "Horror", "Space epic"]
# Five viewers (rows) rate the shows (columns), 1 to 5 stars; 0 = not rated.
# Viewer 4 is brand new and has rated nothing.
R = np.array([[5, 4, 0, 1, 0],
[4, 0, 5, 1, 1],
[1, 1, 0, 5, 4],
[0, 2, 1, 4, 0],
[0, 0, 0, 0, 0]], dtype=float)
known = R > 0
mu = R[known].mean() # the average rating
rng = np.random.default_rng(0)
k, lam, lr = 2, 0.5, 0.02 # factors, penalty, step size
P = rng.normal(0, 0.1, (5, k)) # one taste vector per viewer
Q = rng.normal(0, 0.1, (5, k)) # one vector per show
bu, bi = np.zeros(5), np.zeros(5) # viewer and show offsets
for step in range(3000):
pred = mu + bu[:, None] + bi[None, :] + P @ Q.T
err = np.where(known, R - pred, 0.0) # only known ratings count
# One gradient step on squared error + lam * (every parameter squared).
P, Q = (P - lr * (-2 * err @ Q + 2 * lam * P),
Q - lr * (-2 * err.T @ P + 2 * lam * Q))
bu -= lr * (-2 * err.sum(axis=1) + 2 * lam * bu)
bi -= lr * (-2 * err.sum(axis=0) + 2 * lam * bi)
pred = mu + bu[:, None] + bi[None, :] + P @ Q.T
rmse = np.sqrt(np.mean((R - pred)[known] ** 2))
print(f"error on known ratings: {rmse:.2f} stars")
for v, j in [(0, 2), (0, 4), (3, 0), (3, 4)]:
print(f"viewer {v}, {shows[j]}: predicted {pred[v, j]:.1f}")
print("new viewer's taste vector is zero:", bool(np.allclose(P[4], 0)))
print("so they get the crowd's order:", [shows[j] for j in np.argsort(-pred[4])])error on known ratings: 0.20 stars
viewer 0, Baking: predicted 4.8
viewer 0, Space epic: predicted 1.6
viewer 3, Sitcom: predicted 1.9
viewer 3, Space epic: predicted 3.3
new viewer's taste vector is zero: True
so they get the crowd's order: ['Sitcom', 'Horror', 'Baking', 'Stand-up', 'Space epic']The ratings in both demos are synthetic. Each show has two hidden traits (light or dark, real or made up) and a quality; each viewer has a taste for each trait and a generosity. A rating is 3.4 stars plus quality, plus generosity, plus 1.4 times the dot product of taste and traits, plus noise with a standard deviation of 0.4 stars, rounded to whole stars from 1 to 5. The model sees only the ratings.