Stemming Versus Lemmatization
Stemming works by chopping endings according to rules, so its output is a stem that may not be a real dictionary word.
Try it: Text Vectorization: Tokens to Vectors
The text-to-tensor pipeline: standardize, split into tokens, build a frequency-ranked vocabulary with padding and [UNK], map tokens to integer ids, pad or truncate, then one-hot or multi-hot encode and look the ids up in an embedding table.
How it works
- Standardize: lowercase and delete punctuation.
- Split on whitespace into tokens.
- Build the vocabulary: 0 = padding, 1 = [UNK], then words by frequency (ties: first appearance), capped at max tokens.
- Map each token to its id (1 if it did not make the vocabulary), then pad with 0 or truncate to the sequence length.
- Encode: one-hot rows per position or one multi-hot row per text; embed: id k selects row k of the embedding table.
Default run (18 steps): Raw text (59 characters): "The movie was great. The plot was great, the acting wasn't!". A model cannot read strings, so it is turned into numbers step by step. … Embedding lookup: id k selects row k of a fixed 8 x 3 table, giving a (8, 3) dense matrix instead of (8, 8) sparse one-hots. Row 0 (padding) is looked up too; a trained Embedding layer would learn these rows.
Simplified: Bounded ASCII text (up to 200 characters, 40 tokens), whitespace splitting only, and a small FIXED embedding table (a real Embedding layer learns its rows). The multi-hot vector keeps the padding slot (always 0) so its positions line up with the ids.
Loading the simulation…
Why Word Forms Split Your Features
When you turn text into numbers with CountVectorizer, every distinct word becomes a column and every document becomes a row of counts. The words are treated exactly as they appear. That means forms such as run, running, and runs are counted as three separate columns, even though they all point to the same underlying word. For a classifier or a search index, this can quietly inflate the vocabulary and split the evidence for one idea across several features.
This article answers a practical question: how do you reduce related word forms to a shared base form when you use CountVectorizer? The answer involves two separate ideas, stemming and lemmatization, and a hook that CountVectorizer provides so you can plug in your own processing. We will trace one document through that processing, read the code that builds it, and finish by learning where things go wrong when the output is unexpected.
Stemming and Lemmatization Compared
Both stemming and lemmatization aim to reduce word forms to a shared base form. They differ in how they get there. Stemming works by chopping endings according to rules, so its output is a stem that may not be a real dictionary word. Lemmatization looks up the base form, called the lemma, which is a proper word. Because lemmatization uses word identity and context rather than suffix rules alone, it tends to produce cleaner base forms for irregular words.
Notice the difference in what you get. A stemmer can leave better unchanged because no suffix rule applies, while a lemmatizer can map it to good. Neither result is wrong in an absolute sense; each reflects a design choice. The important point for this article is that CountVectorizer performs neither operation itself, so whichever result you want must come from the text processing you hand it.
The Tokenizer Parameter as a Callable Hook
The source is explicit that scikit-learn implements neither stemming nor lemmatization as part of CountVectorizer. What it does provide is a tokenizer parameter that accepts your own callable. A callable is a function you supply. It takes a string and returns a list of tokens. CountVectorizer calls your function on each document, receives the list, and counts the items in it.
If you supply a function that returns lemmas instead of raw words, CountVectorizer will count those lemmas. The vectorizer itself is unchanged. Only the list it receives from your function has changed.
This division of labour is useful to keep in mind. CountVectorizer handles the counting, the matrix layout, and the vocabulary. Your function handles the decision about what a token is and what form it takes. When the output looks odd, you can ask which of those two responsibilities produced it.
Tracing One Document Through the Pipeline
Follow a single document as lemma_vect fits or transforms it. The path has four stages. The raw string enters the regular-expression tokenizer, which produces word-like tokens. Those tokens are then passed through spaCy's lemmatization so that each one is replaced by its lemma. The resulting list of lemmas is handed to CountVectorizer, which builds columns and counts. Read the diagram from top to bottom. The regular expression is applied before spaCy's lemmatization, not after.
Each arrow in this chain is a call made by the code. The string is the input to the first stage, and each stage's output is the input to the next. If any stage returns something unexpected, every stage after it will carry that problem forward, which is why the order of inspection matters when you debug.
Reading the Setup Code Line by Line
The complete setup has three parts. First is a regular-expression tokenizer copied from CountVectorizer. Second is a spaCy language model whose tokenizer is swapped out for that regular-expression tokenizer. Third is a custom_tokenizer function that returns lemmas. The final line creates the vectorizer that uses that function. The source assumes that the spacy and CountVectorizer imports have already been made earlier in the course, and that text_train, a collection of training documents, is already loaded as strings.
Reading the setup, notice that it runs without transforming any document. Once the cell has run, lemma_vect exists and is ready to be fitted, but no counts have been produced yet. Keep the three parts separate in your head: the tokenizer swap, the lemma-producing function, and the vectorizer that receives the function.
How the Tokenizer and the Lemmatizer Connect
The connection is a chain of hand-offs through one object. The regular-expression tokenizer is installed on en_nlp, so when custom_tokenizer passes a document to en_nlp, spaCy splits it using the copied regular expression. spaCy then runs its lemmatization on those tokens. custom_tokenizer reads the lemma_ attribute of each token and collects the results into a list. That list is the value CountVectorizer receives back from the tokenizer parameter.
A useful way to check the connection is to ask of each function, what does it receive, and what does it return? The regular expression receives characters and returns word-like tokens. The spaCy step receives those tokens and returns annotated tokens with lemmas. custom_tokenizer receives a string and returns a list of lemma strings. CountVectorizer receives that list and returns counts.
Comparing Vocabulary Shapes
The source fits the lemmatizing vectorizer on the training text, transforms the same text, and prints the shape of the result. It then creates a standard CountVectorizer with the same min_df setting as a reference and prints its shape as well. Comparing the two printed shapes shows what lemmatization changed in the feature matrix.
Both shapes report rows and columns. The rows correspond to documents and are the same for both vectorizers, because the same training text is used. The columns correspond to the vocabulary, so any difference between the two column counts comes from the tokenizer. The source does not state the printed values, so the numbers are something you should run and observe yourself.
Predicting the column difference before running it
Two vectorizers, one lemmatizing and one standard, are fitted on the same text_train with the same min_df. Which one should have more columns, and why?
- 1Identify the forms that share a base
Suppose the text contains run, running, and runs. The standard vectorizer keeps each form as its own column.
- 2Apply the lemmatizing function
The lemmatizing vectorizer returns run for each of those forms, so all three contribute to one shared lemma.
- 3Count the distinct columns
Collapsing several forms into one lemma reduces the number of distinct vocabulary items, so the lemmatizing vectorizer should usually have fewer columns when related forms are present.
- 4Check the rows
The number of rows is the same in both cases, because both see the same documents.
The lemmatizing vectorizer is expected to have the same number of rows and typically fewer columns, provided related word forms appear in the training text. Run the code to confirm the actual printed values.
Mistakes Beginners Make
Common mistake Assuming CountVectorizer lemmatizes by default
Why it failsThe source states that scikit-learn implements neither stemming nor lemmatization as part of CountVectorizer. Without a custom tokenizer, forms such as run, running, and runs are counted separately.
Do this insteadSupply a tokenizer callable that returns lemmas, and confirm the output of that callable before fitting.
Common mistake Expecting the vectorizer to change because the setup cell ran
What it looks likeRunning the setup creates lemma_vect, but no document has been transformed yet.
Why it failsBuilding the vectorizer only configures it. Counts appear only when the vectorizer is fitted or transformed on text.
Do this insteadCall fit or transform on text_train and then inspect the resulting shape.
Common mistake Thinking the regular expression and the lemmatizer run in either order
Why it failsThe regular expression is applied before
spaCy's lemmatization, so the lemmatizer receives the regex-produced tokens.Do this insteadRead the chain from top to bottom: string, regex tokens, spaCy lemmas, CountVectorizer counts.
Common mistake Comparing shapes from differently configured vectorizers
Why it failsA difference in column count is only meaningful if both vectorizers use the same training text and the same
min_df.Do this insteadCreate the standard reference with the same min_df and fit both on the same text_train.
Locating a Divergence When Debugging
When a lemmatized vectorizer produces an unexpected result, debug in the same order as the data flow. Work from the earliest stage to the latest so that you find the first point where the output stops matching your expectation. Each stage depends on the ones before it, so a fault in an early stage will look like a fault in a later one.
Begin by confirming that the regular-expression tokenizer is in place on en_nlp, because every later step depends on it. Next, confirm that custom_tokenizer returns lemmas rather than raw words. Finally, confirm that both vectorizers were fitted on the same text_train. The first step where the output stops matching your expectation is the place to investigate.
Predicting Your Own Result
Before you run the comparison, write down your prediction. If two words in a document share a base form, how many columns could they occupy in each vectorizer? Would the lemmatized vectorizer have more or fewer columns than the standard one? Explain which stage of the pipeline determines the answer.
Hints
- Think about what the standard vectorizer does with run and running as separate strings.
- Think about what
custom_tokenizerreturns for each of those strings. - Remember that the number of rows is the same for both vectorizers.
Key Takeaways
- CountVectorizer does not stem or lemmatize on its own, but its tokenizer parameter accepts a callable that takes a string and returns a list of tokens.
- A custom tokenizer built with
spaCyruns a regular-expression split first, then collects the lemma_ value of each token, and passes that lemma list to CountVectorizer. - The copied regular-expression tokenizer and the
spaCylemmatizer are linked through the shareden_nlpobject, so the order of setup lines matters. - Compare a lemmatizing and a standard vectorizer fitted on the same text with the same
min_dfto see the effect of lemmatization on the vocabulary. - When debugging, check the tokenizer chain in order: regex tokenizer, lemma output, then identical training text, and investigate the first mismatch.