Score every user against every item

Medium12 min11 hidden tests

A shopping app's recommender keeps an embedding for every user and every item, plus one bias per item for how popular it is overall. The score of a user for an item is the dot product of their embeddings plus that item's bias. Write score_items(users, items, item_bias) that scores a whole batch with one matrix product. Mismatched shapes are a common silent bug here, so check them first and return None when they do not fit.

Drag the users

scores = users @ items.T + item_bias
UserItem
item 0 +0.5item 1 +0item 2 −1
user 03.501
user 12.523

Shapes: users (2, 2) @ items.T (2, 3) + bias (3,) gives (2, 3). User 0 and item 0: 1 × 3 + 0 × 1 = 3, plus bias 0.5 = 3.5. Point a user toward an item and their score climbs.

User 0, item 0 score 3.5.

Examples

Two users and three items give a (2, 3) table

score_items([[1, 0], [0, 2]], [[3, 1], [0, 1], [2, 2]], [0.5, 0.0, -1.0])

returns [[3.5, 0, 1], [2.5, 2, 3]]

Users have 3 numbers and items have 2, so the shapes do not fit

score_items([[1, 2, 3]], [[1, 1], [2, 2]], [0.0, 0.0])

returns None

Requirements

  • users has shape (u, d), items has shape (m, e), and item_bias is a list or 1-D array of length k.
  • When d equals e and k equals m, return a float array of shape (u, m). Entry [i, j] is user i's embedding dotted with item j's, plus item_bias[j].
  • Otherwise return None. A bias of length 1 is a mismatch whenever m is larger than 1, even though NumPy would broadcast it without complaint.
  • Compute the scores with one matrix product and broadcasting, without Python loops.
The idea behind it

Many recommenders score with dot products. Matrix factorization and two-tower models both learn a vector for each user and each item, and a user's predicted interest in an item is their dot product. A per-item bias adds how popular the item is overall.

Scoring one pair at a time in Python is slow. Stack the users into a (u, d) matrix and the items into an (m, d) matrix, and one product, U @ V.T, gives the whole (u, m) table. Matrix products are what CPUs and GPUs do fastest.

Broadcasting then adds the (m,) bias to every row. That same rule is a trap. NumPy stretches any axis of length 1, so a bias of length 1 silently gets added to every item, and a model wired up with the wrong tensor still runs and returns numbers. Checking shapes at the boundary turns that kind of bug into a clear failure. Production code would raise a ValueError; here the function returns None so the tests can check it.

pythonCtrl+Enter runs
import numpy as np

def score_items(users, items, item_bias):
"""Score every user against every item.

users: 2-D list or array of shape (u, d)
items: 2-D list or array of shape (m, d)
item_bias: list or 1-D array of length m
Returns a float array of shape (u, m) with
scores[i, j] = users[i] . items[j] + item_bias[j],
or None when the shapes do not fit together.
"""
raise NotImplementedError

Stuck? Reveal one rung at a time

Worked solution

Pro includes the solution and walkthrough for every problem, plus the hands-on labs. The first problem of each topic is free.