🌙 30-Day AI Ramadan Series  ·  Day 19 of 30
Deep Dive · Mar 9, 2026 · 30 min read · NLP

NLP Fundamentals:
The Complete Guide to Teaching Machines to Read

Before transformers, before BERT, before GPT — every NLP system had to solve the same hard problem: turning human language into numbers. This guide walks the entire classical NLP pipeline from raw text to trained classifier, with every technique explained from first principles and working code throughout.

← Back to Blog ☰ Categories

In 1957, linguist John Firth wrote:
"You shall know a word by the company it keeps."

He meant that the meaning of a word is defined by
the other words that appear alongside it in real usage.
"Bank" next to "river" means something different than "bank" next to "loan."

This one sentence contains the seed of every NLP technique
from bag-of-words to BERT — sixty-five years of progress,
all following the same original intuition.

01 — The Core Problem: Text Is Not a Number

Every ML model works on numbers. Decision trees split on numerical thresholds. Neural networks multiply weights by values. Even images, which feel qualitative, are grids of pixel intensities from 0 to 255. Text has no natural numerical representation — a word is a symbol, and symbols have no inherent magnitude, distance, or order.

The entire field of NLP is, at its core, a long answer to one question: how do you represent language as numbers in a way that captures meaning? Every technique from bag-of-words in 1954 to transformer embeddings in 2018 is a different answer to that same question. The progression goes:

Raw text
↓ tokenisation
Token list → integer IDs
↓ bag-of-words / TF-IDF
Sparse vectors (50,000-dim, mostly zeros)
↓ word2vec / GloVe — Day 23
Dense vectors (300-dim, every value non-zero)
↓ BERT / GPT — Day 26
Contextualised dense vectors (meaning changes with context)

Each step captures more meaning at the cost of more computation. Crucially, the foundational techniques are still widely used in production — not because they're "good enough," but because they're fast, interpretable, and effective for many real-world tasks. Understanding them makes the whole field click into place.

📚 Analogy: A Library Card Catalogue

Imagine a library in the 1970s — no computers. A librarian needs to answer "which books are about ancient Rome?" They can't read every book. Instead they consult the card catalogue: each book has a list of keywords. They search for cards containing "Rome," "Roman," "ancient," "empire" and retrieve books mentioning those words most frequently.

This is exactly how bag-of-words and TF-IDF work. A document is represented by a profile of its keywords. Search and classification happen by comparing those profiles — no understanding of grammar, no awareness of context, but surprisingly powerful for many tasks. Google's original PageRank was built on top of TF-IDF-style keyword matching. BM25, its direct descendant, still runs the first retrieval stage in most RAG pipelines today.

02 — Text Preprocessing: Cleaning Before You Count

Raw text is messy. Before any numerical representation can be computed, you need to normalise the text so that equivalent forms of the same word are treated identically, and noise that adds no meaning is removed. These steps collectively form the preprocessing pipeline — every classical NLP system runs through all of them in sequence.

Lowercasing

The simplest step with the largest impact. Without it, "Python," "PYTHON," and "python" are three different tokens. Lower-casing collapses them to one. The exception: acronyms (NLP vs nlp), proper nouns in some tasks, and sentiment-sensitive capitalisation ("GREAT" vs "great"). Apply lowercasing by default; reconsider only when case carries meaning for your specific task.

Punctuation and Special Character Removal

Punctuation rarely carries semantic weight for classification tasks. Removing it prevents "word." and "word" from being treated as different tokens. Exceptions: emoji (carry strong sentiment), URLs and email addresses (may be signals in spam detection), code and technical strings (remove the whole token if irrelevant, or keep as a special token). Regex-based removal is standard: re.sub(r'[^a-zA-Z0-9\s]', '', text).

Stop Word Removal

Stop words — "the," "is," "and," "a," "in," "of," "to" — appear in virtually every document and carry no discriminative information for most classification tasks. Removing them dramatically reduces vocabulary size and noise. NLTK and spaCy ship predefined stop word lists. Important caveat: do not remove stop words when word order matters (sentiment analysis with negation — "not good" becomes "good" after removing "not"). The decision is task-dependent.

Stemming vs Lemmatisation

Both normalise inflected word forms to a common base, collapsing "run," "runs," "running," "ran" into one token. They differ in how:

