Skip to content

Expert: deep learning and AI · NLP, embeddings and large language models · topic 49 of 62 · 35 min

Bag of words, TF-IDF and text classification

Explanation

A bag of words turns each document into counts of every word in a vocabulary, ignoring order. TF-IDF improves this by giving high weight to words that are frequent in one document but rare across all documents. These vectors feed simple, fast classifiers like Naive Bayes or logistic regression, which work well for spam, sentiment and ticket routing.

Turning documents into numbers

Models need fixed-length lists of numbers. The bag-of-words idea: collect every distinct word in all documents into a vocabulary, and represent each document as a list of counts, one per vocabulary word. "chai is hot" with vocabulary [biscuit, chai, coffee, hot, is] becomes [0, 1, 0, 1, 1]. The order of words is thrown away, like tipping the words into a bag.

The problem with raw counts

Common words like "is", "the" or, in support tickets, "order" appear everywhere, so they dominate the counts without telling documents apart. We want to boost words that are special to a document.

TF-IDF

  • TF (term frequency): how often a word appears in this document. One common version is count ÷ number of tokens in the document.
  • IDF (inverse document frequency): how rare the word is across all N documents: ln(N ÷ df), where df is the number of documents containing the word. A word in every document gets ln(1) = 0.
  • TF-IDF = TF × IDF: high when a word is frequent here but rare elsewhere.
python
import math

docs = [["chai", "is", "hot"], ["coffee", "is", "hot"], ["chai", "chai", "biscuit"]]
N = len(docs)
df = {w: sum(1 for d in docs if w in d) for d in docs for w in d}
d3 = docs[2]
print({w: round(d3.count(w) / len(d3) * math.log(N / df[w]), 4) for w in set(d3)})
Shows chai: 0.2703 and biscuit: 0.3662 (in some order). "biscuit" is in only one document, so it scores highest.
Careful: There are several TF-IDF formulas. scikit-learn's TfidfVectorizer by default uses raw counts for TF, a smoothed IDF (ln((1 + N) ÷ (1 + df)) + 1) and then scales each vector to length 1. Its numbers will differ from the textbook version; the idea is the same.

n-grams

Bag of words loses order, so "not good" and "good" look similar. Adding n-grams, sequences of n neighbouring words, brings some order back: bigrams (n = 2) include "not good" as one feature. The vocabulary grows fast, so limit it (for example, keep the 20,000 most frequent features).

Text classification

python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline

clf = make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), LogisticRegression(max_iter=1000))
clf.fit(train_texts, train_labels)               # e.g. "billing", "delivery", "technical"
print(clf.predict(["my refund has not come yet"]))
A strong, fast baseline for routing tickets or detecting spam. Multinomial Naive Bayes is another classic choice. Shown only.

Limits

Bag-of-words models do not understand meaning. "Cheap" and "inexpensive" are unrelated columns; a word never seen in training is ignored. They also miss sarcasm and long-range context. Embeddings (next topic) and transformers address these limits, but TF-IDF with logistic regression remains a surprisingly strong baseline that trains in seconds.

Common mistakes

  • Fitting the vectoriser (building the vocabulary and IDF) on all data, including the test set. Fit it on training text only, inside a pipeline.
  • Not limiting the vocabulary, giving hundreds of thousands of sparse columns.
  • Skipping this baseline and jumping to a large model that is slower and only slightly better.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Bag of words, TF-IDF and text classification · 1/6

Text → vector

Models need numbers, so we turn each document into a vector.

Prefer a video by a teacher? Find free videos on “bag of words tf-idf explained text classification scikit-learn” on YouTube ↗

In real life: Routing bank customer emails

A bank receives thousands of customer emails a day about cards, loans, KYC updates and net banking. A TF-IDF plus logistic regression classifier, trained on a few months of emails already sorted by staff, routes new emails to the right team instantly. Low-confidence predictions go to a general queue. It is cheap to run, easy to retrain monthly, and the most useful words for each team can be listed to check that the model makes sense.

Practice (0/2 done)

Task 1 · Code · Python

Write build_vocab(docs) where docs is a list of token lists; return the sorted list of distinct tokens. Then write vectorize(tokens, vocab) that returns the count of each vocabulary word in tokens, in vocabulary order (words not in vocab are ignored).

Task 2 · Code · Python

Write tfidf(docs) for a list of token lists. For each document return a dict mapping each of its distinct words to tf × idf rounded to 4 decimal places, where tf = (count in the document) / (number of tokens in the document) and idf = math.log(N / df) (natural log; N = number of documents, df = number of documents containing the word).

Quick quiz

Question 1

1.What does a bag-of-words vector ignore?

Question 2

2.A word appears in every document. What is its IDF with ln(N / df)?

Question 3

3.Why add bigrams?

Question 4

4.Which is a known weakness of TF-IDF features?

Question 5

5.Where should the TF-IDF vectoriser be fitted?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does a bag-of-words vector ignore?

Answer: The order of the words

Only counts remain; order is lost.

A word appears in every document. What is its IDF with ln(N / df)?

Answer: 0

ln(N / N) = ln 1 = 0.

Why add bigrams?

Answer: To capture short phrases like "not good" that single words miss

Bigrams bring back some local word order.

Which is a known weakness of TF-IDF features?

Answer: They do not know that "cheap" and "inexpensive" mean nearly the same

Each word is a separate column with no notion of meaning.

Where should the TF-IDF vectoriser be fitted?

Answer: On the training texts only (inside a pipeline)

The vocabulary and IDF are learned values; fitting on test text leaks.

Is logistic regression used for regression or classification?Related

Answer: Classification

It predicts class probabilities; the name is historical.

From Logistic regression and classification

You embed documents with model A and queries with model B. What happens?Related

Answer: Meaningless comparisons, because the vector spaces are different

Use one model for both sides.

From Embeddings and cosine similarity

What is the out-of-bag score?Related

Answer: A test-like score computed on rows each tree did not see

It is a built-in validation estimate.

From Random forests and bagging

Two models score 0.861 and 0.864 AUC; the first is a readable logistic regression, the second a large ensemble. Explanations are legally required. Which is usually better to ship?Related

Answer: The logistic regression

A tiny gain rarely justifies losing explainability when it is required.

From Choosing and explaining a model

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “NLP, embeddings and large language models” before its exam: Embeddings and cosine similarity, How large language models work, Prompting well.
  2. Module 10 exam: NLP, embeddings and large language models · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Building with generative AI, and MLOps. 62 topics left in the course before the final exam.

← Previous: Working with text: tokens and cleaning