Skip to content

Text Preprocessing (Tokenization, Stemming, Lemmatization)

A network takes numbers. Text is not numbers, and every decision on the way from one to the other — where a token starts, what counts as the same word, which words to keep — is made before the model sees anything and cannot be undone by training.

This page counts those decisions on 5,000 IMDB reviews decoded back into words: 1,219,106 tokens, 43,783 distinct ones. It opens with an uncomfortable observation about that corpus, because it is the most common way this material misleads people.

keras.datasets.imdb ships integer ids, not text. Somebody already tokenized, lowercased and stripped punctuation. Apply those steps yourself and watch nothing happen:

StepDistinct wordsTokens
raw (as shipped)43,7831,219,106
lowercased43,7831,219,106
punctuation stripped43,6181,218,706
27 stopwords removed43,591820,241
suffixes stripped35,215820,241

Lowercasing changes the vocabulary by zero words. Stripping punctuation removes 165. If you benchmark preprocessing on this dataset you will conclude that preprocessing does not matter, and you will be measuring the previous engineer’s work rather than your own.

The evidence that the cleanup was imperfect sits at rank 7 of the frequency table:

RankTokenUsesShare of all tokens
1the68,7380.0564
2and33,4620.0274
3a33,3250.0273
7br20,8640.0171

br is the remains of <br /> tags. It is the seventh most common “word” in the corpus — ahead of in, it and i — and it carries no meaning at all. Every model in this phase spends capacity on it.

  • Why the vocabulary never stops growing: Heaps’ exponent 0.573 on this corpus.
  • Zipf’s law, measured: slope −1.0731 over the first 2,000 ranks.
  • What a vocabulary cap costs: 10,000 words cover 0.9450 of tokens, so 5.5% of the text becomes <oov>.
  • Why 42.9% of the vocabulary is words seen exactly once, and they are 1.54% of the text.
  • What each normalisation step buys and what it destroys.
  • Where classic preprocessing ends and subword tokenization begins.
figure 5,000 IMDB reviews matplotlib
Two panels. Left: distinct words seen against documents read — a curve rising steeply then flattening but never levelling off, reaching 43,783 words at 5,000 reviews, titled with Heaps' exponent 0.573. Right: bars showing 18,762 words seen once at 42.9%, 6,333 seen twice at 14.5%, and 18,688 seen three or more times at 42.7%. Two panels. Left: distinct words seen against documents read — a curve rising steeply then flattening but never levelling off, reaching 43,783 words at 5,000 reviews, titled with Heaps' exponent 0.573. Right: bars showing 18,762 words seen once at 42.9%, 6,333 seen twice at 14.5%, and 18,688 seen three or more times at 42.7%.
The left curve is Heaps' law: distinct words grow as tokens^0.573, so reading twice as much text still turns up about 49% more new words. It never saturates, which is why 'just use the whole vocabulary' is not a strategy. The right panel says what those extra words are — 42.9% of the vocabulary appeared exactly once, and no model learns anything from a single occurrence.
V(n)=knβ,β=0.573f(r)rs,s=1.0731V(n) = k\,n^{\beta}, \quad \beta = 0.573 \qquad\qquad f(r) \propto r^{-s}, \quad s = 1.0731

Heaps’ law says the vocabulary grows without bound. Zipf’s law says the extra words are nearly worthless: frequency falls as roughly 1/r1/r, so a few thousand words are almost all of the text.

figure Zipf's law and what it means for a vocabulary cap matplotlib
Two panels. Left: log-log rank against frequency, a near-straight line with a fitted dashed slope of -1.073, deviating at the head and stepping down to a floor of one at the tail. Right: cumulative token coverage against vocabulary size on a log axis, marked at 1,000 words covering 0.7624, 5,000 covering 0.9013, 10,000 covering 0.9450 and 20,000 covering 0.9764. Two panels. Left: log-log rank against frequency, a near-straight line with a fitted dashed slope of -1.073, deviating at the head and stepping down to a floor of one at the tail. Right: cumulative token coverage against vocabulary size on a log axis, marked at 1,000 words covering 0.7624, 5,000 covering 0.9013, 10,000 covering 0.9450 and 20,000 covering 0.9764.
The straight line on log-log axes is the law. The right panel is the practical consequence, and the reason 10,000 is such a common default: it covers 94.5% of tokens with a quarter of the words. The flat tail on the left — the step down to frequency 1 — is those 18,762 once-seen words, and it is also why the slope is fitted over the first 2,000 ranks only.
VocabularyToken coverageOOV rate
1,0000.76240.2376
5,0000.90130.0987
10,0000.94500.0550
20,0000.97640.0236

Every OOV rate there is text your model never sees. Doubling the vocabulary from 10,000 to 20,000 halves the loss — and doubles the embedding matrix. The next page gets the OOV rate to exactly zero without either cost.

Normalisation: what it buys, what it destroys