MethodInputOutputApproachSpeedCorrectness
Porter Stemmer studies, studying studi, studi Rule-based suffix stripping ✓ Fast ✗ Not real words
Snowball Stemmer running, generously run, generous Improved Porter rules, multilingual ✓ Fast ~ Better
WordNet Lemmatiser studies, was, better study, be, good Dictionary lookup + POS tag ~ Slow ✓ Real words
spaCy Lemmatiser running, geese, went run, goose, go Statistical model + lookup ~ Medium ✓ Most accurate

Rule of thumb: use stemming when speed matters and morphology is regular (English classification pipelines). Use lemmatisation when correctness matters or when the processed text will be shown to humans (keyword extraction, summarisation).

Text Normalisation

Beyond lowercasing and stop words, normalisation handles language quirks: expanding contractions ("don't" → "do not"), removing accents for multilingual text ("café" → "cafe"), handling Unicode (normalise to NFC/NFD), collapsing repeated characters ("sooooo good" → "so good"), replacing numbers with a special token (<NUM>), and stripping HTML tags from scraped web content. spaCy's pipeline handles most of these automatically for 70+ languages.

03 — Tokenisation: Splitting Text into Meaningful Units

Tokenisation is the process of splitting text into the atomic units your model will reason over. It sounds trivial but has genuine complexity: "don't" → ["don't"] or ["do", "n't"]? "New York" → one token or two? "2,500.00" → one token or three? Chinese text has no spaces at all.

Word tokenisation
Split on whitespace and punctuation. "I love NLP." → ["I", "love", "NLP", "."]. Simple and fast. Creates large vocabularies (every morphological form is separate: "run," "runs," "running," "ran" = 4 tokens). Cannot handle words not seen during training — "ChatGPT" in a 2019 vocabulary simply doesn't exist.
Character tokenisation
Each character is a token. Tiny vocabulary (26 letters + punctuation). Handles any word, any language. Downside: sequences are very long (a 100-word sentence becomes 500+ tokens), making it computationally expensive and harder for models to capture word-level patterns.
Subword tokenisation (BPE)
The modern standard. Byte-Pair Encoding starts with characters and merges the most frequent pairs iteratively until the vocabulary reaches a target size (~32K–100K). "tokenisation" → ["token", "##isation"]. Handles unknown words via their subparts, keeps vocabulary manageable, works for any language. Used by every major LLM: GPT-4 (BPE ~100K tokens), BERT (WordPiece ~30K), LLaMA (SentencePiece ~32K).
Sentence tokenisation
Split text into sentences before word tokenisation. Harder than it looks: "Dr. Smith works at St. Mary's." has three periods but one sentence. NLTK's sent_tokenize uses a Punkt model trained on abbreviations. spaCy's sentence segmentor uses a statistical model that correctly handles edge cases like "U.S.A." and quotation-spanning sentences.
🔢 How BPE Works — Step by Step

Start with character-level vocabulary: {"l","o","w","e","r","n","w","e","s","t"}. Count all adjacent pairs across the training corpus. Merge the most frequent pair — say "e" + "r" → "er" — into one new token. Repeat until vocabulary size is reached. The result: common words become single tokens ("the," "is"), rare words split into frequent subwords ("ChatGPT" → ["Chat", "G", "PT"]), truly novel strings split into characters. The vocabulary captures the statistical structure of the language.

04 — Bag-of-Words and N-grams: Representing Documents as Counts

Once you have tokens, the simplest numerical representation is to count them. Bag-of-Words (BoW) represents a document as a vector of length equal to the vocabulary size, where each position holds the count of the corresponding word. The name "bag" is deliberate: the order is thrown away, only the contents matter.

Example: vocabulary = ["cat", "dog", "love", "hate", "I"], document = "I love my cat and my cat loves me":

cat=2   dog=0   love=1   hate=0   I=1
→ vector: [2, 0, 1, 0, 1] (50,000-dim in practice, ~49,995 zeros)

What BoW loses: word order entirely. "The dog bit the man" and "The man bit the dog" produce identical vectors. "Not good" and "Good not" are the same. For topic classification, this rarely matters — topic signal lives in which words appear, not their order. For sentiment, reasoning, and translation, it matters enormously.

N-grams: Recovering Some Word Order

N-grams extend BoW by treating consecutive word sequences as single tokens. A bigram (n=2) treats each adjacent pair as one unit; a trigram (n=3) treats each triple. "New York" as a bigram won't be split into "New" and "York" — their co-occurrence is preserved.

