Contents Language as Data Code Course home

Chapter 4Term Weighting and Similarity

Documents as vectors

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 V={w1,…,wn}V = \{w_1, \ldots, w_n\} 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 nn-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.

Why the matrix is never stored

The matrix has one row per term and one column per document, so it holds |V|×N|V| \times N cells, and that number gets large very fast. Take a million documents over half a million distinct terms and the matrix has 5×10115 \times 10^{11} cells, which even at one bit each is 62.562.5 gigabytes. An average document contains perhaps a thousand distinct terms, so only about 10910^{9} cells are non-zero and the density is 0.20.2 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.

Weighting: not all words are equal

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 tft,d\mathrm{tf}_{t,d}, or the count normalised by document length, tft,d/Md\mathrm{tf}_{t,d}/M_d, so that long documents do not win by sheer size, or a dampened log form, 1+log⁡tft,d1 + \log \mathrm{tf}_{t,d}, 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.

tft,d\mathrm{tf}_{t,d} raw tf/Md\mathrm{tf}/M_d at Md=1000M_d = 1000 1+log⁡10tf1 + \log_{10}\mathrm{tf}
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 +1+1 exists so that a single occurrence scores 11 rather than 00.

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:

idft=log⁡Ndft,(4.1)\begin{equation} \mathrm{idf}_t \;=\; \log \frac{N}{\mathrm{df}_t}, \label{eq:idf} \quad\text{(4.1)} \end{equation}

where NN is the number of documents in the collection, and dft\mathrm{df}_t is how many of them contain term tt.

Note what dft\mathrm{df}_t counts, which is documents and not occurrences, so a term used five hundred times in one document still has dft=1\mathrm{df}_t = 1. Since dft≤N\mathrm{df}_t \le N always, the ratio is at least 11 and the idf is never negative.

Equation (4.1) behaves exactly as we want. A term appearing in every document has dft=N\mathrm{df}_t = N, so idft=log⁡1=0\mathrm{idf}_t = \log 1 = 0, which annihilates the weight of a term that is useless for telling documents apart, while a term appearing in few documents has a small dft\mathrm{df}_t and so a large idf.

Why a logarithm

The logarithm is not a fudge factor. Pick a document from the collection at random, and the chance that it contains tt is P(t)=dft/NP(t) = \mathrm{df}_t / N, so

idft=log⁡Ndft=−log⁡P(t),(4.2)\begin{equation} \mathrm{idf}_t \;=\; \log \frac{N}{\mathrm{df}_t} \;=\; -\log P(t), \label{eq:idf-information} \quad\text{(4.2)} \end{equation}

