Chapter 3 showed that word frequencies are wildly uneven, because Zipf’s law guarantees a handful of words dominate every corpus while a long tail barely occurs at all.
This chapter turns that raw material into something a machine can compare, which means we need a numerical representation of a document and a way to measure when two documents are alike. The construction is the foundation of information retrieval, and its two ideas come back in almost every later chapter: weight the words by how informative they are, and measure similarity as an angle.
Start with the simplest representation there is. Fix the vocabulary of the corpus, then represent a document as a vector with one entry per vocabulary word, recording how often that word appears.
This is the bag of words, and it keeps word counts while throwing word order away completely, so “dog bites man” and “man bites dog” get the same vector. That is a real loss, and it is also tolerable for many tasks where topic matters more than syntax. In exchange we get a document as a point in an -dimensional space, which is a form we can do arithmetic on.
Keeping only presence or absence, rather than counts, gives the binary incidence matrix of classical Boolean retrieval (Manning and Schütze 1999), and counts are the richer choice.
The matrix has one row per term and one column per document, so it holds cells, and that number gets large very fast. Take a million documents over half a million distinct terms and the matrix has cells, which even at one bit each is gigabytes. An average document contains perhaps a thousand distinct terms, so only about cells are non-zero and the density is per cent.
The sparsity is Zipf’s law again, because most types are rare and so most rows are nearly empty.
Nobody therefore stores the matrix. For each term you store the list of documents that contain it, which is the inverted index, and the empty cells cost nothing. The same argument returns for the co-occurrence matrices of Chapter 7.
The bit matrix has a second problem, and it is the one that motivates the rest of this chapter. A document that mentions caesar once and one that mentions it forty times look identical, so every matching document is equally good and Boolean retrieval cannot rank.
Raw counts bring the trouble Chapter 3 predicted, because the most frequent word in a document is almost always the or of, and those words occur everywhere while saying nothing about what the document is about.
A representation dominated by its largest counts is therefore dominated by its least informative words, so we need to weight each term by how much it discriminates. The classic solution has two parts.
The first part measures how important a term is within a document, which is its term frequency. It can be the raw count , or the count normalised by document length, , so that long documents do not win by sheer size, or a dampened log form, , so that the tenth occurrence counts for less than the second.
The damping deserves a sentence of justification, because it looks arbitrary. Relevance does not grow linearly with occurrences, so while a document that says cricket twenty times is more about cricket than one that says it twice, it is not ten times more about cricket.
| raw | at | ||
|---|---|---|---|
| 1 | 1 | 0.001 | 1.000 |
| 2 | 2 | 0.002 | 1.301 |
| 10 | 10 | 0.010 | 2.000 |
| 100 | 100 | 0.100 | 3.000 |
| 1000 | 1000 | 1.000 | 4.000 |
A thousandfold spread in raw counts becomes a fourfold spread in weight, because the logarithm turns multiplicative differences into additive ones, which is the same trick that made Zipf’s law a straight line in Chapter 3. The exists so that a single occurrence scores rather than .
Length normalisation and log damping fix different problems, and they are often used together.
Term frequency alone still rewards the, though, so we multiply it by a second factor measuring how informative the term is across the whole collection. That is the inverse document frequency:
where is the number of documents in the collection, and is how many of them contain term .
Note what counts, which is documents and not occurrences, so a term used five hundred times in one document still has . Since always, the ratio is at least and the idf is never negative.
Equation (4.1) behaves exactly as we want. A term appearing in every document has , so , which annihilates the weight of a term that is useless for telling documents apart, while a term appearing in few documents has a small and so a large idf.
The logarithm is not a fudge factor. Pick a document from the collection at random, and the chance that it contains is , so
which is the self-information of the event “this document contains ” (Manning et al. 2009). The weight is a count of bits of surprise, in whatever base the logarithm uses.
That reading explains the behaviour at both ends, since a term you fully expected to see carries no information and scores zero, while a term you did not expect carries a lot and scores high.
The logarithm also tames the scale, because without it ranges over five orders of magnitude and one rare word would dominate every score it touched. The base of the logarithm only rescales every weight by a constant, so it never changes a ranking, which is why you can use base by hand and natural logs in code.
Implementations rarely use Equation (4.1) unmodified. Two variants are common:
The
in Equation (4.7) guards against a
query term that appears in no document at all, where the plain formula
divides by zero, and the matching
above it keeps the ratio at exactly
for a term that appears everywhere, so the trailing
leaves such a term with a weight of exactly
rather than
.
Smoothing both counts is the part that is easy to get wrong: drop the
from the numerator and a term present in every document scores
,
which is slightly under
and drifts with
.
Equation (4.7) as written, with
natural logarithms, is what scikit-learn computes by
default, so its numbers will not match a hand computation in base
unless you ask for the unsmoothed form.
Equation (4.8) goes negative for any term in more than half the collection, which is a stronger punishment of the head, and it is the ancestor of BM25 as developed in Chapter 21.
All three variants are monotonically decreasing in , so all three agree on the ordering of terms and differ only in how hard they push the head down.
The product of the two factors is tf-idf:
It is high only when a term occurs often in this document but rarely across the collection, which is precisely the profile of a word that characterises the document.
This answers a puzzle Chapter 3 left open. At bottom, idf is a machine for undoing Zipf’s law, because it discounts exactly the frequent words that the frequency distribution guarantees you will have.
Numbers make the mechanism concrete. Take a collection of documents. The word moon occurs in of them, so
Take a token document where moon appears times, so that its length normalised term frequency is and its tf-idf is therefore .
Now take the, which appears in all documents, so its idf is and its tf-idf is no matter how many times it occurs in the document.
The weighting has silenced the most frequent word in the language and amplified the one that signals the topic, and it did so with nothing but a logarithm of a ratio of counts.
Try it yourself.
code/worked_examples/weighting.pyprints this table and the PMI one from Chapter 7. It adds a word that occurs once and still outscores the.
Now that documents and words are weighted vectors, the question “how similar are these two?” becomes a geometric one.
The natural first guess is the dot product , and it has a flaw: it grows with the lengths of the vectors, so a long document scores high similarity with everything purely by having large entries.
The fix is to divide the magnitudes out and measure the angle between the vectors instead of their lengths, which gives cosine similarity:
It is when the vectors point the same way and so are maximally similar, when they are orthogonal and share no terms, and negative only when they point oppositely, which cannot happen for count vectors, whose entries are never negative.
Cosine is the standard measure of similarity in this book. It compares word vectors in Chapter 6, embeddings in Chapter 9, and retrieved passages in Chapter 21.
The unnormalised dot product is not discarded, though, because the attention mechanism at the heart of every transformer is built on exactly that operation, and Chapter 16 puts it to a different use.
Euclidean distance is the third obvious choice, and being sensitive to magnitude it is rarely used for comparing these vectors directly. There is one case where all three agree, because for unit length vectors,
so ranking by cosine and ranking by Euclidean distance give the same order. Vector databases exploit exactly this, normalising once at indexing time and then using whichever operation the hardware does faster.
The moon example weighted a single term, whereas this section takes four documents all the way from raw text to a ranking, with every number small enough to check by hand.
The collection is documents:
the moon orbits the earth |
|
the earth orbits the sun |
|
the telescope shows the moon |
|
the sun is a star |
There are ten distinct terms, so the matrix is , and each cell holds .
| term | ||||
|---|---|---|---|---|
| the | 2 | 2 | 2 | 1 |
| moon | 1 | 0 | 1 | 0 |
| orbits | 1 | 1 | 0 | 0 |
| earth | 1 | 1 | 0 | 0 |
| sun | 0 | 1 | 0 | 1 |
| telescope | 0 | 0 | 1 | 0 |
| shows | 0 | 0 | 1 | 0 |
| is | 0 | 0 | 0 | 1 |
| a | 0 | 0 | 0 | 1 |
| star | 0 | 0 | 0 | 1 |
By raw count, the is the most important word in all four documents, and that is the problem this chapter exists to solve.
| term | |||
|---|---|---|---|
| the | 4 | 1.00 | 0.000 |
| moon, orbits, earth, sun | 2 | 2.00 | 0.301 |
| telescope, shows, is, a, star | 1 | 4.00 | 0.602 |
The is in every document, so and its weight is exactly zero.
One caveat is worth making, and it is an honest one. In this collection a has and so scores the highest idf available, which is a reminder that idf is a statistic of a collection rather than a judgement about language, and with it is measuring almost nothing. Give it a hundred thousand documents and a falls to zero alongside the.
| term | ||||
|---|---|---|---|---|
| the | 0.000 | 0.000 | 0.000 | 0.000 |
| moon | 0.301 | 0.000 | 0.301 | 0.000 |
| orbits | 0.301 | 0.301 | 0.000 | 0.000 |
| earth | 0.301 | 0.301 | 0.000 | 0.000 |
| sun | 0.000 | 0.301 | 0.000 | 0.301 |
| telescope | 0.000 | 0.000 | 0.602 | 0.000 |
| shows | 0.000 | 0.000 | 0.602 | 0.000 |
| is | 0.000 | 0.000 | 0.000 | 0.602 |
| a | 0.000 | 0.000 | 0.000 | 0.602 |
| star | 0.000 | 0.000 | 0.000 | 0.602 |
| 0.5214 | 0.5214 | 0.9031 | 1.0854 |
The entire first row is zero, so two documents that share only the now share nothing at all.
Take and . Each has three non-zero entries of , and they share two of them, orbits and earth:
That is exactly , being two shared terms out of three equally weighted ones, and doing the same for and , which share only moon, gives .
Table 1.1 scores all six pairs twice, once on raw counts and once on tf-idf.
| pair | on raw tf | on tf-idf |
|---|---|---|
| 0.8571 | 0.6667 | |
| 0.7143 | 0.1925 | |
| 0.5071 | 0.1601 | |
| 0.3381 | 0.0000 | |
| 0.5714 | 0.0000 | |
| 0.3381 | 0.0000 |
Read the row for , where raw counts call the pair similar even though the only word they share is the. Under tf-idf their cosine is exactly zero, which is the right answer.
Now read against . Raw counts separate them by a factor of while tf-idf separates them by a factor of , so the ranking was already correct and the weighting made it decisive. A retrieval system needs decisive.
A query is just a short document, so score each document by adding the tf-idf of whichever query terms it contains:
For the query “the moon”:
| document | raw count | tf-idf score |
|---|---|---|
| 3 | 0.3010 | |
| 3 | 0.3010 | |
| 2 | 0.0000 | |
| 1 | 0.0000 |
Raw counts rank third with a score of , ahead of , even though neither document mentions the moon at all. Weighting sends both to exactly zero and keeps the correct tie between and .
Notice what that means for the user. The word the contributed nothing to the score, so typing it cost nothing, and weighting is what lets people write queries in ordinary English.
Try it yourself.
python3 weighting.py corpusprints every table in this section. Add a fifth document mentioning the moon and watch move, taking every weight in the matrix with it.
Two ideas leave this chapter and never depart. Weight terms by informativeness, so that the frequent words Zipf’s law forces on us do not drown the signal, and measure similarity by angle, so that comparison is about direction rather than magnitude.
Together, tf-idf and cosine turn a corpus into a searchable, comparable space, and they were the engine of information retrieval for decades.
Notice what they assume, though. Every word is still an independent dimension orthogonal to every other, so cat and dog stay exactly as unrelated as cat and the. Weighting scales the axes without making similar words share them.
Repairing that is the next leap, because we want cat and dog to have nearby directions rather than perpendicular ones. That takes us to the distributional vectors of Chapter 6 and then to the learned embeddings of Chapter 9. The measuring stick, cosine, comes along unchanged, and what improves is the space it measures.
Manning, Raghavan and Schütze’s Introduction to Information
Retrieval (Manning et al. 2009) is the definitive modern treatment
of tf-idf weighting, the vector space model and cosine similarity.
Manning and Schütze (Manning and Schütze 1999) cover the incidence
matrix and the statistical foundations. BM25, the probabilistic
descendant of tf-idf, is developed in Chapter 21. The
implementation nearly everyone uses is
sklearn.feature_extraction.text.TfidfVectorizer, and the
last exercise below asks what it does to the four documents worked
through above.
idf by hand. In a collection of documents, term appears in documents, term in , and term in all . Compute in base for each. Rank them by informativeness. In one sentence, explain why ’s value is what it is.
tf-idf silences the stopword. A token document contains data times, and data appears in of documents. The same document contains the times, and the appears in all . Compute the length normalised tf-idf of each. Which word does the weighting say the document is about? Why is the raw count the wrong guide?
Cosine versus dot product. Let and over the vocabulary . Compute the dot product and the cosine similarity of Equation (4.4). These two vectors describe documents with identical word proportions but different lengths. Which measure recognises them as the same? Why is that the property you want when comparing documents?
Why orthogonal axes are not enough. Under a tf-idf bag of words representation, compute the cosine similarity between a document containing only cat and one containing only dog. Is the value what a human would judge? Explain how this motivates the distributional vectors of Chapter 6, where cat and dog are no longer perpendicular.
idf is bits. Using Equation (4.2), state in one sentence what means about the probability that a random document contains . Then say why changing the base of the logarithm cannot change a ranking.
Refit with the smoothed idf. Rebuild Section 4.4 using Equation (4.7) instead, keeping
natural logarithms throughout. Which cells change, and does the query
ranking change? Explain why the answer to those two questions differs.
Check your idf column against scikit-learn, which computes
exactly this variant.
Damping changes the winner. Take a document of tokens containing cricket times and a document of tokens containing it times. Rank them by raw count, by , and by . Report the three rankings and say which one you would ship.
Cosine and Euclidean. Normalise and to unit length, then verify Equation (4.5) numerically. Construct a pair of unnormalised vectors for which cosine and Euclidean distance rank a third vector differently.
Count the storage. A collection has documents and terms, and an average document holds distinct terms. Compute the size of the dense bit matrix and the number of non-zero cells. What fraction of the matrix would an inverted index actually store?
The library will not annihilate your stopword. Run
TfidfVectorizer over the four document collection of this
chapter and print its idf_ entry for the. This
chapter gives that term an idf of
,
the library gives it
,
and passing smooth_idf=False does not change the answer.
Now take the cosine between
and
,
which share no word except the: this chapter reports
and the library reports
.
Find the term in Equation (4.7) responsible, then
argue for one convention or the other and say what each one costs
you.
Each one reproduces the tables above. Open it in Colab, change an input and watch which numbers move.
weighting.pytf-idf and PMI, by hand
The weighting arithmetic the chapters print, on the four document matrix.
standard library only Run it in Colab Notebook SourceFrom Natural Language Processing: From Corpus Statistics to Grounded LLMs, by Ramaseshan Ramachandran.