Chapter 1 · Section 1.7 · 65 min

Embeddings are everywhere

Everything so far has been about words. But look back at what we actually needed.

  1. A set of discrete things with IDs (a vocabulary).
  2. A source of co-occurrence or interaction signal ("these appear together", "this user liked that item").
  3. An objective that rewards giving related things similar vectors.

None of that is specific to language. If it can be indexed, it can be embedded. And in every case, the objective defines what "similar" means. This section is a tour of the most important non-word embeddings, and it previews where the rest of the book takes the idea.

domainthe "words"the "contexts"typical objective
words (§1.5)tokensnearby tokensskip-gram with negative sampling
recommendersusers, itemswho interacted with whatmatrix factorization, two-tower contrastive
tabular MLcategory values (store ID, zip code)the prediction targetsupervised, end to end
graphsnodesnodes on random walksskip-gram on walks (DeepWalk, node2vec)
shopping and musicproducts, songsitems in the same basket or sessionSGNS (item2vec)
sentences, images, audiowhole inputsother views or paired modalitiescontrastive, as in InfoNCE, SimCLR, CLIP, and wav2vec

Recommender systems put users and items in one space

The Netflix Prize (2006 to 2009) made matrix factorization the workhorse of recommendation (Koren, Bell & Volinsky, 2009). Give every user uu a vector pu∈Rd\mathbf{p}_u\in\mathbb{R}^d and every item ii a vector qi∈Rd\mathbf{q}_i\in\mathbb{R}^d, and predict a rating as

r^ui=μ+bu+bi+pu⊤qi,\hat r_{ui} = \mu + b_u + b_i + \mathbf{p}_u^\top\mathbf{q}_i ,

where μ\mu is the global average rating, bub_u captures "this user rates everything high", and bib_i captures "this item is generally popular". The dot product captures the personal match, whether this user's tastes align with this item's traits. A worked example. If the average rating is μ=3.6\mu = 3.6, this user rates 0.30.3 above average, this film is rated 0.50.5 below average, and their vectors have dot product 0.40.4, the prediction is 3.6+0.3−0.5+0.4=3.83.6 + 0.3 - 0.5 + 0.4 = 3.8 stars.

Train on the observed ratings only (a set Ω\Omega of user and item pairs), with L2 regularization of strength λ\lambda,

L=∑(u,i)∈Ω(rui−r^ui)2  +  λ(∑u∥pu∥2+∑i∥qi∥2).\mathcal{L} = \sum_{(u,i)\in\Omega} \big(r_{ui} - \hat r_{ui}\big)^2 \;+\; \lambda\Big(\sum_u\lVert\mathbf p_u\rVert^2 + \sum_i\lVert\mathbf q_i\rVert^2\Big).

In plain words, fit the ratings we have, with user and item vectors whose dot products explain what's left after the biases, while keeping the vectors small. Training only on observed entries is the crucial difference from the SVD of §1.4. A missing rating is unknown, not zero, so we can't just decompose a matrix with holes filled in.

Nobody tells the model about genres, but the item vectors end up encoding them. Two horror films get similar q\mathbf{q}'s because the same users like both. It's the distributional hypothesis again. Items are known by the users they keep.

Interview

Matrix factorization, loss and gradients, vectorized

Implement mf_loss_grad(P, Q, bu, bi, mu, u, i, r, lam), where u, i, r are arrays of observed (user, item, rating) triples. Return (loss, dP, dQ, dbu, dbi) for the loss above. Regularize only P\mathbf P and Q\mathbf Q, over all their rows. Users and items repeat across triples, so gradients must accumulate (your old friend np.add.at).

Hidden tests, hints, and AI help, from $1.99 a month.

Let's watch genres emerge. We simulate users with hidden taste for three genres, generate ratings, hide 70% of them, and fit the factorization with full-batch gradient descent.

Python · cell 1

Alternating least squares

Gradient descent is not the only way to fit this. Look at the loss with the item vectors held fixed (drop the biases for clarity). Each user's vector then appears in a separate ridge regression, with the items that user rated as the "features",

pu=(∑i∈Ωuqiqi⊤+λI)−1∑i∈Ωurui qi,\mathbf{p}_u = \Big(\sum_{i \in \Omega_u}\mathbf{q}_i\mathbf{q}_i^\top + \lambda\mathbf{I}\Big)^{-1}\sum_{i\in\Omega_u} r_{ui}\,\mathbf{q}_i ,