which is the self-information of the event “this document contains tt” (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 N/dftN/\mathrm{df}_t 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 1010 by hand and natural logs in code.

Two variants you will meet

Implementations rarely use Equation (4.1) unmodified. Two variants are common:

smoothedidft=log⁡1+N1+dft+1,probabilisticidft=log⁡N−dftdft.\begin{align} \text{smoothed} &\qquad \mathrm{idf}_t = \log\frac{1 + N}{1 + \mathrm{df}_t} + 1, \label{eq:idf-smooth}\\ \text{probabilistic} &\qquad \mathrm{idf}_t = \log\frac{N - \mathrm{df}_t}{\mathrm{df}_t}. \label{eq:idf-prob} \end{align}

The 1+dft1 + \mathrm{df}_t 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 1+N1 + N above it keeps the ratio at exactly 11 for a term that appears everywhere, so the trailing +1+1 leaves such a term with a weight of exactly 11 rather than 00. Smoothing both counts is the part that is easy to get wrong: drop the 11 from the numerator and a term present in every document scores log⁡NN+1+1\log\frac{N}{N+1} + 1, which is slightly under 11 and drifts with NN. 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 1010 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 dft\mathrm{df}_t, 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:

tf-idft,d=tft,d×idft.(4.3)\begin{equation} \text{tf-idf}_{t,d} \;=\; \mathrm{tf}_{t,d} \times \mathrm{idf}_t. \label{eq:tfidf} \quad\text{(4.3)} \end{equation}

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.

A worked weighting

Numbers make the mechanism concrete. Take a collection of N=100,000N = 100{,}000 documents. The word moon occurs in df=100\mathrm{df} = 100 of them, so

idf(𝑚𝑜𝑜𝑛)=log⁡10100,000100=log⁡101000=3.\mathrm{idf}(\mathit{moon}) = \log_{10}\frac{100{,}000}{100} = \log_{10} 1000 = 3.

Take a 427427 token document where moon appears 2020 times, so that its length normalised term frequency is 20/427≈0.04720/427 \approx 0.047 and its tf-idf is therefore 0.047×3≈0.1410.047 \times 3 \approx 0.141.

Now take the, which appears in all 100,000100{,}000 documents, so its idf is log⁡10(100,000/100,000)=log⁡101=0\log_{10}(100{,}000/100{,}000) = \log_{10} 1 = 0 and its tf-idf is 00 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.py prints this table and the PMI one from Chapter 7. It adds a word that occurs once and still outscores the.

Run it in ColabNotebookSource

Similarity: measuring the angle

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 u→⋅v→\vec{u}\cdot\vec{v}, 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:

cos⁡(u→,v→)=u→⋅v→‖u→‖‖v→‖∈[−1,1].(4.4)\begin{equation} \cos(\vec{u}, \vec{v}) \;=\; \frac{\vec{u}\cdot\vec{v}}{\lVert \vec{u}\rVert\,\lVert \vec{v}\rVert} \;\in\; [-1, 1]. \label{eq:cossim} \quad\text{(4.4)} \end{equation}

It is 11 when the vectors point the same way and so are maximally similar, 00 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,

‖u→−v→‖2=‖u→‖2+‖v→‖2−2u→⋅v→=2−2cos⁡(u→,v→),(4.5)\begin{equation} \lVert \vec{u} - \vec{v} \rVert^{2} = \lVert \vec{u} \rVert^{2} + \lVert \vec{v} \rVert^{2} - 2\,\vec{u}\cdot\vec{v} = 2 - 2\cos(\vec{u}, \vec{v}), \label{eq:cos-euclid} \quad\text{(4.5)} \end{equation}

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.

A collection worked end to end

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 N=4N = 4 documents:

d1d_1 the moon orbits the earth
d2d_2 the earth orbits the sun
d3d_3 the telescope shows the moon
d4d_4 the sun is a star

Stage 1: count

There are ten distinct terms, so the matrix is 10×410 \times 4, and each cell holds tft,d\mathrm{tf}_{t,d}.

term d1d_1 d2d_2 d3d_3 d4d_4
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.

Stage 2: count documents, not occurrences

term dft\mathrm{df}_t N/dftN/\mathrm{df}_t idft=log⁡10(N/dft)\mathrm{idf}_t = \log_{10}(N/\mathrm{df}_t)
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 N/df=1N/\mathrm{df} = 1 and its weight is exactly zero.

One caveat is worth making, and it is an honest one. In this collection a has df=1\mathrm{df} = 1 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 N=4N = 4 it is measuring almost nothing. Give it a hundred thousand documents and a falls to zero alongside the.

Stage 3: multiply

term d1d_1 d2d_2 d3d_3 d4d_4
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
‖d→‖\lVert \vec{d} \rVert 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.

Stage 4: compare by angle

Take d1→\vec{d_1} and d2→\vec{d_2}. Each has three non-zero entries of 0.3010.301, and they share two of them, orbits and earth:

d1→⋅d2→=(0.301)(0.301)+(0.301)(0.301)=0.1812,‖d1→‖=‖d2→‖=3×0.3012=0.3013=0.5214,cos⁡(d1→,d2→)=0.18120.5214×0.5214=0.6667\begin{align*} \vec{d_1}\cdot\vec{d_2} &= (0.301)(0.301) + (0.301)(0.301) = 0.1812, \\ \lVert \vec{d_1} \rVert = \lVert \vec{d_2} \rVert &= \sqrt{3 \times 0.301^{2}} = 0.301\sqrt{3} = 0.5214, \\ \cos(\vec{d_1}, \vec{d_2}) &= \frac{0.1812}{0.5214 \times 0.5214} = \boxed{\mathbf{0.6667}} \end{align*}

That is exactly 2/32/3, being two shared terms out of three equally weighted ones, and doing the same for d1→\vec{d_1} and d3→\vec{d_3}, which share only moon, gives 0.0906/(0.5214×0.9031)=0.19250.0906 / (0.5214 \times 0.9031) = 0.1925.

Table 1.1 scores all six pairs twice, once on raw counts and once on tf-idf.

Cosine similarity of every pair, before and after weighting.
pair cos\cos on raw tf cos\cos on tf-idf
d1,d2d_1, d_2 0.8571 0.6667
d1,d3d_1, d_3 0.7143 0.1925
d2,d4d_2, d_4 0.5071 0.1601
d1,d4d_1, d_4 0.3381 0.0000
d2,d3d_2, d_3 0.5714 0.0000
d3,d4d_3, d_4 0.3381 0.0000

Read the row for d2,d3d_2, d_3, where raw counts call the pair 0.570.57 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 d1,d2d_1, d_2 against d1,d3d_1, d_3. Raw counts separate them by a factor of 1.21.2 while tf-idf separates them by a factor of 3.53.5, so the ranking was already correct and the weighting made it decisive. A retrieval system needs decisive.

Stage 5: rank for a query

A query is just a short document, so score each document by adding the tf-idf of whichever query terms it contains:

score⁡(q,d)=∑t∈qtf-idft,d.(4.6)\begin{equation} \operatorname{score}(q, d) \;=\; \sum_{t \in q} \text{tf-idf}_{t,d}. \label{eq:query-score} \quad\text{(4.6)} \end{equation}

For the query “the moon”:

document raw count tf-idf score
d1d_1 3 0.3010
d3d_3 3 0.3010
d2d_2 2 0.0000
d4d_4 1 0.0000

Raw counts rank d2d_2 third with a score of 22, ahead of d4d_4, even though neither document mentions the moon at all. Weighting sends both to exactly zero and keeps the correct tie between d1d_1 and d3d_3.

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 corpus prints every table in this section. Add a fifth document mentioning the moon and watch idf(𝑚𝑜𝑜𝑛)\mathrm{idf}(\mathit{moon}) move, taking every weight in the matrix with it.

A toolkit that keeps returning

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.

Further reading.

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 N=10,000N = 10{,}000 documents, term AA appears in 1010 documents, term BB in 1,0001{,}000, and term CC in all 10,00010{,}000. Compute idf\mathrm{idf} in base 1010 for each. Rank them by informativeness. In one sentence, explain why CC’s value is what it is.

tf-idf silences the stopword. A 500500 token document contains data 1515 times, and data appears in 5050 of 10,00010{,}000 documents. The same document contains the 4040 times, and the appears in all 10,00010{,}000. 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 u→=(3,0,4)\vec{u} = (3, 0, 4) and v→=(6,0,8)\vec{v} = (6, 0, 8) over the vocabulary {𝑐𝑎𝑡,𝑑𝑜𝑔,𝑡ℎ𝑒}\{\mathit{cat}, \mathit{dog}, \mathit{the}\}. 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 idft=3\mathrm{idf}_t = 3 means about the probability that a random document contains tt. 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 10001000 tokens containing cricket 6060 times and a document of 100100 tokens containing it 88 times. Rank them by raw count, by tf/Md\mathrm{tf}/M_d, and by 1+log⁡10tf1 + \log_{10}\mathrm{tf}. Report the three rankings and say which one you would ship.

Cosine and Euclidean. Normalise u→=(3,0,4)\vec{u} = (3, 0, 4) and v→=(6,0,8)\vec{v} = (6, 0, 8) 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 N=200,000N = 200{,}000 documents and |V|=120,000|V| = 120{,}000 terms, and an average document holds 800800 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 00, the library gives it 1.0001.000, and passing smooth_idf=False does not change the answer. Now take the cosine between d2d_2 and d3d_3, which share no word except the: this chapter reports 00 and the library reports 0.32900.3290. Find the term in Equation (4.7) responsible, then argue for one convention or the other and say what each one costs you.

Code for this chapter

Each one reproduces the tables above. Open it in Colab, change an input and watch which numbers move.

From Natural Language Processing: From Corpus Statistics to Grounded LLMs, by Ramaseshan Ramachandran.