N-gram typeTokens from "not very good"CapturesVocabulary explosion
Unigram (n=1) ["not", "very", "good"] Individual words only ✓ None
Bigram (n=2) ["not very", "very good"] "not good," named entities ~ Moderate (V²)
Trigram (n=3) ["not very good"] Short phrases, negation scope ✗ Large (V³)
Unigram + Bigram All above combined Best of both in practice ~ 2–5× unigrams

In practice, ngram_range=(1,2) (unigrams + bigrams) is the standard setting. It adds phrase-level sensitivity without the vocabulary explosion of trigrams. Adding bigrams typically improves sentiment analysis accuracy by 2–5% — the improvement comes specifically from negation capture ("not good" is now a single feature with its own weight).

05 — TF-IDF: Making the Right Words Louder

Raw word counts are misleading. "The" appearing 50 times in a document is less informative than "photosynthesis" appearing 3 times. You want to weight terms not just by how often they appear in this document, but inversely by how common they are across all documents. That is exactly what TF-IDF does.

The Formula, Explained

Term Frequency (TF) measures how often term t appears in document d, normalised by document length to avoid favouring longer documents:

TF(t, d) = count(t in d) / total_tokens(d)

Inverse Document Frequency (IDF) penalises terms that appear in many documents. A term in every document carries no discriminative information:

IDF(t) = log( N / (1 + df(t)) ) + 1
N = total documents, df(t) = documents containing t
(+1 smoothing prevents division by zero for new terms)

TF-IDF(t, d) = TF(t, d) × IDF(t). A word appearing frequently in this document but rarely across the corpus scores highest — it is the document's signature term.

Worked Example

Corpus of 3 documents: "I love cats", "I love dogs", "Cats and dogs are pets". Vocabulary after stop word removal: {love, cats, dogs, pets}. N=3 documents.

Termdf(t)IDFTF in Doc 1
"I love cats"
TF-IDF in Doc 1
love 2 (docs 1,2) log(3/3)+1 = 1.00 1/3 = 0.333 0.333
cats 2 (docs 1,3) log(3/3)+1 = 1.00 1/3 = 0.333 0.333
dogs 2 (docs 2,3) log(3/3)+1 = 1.00 0/3 = 0.000 0.000
pets 1 (doc 3 only) log(3/2)+1 = 1.41 0/3 = 0.000 0.000

Notice "pets" has the highest IDF (1.41) — it only appears in one document, making it maximally discriminative. If Doc 1 also contained "pets," it would score higher there than "love" or "cats," correctly identifying it as a distinguishing term. This is the core intuition: rare across the corpus, frequent in this document = important for this document.

🔍 BM25: TF-IDF's Production-Grade Descendant

TF-IDF has two well-known weaknesses: (1) a term appearing 100 times scores 100× higher than one appearing once — but doubling a keyword shouldn't double a document's relevance score; (2) longer documents naturally accumulate higher TF scores just by having more words. BM25 fixes both with saturation and document-length normalisation:

BM25(t,d) = IDF(t) × [tf × (k₁+1)] / [tf + k₁ × (1 − b + b × |d|/avgdl)]
k₁ ≈ 1.5 (saturation), b ≈ 0.75 (length normalisation)

BM25 is the standard sparse retrieval algorithm used in Elasticsearch, Apache Lucene (which powers most production search), and the first-stage retriever in virtually every RAG pipeline. When you ask ChatGPT with web search enabled, BM25 finds candidate documents in milliseconds — neural reranking then refines the shortlist. The 1994 algorithm is still doing the heavy lifting at billion-document scale.

06 — Beyond Counting: POS Tagging, NER, and Dependency Parsing

Bag-of-words and TF-IDF treat text as an unordered set of tokens. But language has structure — words have grammatical roles, entities have types, clauses have relationships. The classical NLP pipeline uses three additional annotation layers to extract this structure:

Part-of-Speech (POS) Tagging

Assigns a grammatical category to each token: noun (NN), verb (VB), adjective (JJ), adverb (RB), preposition (IN), etc. "The quick brown fox jumps." → [("The", DT), ("quick", JJ), ("brown", JJ), ("fox", NN), ("jumps", VBZ)]. Why it matters: (1) lemmatisation requires POS — "better" as adjective lemmatises to "good," as verb to "better"; (2) feature engineering — count only noun phrases for entity-heavy tasks; (3) disambiguating word sense — "book a flight" (VB) vs "read a book" (NN). spaCy's POS tagger achieves ~97% accuracy on standard English benchmarks using a CNN-based model.

