You shall know a word by the company it keeps
We want vectors where similar words are close together. But "similar" according to whom? Nobody hands us a similarity score for every pair of the 100,000 words in English. That would be five billion pairs. We need a signal that comes for free from raw text.
That signal is one of the most productive ideas in all of AI, and it's older than computers doing language. This section turns it into vectors in three steps (count, reweight, compress), with no neural network anywhere, and the result is surprisingly good.
The distributional hypothesis
Read these sentences about a word you've never seen.
A bottle of zorbleflax is on the table. Everybody likes zorbleflax. Too much zorbleflax makes you dizzy. We make zorbleflax out of fermented corn.
You already know roughly what zorbleflax is, some kind of alcoholic drink. Nobody defined it. You inferred it from the words around it. (This is a remake of a classic example that Dekang Lin used in 1998, adapted from the linguist Eugene Nida, built around tezgüino, a real Mexican corn beer. Jurafsky and Martin's textbook does the same with ongchoi, a leafy vegetable.)
Linguists turned that observation into a principle in the 1950s.
"You shall know a word by the company it keeps." (J. R. Firth, 1957)
The distributional hypothesis (Harris, 1954) says that words that occur in similar contexts tend to have similar meanings. "Cat" and "kitten" both appear near purr, fur, pet, meow. "Carburetor" appears near engine, fuel, repair. So if we describe each word by the contexts it appears in, similar words automatically get similar descriptions.
Step 1. Count co-occurrences
Pick a window half-width (the number of words on each side that count as "nearby"). Slide the window over the corpus. Every time word appears within positions of another word, increment the counter for that pair. The result is the word–context co-occurrence matrix , where is the vocabulary size. From here on we follow the book's notation and write for a context word in a pair . The window size won't appear in another formula.
In plain words, row of counts, for every other word, how often it showed up near . That row is already a vector representation of word , its context profile. Let's build one from a tiny corpus of 93 tokens and 29 distinct words, split on spaces and numbered exactly as in §1.1.
Notice the problem right away. The biggest counts for cat are for a, ., and the. Those words co-occur with everything, so they tell us almost nothing about meaning. Raw counts reward frequency, not association.
Two design choices hide in this step. The window can be symmetric (as here) or only look left, and it can weight neighbors equally or by distance. GloVe, for instance, adds for a neighbor positions away, so adjacent words count most. The window size matters more, as we'll see at the end of the section.
Build a co-occurrence matrix
Write cooccurrence(tokens, vocab, window) returning a (V, V) count matrix, where vocab is a list of words. Tokens not in vocab are skipped, but they still take up a position (they count toward distance). Count symmetrically, so if b is within window positions of a, increment C[a, b] (and, from b's point of view, C[b, a]). Avoid looping over tokens in pure Python. Loop over the offsets and use vectorized NumPy for each.
Step 2. Reweight with PMI
We want to ask not "how often do and co-occur?" but "do they co-occur more than we'd expect by chance?". If and were independent, the probability of seeing them together would be . Pointwise mutual information compares what we observe to that baseline,
In plain words, PMI is the log of how many times more often the two words co-occur than they would if they were unrelated. PMI = 0 means "exactly chance", means "twice as often as chance", and negative means "less than chance".
A worked example by hand
Take two words and two contexts with counts
The marginals are , , , . Now each cell.
- . They co-occur twice as often as chance.
- . They avoid each other.
- .
- . They never co-occurred.
PMI has a ceiling that explains its main weakness. Since , we get . The maximum is reached when one word only ever appears with the other, and it is largest for rare words. A word seen twice, both times next to , can out-score a strong association between two common words.
PMI is also the per-pair piece of a familiar quantity. The mutual information between the word and context variables is the average PMI under the joint distribution, (§0.5 defines it). That is where the name comes from.
Two practical fixes are standard.
- PPMI (positive PMI). Negative PMI values ("these words avoid each other") are unreliable unless the corpus is enormous, because estimating that something rarely co-occurs needs a lot of data. And gives . So we clip, . In the worked example, the PPMI matrix is .
- Context distribution smoothing. To tame the rare-word bias, Levy, Goldberg and Dagan (2015) raise the context counts to a power before normalizing,
In plain words, rare contexts get a bigger share of probability than their raw count gives them, so a co-occurrence with a rare context is less surprising and earns a smaller PMI. A worked example shows how strong this is. Three words each co-occur 50 times with a common context, and word 1 also co-occurs twice with a rare context. With the rare context has and . With , rises to about 0.038 and the PMI drops to 0.02. Keep that in mind. It shows up again, for a closely related reason, in word2vec's negative sampling (§1.5).
PPMI with context smoothing
Implement ppmi(C, alpha=1.0) for a count matrix C (rows are words, columns are contexts). Use and as defined above, and the smoothed . Return , with 0 wherever (no -inf, no NaN, no warnings).
Here is what PPMI does to the rows we printed before.
Now cat's strongest contexts are chases and milk, and car's are work, drive, and fuel. The function words haven't vanished (our corpus is tiny, so even a gets a small positive score), but they no longer dominate.
Documents as contexts, and tf-idf
A window is not the only choice of context. Take the whole document instead. Count how often word appears in document , its term frequency , for each of documents. Row of this term–document matrix describes a word by the documents it appears in, and column describes a document by its words, which is how search engines represented documents for decades. The same problem appears at once, since the is in nearly every document and says nothing about any of them. The standard reweighting is tf-idf,
where the document frequency is the number of documents that contain , and the log factor is the inverse document frequency (idf). In plain words, a word counts in proportion to how often it appears in this document, discounted by how common it is across all documents. A word found in every document gets . The instinct is the one behind PMI, since is the surprise of finding in a random document, and common words carry little surprise.
Treat each of our 16 sentences as a document and check it.
the appears in 11 of the 16 documents, so its idf is , while ball appears in one and gets , about seven times more weight per occurrence. Latent Semantic Analysis, which we meet in the next step, factorizes a term–document matrix like this one, usually after a weighting like tf-idf. Documents behave like a very wide window, so their neighbors are topical (fuel near car) rather than substitutable, the trade-off we'll measure at the end of the section. §7.5 turns tf-idf into a ranking function for search and derives its stronger successor, BM25.
Step 3. Compress with SVD
The PPMI matrix is , which makes it huge, sparse, and noisy. Row vectors of length 100,000 are clumsy, and two related words might share few exact contexts ("kitten" with meow, "cat" with purr). We want short, dense vectors that capture the main patterns of co-occurrence and smooth over the noise.
The tool for this is the singular value decomposition (SVD), which §0.10 develops from the geometry up. Here is the part we need. Any real matrix factors as
where the columns of and of are orthonormal. (We add the subscript because and will mean other things in the next section.) In plain words, the matrix is a sum of rank-one patterns, each an outer product of a "word pattern" and a "context pattern" , and the singular value says how much of the matrix that pattern explains.
A two-word example you can check by hand. Let word 1 have context counts and word 2 have , so
(Here because is symmetric with positive eigenvalues.) Multiply it out to check, since . The first pattern, with , says "both words use both contexts". The second, with , says "word 1 prefers context 1 and word 2 prefers context 2". Keeping only the first gives the rank-one approximation , which keeps what the words share and forgets how they differ.
Eckart–Young theorem. Keeping only the top terms, , gives the best possible rank- approximation of , in both Frobenius norm (the square root of the sum of squared entries) and spectral norm (the largest factor by which the matrix stretches a unit vector),
In plain words, the truncated SVD is the optimal way to compress the matrix to patterns, and the error is exactly the "energy" in the patterns you threw away. In the two-word example, the error of the rank-one approximation is , whose Frobenius norm is , as promised. For our corpus's PPMI matrix the top singular values are 7.71, 5.44, 5.23, 4.56, 4.00, 3.89, and keeping of the 29 patterns retains 63% of the squared Frobenius norm.
The word vectors are then the rows of , each word's coordinates in the space of the top patterns, where keeps the first columns and the top singular values. The exponent is a hyperparameter. is the textbook choice, but Levy et al. (2015) found that or even often works better for similarity tasks, because it stops the first few patterns from dominating every cosine. This approach is the heart of Latent Semantic Analysis (LSA; Deerwester et al., 1990), which originally applied it to term–document matrices like the one in the previous subsection.
∂ Why SVD vectors put similar words close together
Suppose two words have nearly identical PPMI rows, meaning they keep the same company. Since , row of is . The matrix is the same for every word, and multiplying a row vector by (orthonormal rows) preserves its length, so
Similar rows in give similar rows in , exactly, for the full SVD. Truncating to dimensions keeps the coordinates along the largest- directions, where the strongest shared patterns live, and drops the small, noisy ones. Words that share contexts only indirectly (cat with purr, kitten with meow, while purr and meow co-occur with similar things) get pulled together, because they load on the same dominant patterns.
Truncated-SVD embeddings (and verify Eckart–Young)
Write svd_embeddings(M, r, p=1.0) that returns the (V, r) matrix and also the rank- reconstruction . The tests verify the Eckart–Young error formula on your output.
The whole pipeline, end to end
Counts, then PPMI, then SVD, then nearest neighbors by cosine. The code below uses the corpus and the PPMI matrix from the cells above.
With 93 tokens, this already discovers that cat's nearest neighbors are dog, puppy, and kitten, and that car's nearest is truck. Look closely at the odd ones too. "drive → ball" is noise from a tiny corpus. But "milk → drinks" is telling. Counting words in a window captures two different kinds of similarity.
- Substitutable words (called paradigmatic, like cat/kitten and car/truck) appear in the same contexts and could replace each other in a sentence.
- Associated words (called syntagmatic, like milk/drinks and fuel/needs) appear next to each other.
Small windows ( or 2 words on each side) tend to emphasize substitutability and syntactic role. Large windows ( to 10) emphasize topic and association. Change window in the first cell and rerun to see the shift. Bullinaria and Levy (2007) mapped these choices systematically and found small windows with PPMI best for most semantic tasks.
At real scale the method holds up. Levy, Goldberg and Dagan (2015) trained on English Wikipedia and found that, once the same tricks are applied to both, PPMI and its SVD match word2vec on word-similarity benchmarks, while word2vec keeps an edge on analogies.
Count-based embeddings end to end
Put the three steps together. Implement ppmi_svd_embeddings(tokens, window, r, alpha=0.75, p=0.5) that returns (vocab, W), where vocab is the sorted list of distinct tokens and W is the (V, r) matrix of unit-length rows of computed from the smoothed PPMI of the symmetric co-occurrence counts (exactly the conventions of Exercises 1.4a to 1.4c, with every token in the vocabulary).
The tests train on a synthetic corpus of four categories (animals, fruit, vehicles, tools) and require that every noun's nearest other noun is in its own category. They also compare your Gram matrix (which is unaffected by the sign ambiguity of the SVD) with a reference, so the window, , and must all be right.
Scaling up, sparse counts and randomized SVD
For words, a dense matrix of float64 would take 80 GB, and a full SVD costs , about operations. Neither is necessary. Most word pairs never co-occur, so the PPMI matrix is sparse (PPMI keeps zeros as zeros), and we only want the top singular vectors.
Randomized SVD (Halko, Martinsson & Tropp, 2011) gets them by multiplying the matrix by a few hundred random vectors. The idea has three steps.
- Sketch the range. Draw a Gaussian matrix with a small oversampling (say 10) and form . Each column of is a random combination of 's columns, so it is dominated by the top singular directions, the same random-projection idea as Johnson–Lindenstrauss in §1.3.
- Sharpen with power iterations. If the spectrum decays slowly, replace by for or 2. This raises every singular value to the power , so the top ones stand out much more. Re-orthonormalize with a QR decomposition between multiplications to avoid losing precision.
- Solve a small problem. With an orthonormal basis of 's columns, . Take the exact SVD of the small matrix , as , and set .
In plain words, random vectors find the subspace where the matrix does most of its work, and the exact SVD is only ever computed inside that small subspace. The cost is a few sparse matrix products, , where counts the nonzeros, instead of .
Randomized SVD from scratch
Implement randomized_svd(M, r, oversample=10, n_iter=2, seed=0) returning (U, s, Vt) with shapes (m, r), (r,), (r, n), following the three steps above. Use np.linalg.qr to orthonormalize after every multiplication in the power iterations.
The tests use a matrix whose singular values decay slowly (), which is where the power iterations matter. With two iterations your top 10 singular values must be within 3% of the exact ones and your reconstruction error within 1% of the Eckart–Young optimum. Without power iterations the singular values come out more than 30% too small.
| count-based (PPMI + SVD) | predict-based (word2vec, §1.5) | |
|---|---|---|
| training | one big matrix factorization | streaming SGD over the corpus |
| memory | sparse matrix, plus SVD workspace | only the two tables |
| adding data | recount and refactorize | just keep training |
| scale | randomized SVD on sparse matrices handles large | scales to billions of tokens easily |
| interpretability | explicit, since you can read the PPMI rows | implicit |
| quality | competitive when tuned (Levy et al. 2015) | strong, robust defaults |
In PyTorch
At real scale you keep as a sparse matrix, compute PPMI only on its nonzeros, and use a randomized low-rank SVD. PyTorch's torch.svd_lowrank implements the Halko algorithm and accepts sparse input.
Read more
- Harris (1954). Distributional Structure. The distributional hypothesis.
- Church & Hanks (1990). Word Association Norms, Mutual Information, and Lexicography. PMI for word association.
- Deerwester et al. (1990). Indexing by Latent Semantic Analysis. Truncated SVD for meaning.
- Turney & Pantel (2010). From Frequency to Meaning. The classic survey of count-based vector spaces.
- Levy, Goldberg & Dagan (2015). Improving Distributional Similarity with Lessons Learned from Word Embeddings. Why tuned counts rival word2vec. Read it alongside Baroni et al. (2014), Don't count, predict!
- Bullinaria & Levy (2007). Extracting Semantic Representations from Word Co-occurrence Statistics. A systematic study of windows, weightings, and distance measures, with PPMI and small windows coming out on top.
- Halko, Martinsson & Tropp (2011). Finding Structure with Randomness. The randomized SVD, with error bounds and the power-iteration trick.