Section titled “Normalisation: what it buys, what it destroys”
figure Each step shrinks the vocabulary — and merges words that were different matplotlib
Two panels. Left: horizontal bars for five cumulative normalisation steps, all near 43,700 distinct words except the last, suffix stripping, which drops to 35,215. Right: a monospace list of word groups a crude suffix stripper merges, such as 'affect' from affect, affected, affectedly, affecting, affectingly and affects. Two panels. Left: horizontal bars for five cumulative normalisation steps, all near 43,700 distinct words except the last, suffix stripping, which drops to 35,215. Right: a monospace list of word groups a crude suffix stripper merges, such as 'affect' from affect, affected, affectedly, affecting, affectingly and affects.
Only the last step does real work on this corpus, cutting the vocabulary by 19.6%. The right panel is the price: 'affectingly' and 'affects' become one token, and so do 'car' and 'caring' — a collision no dictionary-based lemmatizer would make. Removing 27 stopwords left the vocabulary almost unchanged (43,618 to 43,591) while deleting 32.7% of all tokens, which is exactly what stopwords are: a handful of types carrying enormous mass.
TechniqueWhat it doesCost
lowercasingThetheloses US vs us, Apple vs apple
punctuation strippingdon'tdontsplits or merges contractions inconsistently
stopword removaldrops the, a, of32.7% of tokens gone; destroys negation and phrasing
stemmingaffectingaffectrule-based; merges words that differ
lemmatizationbettergooddictionary-based, slower, needs a POS tagger

Two of these deserve a warning rather than a description:

  • Stopword removal deleted 32.7% of the corpus. For a bag-of-words model that is usually harmless. For anything that reads word order it is destructive — “not good” and “good” become identical once not is dropped, and plenty of published stopword lists contain not.
  • Stemming merges words a reader would not. The measured collisions include car, cared, cares, caringcar. It buys a 19.6% smaller vocabulary; whether that is worth it depends on whether your model has enough data to learn the inflections separately.

Modern practice is to do less of this, not more. Subword tokenization removes the vocabulary pressure that made aggressive normalisation attractive, and a model with enough data learns that “affects” and “affecting” are related without being told.

diagram Diagram mermaid
sketch Zipf, coverage and the cap you choose p5.js
Drag the vocabulary cap. The bars follow the measured Zipf slope; the readout is what that cap covers and what it throws away.
sketch The measured table, ranked p5.js
Click a column to rank every row by it. The bars are that column's values and the highest and lowest are computed from the numbers, not written in.
  • Benchmarking preprocessing on a pre-processed corpus. Lowercasing IMDB changes the vocabulary by 0 words; you are measuring somebody else’s pipeline.
  • Trusting the cleanup. br is the 7th most frequent token here, at 20,864 uses.
  • Removing stopwords before a sequence model. It deleted 32.7% of tokens, and many stopword lists include not.
  • Calling a rule-based suffix stripper a lemmatizer. caringcar is a measured collision, not a hypothetical one.
  • Choosing a vocabulary cap without measuring coverage. 1,000 words sounds reasonable and discards 23.8% of the text.
  • Assuming a bigger vocabulary is free. It is a bigger embedding matrix, and 42.9% of the words it adds appear exactly once.
  • Fitting the vocabulary on train and test. The cap is learned from training data only, exactly like a scaler.
  • 5,000 IMDB reviews decode to 1,219,106 tokens and 43,783 distinct words; the shipped index has 88,584 entries.
  • Heaps’ law with exponent 0.573 — the vocabulary never stops growing.
  • Zipf’s law with slope −1.0731 — 10,000 words cover 0.9450 of tokens, 1,000 cover 0.7624.
  • 18,762 words (42.9% of the vocabulary) appear exactly once and make up 1.54% of the text.
  • Lowercasing this corpus changed nothing; stopword removal deleted 32.7% of tokens; suffix stripping cut the vocabulary 19.6% and merged car with caring.
  • br, an HTML remnant, is the 7th most common token.

Classic preprocessing manages the vocabulary. Subword tokenization removes the problem instead, and it is what every modern model actually uses: Subword Tokenization (BPE and WordPiece).

pch.quizTag pch.quizDefaultTitle
  1. Lowercasing the decoded IMDB corpus changed the vocabulary from 43,783 words to 43,783 words. What does that tell you?

    pch.quizShowAnswer

    B — This corpus was already lowercased before it shipped — benchmarking preprocessing on it measures the previous engineer's pipeline, not yours — keras.datasets.imdb ships integer ids. Tokenization, lowercasing and punctuation stripping all happened before you loaded it — imperfectly, since 'br' survived as the 7th most frequent token.

  2. A vocabulary of 10,000 words covers 0.9450 of tokens on this corpus. What happens to the rest?

    pch.quizShowAnswer

    B — They collapse to a single <oov> id — 5.5% of every review becomes one meaningless token, and no amount of training recovers it — This is exactly the failure subword tokenization removes: any word can be built from a small set of pieces, so the OOV rate is zero.

  3. 42.9% of the vocabulary appears exactly once, but those words are only 1.54% of the tokens. What follows for the embedding matrix?

    pch.quizShowAnswer

    B — Their embedding rows receive one gradient update each in the entire corpus — they cost memory and learn essentially nothing — Heaps' law guarantees it gets worse with scale: the vocabulary grows as tokens^0.573, so more text brings proportionally more once-seen words.

  4. Removing 27 stopwords deleted 32.7% of all tokens but only 27 distinct words. When is that a bad trade?

    pch.quizShowAnswer

    B — Whenever word order matters — 'not good' becomes 'good' if 'not' is on the list, and many published lists include it — For bag-of-words it is usually harmless. For a sequence model it destroys exactly the structure the model exists to read.

  5. A crude suffix stripper merged 'car', 'cared', 'cares' and 'caring' into 'car'. What is the honest summary of stemming?

    pch.quizShowAnswer

    B — It trades vocabulary size for precision — 19.6% fewer words here, at the cost of collisions a dictionary-based lemmatizer would not make — Modern practice is less normalisation, not more: subword tokenization removes the vocabulary pressure that made stemming attractive.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading