Neural Mastery

Classical NLP: Tokenization, POS Tagging, NER & Parsing

Before "tokenization" meant BPE splitting text for a Transformer (see Foundation Model Internals), it meant something more basic: turning raw text into linguistically meaningful units at all. This page is that older, still-relevant layer.

Intuition: Structure Before Meaning

Every technique on this page answers a question that has to be settled before a model can reason about meaning at all: where does one unit end and the next begin (tokenization), what grammatical role does each unit play (POS tagging), which spans refer to real-world entities (NER), and how do the units relate to each other (parsing). None of these require understanding what a sentence means — only its structure — which is exactly why they predate (and still coexist alongside) modern semantic, embedding-based NLP.

Text Normalization

Tokenization (classical sense): splitting text into words/sentences — genuinely harder than it sounds. Watch a real rule-based tokenizer handle exactly the cases that break naive whitespace splitting:

Example
"Dr. Smith didn't arrive."
naive whitespace split (4 tokens)
Dr.Smithdidn'tarrive.
rule-based tokenizer (5 tokens)
Dr.Smithdidn'tarrive.
Naive whitespace split produces 4 tokens; the rule-based tokenizer produces 5 -- it correctly keeps "Dr." intact (not splitting the abbreviation's period as a separate sentence-ending token) while still separating the trailing period after "arrive" and the contraction "didn't"/"can't" as single tokens. Real string processing, not a picture of the concept.

This is a different, coarser-grained operation than an LLM's subword tokenizer, which operates on the output of (or in place of) this step.

Stemming (crude, rule-based suffix stripping) and lemmatization (dictionary-backed, linguistically correct base-form lookup) both aim at the same goal — reducing a word to a canonical form — through genuinely different mechanisms. Real rules applied to real words, side by side:

wordstem()lemmatize()
runningrunnrun
universitiesuniversityuniversity
betterbettergood
studiesstudystudy
micemicemouse
runsrunrun
Watch "mice" and "better" -- no suffix rule connects them to "mouse" or "good," so the stemmer leaves them untouched while the lemmatizer, backed by real vocabulary knowledge, gets them right.
Stemming is blind suffix-stripping (real rules, applied live) -- fast but sometimes produces a non-word or misses irregular forms entirely. Lemmatization uses an actual vocabulary lookup -- slower to build, but always returns a real dictionary form, including irregulars a suffix rule can never catch.

Why this still matters: search and information retrieval systems (matching "running shoes" against a document containing "run shoe") still commonly use stemming/lemmatization for query expansion and indexing, even in systems that also use modern embeddings — see RAG — Dense vs. Sparse Retrieval's BM25/sparse retrieval, which benefits directly from this normalization.

Part-of-Speech (POS) Tagging

Assigning a grammatical category (noun, verb, adjective, ...) to every word in a sentence. Classical approaches — Hidden Markov Models and Conditional Random Fields — frame this as sequence labeling: predict the most likely tag sequence given the word sequence, modeling the dependency between adjacent tags as well as each word's own likely tags. Step through a real Viterbi decode over a tiny HMM — the exact dynamic-programming algorithm, not a diagram of it:

thedogchasesthecatDETNOUNVERBDET4.2e-1NOUN3.0e-3VERB1.0e-3
the
"the" and "chases" have near-unambiguous emission probabilities (DET, VERB respectively); "dog" and "cat" are only disambiguated as NOUN through the transition structure -- DET is overwhelmingly likely to be followed by NOUN, not by another DET or a VERB.
Step through real Viterbi decoding, word by word: at each column, every tag's probability = max over the PREVIOUS column's tags of (that tag's probability × transition probability × this word's real emission probability for the current tag) -- the actual dynamic-programming recurrence, not an animation of it. Final path (bold): DET

Modern approaches use a small Transformer or BiLSTM (see Sequence Models — Bidirectional RNN/LSTM) trained end-to-end on labeled data, but the underlying task framing — sequence labeling — is unchanged and reused directly for NER below.

Why it still matters: POS tags are a useful feature for downstream tasks (parsing below depends on them), and understanding sequence labeling here is the direct conceptual bridge to NER and to general sequence-labeling problems in NLP Task Taxonomy.

Named Entity Recognition (NER)

Identifying and classifying spans of text into predefined categories — person names, organizations, locations, dates, monetary amounts. Framed identically to POS tagging as a sequence-labeling problem, using the standard BIO tagging scheme. Real decoding from per-token tags into grouped entity spans:

New
B-LOC
York
I-LOC
City
I-LOC
is
O
bigger
O
than
O
San
B-LOC
Francisco
I-LOC
Grouped entity spans (real BIO decoding)
"New York City" — one LOC entity spanning 3 tokens
"San Francisco" — one LOC entity spanning 2 tokens
B-(begin) and I-(inside) are what let the scheme tell 'two separate one-word entities back to back' apart from 'one multi-word entity' -- a plain 'is this token part of an entity' binary tag couldn't make that distinction at all.

Why it still matters in an LLM-heavy stack: NER remains a common production pipeline stage even alongside LLMs — extracting structured entities (customer names, order IDs, dates) from unstructured text for downstream systems is often cheaper, faster, and more reliably schema-conformant with a dedicated NER model than with an LLM prompted to "extract the entities," especially at high request volume where a small fine-tuned NER model's latency and cost advantage over an LLM call matters.

Syntactic Parsing

Recovering a sentence's grammatical structure — not just tagging individual words, but the relationships between them. Toggle between the two dominant representations of the same sentence:

Representation
Thedogchasesthecatsubjobjroot
Same sentence, same real grammatical relationships -- dependency parsing has become the more commonly used representation in modern pipelines because it exposes those relationships more directly.
"chases" is the root -- both "dog" (subject) and "cat" (object) depend directly on it, "The"/"the" depend on their nouns. A directed graph exposing "who did what to whom" directly, useful for information extraction.
  • Constituency parsing: breaks a sentence into nested phrases (noun phrases, verb phrases) forming a tree, following a formal grammar — "the structure a sentence diagram in grade school represents," made rigorous.
  • Dependency parsing: instead of nested phrases, represents each word as depending on exactly one "head" word (a verb's subject and object both depend on the verb) — a directed graph (a tree, rooted at the sentence's main verb) rather than a nested-phrase tree. Dependency parsing has become the more commonly used representation in modern NLP pipelines because it more directly exposes "who did what to whom" relationships useful for downstream tasks like information extraction.
  • Why this still matters: even though end-to-end neural models (and LLMs) rarely require an explicit parse tree as an input feature anymore, parsing-derived relationships remain useful for structured information extraction, grammar/style checking tools, and as an interpretability lens — asking "does this model's attention pattern correspond to real syntactic dependencies" is a genuine, still-active interpretability research question, directly reusing this classical structure as a ground truth to compare against.

Code: A Real Viterbi Decoder

The exact algorithm the POS-tagging diagram above steps through, in full:

import numpy as np

def viterbi(words, tags, transition, emission, start_probs):
    n, T = len(words), len(tags)
    prob = np.zeros((n, T))
    backpointer = np.zeros((n, T), dtype=int)

    for t, tag in enumerate(tags):
        prob[0, t] = start_probs[tag] * emission[tag].get(words[0], emission[tag]["<UNK>"])

    for i in range(1, n):
        for t, tag in enumerate(tags):
            scores = [prob[i-1, tp] * transition[tags[tp]][tag] for tp in range(T)]
            backpointer[i, t] = np.argmax(scores)
            prob[i, t] = max(scores) * emission[tag].get(words[i], emission[tag]["<UNK>"])

    # Backtrace from the best final state -- exactly what the diagram's
    # bold path traces, column by column, in reverse.
    path = [int(np.argmax(prob[-1]))]
    for i in range(n - 1, 0, -1):
        path.insert(0, backpointer[i, path[0]])
    return [tags[i] for i in path]

Next: Word Embeddings — moving from discrete linguistic categories to continuous, learned representations of meaning.

Last updated Sep 5, 2026Edit this pageReport an issue
← Previous
NLP — Roadmap
Next →
Word Embeddings