where Ωu\Omega_u is the set of items user uu rated. In plain words, with the items frozen, the best vector for each user has a closed form, and all users can be solved independently and in parallel. Then freeze the users and solve for every item the same way, and alternate. Each half-step can only lower the loss, so alternating least squares (ALS) converges steadily without a learning rate (Hu, Koren & Volinsky, 2008, extended it to implicit feedback, and Spark's recommender uses it). Each solve is a d×dd\times d system, so one sweep costs O(∣Ω∣d2+(nusers+nitems)d3)O(\lvert\Omega\rvert d^2 + (n_{\text{users}} + n_{\text{items}})d^3).

Interview

One ALS half-step for all users at once

Implement als_user_step(Q, u, i, r, n_users, lam) that returns the (n_users, d) matrix of user vectors solving the closed form above (no biases). Users with no ratings must get the zero vector. Build all the d×dd\times d systems with one scatter-add and solve them with one batched np.linalg.solve, with no Python loop over users.

Hidden tests, hints, and AI help, from $1.99 a month.

Implicit feedback, two towers, and retrieval

Most real data isn't star ratings. It's implicit, such as clicks, plays, and purchases. There are no explicit negatives, so systems use the trick you know from §1.5. They sample negatives and train "interacted versus random". item2vec (Barkan & Koenigstein, 2016) literally runs SGNS on shopping baskets or listening sessions, treating items that appear together as context.

Modern systems generalize matrix factorization into two-tower models. One neural network (the user or query tower) maps everything known about a user (history, location, time) to a vector. Another (the item tower) maps item features to a vector. The score is their dot product. YouTube's candidate generator (Covington, Adams & Sargin, 2016) framed retrieval as a huge softmax over videos given a user vector, and a later YouTube system (Yi et al., 2019) trained both towers with an in-batch softmax. Because item vectors don't depend on the user, you can precompute them for all items and answer "best items for this user" with a fast maximum inner-product search over millions of vectors (the norm-augmentation trick of §1.3, and the indexes of §7.4). Feature-based towers also soften the cold-start problem, because a brand-new item with no interactions still gets a vector from its features.

In-batch negatives, and the popularity bias they create

The cheapest negatives are already in memory. Take a batch of BB (user, item) pairs, embed the users as rows of U∈RB×d\mathbf{U} \in \mathbb{R}^{B\times d} and the items as rows of I∈RB×d\mathbf{I} \in \mathbb{R}^{B\times d}, and treat every other item in the batch as a negative for each user. The logits are UI⊤/τ\mathbf{U}\mathbf{I}^\top/\tau for a temperature τ\tau, and the loss is cross-entropy with the diagonal as the right answers, the same structure as the CLIP loss in §1.8.

There's a catch. Items enter batches in proportion to their popularity, so a popular item is used as a negative far more often than a niche one, and the model learns to push popular items down. Yi et al. (2019) correct for this the same way NCE corrects negative sampling (§1.5). Subtract the log of each item's sampling probability qjq_j from its logit,

sij=ui⊤vjτ−log⁡qj,s_{ij} = \frac{\mathbf{u}_i^\top\mathbf{v}_j}{\tau} - \log q_j ,

where ui\mathbf{u}_i and vj\mathbf{v}_j are rows ii and jj of U\mathbf{U} and I\mathbf{I}. In plain words, an item that shows up as a negative ten times more often gets its logit lowered by log⁡10≈2.3\log 10 \approx 2.3 in training, so the model doesn't have to distort its vector to compensate. At serving time the correction is dropped and the raw dot product is used. A second practical fix is to mask out "negatives" that are actually the same item as the positive, which happens often for popular items in large batches.

Interview

In-batch softmax with logQ correction and duplicate masking

Implement inbatch_softmax_loss(U, I, item_ids, item_prob, tau=0.1, correct=True). U and I are (B, d) user and item embeddings for the batch's positive pairs, item_ids the (B,) item IDs, and item_prob an array giving each item's sampling probability (index it with item_ids). Return the mean cross-entropy over rows with the diagonal as targets, using the logQ correction when correct=True, and setting the logit to −∞-\infty wherever column j≠ij \neq i holds the same item as row ii's positive. Use a stable log-sum-exp.

Hidden tests, hints, and AI help, from $1.99 a month.

Categorical features in tabular models

Tabular datasets are full of high-cardinality categories, such as store ID, product SKU, zip code, and user agent. One-hot encoding them hits all the problems of §1.1. Entity embeddings (Guo & Berkhahn, 2016) give each category value a learned vector, trained end to end with the rest of the network on the actual prediction target. In the Rossmann store-sales Kaggle competition, the learned embeddings of the German states the stores were in, visualized in 2-D, roughly recovered the map of Germany, purely from sales patterns.

The hashing trick, and how often it collides

When a feature has millions of values, or values unseen at training time (new users, new URLs), the hashing trick (Weinberger et al., 2009) maps each value to one of BB buckets with a hash function and embeds the buckets. (In this subsection BB counts buckets, not batch items.) Memory is fixed at B×dB\times d. The price is collisions, where unrelated values share a vector.

How often? Hash nn values uniformly into BB buckets. A given bucket stays empty with probability (1−1/B)n≈e−n/B(1 - 1/B)^n \approx e^{-n/B}, and a given value shares its bucket with at least one other value with probability

1−(1−1B)n−1≈1−e−n/B.1 - \Big(1 - \frac1B\Big)^{n-1} \approx 1 - e^{-n/B} .

In plain words, the collision rate depends only on the load n/Bn/B. With as many buckets as values (n=B=106n = B = 10^6), 63% of values share their vector with someone and 37% of the buckets sit empty. Even with ten times more buckets than values, 9.5% of values collide. Collisions are not rare events. They are a constant tax that only shrinks linearly with memory.

Using two or more independent hashes per value and summing (or concatenating) their vectors helps a lot, because two values now share their entire representation only if they collide in every hash. With two hashes into 10610^6 buckets each, a given value fully collides with another with probability about n/B2=10−6n/B^2 = 10^{-6}. This is the idea behind hash embeddings (Svenstrup, Hansen & Winther, 2017) and the compositional embeddings used in large recommendation models (Shi et al., 2020).

Warm-up

A stable hash for embedding buckets

Implement bucket(value, n_buckets) mapping a string to an integer in [0, n_buckets) using 64-bit FNV-1a over its UTF-8 bytes. Start with h = 0xcbf29ce484222325, and for each byte do h ^= byte; h = (h * 0x100000001b3) mod 2**64. Return h % n_buckets.

Why not Python's built-in hash()? Try hash("cat") in two different Python processes. String hashing is randomized per process (since Python 3.3, for security), so a model trained with hash() buckets would look up the wrong rows after a restart. It's a real, silent production bug.

Hidden tests, hints, and AI help, from $1.99 a month.
Interview

Measure hashing collisions, and what a second hash buys

Implement two functions.

  • expected_collision_rate(n, B) returns the probability from the formula above that a given value shares its bucket with at least one of the other n−1n - 1 values.
  • collision_rate(buckets) takes an int array of shape (n,) (one hash per value) or (n, h) (h hashes per value) and returns the measured fraction of values whose bucket, or whole tuple of buckets, is shared with at least one other value. No Python loops over values.
Hidden tests, hints, and AI help, from $1.99 a month.

Graphs, where random walks are sentences

How do you embed the nodes of a graph (people in a social network, papers in a citation graph, proteins in an interaction network)? DeepWalk (Perozzi, Al-Rfou & Skiena, 2014) had a delightfully simple answer. Generate random walks and treat them as sentences. Then run skip-gram. Nodes that appear close together on walks, meaning nodes in the same neighborhood or community, get similar vectors.

node2vec (Grover & Leskovec, 2016) biases the walks with two parameters. Suppose the walk just moved from node ss to node vv, and xx is a neighbor of vv. The next step goes to xx with unnormalized weight 1/p1/p if x=sx = s (going back), 11 if xx is also a neighbor of ss (staying close), and 1/q1/q otherwise (moving outward). With p=4p = 4 and q=0.5q = 0.5, a node with one way back, one shared neighbor, and two outward neighbors gives weights (0.25,1,2,2)(0.25, 1, 2, 2), so the walk moves outward with probability 4/5.25≈0.764/5.25 \approx 0.76. Small qq gives depth-first walks that explore communities, and large qq gives breadth-first walks that stay local and capture structural roles, like "hub". Qiu et al. (2018) later showed that DeepWalk, like SGNS (§1.5), implicitly factorizes a matrix, in this case a log-transformed sum of powers of the random-walk transition matrix.

Here is DeepWalk end to end, reusing the SGNS step from §1.5 unchanged.

Python · cell 2

Same algorithm, different "corpus". The model never saw the community labels.

From tables to encoders

Every embedding in this section lived in a table, one row per ID. The most powerful embeddings today aren't lookup tables at all. They're neural networks that compute a vector from raw input, whether that's a sentence, a photograph, or a recording. The geometry from §1.3 carries over completely, and so do the visualization caveats from §1.6. Here's a real sentence-embedding model running in your browser, as a preview of §1.8.

Can whole sentences be points in space too?
Each line is turned into one 384-d vector by a real model in your browser. The heat map shows the cosine similarity between every pair. Edit the sentences and press Embed.
Loading interactive figure…

In PyTorch

🔥 In PyTorchfor a machine with PyTorch

That loss, in-batch negatives with a cross-entropy over a similarity matrix, is the same InfoNCE loss behind CLIP and modern sentence embedders. You'll derive it properly in §7.2.

Read more

Watch next

🎬 How does Netflix recommend movies? Matrix Factorization · Luis Serrano AcademyMatrix factorization for recommendations, explained from scratch with small examples.
🎬 The Math Behind Recommender Systems · Art of the ProblemA short visual story of latent factors in recommendation.