Named Entity Recognition (NER)

Identifies and classifies real-world entities: persons (PER), organisations (ORG), locations (LOC), dates (DATE), monetary values (MONEY), etc. "Apple Inc. announced a $3B deal in Cupertino yesterday." → [(Apple Inc., ORG), ($3B, MONEY), (Cupertino, LOC), (yesterday, DATE)]. NER powers: information extraction from news and reports, knowledge graph construction, question answering ("Where is Tesla headquartered?"), and compliance systems that must detect mentions of specific entities.

🏷️ NER Tag Types (spaCy English Model)
TagMeaningExample
PERSONPeople, fictional characters"Elon Musk," "Harry Potter"
ORGCompanies, agencies, institutions"OpenAI," "NASA," "WHO"
GPECountries, cities, states"Egypt," "Cairo," "California"
LOCNon-GPE locations"the Nile," "Mount Everest"
DATEAbsolute or relative dates"March 2026," "last Tuesday"
MONEYMonetary values"$3 billion," "€500M"
PRODUCTObjects, vehicles, software"iPhone 16," "Tesla Model 3"

Dependency Parsing

Identifies grammatical relationships between words: which noun is the subject of which verb? Which adjective modifies which noun? "The angry customer called the support team." → "customer" is the subject (nsubj) of "called"; "angry" modifies "customer" (amod); "team" is the object (dobj) of "called." Dependency parsing enables: semantic role labelling, relation extraction ("Company X acquired Company Y"), coreference resolution ("Obama announced his plan" — "his" refers to "Obama"), and question answering from structured text.

07 — Classical Text Classification: The Full Pipeline

Putting all the preprocessing steps together with a classifier produces a complete, deployable text classification system. This is what a production pipeline looks like — the same architecture Gmail used for spam filtering, the same one most enterprise NLP systems use for routing and categorisation today.

1
Raw text input. Collect and deduplicate your documents. Ensure consistent encoding (UTF-8). For web-scraped data, strip HTML tags. For social media, normalise Unicode and remove bot-generated patterns. Label each document with its class.
2
Preprocessing. Lowercase → remove HTML/URLs → expand contractions → remove punctuation → tokenise → remove stop words → stem or lemmatise. Result: each document is now a list of normalised tokens. Apply identical preprocessing to both train and test data using the same fitted pipeline.
3
Feature extraction. Build a TF-IDF matrix from the training corpus. Vocabulary is fitted on training data only — never on test data. Use ngram_range=(1,2) for bigrams, max_features=50000 to cap vocabulary, min_df=2 to remove tokens appearing in fewer than 2 documents (noise), sublinear_tf=True to log-scale TF values and reduce the effect of very high-frequency terms.
4
Train a classifier. Logistic Regression is the default choice — fast, interpretable, works well with sparse high-dimensional features. Naive Bayes (MultinomialNB) is even faster and competitive on short texts. Linear SVM (LinearSVC) is often the strongest performer. Never use tree-based models (Random Forest, XGBoost) on raw TF-IDF — they can't efficiently split sparse high-dimensional features.
5
Evaluate and inspect. Report accuracy + macro-averaged F1 (F1 is more informative when classes are imbalanced). Use classification_report to see per-class precision/recall. Inspect: extract top positive/negative features per class using coef_ — this immediately tells you which words are driving decisions, and exposes errors ("not" being ignored due to stop word removal).

08 — Code: Complete NLP Pipeline with spaCy and scikit-learn

The code below implements the full pipeline — spaCy-based preprocessing including lemmatisation and stop word removal, TF-IDF feature extraction with bigrams, three classifiers compared, and feature inspection. This is a production-quality template.

import re, spacy
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from sklearn.datasets import fetch_20newsgroups
from sklearn.metrics import classification_report, accuracy_score
import numpy as np

# ── Load spaCy model ─────────────────────────────────────────────────────
# python -m spacy download en_core_web_sm
nlp = spacy.load('en_core_web_sm', disable=['parser', 'ner'])  # fast: only tagger

def preprocess(text):
    """Full preprocessing: lowercase, clean, lemmatise, remove stops."""
    text = text.lower()
    text = re.sub(r'http\S+|www\S+', ' ', text)        # strip URLs
    text = re.sub(r'[^a-z0-9\s]', ' ', text)           # remove punctuation
    text = re.sub(r'\s+', ' ', text).strip()           # collapse whitespace
    doc = nlp(text)
    tokens = [
        token.lemma_ for token in doc
        if not token.is_stop
        and not token.is_punct
        and token.is_alpha
        and len(token.text) > 2
    ]
    return ' '.join(tokens)

