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
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.
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)})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
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"]))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
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)
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).
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Embeddings and cosine similarityNLP, embeddings and large language models · same module
- How large language models workNLP, embeddings and large language models · same module
- Choosing and explaining a modelk-NN, decision trees and ensembles
- Conditional probability and Bayes' theoremProbability and inference
- k-nearest neighboursk-NN, decision trees and ensembles
- Counting and grouping with hashingData Structures and Algorithms (DSA) for interviews · Hashing, two pointers and sliding window
Questions people ask about this
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.
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.
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.
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.
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
- 3 more topics in “NLP, embeddings and large language models” before its exam: Embeddings and cosine similarity, How large language models work, Prompting well.
- Module 10 exam: NLP, embeddings and large language models · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Building with generative AI, and MLOps. 62 topics left in the course before the final exam.