Text Preprocessing: Tokenization › Core Preprocessing Techniques
Text Preprocessing: Tokenization › Core Preprocessing TechniquesOne section · Sections in this article2 / 8 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Imagine you're teaching a computer to read. Unlike humans who instantly recognize words, sentences, and meaning, computers see text as nothing more than a sequence of characters. The sentence "The cat's sitting on the mat" appears to a computer as 30 characters: letters, spaces, an apostrophe, and a period. Before any machine learning model can understand this text, we must transform it from a continuous stream of characters into discrete, meaningful units that algorithms can process.
This transformation is the essence of text preprocessing, and it requires answering three fundamental questions:
- How do we break text into meaningful units? Where do words begin and end? How do we handle punctuation, contractions, and special cases?
- How do we handle variation? "Running," "running," and "RUNNING" represent the same concept but appear as different strings. How do we standardize these variations?
- What should we remove? Real-world text contains noise: HTML tags, URLs, typos, and formatting artifacts. What information is essential, and what can we safely discard?
These questions lead us to three core preprocessing operations: tokenization breaks text into discrete units, normalization standardizes variations, and cleaning removes noise. But understanding these techniques requires more than memorizing definitions. We need to grasp why each operation is necessary and how they work together to transform messy human language into structured data that models can learn from.
Let's build this understanding step by step, starting with the most fundamental question: how do we identify where one word ends and another begins?
Tokenization: Breaking Text Into Units
Tokenization answers our first fundamental question: how do we break continuous text into discrete, meaningful units? This is the foundation of all text processing because you cannot normalize, clean, or analyze text until you've identified its basic building blocks.
Think of tokenization like cutting a loaf of bread into slices. The loaf is the continuous text string, and each slice is a token, a discrete unit we can work with. But here's the challenge: unlike bread, text doesn't have natural "cut lines." Where exactly should we split "don't"? Is it one word or two? Should "U.S.A." be one token or three? These ambiguities force us to make decisions that affect everything downstream.
Why tokenization matters: Without tokenization, we can't count word frequencies, build vocabularies, or apply any statistical or machine learning techniques. A model that sees "The cat's sitting" as 30 characters has no way to learn that "cat" and "sitting" are meaningful units. Tokenization transforms the problem from "understanding 30 characters" to "understanding 7 tokens," dramatically reducing complexity.
The challenge of tokenization lies in ambiguity. Spaces seem like obvious delimiters in English, but they're insufficient for handling:
- Contractions like "don't" (one word or two?)
- Hyphenated words like "state-of-the-art" (one word or four?)
- Punctuation: should "mat." be "mat" + "." or just "mat."?
- Special cases: URLs, email addresses, abbreviations
Different languages compound these challenges: Chinese and Japanese don't use spaces between words, German creates long compound words, and Arabic uses diacritical marks that may or may not be present.
The progression of tokenization methods: We'll explore tokenization by starting with the simplest approach and progressively building more sophisticated methods. Each method solves some problems while introducing new tradeoffs, teaching us what's truly necessary for robust text processing.
A token is a discrete unit of text used in NLP processing. Tokens are typically words, but can also be punctuation marks, numbers, subwords, or any meaningful unit depending on the tokenization strategy.
The most naive approach simply splits text on whitespace characters (spaces, tabs, newlines). This works surprisingly well for English text that has already been cleaned, but fails on several common cases:
text = "The cat's sitting on the mat."
tokens = text.split()
print(tokens)['The', "cat's", 'sitting', 'on', 'the', 'mat.']
Notice that "cat's" remains as a single token with the apostrophe, and "mat." includes the period. For many applications, we want to separate these punctuation marks as distinct tokens.
Punctuation-Aware Tokenization
A more robust approach treats punctuation as separate tokens. We can use regular expressions to split on word boundaries while keeping punctuation:
import re
text = "The cat's sitting on the mat."
tokens = re.findall(r"\w+|[^\w\s]", text)
print(tokens)['The', 'cat', "'", 's', 'sitting', 'on', 'the', 'mat', '.']
This pattern \w+|[^\w\s] matches sequences of word characters or individual non-whitespace, non-word characters. The apostrophe and period are now separated, but we've lost the distinction between "cat's" as a possessive and other uses of apostrophes.
Linguistic Tokenization
Production NLP systems typically use linguistic tokenizers that understand language-specific rules. These tokenizers know that "don't" should become "do" and "n't," that "U.S.A." is a single token despite the periods, and that URLs should remain intact. Libraries like NLTK and spaCy provide industrial-strength tokenizers:
import nltk
nltk.download("punkt_tab", quiet=True)
nltk.download("punkt", quiet=True)
text = (
"Dr. Smith doesn't work at U.S.A. Inc. anymore. Visit https://example.com!"
)
tokens = nltk.word_tokenize(text)
print(tokens)['Dr.', 'Smith', 'does', "n't", 'work', 'at', 'U.S.A.', 'Inc.', 'anymore', '.', 'Visit', 'https', ':', '//example.com', '!']
Notice how "Dr." stays together, "doesn't" splits into "does" + "n't", and the URL remains intact. These tokenizers use trained models and hand-crafted rules to handle thousands of edge cases.
Modern deep learning models often use subword tokenization methods like Byte-Pair Encoding (BPE) or WordPiece. These methods split rare words into common subword units, handling unknown words gracefully while keeping vocabulary size manageable:
## Example conceptual breakdown (actual BPE requires trained vocabulary)
## "unbelievable" might become: ["un", "believ", "able"]
## "unhappiness" might become: ["un", "happiness"]The key insight is that morphological patterns (prefixes like "un-", suffixes like "-able") appear across many words. By learning these subword units from data, models can understand rare or novel words by composing their parts. This is how BERT and GPT handle words they've never seen before.
Normalization: Reducing Variation
Once we've tokenized our text, we've solved the problem of identifying word boundaries. But we've created a new problem: vocabulary explosion. Consider what happens when we tokenize a simple sentence:
- "The cat runs" → ["The", "cat", "runs"]
- "The cat ran" → ["The", "cat", "ran"]
- "The CAT RUNS" → ["The", "CAT", "RUNS"]
To a computer, these are completely different tokens. "runs," "ran," and "RUNS" are treated as three distinct words, even though they represent the same concept. This variation creates several problems:
-
Vocabulary size explosion: Instead of learning one representation for "run," a model must learn separate representations for "run," "runs," "ran," "running," "RUN," "RUNS," etc. This wastes model capacity and training data.
-
Data sparsity: With more unique tokens, each token appears less frequently. Rare tokens have unreliable statistics, making it harder for models to learn meaningful patterns.
-
Generalization failure: A model trained on "running" might not recognize "runs" as related, even though they're linguistically connected.
Normalization solves this by mapping variations to canonical forms. Instead of treating "running," "runs," and "RUNNING" as different tokens, normalization reduces them to a common representation. This decreases vocabulary size, increases token frequency, and helps models recognize that morphologically related words share meaning.
But normalization is a tradeoff: we gain efficiency and generalization at the cost of losing information. "EXCITED!!!" conveys more emotion than "excited," and "Apple" (the company) differs from "apple" (the fruit). Understanding when to normalize and how aggressively is crucial for building effective NLP systems.
Case Normalization
The simplest normalization converts all text to lowercase. This reduces vocabulary size by treating "The," "the," and "THE" as identical:
tokens = ["The", "Cat", "SLEEPS"]
normalized = [token.lower() for token in tokens]
print(normalized)['the', 'cat', 'sleeps']
Case normalization is almost universal in NLP, but consider your task carefully. For sentiment analysis, "EXCITED!!!" conveys more emotion than "excited." For named entity recognition, "Apple" (the company) differs from "apple" (the fruit). Modern transformer models often preserve case and learn case-sensitive representations, capturing these nuances.
Stemming: Crude But Fast
Stemming algorithms use heuristic rules to chop off word endings, reducing words to their approximate root form. The Porter Stemmer, developed in 1980, remains widely used despite its crudeness:
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
words = ["running", "runs", "ran", "runner", "easily", "fairly"]
stems = [stemmer.stem(word) for word in words]
print(list(zip(words, stems)))[('running', 'run'), ('runs', 'run'), ('ran', 'ran'), ('runner', 'runner'), ('easily', 'easili'), ('fairly', 'fairli')]Notice the issues: "ran" doesn't reduce to "run" because it's irregular, "runner" stays unchanged, and "easily" becomes the non-word "easili." Stemming is fast and language-independent, but produces non-words and misses linguistic relationships.
Stemming is the process of reducing words to their root form by removing suffixes using heuristic rules. Unlike lemmatization, stemming produces stems that may not be valid words, prioritizing speed and simplicity over linguistic accuracy.
Lemmatization: Linguistically Informed
Lemmatization uses vocabulary and morphological analysis to return words to their dictionary form (lemma). It's slower than stemming but more accurate:
from nltk.stem import WordNetLemmatizer
nltk.download("wordnet", quiet=True)
nltk.download("omw-1.4", quiet=True)
lemmatizer = WordNetLemmatizer()
words = ["running", "runs", "ran", "better", "was"]
lemmas = [lemmatizer.lemmatize(word, pos="v") for word in words]
print(list(zip(words, lemmas)))[('running', 'run'), ('runs', 'run'), ('ran', 'run'), ('better', 'better'), ('was', 'be')]Lemmatization correctly handles irregular verbs like "ran" → "run" and "was" → "be," but requires knowing the part-of-speech (verb, noun, adjective) for accurate results. This makes it more expensive but linguistically sound.
Lemmatization is the process of reducing words to their base dictionary form (lemma) using vocabulary knowledge and morphological analysis. Unlike stemming, lemmatization produces valid words and handles irregular forms correctly, but requires more computational resources and linguistic knowledge.
When to Normalize
The tradeoff between stemming and lemmatization mirrors a broader question: how much normalization is appropriate? Aggressive normalization reduces vocabulary size and improves generalization but loses information. For small datasets and simple models like Naive Bayes, aggressive normalization helps by reducing sparsity. For large datasets and neural models, minimal normalization often works better because models can learn morphological patterns from data.
Modern pretrained transformers like BERT often use subword tokenization instead of stemming or lemmatization. Their WordPiece vocabulary naturally handles morphology: "running" might tokenize as ["run", "##ning"], allowing the model to learn that words ending in "##ning" relate to their base form.
Cleaning: Removing Noise
After tokenization and normalization, we have discrete, standardized tokens. But real-world text contains more than just words. It's filled with noise: formatting artifacts, non-linguistic content, and encoding inconsistencies that don't contribute to meaning but can confuse models and waste computational resources.
Why cleaning matters: Consider what happens when we process web-scraped text. A sentence like "Check out this site: https://example.com!" contains:
- HTML tags (
<b>,</b>) that are formatting instructions, not linguistic content - A URL that's a reference, not a word
- Punctuation that may or may not be meaningful
If we don't clean this text, our tokenizer might create tokens like <b>, </b>, https://example.com, which:
- Add noise to our vocabulary without contributing semantic meaning
- Waste model capacity learning patterns in formatting artifacts
- Create spurious correlations (e.g., learning that
<b>appears near certain words)
The cleaning challenge: Cleaning requires deciding what's signal versus noise, and this decision is task-dependent. For sentiment analysis, emojis and punctuation intensity markers (like "!!!") are crucial signals. For document classification, they might be noise. For medical text, numbers are essential; for topic modeling, they might be irrelevant.
Cleaning techniques remove:
- Formatting artifacts: HTML tags, markdown, special characters
- Non-linguistic content: URLs, email addresses, phone numbers (depending on task)
- Encoding inconsistencies: Unicode normalization, character encoding issues
- High-frequency noise: Stopwords (words like "the," "is" that appear everywhere but carry little semantic information for many tasks)
The key insight is that cleaning is about selective removal. We remove what doesn't help our specific task while preserving what does. This requires understanding both your data and your downstream application.
Stopword Removal
Stopwords are high-frequency words like "the," "is," "at," and "which" that occur in nearly every document but carry little semantic information for many tasks. Removing them reduces feature space dramatically:
from nltk.corpus import stopwords
nltk.download("stopwords", quiet=True)
stop_words = set(stopwords.words("english"))
tokens = ["the", "cat", "is", "sitting", "on", "the", "mat"]
filtered = [word for word in tokens if word not in stop_words]
print(filtered)['cat', 'sitting', 'mat']
Stopwords are high-frequency words that carry little semantic information for certain NLP tasks. Common examples include articles ("the", "a"), prepositions ("on", "at"), and auxiliary verbs ("is", "have"). Removing stopwords reduces dimensionality but may hurt performance on tasks where these words matter.
However, stopword removal is controversial. For document classification, removing stopwords improves efficiency without hurting accuracy. For semantic similarity or machine translation, stopwords provide crucial grammatical structure. The phrase "not good" means something very different from "good," but "not" is often in stopword lists.
Modern neural models rarely use stopword removal. Attention mechanisms in transformers learn to ignore uninformative words automatically, and the computational cost of a few extra tokens is negligible. Stopword removal remains useful primarily for classical methods like TF-IDF where vocabulary size directly impacts memory usage.
Handling Special Characters and Noise
Web text requires aggressive cleaning to remove HTML tags, URLs, email addresses, and other non-linguistic content:
import re
def clean_text(text):
# Remove HTML tags
text = re.sub(r"<[^>]+>", "", text)
# Remove URLs
text = re.sub(r"http\S+|www.\S+", "", text)
# Remove email addresses
text = re.sub(r"\S+@\S+", "", text)
# Remove numbers (optional, depends on task)
text = re.sub(r"\d+", "", text)
# Remove extra whitespace
text = re.sub(r"\s+", " ", text).strip()
return text
messy = "Check out <b>this</b> site: https://example.com! Email me at test@email.com for 50% off!"
cleaned = clean_text(messy)
print(cleaned)Check out this site: Email me at for % off!
Be cautious with aggressive cleaning. For social media sentiment analysis, emojis and emoticons carry emotional content. For medical text, numbers matter. For code documentation, URLs to API references may need to stay. Always consider your specific task when deciding what to remove.
Character Normalization
Text from different sources may use different character encodings or representations. Unicode normalization ensures consistent representation:
import unicodedata
# Different representations of "é"
text1 = "café" # é as single character (U+00E9)
text2 = "café" # é as e + combining accent (U+0065 + U+0301)
print(f"Are they equal? {text1 == text2}") # False - different byte sequences
# Normalize to composed form (NFC)
norm1 = unicodedata.normalize("NFC", text1)
norm2 = unicodedata.normalize("NFC", text2)
print(f"After normalization: {norm1 == norm2}") # TrueAre they equal? True After normalization: True
This is particularly important for multilingual text where the same visual character can have multiple Unicode representations.