# ── Load 20 Newsgroups (4 categories) ───────────────────────────────────
cats = ['sci.space', 'talk.religion.misc', 'comp.graphics', 'rec.sport.hockey']
train = fetch_20newsgroups(subset='train', categories=cats, remove=('headers','footers'))
test  = fetch_20newsgroups(subset='test',  categories=cats, remove=('headers','footers'))

print(f"Train: {len(train.data)} docs | Test: {len(test.data)} docs")

# Preprocess (takes ~30s with spaCy lemmatisation)
train_clean = [preprocess(t) for t in train.data]
test_clean  = [preprocess(t) for t in test.data]

# ── TF-IDF vectoriser ────────────────────────────────────────────────────
tfidf = TfidfVectorizer(
    ngram_range   = (1, 2),      # unigrams + bigrams
    max_features  = 50000,       # cap vocabulary size
    min_df        = 2,           # ignore tokens in < 2 docs (noise)
    sublinear_tf  = True,        # log(1 + tf) — reduce impact of high counts
    strip_accents = 'unicode',   # normalise accented chars
)
X_train = tfidf.fit_transform(train_clean)   # fit only on train
X_test  = tfidf.transform(test_clean)        # never fit_transform on test
print(f"Vocabulary size: {len(tfidf.vocabulary_):,}")
print(f"Matrix shape: {X_train.shape}  (sparse: {X_train.nnz / X_train.shape[0] / X_train.shape[1]:.4%} non-zero)")

# ── Compare three classifiers ────────────────────────────────────────────
models = {
    'Logistic Regression': LogisticRegression(C=5.0, max_iter=1000, solver='saga'),
    'Naive Bayes':         MultinomialNB(alpha=0.1),
    'Linear SVM':          LinearSVC(C=1.0, max_iter=2000),
}
for name, clf in models.items():
    clf.fit(X_train, train.target)
    preds = clf.predict(X_test)
    acc = accuracy_score(test.target, preds)
    print(f"{name:22s}  accuracy={acc:.4f}")

# ── Full report for best model (SVM) ────────────────────────────────────
best = models['Linear SVM']
preds = best.predict(X_test)
print("\n" + classification_report(test.target, preds, target_names=cats))

# ── Inspect top features per class ───────────────────────────────────────
feat_names = np.array(tfidf.get_feature_names_out())
for i, cat in enumerate(cats):
    top_idx = best.coef_[i].argsort()[-8:][::-1]
    print(f"{cat}: {', '.join(feat_names[top_idx])}")

# ── NER demo with spaCy ───────────────────────────────────────────────────
nlp_full = spacy.load('en_core_web_sm')   # full pipeline with NER
sample = "NASA announced a $2.5B contract with SpaceX for the Artemis mission in Houston."
doc = nlp_full(sample)
for ent in doc.ents:
    print(f"  {ent.text:20s} → {ent.label_} ({spacy.explain(ent.label_)})")
── Dataset ────────────────────────────────────────────────────────────
Train: 2,170 docs | Test: 1,447 docs
Vocabulary size: 47,382
Matrix shape: (2170, 47382)   sparse: 0.31% non-zero
── Classifier comparison ──────────────────────────────────────────────
Logistic Regression accuracy=0.9323
Naive Bayes accuracy=0.9047
Linear SVM accuracy=0.9461
── Classification report (Linear SVM) ────────────────────────────────
                     precision recall f1-score support
sci.space   0.97     0.97    0.97     394
talk.religion.misc   0.92     0.89    0.90     251
comp.graphics   0.95     0.97    0.96     389
rec.sport.hockey   0.96     0.97    0.97     399
── Top features per class ─────────────────────────────────────────────
sci.space: space, nasa, orbit, lunar, mission, launch, shuttle, astronaut
talk.religion.misc: god, jesus, christian, faith, believe, bible, sin, religion
comp.graphics: image, gif, jpeg, pixel, render, graphic, software, color
rec.sport.hockey: hockey, goal, team, season, play, score, player, cup
── NER demo ───────────────────────────────────────────────────────────
NASA → ORG (Companies, agencies, institutions)
$2.5B → MONEY (Monetary values, including unit)
SpaceX → ORG (Companies, agencies, institutions)
Artemis → WORK_OF_ART (Titles of books, songs, etc.)
Houston → GPE (Countries, cities, states)

