Text Normalization
Clean and standardize text before the model sees it. Lowercasing, lemmatizing and stripping noise collapse variants of one word into one form.
Why Does This Exist?
Humans read Run, RUN, running and ran as one idea without thinking. A model with a fixed vocabulary does not. Each surface form gets its own row, its own vector, and its own training bill. The signal splits four ways, rare forms barely get trained, and the vocabulary balloons with entries that teach the same thing.
Normalization exists to collapse those surface differences into one canonical form before tokenization and learning. Four variants become one lemma, three duplicate rows disappear, and every example trains the same entry. It trades a little upfront rule writing for a smaller table and a stronger signal per entry.
This page covers the cleaning pass only. The cut into pieces lives in tokenization, the full order of steps in the NLP pipeline, and the grammar step that needs clean tokens in part of speech tagging.
The one idea you need first
A token is the unit the model counts, often a word or subword with its own ID. If Run and run map to two IDs, the model learns them separately. Normalization decides which differences to erase so one meaning maps to one ID.
Think of It Like This
A parts washer before the assembly line
You run a shop that rebuilds used bolts. They arrive rusty, painted, greasy or bent, but underneath they are the same thread size. If you stock each finish as its own part number, your shelves fill with four bins for one bolt and every reorder splits four ways.
So you run everything through a parts washer first. Off comes paint, grease and rust, and four dirty variants become one clean bolt in one bin. The line downstream only stocks clean sizes.
Where it stops: a washer can strip markings you needed. Wash away case and you merge US the country with us the pronoun. Wash away stop words and you delete the not in not good. Clean for the job at hand, not for cleanliness itself.
How It Actually Works
Normalization is a short pipeline of fixed rules, applied in order. Each rule erases one kind of variation. Use only the rules your task can afford.
Lowercase and fold
Lowercasing maps RUN to run and The to the. Case folding goes further for special letters, such as mapping the German sharp s to ss so it matches the expected spelling. One line, huge dedup. Skip it when case carries meaning, such as names in entity tagging.
Expand and strip
Contractions pack two words into one. A small dictionary maps short forms back to two words, turning do not written as one token into do and not, so the negation stands alone where the model can weigh it. Then strip what adds no signal for your task: commas that split dog from dog, links, tags and extra spaces. Keep what your task reads. Code keeps punctuation, prose often does not.
Reduce to base form
This is the morphological core. Stemming chops endings with blind rules, so running becomes run but ponies becomes poni. Fast, crude, no dictionary. Lemmatization looks the word up and returns its dictionary form, so better becomes good and ran becomes run. It needs the grammar tag to choose well, since saw can come from see or name a tool.
Worked collapse
Take four surface forms: Run, RUN, running, ran.
Lowercase: Run becomes run, RUN becomes run, running stays running, ran stays ran. Distinct forms drop from 4 to 3.
Lemmatize as verbs: run stays run, running becomes run, ran becomes run. Distinct forms drop from 3 to 1.
Four rows become one lemma run. Three duplicate entries disappear, and every later example of any variant trains the same row. Those are the numbers in the diagram above.
The trade is plain. Aggressive cleaning buys a small table and dense counts, and it sells off distinctions your later step may need. Modern fluent models clean less and keep stop words, because word order and small words carry the structure they generate from.
Code
The collapse from the worked example, with plain rules so the 4 to 3 to 1 counts can be checked.
import re
def normalize(tokens: list[str]) -> list[str]: lowered: list[str] = [t.lower() for t in tokens] lemma_map: dict[str, str] = {"running": "run", "ran": "run"} return [lemma_map.get(t, t) for t in lowered]
raw: list[str] = ["Run", "RUN", "running", "ran"]print(raw) # -> ['Run', 'RUN', 'running', 'ran']print(len(set(raw))) # -> 4mid: list[str] = [t.lower() for t in raw]print(mid) # -> ['run', 'run', 'running', 'ran']print(len(set(mid))) # -> 3done: list[str] = normalize(raw)print(done) # -> ['run', 'run', 'run', 'run']print(len(set(done))) # -> 1Four distinct forms, then three after lowercasing, then one after mapping running and ran to run.
Watch Out For
Lowercasing away a distinction you needed
Symptom: entity scores drop after a cleanup pass, and Apple the company now counts the same as apple the fruit. The washer erased case, which was the only signal separating them.
Fix: clean for the downstream job. Keep case for entity and truecasing work, keep stop words for fluent generation, and test each rule on or off with the final metric before locking the pipeline.
Deleting the negation with stop words
Symptom: sentiment flips on phrases like not good, because the stop list removed not and left good. Accuracy looks fine on plain reviews and collapses on negated ones.
Fix: keep negations and intensifiers out of the stop list for sentiment work, or skip stop removal entirely for neural models that read order. Check negated examples as their own slice.
The Quick Version
- Text normalization maps surface variants to one canonical form before tokenization.
- Lowercasing and folding erase case differences in one pass.
- Expanding contractions frees negations, and stripping removes marks your task does not read.
- Stemming chops blindly while lemmatization looks words up, so ran becomes run only in the second.
- Worked case: Run, RUN, running and ran collapse from 4 forms to 3 to 1 lemma.
- Clean for the task at hand, since each rule sells off a distinction some later step may need.