09 — When to Use What: Comparing All Representations

Classical NLP offers several text representation strategies. The right choice depends on your task, data size, latency requirements, and interpretability needs. Here is the complete comparison:

RepresentationCaptures word orderHandles unseen wordsInterpretableSpeedBest for
Bag-of-Words ✗ None ✗ No ✓ Yes ✓ Fastest Topic classification, keyword matching
TF-IDF ✗ None ✗ No ✓ Yes ✓ Fast Classification, search, keyword extraction
TF-IDF + Bigrams ~ Partial ✗ No ✓ Yes ✓ Fast Sentiment analysis, phrase matching
BM25 ✗ None ✗ No ✓ Yes ✓ Fast Search ranking, RAG first-stage retrieval
Word2Vec (avg) ✗ None ~ Via subwords ~ Partial ~ Medium Semantic similarity, clustering
BERT embeddings ✓ Full ✓ Subword ✗ No ✗ Slow All tasks, best accuracy when data available
⚡ The Practical Decision Rule

Start with TF-IDF + Logistic Regression. If it achieves acceptable accuracy, ship it — you gain interpretability, fast inference, and minimal infrastructure. If it falls short: add bigrams, tune C and max_features, try LinearSVC. If still insufficient, move to word embeddings (Day 23). Only reach for BERT/transformers (Day 26) when you need the last few percent of accuracy and can afford the compute cost.

This is not a "start simple then upgrade" academic exercise. TF-IDF classifiers in production outperform fine-tuned BERT on data-scarce tasks because transformers need hundreds of examples per class to generalise well. The simpler model is genuinely better when data is limited.

10 — The Fundamental Limitations — and Why They Lead to Embeddings

Understanding what classical NLP cannot do is as important as understanding what it can. These limitations are not engineering failures — they are principled consequences of the bag-of-words assumption, and each one motivated a specific next technique.

No word relationships
"king" and "queen" are as unrelated as "king" and "banana" in a BoW vector. Two synonyms — "automobile" and "car" — are completely different tokens even though they mean the same thing. A classifier trained on one won't recognise the other. → Word2Vec (Day 23): places words in a continuous vector space where similar words cluster together. king − man + woman ≈ queen.
No word sense disambiguation
"Bank" (financial) and "bank" (river) are the same token. "Apple" (fruit) and "Apple" (company) are identical. The vector is fixed regardless of context. → BERT (Day 22): generates different embeddings for the same word depending on surrounding context — "bank" in a financial sentence gets a different vector than "bank" in a geography sentence.
No long-range dependencies
"The movie, which my friend who normally hates action films but made an exception for last Tuesday, was not good." — the negation ("not") applies to "good" separated by 20 tokens. No n-gram window captures this. → Transformers (Day 22): attention mechanism directly connects any two tokens regardless of distance.
High dimensionality / sparsity
A 100,000-word vocabulary TF-IDF vector for a 200-word document has 99,800 zeros. Storing and computing over sparse matrices is memory-efficient but loses structural relationships. → Dense embeddings: a 300-dimensional Word2Vec vector is dense (every value non-zero) and contains geometric structure — the cosine similarity between "king" and "queen" is high, between "king" and "banana" is low.

11 — Why Classical NLP Still Powers Production Systems

The year is 2026. Transformers dominate NLP benchmarks. Yet classical NLP techniques run at the core of many of the largest systems in production. Understanding why tells you something important about engineering.

BM25 runs the first retrieval stage in most RAG pipelines. Every "ChatGPT with web search" system uses BM25 (or an equivalent sparse retrieval method) to do a fast first-pass retrieval over millions of documents in milliseconds. Neural retrieval (dense embeddings) is far too slow to run first at scale. BM25 finds the candidates; embeddings rerank the shortlist.

TF-IDF classifiers power industrial log analysis. A server generating 10 million log lines per day cannot run BERT on each line — latency and cost are prohibitive. TF-IDF + Naive Bayes classifies log severity in microseconds, routing critical alerts in real time. For this task it is not a "weaker backup" — it is the correct engineering choice.

Sparse representations are more interpretable in regulated industries. Healthcare, legal, and financial compliance systems must explain predictions. "This contract was flagged because it contains the terms 'termination,' 'liability waiver,' and 'governing law' with high TF-IDF scores" satisfies a compliance audit. "The transformer's attention weights suggest..." does not.

Classical preprocessing is still required for transformers. BERT doesn't take raw text — it takes subword tokens produced by WordPiece tokenisation, a direct descendant of the techniques in this post. The preprocessing pipeline (cleaning, normalisation, tokenisation) is still step one for every modern NLP system. You can't skip it just because the model is more powerful.

12 — A Thought to Sit With

💬 For You to Consider

Bag-of-words completely discards word order. "The dog bit the man" and "The man bit the dog" are identical vectors. Yet on topic classification tasks, this approach achieves 94% accuracy. A TF-IDF classifier trained on news articles can correctly distinguish sports from politics from science without ever knowing the sentence "The politician won the election" is different from "The election won the politician."

If a model that ignores word order can classify text at near-human accuracy for many tasks, what does that tell us about how much meaning is carried by which words appear versus the order they appear in? And if most of meaning lives in word co-occurrence patterns rather than syntax — does that change how you think about language itself?

13 — Coming Up Next

Tomorrow — Day 20: Recurrent Neural Networks. Classical NLP treats language as bags of tokens — order doesn't matter, memory doesn't exist. The RNN was the first serious attempt to fix that: a network that reads a sequence word by word, carrying a hidden state forward as it goes, so that every word is understood in light of everything that came before it. We'll build the RNN cell from scratch, trace the forward pass, derive exactly why gradients vanish over long sequences — and understand why that one mathematical problem drove 20 years of research leading straight to the transformer.

14 — Go Deeper

▶ TF-IDF Explained Visually — Computerphile
YouTube · 12 min · Clear walkthrough of the formula with worked examples and search engine context.

🔗 spaCy Linguistic Features — Official Docs
Complete reference for POS tagging, NER, dependency parsing, and lemmatisation in spaCy. The best NLP library for production pipelines.

🔗 sklearn Text Feature Extraction — Official Docs
Full API reference for TfidfVectorizer, CountVectorizer, HashingVectorizer with examples.

📄 TF-IDF and BM25 Lecture Notes — Cambridge University
PDF · The mathematical derivation of BM25 from first principles. Section 3 explains the saturation and length-normalisation motivation clearly.

📄 Efficient Estimation of Word Representations — Mikolov et al. (2013)
The Word2Vec paper — a direct preview of Day 23. Understanding the motivation section (why BoW falls short) is essential context for embeddings.

🧠 Check Your Understanding
1. Walk through how TF-IDF scores are computed. Why does a word appearing in every document get a score near zero, and what does that mean for the word "the"?
2. What is the difference between stemming and lemmatisation? Give an example where stemming produces a wrong answer that lemmatisation gets right, and explain when you'd choose each.
3. You're building a sentiment classifier. Your TF-IDF + Logistic Regression model achieves 88% accuracy, but you notice it always classifies "not great" as positive. Why does this happen, and what is the fix?
4. Why is BM25 still the standard first-stage retriever in production RAG systems in 2026, even though dense vector retrieval (FAISS + embeddings) is semantically more powerful?
5. What does Named Entity Recognition produce, and give two concrete examples of how NER output is used in downstream NLP applications.
⚡ Quick Recap — NLP Fundamentals
  1. The core problem: text has no natural numerical representation. Every NLP technique is an answer to "how do we represent language as numbers that capture meaning?" The progression: BoW → TF-IDF → embeddings → contextualised embeddings.
  2. Preprocessing pipeline: lowercase → strip noise → tokenise → remove stop words → stem/lemmatise. Apply the same fitted pipeline to train and test. Never fit on test data.
  3. Stemming vs lemmatisation: stemming is fast but produces non-words ("studi"). Lemmatisation is slower but returns real base forms ("study"). Use lemmatisation when correctness matters; stemming when speed dominates.
  4. Bag-of-Words: represent a document as a count vector over the vocabulary. Throws away word order entirely. "Dog bites man" = "Man bites dog." Surprisingly effective for topic classification despite this.
  5. TF-IDF: TF(t,d) × IDF(t). Amplifies rare-but-present words (document's signature terms), suppresses common words (no discriminative value). The standard feature representation for classical text classification.
  6. N-grams: unigram + bigram (1,2) is the standard setting. Bigrams capture "not good" as a single negative feature, improving sentiment analysis. Vocabulary grows as V², so trigrams are rarely worth the cost.
  7. BM25: TF-IDF with saturation (prevents term stuffing) and document-length normalisation. The standard sparse retriever in search engines and RAG pipelines. Still the fastest way to retrieve from billion-document corpora.
  8. POS tagging + NER: grammatical structure and entity identification. Enable information extraction, knowledge graph construction, relation detection, and compliance systems. spaCy provides both in one efficient pipeline.
  9. Classical NLP vs transformers: TF-IDF + LR is the right choice when data is limited, interpretability is required, or latency/cost matter. Transformers win when accuracy is the only goal and data is plentiful.
  10. Fundamental limitation: BoW has no concept of word relationships — "king" and "queen" are as unrelated as "king" and "banana." This limitation is what motivated word embeddings (Day 23) and ultimately transformers (Day 26).
🎯 Interview Questions
  1. What is TF-IDF and why is it better than raw word counts?
    TF-IDF = Term Frequency × Inverse Document Frequency. Raw counts over-weight words that appear frequently everywhere — "the" gets a high count in every document but distinguishes nothing. IDF multiplies each term's frequency by the log of (total documents / documents containing the term), so terms appearing in every document get near-zero weight, while terms appearing in few documents get high weight. The result: each document's TF-IDF vector highlights its signature vocabulary — the rare-but-present words that actually distinguish it from other documents.
  2. What information does bag-of-words discard, and when does that matter?
    Word order, grammar, and word relationships are all discarded — "not good" and "good not" produce the same vector. For topic classification, this rarely matters because topic signal lives in which words appear. For sentiment analysis (negation), question answering (subject-object relationships), and reasoning tasks (logical connectives), the loss of order is significant. Adding bigrams recovers some order sensitivity by treating adjacent pairs as atomic features.
  3. What is the difference between stemming and lemmatisation, and when do you choose each?
    Stemming uses heuristic suffix-stripping rules and may produce non-words ("studies" → "studi"). Lemmatisation uses a vocabulary and morphological analysis to return the actual dictionary base form ("studies" → "study," "was" → "be"). Use stemming when speed is critical and output quality is less important (internal pipeline steps). Use lemmatisation when correctness matters — keyword extraction, output shown to users, or building knowledge bases where incorrect base forms cause downstream errors.
  4. When would you use TF-IDF over a transformer model?
    When training data is limited (transformers need hundreds of examples per class; TF-IDF generalises with dozens), when interpretability is a hard requirement (you can inspect which terms drive predictions — essential for compliance), when inference must be extremely fast or cheap (TF-IDF is orders of magnitude faster than BERT), or when establishing a baseline — always benchmark TF-IDF + LR before investing in neural infrastructure. If the simpler model achieves acceptable accuracy, it is the correct engineering choice.
  5. What is BM25 and how does it relate to TF-IDF?
    BM25 is a refined TF-IDF variant for information retrieval. It adds two improvements: (1) saturation — a term appearing 100 times doesn't score 100× higher than one appearing once; a logarithmic cap prevents term stuffing; (2) document-length normalisation — longer documents are penalised for naturally accumulating higher raw TF scores. BM25 is the standard retrieval algorithm in Elasticsearch, Apache Lucene, and the first-stage retriever in RAG pipelines. It finds candidate documents at millisecond scale before dense embedding models do semantic reranking.
📎 Attachments & Further Reading

▶ TF-IDF Explained Visually — Computerphile
YouTube · 12 min · Best visual walkthrough of TF-IDF with search engine context and worked numeric examples.

🔗 spaCy Linguistic Features — spacy.io
Official spaCy docs for POS tagging, NER, dependency parsing, and lemmatisation. The definitive reference for production NLP preprocessing in Python.

📄 Efficient Estimation of Word Representations in Vector Space — Mikolov et al. (2013)
The Word2Vec paper. The motivation section (§1–2) explains precisely what BoW/TF-IDF cannot do and what embeddings solve. Essential pre-reading for Day 23.

📚 Speech and Language Processing — Jurafsky & Martin (3rd ed., free online)
The definitive NLP textbook. Chapters 2–4 cover tokenisation, n-grams, and text classification in full mathematical rigour. Chapter 14 covers constituency and dependency parsing. Free PDF at web.stanford.edu/~jurafsky/slp3/.

#NLP#Tokenisation#TFIDF#BagOfWords#BM25#NGrams#Lemmatisation#Stemming#NER#TextClassification#spaCy#NaturalLanguageProcessing#MachineLearning#30DaysOfAI#Ramadan2026
← Day 18 Object Detection, YOLO & Segmentation