Part-of-Speech Tagging: Tag Sets, Algorithms, Implementation

Michael BrenndoerferApril 16, 202564 min read

Covers POS tagging from tag sets to statistical taggers. Topics include Penn Treebank, Universal Dependencies, emission and transition probabilities.

Language AI HandbookAll chapters
Part 6 of 67Sequence Labeling
Chapter 36Part-of-Speech Tagging
Reading the full chapter · 18 sections · 64 min18 sections · 64 min

All 18 sections · 64 min read

Part-of-Speech TaggingFull chapter · Sections in this chapter1 / 18 ↓
Reading tools Definitions, links & notes

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

Notes & comments

Part-of-Speech Tagging

Every word in a sentence carries two kinds of information: its meaning and its grammatical role. Part-of-speech tagging (POS tagging) is the task of automatically assigning each word in a sentence a label that captures that grammatical role. Given "The quick brown fox jumps over the lazy dog," a POS tagger produces output like: DET ADJ ADJ NOUN VERB ADP DET ADJ NOUN.

Why does this seemingly simple annotation matter so much? POS tags give downstream NLP systems a compact, structured summary of sentence structure without the full overhead of a complete syntactic parse. Syntactic parsers use POS tags to prune the search space of possible trees. Named entity recognizers use them to identify entity boundaries, since proper noun sequences (NNP NNP) are strong signals for person or organization names. Question answering systems use verb tags to locate the action a question is asking about. Even modern neural language models, which learn representations end-to-end without explicit POS supervision, benefit from POS auxiliary tasks during training.

The importance of POS tagging extends beyond these direct applications. POS labels function as a compact encoding of local syntactic context. A model that knows a given token is a past-tense verb immediately knows something important: the sentence likely has a predicate at this position, the argument structure involves a subject and possibly an object, and the temporal framing of the clause is in the past. Encoding all of that information through a single two- or three-character tag is extraordinarily economical. For classical NLP pipelines built around hand-crafted feature engineering, POS tags were not just useful; they were indispensable.

This chapter covers the full arc of POS tagging: the tag sets NLP practitioners use, the deep ambiguity problem that makes tagging non-trivial, how both rule-based and statistical approaches tackle that ambiguity, and how we evaluate and implement taggers in practice. By the end you will have working code for both training and applying POS taggers and a clear mental model of where accuracy gains come from.

What Are Parts of Speech?

Parts of speech, also called word classes or lexical categories, group words by their grammatical function and syntactic behavior. The classification dates to ancient Greek grammarians and was systematized by Pāṇini for Sanskrit around 400 BCE. Modern linguistics recognizes that word classes are defined by distributional properties: words that occupy the same syntactic positions and combine with the same kinds of modifiers belong to the same class.

Part of Speech

A part of speech (POS) is a grammatical category assigned to a word based on its syntactic role and morphological properties. Major categories include nouns, verbs, adjectives, adverbs, determiners, prepositions, and conjunctions.

The traditional list of eight parts of speech taught in school (noun, verb, adjective, adverb, pronoun, preposition, conjunction, interjection) is a coarse-grained starting point. Computational linguistics requires finer distinctions. Instead of a single "verb" category, we need to distinguish base form verbs, past tense verbs, gerunds, past participles, and third-person singular present forms, because they behave differently in syntax and morphology.

The core intuition behind any POS system is that words group into categories based on:

  • Distributional behavior: Nouns follow determiners ("the dog"), verbs follow noun phrases ("the dog barked")
  • Morphological properties: Nouns form plurals (-s), verbs inflect for tense (-ed, -ing), adjectives form comparatives (-er)
  • Semantic tendencies: Nouns typically name entities, verbs typically name events, adjectives name properties

These three criteria often agree but sometimes conflict, which is why POS classification requires careful linguistic thought.

It is worth pausing on why distributional behavior is the most reliable of these three criteria. Consider the word "table." Semantically it names a physical object, so you might guess it is always a noun. But "table the motion" is perfectly grammatical English where "table" functions as a verb. The semantic criterion would lead you astray. The distributional criterion works better: in "table the motion," "table" appears immediately after a subject noun phrase and before a direct object noun phrase, which is a verb position. The morphological criterion is also informative but incomplete: "table" has no distinctive verb morphology here, but English is a low-inflection language where verbs frequently appear in their bare stem form.

Computational systems that learn POS categories from annotated corpora are, implicitly, learning these distributional patterns. When a neural tagger encounters a word it has never seen before, its prediction is guided by the context words that surround it, not by any innate category knowledge. This distributional learning is one of the key mechanisms that allows modern taggers to handle out-of-vocabulary words gracefully.

Tag Sets

A tag set defines the inventory of labels a tagger assigns. The choice of tag set involves a fundamental tradeoff: more tags allow finer-grained distinctions that benefit some downstream tasks, but require more annotated data to learn reliably and are harder to assign consistently.

The Penn Treebank Tag Set

The Penn Treebank (PTB) tag set, developed at the University of Pennsylvania as part of the Penn Treebank project in the early 1990s, became the standard for English NLP. It contains 36 POS tags plus 12 punctuation and symbol tags.

PythonIn [3]
CollapseExpand
# Penn Treebank tag set - core POS categories
penn_tags = {
    # Nouns
    "NN": "Noun, singular or mass",
    "NNS": "Noun, plural",
    "NNP": "Proper noun, singular",
    "NNPS": "Proper noun, plural",
    # Verbs
    "VB": "Verb, base form",
    "VBD": "Verb, past tense",
    "VBG": "Verb, gerund or present participle",
    "VBN": "Verb, past participle",
    "VBP": "Verb, non-3rd person singular present",
    "VBZ": "Verb, 3rd person singular present",
    "MD": "Modal auxiliary",
    # Adjectives
    "JJ": "Adjective",
    "JJR": "Adjective, comparative",
    "JJS": "Adjective, superlative",
    # Adverbs
    "RB": "Adverb",
    "RBR": "Adverb, comparative",
    "RBS": "Adverb, superlative",
    # Pronouns and determiners
    "PRP": "Personal pronoun",
    "PRP$": "Possessive pronoun",
    "WP": "Wh-pronoun",
    "WP$": "Possessive wh-pronoun",
    "DT": "Determiner",
    "PDT": "Predeterminer",
    "WDT": "Wh-determiner",
    # Prepositions and conjunctions
    "IN": "Preposition or subordinating conjunction",
    "CC": "Coordinating conjunction",
    "TO": "to (infinitive marker)",
    "RP": "Particle",
    # Other
    "CD": "Cardinal number",
    "EX": "Existential there",
    "FW": "Foreign word",
    "POS": "Possessive ending",
    "UH": "Interjection",
    "SYM": "Symbol",
}
ConsoleOut [4]
CollapseExpand
Penn Treebank Tag Set
==================================================

Nouns:
  NN      Noun, singular or mass
  NNS     Noun, plural
  NNP     Proper noun, singular
  NNPS    Proper noun, plural

Verbs:
  VB      Verb, base form
  VBD     Verb, past tense
  VBG     Verb, gerund or present participle
  VBN     Verb, past participle
  VBP     Verb, non-3rd person singular present
  VBZ     Verb, 3rd person singular present
  MD      Modal auxiliary

Adjectives:
  JJ      Adjective
  JJR     Adjective, comparative
  JJS     Adjective, superlative

Adverbs:
  RB      Adverb
  RBR     Adverb, comparative
  RBS     Adverb, superlative

Pronouns:
  PRP     Personal pronoun
  PRP$    Possessive pronoun
  WP      Wh-pronoun
  WP$     Possessive wh-pronoun

Determiners:
  DT      Determiner
  PDT     Predeterminer
  WDT     Wh-determiner

Other:
  IN      Preposition or subordinating conjunction
  CC      Coordinating conjunction
  TO      to (infinitive marker)
  RP      Particle
  CD      Cardinal number
  EX      Existential there
  FW      Foreign word
  POS     Possessive ending
  UH      Interjection
  SYM     Symbol

The fine-grained verb distinctions matter considerably in practice. Knowing that "barked" is VBD (past tense) versus "barking" is VBG (gerund/participle) tells a parser whether to attach it as the main verb or as a modifier. The six verb tags capture the full English verbal paradigm and allow downstream systems to reason about tense and aspect.

The Penn Treebank tag set was built around the Wall Street Journal corpus and reflects the characteristics of formal written English. This origin has consequences: the tag set handles financial text well but struggles with informal registers, social media text, and domain-specific terminology.

One design decision in the PTB tag set deserves particular attention: the treatment of "IN" as a single tag covering both prepositions and subordinating conjunctions. This conflation is linguistically controversial. "After dinner" uses "after" as a preposition governing a noun phrase, while "after she left" uses "after" as a subordinating conjunction introducing a clause. These are syntactically distinct, but they were collapsed into a single tag because reliable disambiguation requires the kind of deep syntactic analysis the tag set was meant to enable in the first place. This circular dependency is a recurring theme in annotation design: the tags you want to assign depend on information that the tags are supposed to provide.

The Universal Dependencies Tag Set

The PTB tag set is designed for English, and extending it to other languages is problematic. German uses case inflections that English lacks. Mandarin Chinese has no morphological inflection at all. Arabic has a complex system of verb derivations. The Universal Dependencies (UD) project introduced a cross-linguistic tag set with 17 UPOS tags designed to apply consistently across languages.

PythonIn [5]
CollapseExpand
# Universal Dependencies UPOS tag set
universal_tags = {
    "ADJ": "Adjective",
    "ADP": "Adposition (pre- or postposition)",
    "ADV": "Adverb",
    "AUX": "Auxiliary verb",
    "CCONJ": "Coordinating conjunction",
    "DET": "Determiner",
    "INTJ": "Interjection",
    "NOUN": "Common noun",
    "NUM": "Numeral",
    "PART": "Particle",
    "PRON": "Pronoun",
    "PROPN": "Proper noun",
    "PUNCT": "Punctuation",
    "SCONJ": "Subordinating conjunction",
    "SYM": "Symbol",
    "VERB": "Verb",
    "X": "Other (unclassifiable)",
}
ConsoleOut [6]
CollapseExpand
Universal Dependencies UPOS Tag Set
==================================================
  ADJ      Adjective
  ADP      Adposition (pre- or postposition)
  ADV      Adverb
  AUX      Auxiliary verb
  CCONJ    Coordinating conjunction
  DET      Determiner
  INTJ     Interjection
  NOUN     Common noun
  NUM      Numeral
  PART     Particle
  PRON     Pronoun
  PROPN    Proper noun
  PUNCT    Punctuation
  SCONJ    Subordinating conjunction
  SYM      Symbol
  VERB     Verb
  X        Other (unclassifiable)

Total: 17 tags (vs 36+ in Penn Treebank)

The 17-tag set achieves cross-linguistic applicability by collapsing distinctions that are English-specific. All six English verb inflection tags (VB, VBD, VBG, VBN, VBP, VBZ) merge into VERB. The key insight is that morphological distinctions like tense can be captured through additional morphological annotation layers (called morphological features in UD) rather than proliferating the POS tag set itself.

The UD project has annotated over 200 treebanks across more than 100 languages using this framework, making it the largest cross-linguistic POS resource available.

The UD tag set also makes principled decisions about categories that vary typologically. The AUX (auxiliary) tag separates auxiliary verbs from main verbs, a distinction that matters in many languages. ADP covers both prepositions (as in English) and postpositions (as in Japanese, Turkish, and many other languages), recognizing that the functional role is the same regardless of word order. The PART tag handles particles that do not fit elsewhere, like the English infinitive marker "to" in its verbal use, or the negative particle in languages that express negation with a separate uninflected word.

Choosing a Tag Set

The right tag set depends on your application. Here is how to think about it:

  • Use Penn Treebank tags when working on English NLP with existing tools like NLTK or spaCy in English mode, or when downstream tasks (like parsing) benefit from fine-grained distinctions
  • Use Universal Dependencies tags when building multilingual systems, working with non-English languages, or when compatibility across language resources matters more than granularity
  • Use custom tags only when your domain has structures neither system handles well (e.g., specialized clinical or legal notation)

Most production systems today use one of these two standards. The Penn Treebank standard dominates English-only pipelines; Universal Dependencies dominates multilingual ones.

The question of granularity is worth thinking about carefully. Finer tag sets can express more information, but they are harder to learn reliably because each individual tag sees fewer training examples. A tag set with 100 labels means that each label appears, on average, in only 1% of tokens. Rare labels may have so few training examples that the model cannot learn their distributional patterns at all. The Penn Treebank's 36 tags strike a balance that proved empirically useful: fine enough to serve syntactic parsers, coarse enough to learn with moderate-sized corpora.

The Ambiguity Problem

The reason POS tagging is non-trivial is lexical ambiguity: most common words in English can function as more than one part of speech. The word "run" appears as VB in "I run every morning," VBZ in "She runs marathons," VBD in "He ran yesterday," VBN in "I have run before," and NN in "That was a good run." Selecting the correct tag requires examining the surrounding context.

PythonIn [7]
CollapseExpand
# Ambiguous words and their possible tags with contexts
ambiguous_words = {
    "light": {
        "JJ": "We need a light snack before the concert.",
        "NN": "The light at the end of the tunnel was faint.",
        "VB": "Please light the candle for me.",
    },
    "book": {
        "NN": "The book was fascinating from start to finish.",
        "VB": "I need to book a flight to Paris.",
        "VBP": "We book flights months in advance.",
    },
    "can": {
        "MD": "She can swim across the lake.",
        "NN": "Open the can and pour the soup.",
        "VB": "They can vegetables in autumn.",
    },
    "fast": {
        "JJ": "She is a fast runner.",
        "RB": "He drove fast around the corner.",
        "VB": "Many people fast during religious observances.",
        "NN": "The fast lasted three days.",
    },
}
ConsoleOut [8]
CollapseExpand
Lexical Ambiguity Examples
=================================================================

'light' (3 possible tags):
  JJ     We need a [light] snack before the concert.
  NN     The [light] at the end of the tunnel was faint.
  VB     Please [light] the candle for me.

'book' (3 possible tags):
  NN     The [book] was fascinating from start to finish.
  VB     I need to [book] a flight to Paris.
  VBP    We [book] flights months in advance.

'can' (3 possible tags):
  MD     She [can] swim across the lake.
  NN     Open the [can] and pour the soup.
  VB     They [can] vegetables in autumn.

'fast' (4 possible tags):
  JJ     She is a [fast] runner.
  RB     He drove [fast] around the corner.
  VB     Many people [fast] during religious observances.
  NN     The [fast] lasted three days.

Research on large corpora shows that roughly 40% of word types in English are ambiguous, but because ambiguous words tend to be common words (like "can," "will," "run," "light"), they account for an even higher fraction of tokens. This means a POS tagger that always picks the most frequent tag for each word will still make mistakes on a significant portion of all words it encounters.

The key statistical insight is that while individual words are ambiguous, most contexts strongly constrain the correct tag. The word "light" after "the" is almost certainly an adjective or noun, not a verb. After a pronoun like "please," it is almost certainly a verb. Context resolves most ambiguity.

Why Ambiguity Is Harder Than It Looks

The scale of lexical ambiguity in English is worth dwelling on. The 40% figure for ambiguous word types is striking, but the distribution is highly skewed. The most common 100 words in English, which account for roughly half of all tokens in typical text, include words like "will," "that," "can," "right," "even," "about," "over," "just," "back," "still," "set," "run," "leave," "play," and "watch." Almost all of these can function as multiple parts of speech. These are the words a tagger encounters most often, and they are also the hardest to tag correctly.

The difficulty compounds because ambiguous words often cluster in the same sentences. Consider "Can you leave the light on and book a table?" Every content word in this sentence is ambiguous: "can" is a modal or a noun, "leave" is a verb or an adjective, "light" is a noun or an adjective, "book" is a verb or a noun, and "table" is a noun or a verb. The correct reading requires parsing the entire sentence structure, not just examining individual tokens.

The good news for tagger developers is that although ambiguous words are frequent, the correct reading in context is usually predictable. In a large corpus of annotated English text, approximately 97% of tokens can be tagged correctly by a tagger that knows only the word identity plus the identity of the one preceding word and the one following word. The residual 3% represents hard cases where longer-range context or world knowledge is required.

Types of Ambiguity

Not all ambiguity is created equal. It helps to distinguish several types:

Local ambiguity: The ambiguity can be resolved by looking at immediately adjacent words. The word "can" after "I" and before "swim" is almost certainly a modal. A simple bigram model resolves this.

Non-local ambiguity: Resolution requires context further away in the sentence. "The horse raced past the barn fell" is a garden-path sentence where "raced" initially looks like the main verb but is a reduced relative clause modifier. Resolving this requires recognizing the clause structure across the whole sentence.

Structural ambiguity: The ambiguity arises from multiple ways of attaching a phrase, not just from the word's category. "I saw the man with the telescope" is ambiguous between two readings of where the prepositional phrase attaches, and this structural ambiguity affects the POS interpretation of intermediate nodes.

Annotation ambiguity: Some tokens are underdetermined even for human experts. Linguistic guidelines sometimes resolve these by convention rather than principle, which means different annotation projects make different choices and taggers trained on those projects will disagree.

Understanding these types matters practically: local ambiguity is easy to resolve with context windows, non-local and structural ambiguity requires sequence-level modeling, and annotation ambiguity sets an irreducible ceiling on tagger accuracy.

POS Tagging as a Classification Problem

POS tagging is a supervised classification problem. Given a word and its context, predict the tag. This framing opens up the full toolkit of machine learning methods.

The challenge is that POS tagging operates at the sequence level: we are not classifying tokens independently but finding the most probable tag sequence for an entire sentence. The tags interact, because knowing that one word is a determiner strongly constrains the possible tags for the following word (determiners are almost always followed by adjectives or nouns).

POS Tagging as Sequence Labeling

POS tagging assigns a tag to every token in a sequence simultaneously, not independently. The optimal tag for each token depends on the tags of its neighbors. This is why sequence models (like HMMs and CRFs) outperform independent classifiers.

The two main approaches to capturing these sequence dependencies are:

  1. Generative models: Model the joint probability P(words,tags)P(\text{words}, \text{tags}) and find the tags that maximize it. Hidden Markov Models (HMMs) are the classic example.
  2. Discriminative models: Model P(tags∣words)P(\text{tags} | \text{words}) directly, conditioning on all observable information. Conditional Random Fields (CRFs) and neural sequence models take this approach.

We will cover HMMs and CRFs in depth in subsequent chapters. Here we focus on the features and evaluation principles that apply across methods.

Features for POS Tagging

What information helps predict a word's POS tag? Feature engineering reveals what contextual signals are informative:

Word identity: The word itself is the strongest single feature. "The" is almost always a determiner. "Quickly" is almost always an adverb.

Suffix features: Morphological endings are highly predictive. Words ending in "-tion" are almost always nouns (perception, relation). Words ending in "-ly" are almost always adverbs (quickly, slowly). Words ending in "-ing" are gerunds or present participles (running, eating).

Prefix features: Less predictive than suffixes but still useful. Words beginning with "un-" are often adjectives or verbs (unhappy, undo).

Capitalization: Capitalized words mid-sentence are likely proper nouns (NNP). Sentence-initial capitalization is uninformative but all-caps words are often abbreviations.

Context window: The tags and words of neighboring tokens. After a determiner, a noun or adjective is expected. After a modal verb like "can," a base-form verb typically follows.

Word shape: Patterns like "Xxxxx" (capitalized word), "XXXXX" (all-caps), "99.99" (decimal number) capture orthographic structure independent of the specific characters.

The relative importance of these features is not uniform across all words. For common, frequent words that appear many times in training data, word identity is by far the most useful feature. For rare or out-of-vocabulary words, morphological features (especially suffixes) become critical. A model that relies entirely on word identity will fail on unseen vocabulary; a model that ignores word identity will fail on common ambiguous words. Good feature engineering combines both.

PythonIn [9]
CollapseExpand
def extract_features(sentence, i):
    """Extract features for the i-th word in a sentence.

    sentence: list of words (strings)
    i: index of the target word

    Returns a dictionary of feature name -> feature value.
    """
    word = sentence[i]
    features = {
        # Word identity
        "word": word.lower(),
        "word_upper": word.isupper(),
        "word_title": word.istitle(),
        # Suffix features (morphological)
        "suffix_2": word[-2:].lower() if len(word) >= 2 else word.lower(),
        "suffix_3": word[-3:].lower() if len(word) >= 3 else word.lower(),
        "suffix_4": word[-4:].lower() if len(word) >= 4 else word.lower(),
        # Prefix features
        "prefix_2": word[:2].lower() if len(word) >= 2 else word.lower(),
        "prefix_3": word[:3].lower() if len(word) >= 3 else word.lower(),
        # Lexical properties
        "is_digit": word.isdigit(),
        "contains_hyphen": "-" in word,
        "contains_digit": any(c.isdigit() for c in word),
        # Position in sentence
        "is_first": i == 0,
        "is_last": i == len(sentence) - 1,
    }
    # Previous word context
    if i > 0:
        prev_word = sentence[i - 1]
        features["prev_word"] = prev_word.lower()
        features["prev_word_title"] = prev_word.istitle()
    else:
        features["prev_word"] = "<START>"
        features["prev_word_title"] = False
    # Next word context
    if i < len(sentence) - 1:
        next_word = sentence[i + 1]
        features["next_word"] = next_word.lower()
        features["next_word_title"] = next_word.istitle()
    else:
        features["next_word"] = "<END>"
        features["next_word_title"] = False
    return features
ConsoleOut [10]
CollapseExpand
Feature extraction for 'jumps' (index 4):
--------------------------------------------------
  contains_digit        False
  contains_hyphen       False
  is_digit              False
  is_first              False
  is_last               False
  next_word             over
  next_word_title       False
  prefix_2              ju
  prefix_3              jum
  prev_word             fox
  prev_word_title       False
  suffix_2              ps
  suffix_3              mps
  suffix_4              umps
  word                  jumps
  word_title            False
  word_upper            False

The suffix features "umps" and "mps" might not look informative for "jumps" in isolation, but when the model sees the suffix "-s" on a word following an adjective-modified noun, it learns that this pattern almost always signals a third-person singular present verb (VBZ). Feature engineering is about giving the model the right vocabulary to express these patterns.

The Feature Interaction Problem

One limitation of feature engineering is that individual features do not capture interactions. The feature "suffix is -ed" fires for both past tense verbs (walked) and past participle adjectives (excited, concerned). The feature alone is ambiguous. But the combination "suffix is -ed AND previous word is 'have' or 'has'" almost uniquely identifies a past participle in perfect aspect. Traditional feature engineering handles this by creating conjunction features, but the number of possible conjunctions grows combinatorially with the number of base features.

This is one key motivation for neural approaches to POS tagging. Rather than manually specifying which feature combinations matter, a neural network learns to form its own internal representations that capture the relevant interactions. A bidirectional LSTM reading the sequence "has walked" will produce a hidden state for "walked" that reflects the presence of the auxiliary "has" in the left context, without any explicit feature engineering. The word embedding for "has" influences the LSTM state, which in turn influences the output distribution over tags for "walked." The model learns the interaction implicitly.

Rule-Based Taggers

The earliest POS taggers were purely rule-based systems. Linguists hand-crafted disambiguation rules based on their knowledge of English grammar.

The ENGTWOL System

The most influential rule-based tagger was ENGTWOL (later called the Brill tagger's predecessor), developed at Helsinki in the 1980s. The approach was:

  1. Look up each word in a lexicon to get its set of possible tags
  2. Apply constraint rules that eliminate impossible tags based on context

For example, a rule might say: "If a word can be DT (determiner) or JJ (adjective) and the next word is a noun, then it is DT." These rules were written by hand by linguists with deep grammar knowledge.

Rule-based systems have a significant advantage: their decisions are interpretable. You can always trace why the tagger made a particular decision to a specific rule. They also perform well on well-formed text where grammatical rules hold cleanly.

The disadvantage is labor intensity: writing complete rule sets for English takes years of expert effort, and maintaining them as language evolves is costly. Rule-based systems also struggle with informal text where grammatical rules break down.

The ENGTWOL system relied on a two-stage architecture. In the first stage, a morphological analyzer looked up each word form in a large lexicon and returned the set of all possible morphosyntactic readings. A word like "running" might receive readings as a present participle (VERB PRES-PART), a gerund (NOUN), and an adjective. In the second stage, a Constraint Grammar formalism applied ordered rules to eliminate incompatible readings until (ideally) only one reading remained for each word. The rules expressed constraints like "a word with a determiner reading should not retain that reading if the preceding word already has a determiner reading," which captured the fact that two consecutive determiners are very rare in English.

The strength of this approach was its modularity. Rules could be added, removed, or reordered to fix specific error types without disrupting the rest of the system. Linguists maintaining the system could reason about it at a level of abstraction that matched their grammatical intuitions. When the system made an error, debugging was straightforward: find the rule that should have applied but did not, or find the rule that applied when it should not have.

The Brill Tagger

The Brill tagger (1992) made a breakthrough by combining rule-based ideas with corpus learning. Instead of hand-writing rules, it learned transformation rules automatically from annotated data.

The algorithm works in two phases:

Phase 1 (Initial tagging): Assign each word its most frequent tag based on a lexicon learned from the training corpus. Words not seen in training get a default tag based on capitalization and suffix.

Phase 2 (Rule learning): Iteratively learn error-correcting transformation rules. Each rule has the form: "Change tag AA to tag BB when condition CC holds." The algorithm finds the rule that corrects the most errors on the training data, applies it, and repeats.

PythonIn [11]
CollapseExpand
from collections import Counter, defaultdict


class SimpleBrillTagger:
    """Simplified Brill-style tagger demonstrating the key ideas."""

    def __init__(self):
        self.word_tag_counts = defaultdict(Counter)
        self.most_common_tag = {}
        self.default_tag = "NN"  # Nouns are most common for unknown words
        self.rules = []

    def train_baseline(self, tagged_sentences):
        """Phase 1: Learn most-frequent tag per word."""
        for sentence in tagged_sentences:
            for word, tag in sentence:
                self.word_tag_counts[word.lower()][tag] += 1
        for word, counts in self.word_tag_counts.items():
            self.most_common_tag[word] = counts.most_common(1)[0][0]

    def tag_baseline(self, sentence):
        """Apply baseline: most frequent tag per word."""
        tagged = []
        for word in sentence:
            tag = self.most_common_tag.get(word.lower(), self.default_tag)
            tagged.append((word, tag))
        return tagged

    def apply_rule(self, tagged_sentence, condition_tag, new_tag, context_fn):
        """Apply a transformation rule to a tagged sentence."""
        result = []
        for i, (word, tag) in enumerate(tagged_sentence):
            if tag == condition_tag and context_fn(tagged_sentence, i):
                result.append((word, new_tag))
            else:
                result.append((word, tag))
        return result
ConsoleOut [12]
CollapseExpand
Brill Tagger Concept: Error-Correcting Rules
=======================================================

Example sentence: 'I can can the tomatoes'

Baseline (most-frequent tag):
  I             PRP
  can           MD
  can           MD
  the           DT
  tomatoes      NNS

After rule: 'Change MD to VB when preceded by MD':
  I             PRP
  can           MD
  can           VB
  the           DT
  tomatoes      NNS

Error corrected: second 'can' is now tagged VB (base verb), not MD (modal).

The Brill tagger typically achieves accuracy around 95-97% on the Penn Treebank test set, competitive with more complex statistical approaches of its era. Its learned rules are interpretable and can be inspected by linguists.

What made the Brill tagger historically important was the insight that the gap between statistical learning and symbolic reasoning could be bridged. The learned transformation rules looked like linguistically motivated rules: "change VBD to VBN when the previous tag is MD or HVZ" is a rule about perfect aspect that any syntactician would recognize. This interpretability distinguished Brill's approach from the black-box statistical models that were also gaining traction in the early 1990s. It showed that you could have both: rules that are learned automatically from data and that remain interpretable after learning.

The Brill tagger's main limitation is its greedy learning algorithm. At each step, it finds the single best transformation rule and applies it, then finds the next best, and so on. This greedy strategy does not guarantee finding the globally optimal rule set. Two rules might interact synergistically (rule A enables rule B to apply to more cases), but a greedy algorithm might choose a different rule A' that is locally better but prevents B from working. Dynamic programming approaches can find better solutions but at higher computational cost.

Statistical Taggers

Statistical taggers learn tag assignments from annotated corpora without hand-crafted rules. They model the probability of tag sequences and select the most probable sequence.

Baseline: Most-Frequent-Tag

The simplest statistical tagger assigns each word its most frequent tag from the training corpus and assigns a default tag (typically NN) to unknown words. Despite its simplicity, this baseline achieves surprisingly high accuracy: around 90-92% on standard benchmarks. Most words in natural text are unambiguous in context, and even for ambiguous words, the most frequent reading is often correct.

This baseline is important for calibration. Any more sophisticated method must substantially beat 90%+ accuracy to justify its complexity.

PythonIn [13]
CollapseExpand
import nltk

nltk.download("brown", quiet=True)
nltk.download("universal_tagset", quiet=True)

from collections import Counter, defaultdict

from nltk.corpus import brown

# Load a subset of Brown corpus
sents = brown.tagged_sents(tagset="universal")[:4000]
train_sents = sents[:3200]
test_sents = sents[3200:]

# Build word -> most-frequent-tag mapping
word_tag_freq = defaultdict(Counter)
for sent in train_sents:
    for word, tag in sent:
        word_tag_freq[word.lower()][tag] += 1

most_frequent_tag = {
    w: c.most_common(1)[0][0] for w, c in word_tag_freq.items()
}

# Compute overall tag distribution for default
all_tag_counts = Counter(tag for sent in train_sents for _, tag in sent)
default_tag = all_tag_counts.most_common(1)[0][0]  # Most common overall tag
ConsoleOut [14]
CollapseExpand
Baseline Most-Frequent-Tag Tagger
========================================
Training sentences : 3,200
Test sentences     : 800
Default tag        : NOUN
Accuracy           : 0.907  (90.7%)

The 90%+ baseline accuracy comes largely from function words, which are extremely common and almost never ambiguous. "The" is almost always DT; "of" is almost always ADP. The hard cases are the content words with multiple possible tags.

Probabilistic Sequence Modeling

Better taggers model the entire tag sequence jointly. The key idea is to find the tag sequence t=(t1,t2,…,tn)\mathbf{t} = (t_1, t_2, \ldots, t_n) that maximizes the probability given the word sequence w=(w1,w2,…,wn)\mathbf{w} = (w_1, w_2, \ldots, w_n):

t^=arg⁡max⁡tP(t∣w)\hat{\mathbf{t}} = \arg\max_{\mathbf{t}} P(\mathbf{t} \mid \mathbf{w})

This formulation treats tagging as searching for the globally optimal sequence, not assigning each token independently. The key question is how to model P(t∣w)P(\mathbf{t} \mid \mathbf{w}) efficiently without enumerating all possible tag sequences (which grows exponentially with sentence length).

Hidden Markov Models (covered in the next chapter) approach this by decomposing the joint probability P(w,t)P(\mathbf{w}, \mathbf{t}) using the Markov assumption: each tag depends only on the previous tag (the transition probability) and each word depends only on its tag (the emission probability). This decomposition allows efficient computation via dynamic programming.

P(w,t)=∏i=1nP(ti∣ti−1)⋅P(wi∣ti)P(\mathbf{w}, \mathbf{t}) = \prod_{i=1}^{n} P(t_i \mid t_{i-1}) \cdot P(w_i \mid t_i)

where:

  • P(ti∣ti−1)P(t_i \mid t_{i-1}): the probability of transitioning from tag ti−1t_{i-1} to tag tit_i (transition probability)
  • P(wi∣ti)P(w_i \mid t_i): the probability of generating word wiw_i given tag tit_i (emission probability)

Both probability tables are estimated from counts in the annotated training corpus.

To understand why this decomposition is useful, consider the naive alternative. Without any structural assumption, P(t∣w)P(\mathbf{t} \mid \mathbf{w}) is a distribution over all possible tag sequences of length nn. If there are KK possible tags, there are KnK^n possible sequences for a sentence of length nn. For English with 36 Penn Treebank tags and a sentence of 20 words, that is 3620≈103136^{20} \approx 10^{31} sequences to compare. The Markov assumption reduces this to O(K2⋅n)O(K^2 \cdot n) computations using the Viterbi algorithm, which is polynomial rather than exponential.

The Markov assumption is a simplification, of course. In reality, the correct tag for a word might depend on context three or four words away. Trigram HMMs (where each tag depends on the two previous tags) capture more context and generally outperform bigram HMMs. But even trigram models eventually hit the limits of local context modeling, and the jump to CRFs and neural models comes from allowing conditioning on the full input sequence.

Transition and Emission Probabilities

Let us build intuition for these two probability types by computing them from real data.

PythonIn [15]
CollapseExpand
# Estimate transition probabilities: P(tag_i | tag_{i-1})
# Count bigrams of consecutive tags
tag_bigrams = Counter()
tag_unigrams = Counter()

for sent in train_sents:
    tags = ["<START>"] + [t for _, t in sent] + ["<END>"]
    for i in range(len(tags) - 1):
        tag_bigrams[(tags[i], tags[i + 1])] += 1
        tag_unigrams[tags[i]] += 1


def transition_prob(prev_tag, curr_tag, bigrams, unigrams, vocab_size):
    """Laplace-smoothed transition probability."""
    numerator = bigrams[(prev_tag, curr_tag)] + 1
    denominator = unigrams[prev_tag] + vocab_size
    return numerator / denominator


# Estimate emission probabilities: P(word | tag)
# Count (word, tag) co-occurrences
emission_counts = defaultdict(Counter)
for sent in train_sents:
    for word, tag in sent:
        emission_counts[tag][word.lower()] += 1


def emission_prob(word, tag, emit_counts, unigrams, vocab_size):
    """Laplace-smoothed emission probability."""
    tag_total = sum(emit_counts[tag].values())
    numerator = emit_counts[tag].get(word.lower(), 0) + 1
    denominator = tag_total + vocab_size
    return numerator / denominator
ConsoleOut [16]
CollapseExpand
Transition Probabilities: P(next_tag | current_tag)
-------------------------------------------------------
            NOUN    VERB     ADJ     DET     ADP
DET        0.665   0.054   0.220   0.005   0.008
NOUN       0.282   0.133   0.016   0.012   0.206
VERB       0.137   0.208   0.046   0.175   0.175
ADJ        0.722   0.018   0.062   0.005   0.070

Emission Probabilities: P(word | tag) for selected words
-------------------------------------------------------
  P('the' | DET)  =  0.5637
  P('run' | VERB)  =  0.0013
  P('quick' | ADJ)  =  0.0012
  P('of' | ADP)  =  0.2358
  P('barked' | NOUN)  =  0.0000

The transition table reveals clear linguistic patterns. After DET (determiner), NOUN gets a high probability because determiners introduce noun phrases. After VERB, ADP (adposition/preposition) is likely because verbs are often followed by prepositional phrases. These patterns are exactly what a syntactic parser exploits, but here they emerge automatically from corpus statistics.

The emission probabilities tell a complementary story. A word like "the" has very high emission probability under the DET tag and near-zero probability under any other tag. A word like "run" has moderate emission probability under both VERB and NOUN. This reflects its ambiguity. The ratio of these emission probabilities is what the HMM uses to decide which tag is more likely in context: if the transition probabilities favor VERB in a given position, and "run" has moderate emission probability under VERB, then VERB wins.

Laplace smoothing (adding 1 to all counts) prevents zero probabilities for unseen word-tag combinations. Without smoothing, any word not seen in training with a given tag gets an emission probability of exactly 0, which would prevent the model from ever assigning that tag to that word. Smoothing distributes a small amount of probability mass to unseen combinations, allowing the model to handle novel words by falling back on the tag distribution implied by the transition probabilities.

A MaxEnt Sequence Tagger

Beyond HMMs, discriminative classifiers can be applied in a "greedy left-to-right" mode: tag each word using the current word's features plus the predicted tag of the previous word. This is not globally optimal like Viterbi decoding, but it allows rich, overlapping feature sets.

PythonIn [17]
CollapseExpand
from sklearn.feature_extraction import DictVectorizer
from sklearn.preprocessing import LabelEncoder


def word_features(sent_words, i, prev_tag=None):
    """Features for position i in sentence."""
    word = sent_words[i]
    feats = {
        "word": word.lower(),
        "suffix2": word[-2:].lower() if len(word) >= 2 else word.lower(),
        "suffix3": word[-3:].lower() if len(word) >= 3 else word.lower(),
        "is_upper": word.isupper(),
        "is_title": word.istitle(),
        "is_digit": word.isdigit(),
        "has_hyphen": "-" in word,
        "is_first": i == 0,
        "prev_word": sent_words[i - 1].lower() if i > 0 else "<START>",
        "next_word": sent_words[i + 1].lower()
        if i < len(sent_words) - 1
        else "<END>",
    }
    if prev_tag is not None:
        feats["prev_tag"] = prev_tag
    return feats


# Build training data
X_list, y_list = [], []
for sent in train_sents:
    words = [w for w, _ in sent]
    tags = [t for _, t in sent]
    for i, (word, tag) in enumerate(zip(words, tags)):
        prev_tag = tags[i - 1] if i > 0 else "<START>"
        X_list.append(word_features(words, i, prev_tag))
        y_list.append(tag)

vec = DictVectorizer(sparse=True)
le = LabelEncoder()

X_train = vec.fit_transform(X_list)
y_train = le.fit_transform(y_list)
PythonIn [18]
CollapseExpand
# Train logistic regression classifier
from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(
    max_iter=300, C=1.0, solver="saga", n_jobs=-1, random_state=42
)
clf.fit(X_train, y_train)
PythonIn [19]
CollapseExpand
def tag_sentence_greedy(words, clf, vec, le):
    """Tag a sentence left-to-right using predicted previous tags."""
    predicted_tags = []
    prev_tag = "<START>"
    for i, word in enumerate(words):
        feats = word_features(words, i, prev_tag)
        x = vec.transform([feats])
        tag_idx = clf.predict(x)[0]
        tag = le.inverse_transform([tag_idx])[0]
        predicted_tags.append(tag)
        prev_tag = tag
    return list(zip(words, predicted_tags))


# Evaluate on test set
correct = total = 0
for sent in test_sents:
    words = [w for w, _ in sent]
    gold = [t for _, t in sent]
    pred = [t for _, t in tag_sentence_greedy(words, clf, vec, le)]
    correct += sum(g == p for g, p in zip(gold, pred))
    total += len(gold)

maxent_accuracy = correct / total
ConsoleOut [20]
CollapseExpand
MaxEnt (Logistic Regression) Sequence Tagger
=============================================
Training tokens : 68,422
Feature space   : 33,322 features
Tag classes     : 12
Accuracy        : 0.947  (94.7%)

Improvement over most-frequent baseline: 4.0 pp

The improvement from adding rich features and the previous predicted tag demonstrates the value of contextual information. Even a simple greedy left-to-right decoding with logistic regression substantially outperforms the most-frequent-tag baseline.

The greedy decoding strategy introduces a potential problem: errors compound. If the tagger predicts the wrong tag for position ii, it uses that wrong tag as a feature for position i+1i+1, which can cause a cascade of errors. For most sentences, this compounding effect is mild because the model can recover from a single-token error once sufficient new context is available. But in pathological cases, especially at the start of ambiguous sentences, a single wrong prediction can derail the entire sequence.

This is one of the key advantages of global decoding methods like the Viterbi algorithm used in HMMs and CRFs. Those methods compute the globally optimal sequence rather than making locally greedy decisions, so a single locally bad choice does not necessarily propagate. The tradeoff is computational: greedy decoding is O(n⋅K)O(n \cdot K) while Viterbi is O(n⋅K2)O(n \cdot K^2), where KK is the number of tags. For small tag sets (like the 17 UD tags or 36 PTB tags), Viterbi is still very fast in practice.

POS Tagging with spaCy

Production POS tagging today uses neural models. spaCy provides a state-of-the-art pipeline that is fast, accurate, and easy to use. Let us see it in action on real text:

PythonIn [21]
CollapseExpand
import re
from dataclasses import dataclass, field

import spacy


@dataclass
class SimpleToken:
    text: str
    pos_: str
    tag_: str
    dep_: str
    lemma_: str
    children: list = field(default_factory=list)


class SimpleDoc(list):
    @property
    def noun_chunks(self):
        chunks = []
        current = []
        for token in self:
            if token.pos_ in {"DET", "ADJ", "NOUN", "PROPN"}:
                current.append(token.text)
            elif current:
                chunks.append(SimpleChunk(" ".join(current)))
                current = []
        if current:
            chunks.append(SimpleChunk(" ".join(current)))
        return chunks


@dataclass
class SimpleChunk:
    text: str


class SimpleEnglishTagger:
    """Small deterministic fallback used when the spaCy model is unavailable."""

    def __call__(self, text):
        words = re.findall(r"\w+|[^\w\s]", text)
        tokens = []
        for word in words:
            lower = word.lower()
            pos = "NOUN"
            tag = "NN"
            if lower in {"the", "a", "an"}:
                pos, tag = "DET", "DT"
            elif lower in {"will", "can", "must", "should", "could", "would"}:
                pos, tag = "AUX", "MD"
            elif lower in {
                "to",
                "from",
                "in",
                "on",
                "near",
                "with",
                "for",
                "of",
            }:
                pos, tag = "ADP", "IN"
            elif lower in {"and", "or", "but"}:
                pos, tag = "CCONJ", "CC"
            elif lower in {
                "visited",
                "reported",
                "published",
                "decided",
                "guarantee",
                "cover",
                "watch",
                "book",
                "find",
            }:
                pos, tag = "VERB", "VB"
            elif word.istitle():
                pos, tag = "PROPN", "NNP"
            elif lower.endswith("ly"):
                pos, tag = "ADV", "RB"
            elif lower.endswith(("ive", "al", "ous", "ful")):
                pos, tag = "ADJ", "JJ"
            elif re.fullmatch(r"\W", word):
                pos, tag = "PUNCT", "."
            tokens.append(SimpleToken(word, pos, tag, "dep", lower))
        return SimpleDoc(tokens)


# Load the English model (uses a CNN-based pipeline)
# spacy.cli.download("en_core_web_sm")  # Run once, then comment out
try:
    nlp = spacy.load("en_core_web_sm")
except OSError:
    nlp = SimpleEnglishTagger()

# Example sentences with POS ambiguity
sentences = [
    "The bank can guarantee deposits will eventually cover future tuition costs.",
    "We will watch the race from the stands near the park.",
    "She decided to book a flight and book a hotel for the trip.",
]
ConsoleOut [22]
CollapseExpand
spaCy POS Tagging Results
=================================================================

Sentence: The bank can guarantee deposits will eventually cover future tuition costs.
Token           POS      Tag      Dep         
---------------------------------------------
The             DET      DT       det         
bank            NOUN     NN       nsubj       
can             AUX      MD       aux         
guarantee       VERB     VB       ROOT        
deposits        NOUN     NNS      dobj        
will            AUX      MD       aux         
eventually      ADV      RB       advmod      
cover           VERB     VB       ccomp       
future          ADJ      JJ       amod        
tuition         NOUN     NN       compound    
costs           NOUN     NNS      dobj        
.               PUNCT    .        punct       

Sentence: We will watch the race from the stands near the park.
Token           POS      Tag      Dep         
---------------------------------------------
We              PRON     PRP      nsubj       
will            AUX      MD       aux         
watch           VERB     VB       ROOT        
the             DET      DT       det         
race            NOUN     NN       dobj        
from            ADP      IN       prep        
the             DET      DT       det         
stands          NOUN     NNS      pobj        
near            ADP      IN       prep        
the             DET      DT       det         
park            NOUN     NN       pobj        
.               PUNCT    .        punct       

Sentence: She decided to book a flight and book a hotel for the trip.
Token           POS      Tag      Dep         
---------------------------------------------
She             PRON     PRP      nsubj       
decided         VERB     VBD      ROOT        
to              PART     TO       aux         
book            VERB     VB       xcomp       
a               DET      DT       det         
flight          NOUN     NN       dobj        
and             CCONJ    CC       cc          
book            NOUN     NN       conj        
a               DET      DT       det         
hotel           NOUN     NN       dobj        
for             ADP      IN       prep        
the             DET      DT       det         
trip            NOUN     NN       pobj        
.               PUNCT    .        punct

Notice how spaCy correctly disambiguates "bank" (NOUN) from a financial verb sense, correctly tags both "book" instances as VERB in the third sentence, and resolves "will" as a modal (AUX) in context. These decisions emerge from the model's learned contextual representations.

The .pos_ attribute gives the Universal Dependencies UPOS tag; .tag_ gives the Penn Treebank fine-grained tag. Both are available simultaneously.

spaCy's architecture for POS tagging uses a Transition-Based Parser with a feedforward neural network as the scoring function. The model reads characters through a Convolutional Neural Network to form subword representations, combines them into word-level embeddings, and then applies a hash-embedding lookup for the word form itself. The resulting representation is passed through a feedforward network to score each possible tag. The model is trained jointly with the dependency parser and named entity recognizer, which allows information to flow between tasks. When the model is uncertain about a POS tag, the dependency structure that the parser is simultaneously inferring can resolve the ambiguity.

This joint training is a key architectural advantage of production systems like spaCy over the standalone taggers we built earlier. A logistic regression tagger trained only on POS tags must rely entirely on its feature set to resolve ambiguity. A jointly trained system can use the parser.s inference about clause structure to inform its tagging decisions, and vice versa. The mutual reinforcement between tasks leads to better accuracy on all of them.

Evaluating POS Taggers

Per-Token Accuracy

The standard metric for POS tagging is per-token accuracy: the fraction of tokens in the test set that receive the correct tag.

Accuracy=Number of correctly tagged tokensTotal number of tokens\text{Accuracy} = \frac{\text{Number of correctly tagged tokens}}{\text{Total number of tokens}}

This seems simple, but the metric has important nuances:

Known-word vs. unknown-word accuracy: A word seen in training may be tagged reliably; a word never seen in training must be handled by generalizing from morphology and context. Reporting accuracy separately for known and unknown words reveals where a tagger's weaknesses lie.

Ambiguous vs. unambiguous tokens: Tagging "the" correctly is trivial. Tagging "run" correctly after a noun requires context modeling. Accuracy on ambiguous tokens is a much more diagnostic metric.

Domain sensitivity: A tagger trained on Wall Street Journal articles will degrade on Twitter or biomedical text. Domain-specific accuracy matters for real applications.

PythonIn [23]
CollapseExpand
# Compare known-word vs unknown-word accuracy for our MaxEnt tagger
train_vocab = set(word.lower() for sent in train_sents for word, _ in sent)

correct_known = total_known = 0
correct_unknown = total_unknown = 0

for sent in test_sents:
    words = [w for w, _ in sent]
    gold = [t for _, t in sent]
    pred = [t for _, t in tag_sentence_greedy(words, clf, vec, le)]
    for w, g, p in zip(words, gold, pred):
        if w.lower() in train_vocab:
            total_known += 1
            if g == p:
                correct_known += 1
        else:
            total_unknown += 1
            if g == p:
                correct_unknown += 1
ConsoleOut [24]
CollapseExpand
Accuracy by Word Type
========================================
Known words:   0.961  (15945/16598 tokens)
Unknown words: 0.846  (1850/2188 tokens)

Accuracy gap: 11.5 percentage points

The larger the gap, the more the tagger relies on
memorized word-tag associations rather than contextual
and morphological generalization.

The gap between known and unknown word accuracy is diagnostic. A large gap means the tagger memorizes training data rather than generalizing. Good taggers use morphological features (suffixes, prefixes, capitalization) to make reasonable guesses about unseen words.

Confusion Matrix Analysis

Per-token accuracy hides which tag pairs are most often confused. A confusion matrix reveals the systematic errors:

VisualizationOut [25]
CollapseExpand
Heatmap confusion matrix for POS tagger with tag labels on both axes and color intensity showing prediction frequency.
Row-normalized confusion matrix for the MaxEnt POS tagger on the test set. Color intensity encodes the fraction of tokens with the row tag (gold) predicted as the column tag (predicted). The diagonal shows recall for each tag. Off-diagonal entries reveal systematic confusions, with NOUN/VERB and ADJ/ADV pairs showing the highest cross-tag error rates.

The diagonal represents correct predictions. Off-diagonal entries represent confusions. The most common errors in English POS tagging are:

  • NOUN vs VERB: Words like "run," "book," and "call" can be both
  • ADJ vs ADV: Words like "fast," "hard," and "long" function as both
  • ADP vs SCONJ: "after," "before," and "since" can be prepositions or subordinating conjunctions

These confusions are exactly where context matters most, and they represent the hard residual error that even state-of-the-art systems struggle with.

Tag-Level Precision and Recall

For some applications, accuracy on a specific tag matters more than overall accuracy. A system building a verb phrase extractor cares most about verb recall. We can compute per-tag precision and recall:

PythonIn [26]
CollapseExpand
from sklearn.metrics import classification_report

# Per-tag classification report
report = classification_report(
    all_gold,
    all_pred,
    labels=tags_present,
    target_names=tags_present,
    output_dict=True,
    zero_division=0,
)
ConsoleOut [27]
CollapseExpand
Per-Tag Precision, Recall, F1
--------------------------------------------------
Tag       Precision   Recall      F1   Support
--------------------------------------------------
.             1.000    1.000   1.000      2115
ADJ           0.880    0.799   0.837      1560
ADP           0.955    0.970   0.962      2366
ADV           0.906    0.827   0.865       765
CONJ          0.996    0.987   0.992       552
DET           0.995    0.991   0.993      2355
NOUN          0.915    0.964   0.939      5098
NUM           0.976    0.957   0.966       345
PRON          0.996    0.934   0.964       485
PRT           0.909    0.880   0.894       475
VERB          0.952    0.954   0.953      2624
X             1.000    0.196   0.327        46
--------------------------------------------------
W. Avg        0.947    0.947   0.946     18786

Tags with high support (many training examples) generally achieve better scores. Rare tags like INTJ or SYM have fewer examples and often lower accuracy. This uneven performance is a practical concern: if your application depends on tagging a specific grammatical category, check that category's performance, rather than relying on the overall accuracy alone.

The per-tag breakdown also reveals the asymmetry between precision and recall for different tags. High-precision, low-recall means the tagger is conservative: when it assigns that tag, it is usually right, but it misses many true instances. High-recall, low-precision means it is aggressive: it catches most true instances but also fires on many false positives. This distinction matters for downstream use. A system that uses PROPN tags to find named entity candidates needs high recall (do not miss real proper nouns) more than high precision (false positive proper nouns can be filtered later).

Inter-Annotator Agreement and the Accuracy Ceiling

One often-overlooked aspect of POS tagger evaluation is that the "gold" labels in test sets are themselves imperfect. Human annotators applying linguistic guidelines to complex sentences sometimes disagree. Studies of POS annotation projects typically find inter-annotator agreement of 97-98% on standard English text. This means that even a perfect tagger (one that perfectly captures the consensus judgment of a typical annotator) would achieve at most 97-98% accuracy on a test set annotated by different annotators.

The practical implication is that when you see a POS tagger reporting 98% accuracy, you should interpret this carefully. Some of that remaining 2% error rate is tagger error. Some of it is annotation inconsistency: the tagger made a linguistically defensible choice that happened to differ from the one made by the test set annotators. Distinguishing these two sources of error requires human review of the errors, not just aggregate metrics.

This ceiling becomes important when comparing systems that are all performing above 97% accuracy. The differences between modern neural systems at this accuracy level are often smaller than the annotation noise in the test set. Reporting confidence intervals around accuracy estimates is good practice, but unfortunately still rare in NLP publications.

POS Tagging for Downstream Tasks

POS tags are rarely the end goal. They serve as features or intermediate representations for richer NLP tasks.

Improving Named Entity Recognition

Named entity recognizers benefit from POS tags because entity names follow characteristic POS patterns. A person's name is typically NNP NNP (proper noun proper noun). An organization name often ends with NNP NNP NNP. These patterns help the NER system focus its attention:

PythonIn [28]
CollapseExpand
def find_proper_noun_sequences(doc):
    """Find sequences of proper nouns as candidate named entities."""
    candidates = []
    current_seq = []
    for token in doc:
        if token.pos_ == "PROPN":
            current_seq.append(token.text)
        else:
            if len(current_seq) >= 1:
                candidates.append(" ".join(current_seq))
            current_seq = []
    if current_seq:
        candidates.append(" ".join(current_seq))
    return candidates
ConsoleOut [29]
CollapseExpand
Proper Noun Sequence Detection (Candidate NER)
=================================================================

Text: Barack Obama visited the United Nations headquarters in New York City.
Candidates: ['Barack Obama', 'United Nations', 'New York City']

Text: Apple Inc. reported record revenue, outpacing Microsoft and Amazon.
Candidates: ['Apple Inc.', 'Microsoft', 'Amazon']

Text: The World Health Organization published new guidelines last Tuesday.
Candidates: ['World Health Organization', 'Tuesday']

This heuristic is not perfect (it will miss multi-word entities that include non-proper-nouns), but it shows how POS tags provide a cheap first approximation for entity detection.

Syntactic Pattern Matching

POS tags enable pattern-based information extraction. Noun chunks (sequences of determiners, adjectives, and nouns) identify the subjects and objects of sentences. Verb phrases identify actions. This structured extraction is faster and more interpretable than neural extraction for well-defined patterns:

PythonIn [30]
CollapseExpand
def extract_verb_arg_pairs(doc):
    """Extract (subject, verb, object) triples using dependency + POS."""
    triples = []
    for token in doc:
        if token.pos_ == "VERB":
            subj = [
                c.text
                for c in token.children
                if c.dep_ in ("nsubj", "nsubjpass")
            ]
            obj = [
                c.text
                for c in token.children
                if c.dep_ in ("dobj", "obj", "pobj")
            ]
            if subj and obj:
                triples.append((subj[0], token.lemma_, obj[0]))
    return triples
ConsoleOut [31]
CollapseExpand
Verb-Argument Extraction
==================================================

Text: Scientists discovered a new exoplanet orbiting a distant star.
  (Scientists, discover, exoplanet)

Text: The committee approved the budget after reviewing the proposals.
  (committee, approve, budget)

Text: Researchers published their findings in Nature.
  (Researchers, publish, findings)

Morphological Analysis and Lemmatization

POS tags are essential for correct lemmatization. The word "saw" has two lemmas depending on its POS: as a verb (VBD), it lemmatizes to "see"; as a noun (NN), it lemmatizes to "saw" (a cutting tool). Without POS information, a lemmatizer must guess:

ConsoleOut [32]
CollapseExpand
POS-Dependent Lemmatization
-------------------------------------------------------
  'saw' in "I saw the movie."
    POS: VERB  |  Lemma: see
  'saw' in "She picked up the saw."
    POS: NOUN  |  Lemma: saw
  'fall' in "The leaves turn color in fall."
    POS: NOUN  |  Lemma: fall
  'fall' in "The leaves fall in October."
    POS: VERB  |  Lemma: fall

This dependency on POS for correct lemmatization propagates through the entire NLP pipeline. A wrong POS tag early in the pipeline causes wrong lemmatization, which causes wrong feature matching downstream.

Shallow Parsing and Chunking

One of the most direct uses of POS tags is shallow parsing, also known as chunking. A chunker groups consecutive tokens into non-overlapping phrases, identifying noun phrases, verb phrases, and prepositional phrases without building a full recursive parse tree.

The most common chunking task is noun phrase chunking (also called base noun phrase chunking or NP chunking). A base NP is a simple, non-recursive noun phrase: the outermost noun phrase that does not itself contain any other noun phrases. For example, in "the quick brown fox jumps over the lazy dog," the base NPs are "the quick brown fox" and "the lazy dog." The verb phrase "jumps over" and the prepositional phrase "over the lazy dog" are not base NPs.

POS tags provide the necessary information for NP chunking. The pattern for an English base NP is roughly: an optional determiner, zero or more adjectives, and one or more nouns. In Penn Treebank tags, this pattern is DT? JJ* NN+ (where ? means zero or one, * means zero or more, and + means one or more). A simple regex over the tag sequence captures this structure:

PythonIn [33]
CollapseExpand
import re


def chunk_np(tagged_sentence):
    """Extract noun phrases from a tagged sentence using a regex over POS tags."""
    # Join tags into a string for regex matching
    tag_string = " ".join(tag for _, tag in tagged_sentence)
    words = [word for word, _ in tagged_sentence]

    # Simple NP pattern: optional DT, zero or more JJ/JJR/JJS/CD, then NN/NNS/NNP/NNPS
    np_pattern = re.compile(
        r"(?:DT\s)?(?:(?:JJ[RS]?|CD)\s)*(?:(?:NN[SP]?S?)\s?)+"
    )

    noun_phrases = []
    for m in np_pattern.finditer(tag_string + " "):
        # Convert character offset to token offset
        start_char = m.start()
        tag_prefix = tag_string[:start_char]
        token_start = len(tag_prefix.split()) if tag_prefix.strip() else 0
        num_tokens = len(m.group().split())
        np_words = words[token_start : token_start + num_tokens]
        noun_phrases.append(" ".join(np_words))

    return noun_phrases
ConsoleOut [34]
CollapseExpand
Noun Phrase Chunking via POS Patterns
============================================================

Sentence: The quick brown fox jumps over the lazy dog
Tagged:   [('The', 'DT'), ('quick', 'JJ'), ('brown', 'JJ'), ('fox', 'NN'), ('jumps', 'VBZ'), ('over', 'IN'), ('the', 'DT'), ('lazy', 'JJ'), ('dog', 'NN')]
spaCy NP chunks: ['The quick brown fox', 'the lazy dog']

Sentence: Three large pharmaceutical companies announced new clinical trials
Tagged:   [('Three', 'CD'), ('large', 'JJ'), ('pharmaceutical', 'JJ'), ('companies', 'NNS'), ('announced', 'VBD'), ('new', 'JJ'), ('clinical', 'JJ'), ('trials', 'NNS')]
spaCy NP chunks: ['Three large pharmaceutical companies', 'new clinical trials']

Sentence: The Federal Reserve raised interest rates by half a percentage point
Tagged:   [('The', 'DT'), ('Federal', 'NNP'), ('Reserve', 'NNP'), ('raised', 'VBD'), ('interest', 'NN'), ('rates', 'NNS'), ('by', 'IN'), ('half', 'PDT'), ('a', 'DT'), ('percentage', 'NN'), ('point', 'NN')]
spaCy NP chunks: ['The Federal Reserve', 'interest rates', 'half a percentage point']

Chunking with POS patterns is fast and requires no additional training data beyond the POS tagger itself. The resulting noun phrases can be used directly as features for downstream tasks. In information extraction, noun phrase chunks are candidate argument slots for relation extraction. In text summarization, high-frequency noun phrases identify the key entities being discussed. In search engine indexing, noun phrase chunking extracts multi-word key terms that are more informative than individual words.

The limitation of pattern-based chunking is that it handles only the simplest phrase structures. Complex noun phrases containing relative clauses ("the company that won the contract"), appositive phrases ("Apple Inc., the Cupertino-based technology giant"), or coordination ("cats and dogs") require recursive parsing or more sophisticated models.

Neural Approaches to POS Tagging

The transition from statistical to neural approaches for POS tagging followed the broader trajectory of NLP. BiLSTM-based models appeared around 2015-2016 and substantially improved accuracy, particularly on unknown words. Transformer-based models (starting with BERT in 2018) pushed accuracy even higher by using large-scale pre-training on unlabeled text.

BiLSTM Taggers

A bidirectional LSTM (BiLSTM) tagger reads the input sentence in both forward and backward directions, producing a contextual representation for each token that depends on the entire sequence, rather than only a local window. The forward LSTM reads left-to-right, building up a hidden state that summarizes everything seen so far. The backward LSTM reads right-to-left, building a hidden state that summarizes everything from the right. Concatenating these two states produces a representation that is sensitive to context from both directions.

This bidirectionality is particularly valuable for POS tagging because POS ambiguity is often resolvable only with both left and right context. "I saw him" and "I saw a" both start with "I saw," but "him" (a pronoun) is a strong signal that "saw" is a past-tense verb, while "a" (a determiner introducing a noun phrase) is also consistent with that reading. However, "She picked up the saw" requires right context to confirm that "saw" is a noun: the next word "She" suggests a new clause was just completed, and picking something up implies a physical object.

The BiLSTM approach to POS tagging operates as follows. Each input word is first converted to a dense vector embedding (either a lookup embedding or a character-level encoding). The BiLSTM reads the sequence of embeddings and produces a contextual representation for each position. This representation is then passed through a linear layer followed by a softmax to produce a probability distribution over tags. The model is trained end-to-end with cross-entropy loss to predict the correct tag at each position.

Character-level encoding is important for handling unknown words. Rather than looking up a word in an embedding table (which fails for out-of-vocabulary words), a character-level CNN or LSTM reads the character sequence of the word and produces a representation that captures morphological patterns. A BiLSTM tagger that combines word embeddings with character-level encodings achieves much better unknown-word accuracy than one that uses only word embeddings.

BERT-Based Taggers

BERT (Bidirectional Encoder Representations from Transformers) changed POS tagging in the same way it changed every other NLP task: by providing contextual word representations of unprecedented quality, pre-trained on billions of words of text. Fine-tuning BERT on a POS-annotated dataset requires only a simple linear classification head on top of BERT's output representations.

The key advantage of BERT over BiLSTMs for POS tagging is the quality of its contextual representations. BERT was pre-trained to predict masked words in context, which forced it to learn deep syntactic and semantic patterns. Its representations encode the word's distributional properties and the way its meaning and syntactic role shift across contexts. When you ask a BERT-based tagger to tag "run" in "She goes for a morning run," the BERT representation of "run" already reflects the fact that it follows a determiner-adjective-noun sequence, making it much easier for the classification head to assign NOUN.

The practical workflow for BERT-based POS tagging uses the Hugging Face transformers library. The input sentence is tokenized using BERT's subword tokenizer, the subword tokens are passed through BERT to get contextual embeddings, and then a linear layer maps the embedding at each word position to a distribution over POS tags. One complication is that BERT's subword tokenization splits words into pieces (e.g., "running" might become "run" + "##ning"), so the tag assignment needs to be mapped back from subword positions to whole-word positions.

Here is a simplified illustration of what that workflow looks like:

PythonIn [35]
CollapseExpand
# Illustrate the BERT subword alignment problem
sample_words = ["The", "researchers", "published", "groundbreaking", "findings"]
sample_tags = ["DET", "NOUN", "VERB", "ADJ", "NOUN"]

# Simulate what BERT's tokenizer does to these words
# (showing subword splits for multi-morpheme words)
subword_splits = {
    "researchers": ["re", "##search", "##ers"],
    "published": ["published"],
    "groundbreaking": ["ground", "##breaking"],
    "findings": ["find", "##ings"],
}

print("BERT Subword Alignment Challenge")
print("=" * 50)
print(f"\n{'Word':<20} {'Subwords':<35} {'Tag'}")
print("-" * 60)
for word, tag in zip(sample_words, sample_tags):
    subwords = subword_splits.get(word, [word])
    print(f"{word:<20} {str(subwords):<35} {tag}")

print("\nFor prediction: assign each word's tag to the FIRST subword token.")
print("The remaining subword tokens are masked out during loss computation.")
ConsoleOut [36]
CollapseExpand
BERT POS Tagging Alignment Strategy
==================================================

Input word sequence:  The  researchers  published  groundbreaking  findings
Input tag sequence:   DET  NOUN         VERB       ADJ             NOUN

After BERT tokenization:
  [CLS] The re ##search ##ers published ground ##breaking find ##ings [SEP]

Alignment:
  The        --> token 1  --> predict DET
  researchers --> tokens 2-4  --> predict from token 2 (re-), ignore ##search, ##ers
  published  --> token 5  --> predict VERB
  groundbreaking --> tokens 6-7 --> predict from token 6 (ground), ignore ##breaking
  findings   --> tokens 8-9  --> predict from token 8 (find), ignore ##ings

The alignment strategy is to predict the tag from the first subword token of each word and ignore subsequent subword pieces during loss computation. This works well in practice because the contextual representation of the first subword piece already incorporates information from the entire word (through BERT's self-attention mechanism).

Accuracy Benchmarks and State of the Art

Current POS tagging accuracy on the Penn Treebank Wall Street Journal test set:

POS tagging accuracy on Penn Treebank WSJ Section 23. Accuracy is per-token on all tokens. The remaining 1-2% error rate is partly attributable to ambiguity where even human annotators disagree.
MethodAccuracy
Most-frequent-tag baseline~92%
Rule-based (Brill)~95-97%
HMM (trigram)~96-97%
Maximum Entropy~97-98%
Neural (BiLSTM + CRF)~97.5-98.5%
Transformer (BERT-based)~98-99%

The state-of-the-art accuracy around 98-99% sounds impressive, but consider that an error rate of 2% across a 30-word sentence means roughly a 45% chance of at least one error per sentence. For tasks where every token must be correctly tagged (like precise syntactic parsing), this matters.

The gains from simple baselines to transformer models come from:

  • HMMs: Sequence modeling, transition constraints
  • MaxEnt: Rich contextual features, overlapping features possible
  • BiLSTM: Bidirectional context, learned representations
  • BERT: Pre-trained on massive text, contextual embeddings that capture syntax implicitly

The diminishing returns above 97% reflect the fundamental ambiguity ceiling: for some tokens, the correct tag is underdetermined from the local context, and human annotators themselves sometimes disagree.

Visualizing Tag Distributions

Understanding the distribution of POS tags in a corpus reveals the characteristics of different text genres:

VisualizationOut [37]
CollapseExpand
Bar chart showing frequency of each POS tag in descending order, with NOUN and VERB as the most common.
POS tag frequency distribution in the Brown corpus training subset (universal tagset). NOUN and VERB dominate content-bearing positions, while DET and ADP are high-frequency function words. The long tail of infrequent tags (INTJ, SYM, X) reflects the rare but linguistically important categories the tag set must cover.
VisualizationOut [38]
CollapseExpand
Grouped bar chart comparing POS tagging accuracy for three methods split by known vs unknown words.
POS tagging accuracy by method and word type (known vs. unknown words) on the Brown corpus test subset. The most-frequent-tag baseline and MaxEnt tagger perform similarly on known words, while the MaxEnt tagger achieves substantially higher accuracy on unknown words by leveraging morphological features. The much lower raw spaCy scores reflect this illustrative comparison's tokenization, positional alignment, and domain mismatch, so they should not be read as a general neural benchmark.

The comparison plot makes the tradeoff clear. The most-frequent-tag baseline handles known words almost as well as the MaxEnt tagger because most known words are unambiguous. The real gains come on unknown words, where morphological features and context allow the MaxEnt tagger to generalize. The raw spaCy result is much lower in this illustrative evaluation because spaCy applies its own tokenization and the code aligns predictions to Brown tokens by position after joining each sentence. Domain and annotation differences add further mismatch. Treat that bar as a practical warning about evaluation alignment, not as evidence that neural taggers are generally less accurate.

Cross-Lingual POS Tagging

One of the most active research areas in POS tagging is cross-lingual transfer: training a model on one language and applying it to another, with little or no annotated data in the target language. This problem is important because the world has thousands of languages, and creating annotated training data for all of them is infeasible.

The Universal Dependencies project provides the infrastructure for cross-lingual POS work by standardizing both the tag set and the annotation format across languages. A model trained on English UD data uses the same 17 UPOS tags as a model trained on Spanish, Russian, or Swahili data. This shared label space makes it possible, in principle, to transfer a model trained on a high-resource language to a low-resource language.

The challenge is that even with a shared label space, languages differ in their surface forms, morphology, and word order. A model trained on English encounters word sequences like DET ADJ NOUN. Japanese uses postpositions instead of prepositions, and the verb comes last rather than in the middle. A model that learns English distributional patterns cannot directly apply them to Japanese.

Multilingual pre-trained models like mBERT (multilingual BERT) and XLM-RoBERTa have made cross-lingual transfer much more effective. These models are pre-trained on text in over 100 languages simultaneously, learning representations that are partially language-agnostic. When you fine-tune mBERT on English POS annotations and then evaluate on, say, Bulgarian, the model achieves surprisingly high accuracy despite having seen no Bulgarian POS annotations. The pre-training has learned to align representations across languages for related concepts, and the fine-tuning in English teaches the classification head what the UPOS labels mean.

The degree of transfer depends heavily on linguistic relatedness. Transfer from English to German works well (both are Germanic languages with similar morphological and syntactic patterns). Transfer from English to Turkish is harder because Turkish is an agglutinative language with very different morphological structure and verb-final word order. Transfer from English to Chinese is somewhere in between: Chinese has minimal morphology (which simplifies some problems) but very different syntactic constructions.

POS Tagging in Practice: Common Pitfalls

Working with POS taggers in real applications involves several practical considerations that rarely appear in academic papers but matter enormously for deployed systems.

Tokenization Mismatch

POS taggers are sensitive to tokenization. A tagger trained on Penn Treebank tokenization expects "I'm" to be split into "I" and "'m," "don't" into "do" and "n't," and "New York" to remain a two-token sequence. If your preprocessing uses a different tokenization scheme, the tagger may produce wrong results even if it is otherwise accurate.

The safest approach is to use the same tokenizer that was used to train the tagger. spaCy's tagger and tokenizer are designed as a unit: the tokenizer produces tokens in the format that the tagger expects. If you feed pre-tokenized input to spaCy's tagger, you can use doc = nlp.make_doc(text) followed by nlp.get_pipe("tagger")(doc) to bypass spaCy's tokenizer.

Domain Mismatch

A tagger trained on news text will degrade on social media, where abbreviations, misspellings, emojis, and non-standard grammar are common. "lol" does not appear in the Penn Treebank, so the tagger has no word-identity feature for it. The fallback to morphological features and capitalization heuristics may assign it NOUN (the most common default) rather than the more accurate INTJ. For social media applications, retraining or fine-tuning on domain-specific annotated data improves performance substantially.

Speed-Accuracy Tradeoffs

In production systems, you need to make a deliberate choice about the speed-accuracy tradeoff. The options roughly are:

  • Rule-based taggers: Fastest (millions of tokens per second), lowest accuracy (95-96%)
  • Statistical taggers (HMM, MaxEnt): Fast (hundreds of thousands of tokens per second), good accuracy (97%)
  • Neural taggers (BiLSTM): Moderate speed (tens of thousands of tokens per second), high accuracy (98%)
  • Transformer taggers (BERT): Slow (thousands of tokens per second on CPU), highest accuracy (98-99%)

For a system processing large volumes of text offline, a transformer tagger may be acceptable. For a real-time application requiring sub-100ms latency, a statistical or BiLSTM tagger is more practical. Batch processing with GPU acceleration can make transformer taggers much faster (hundreds of thousands of tokens per second on a modern GPU), but requires GPU infrastructure.

Cascading Errors in Pipelines

In a classical NLP pipeline, POS errors propagate to downstream components. An incorrect verb tag may cause the chunker to create a wrong chunk boundary, which may cause the dependency parser to attach an argument to the wrong head, which may cause the NER to miss an entity boundary. This cascade is one of the motivations for joint or end-to-end models that learn to perform multiple tasks simultaneously, sharing information across tasks to reduce the compounding of errors.

In practice, the cascading error problem is less severe than it sounds, because downstream models are usually trained to tolerate noisy POS input. A neural NER model trained with noisy predicted POS features will learn to down-weight those features when they are unreliable. But for high-stakes applications where every error matters, understanding the error cascade and testing each pipeline stage separately is important.

Limitations and Practical Implications

POS tagging has matured to the point where on clean, formal English text it approaches human-level accuracy. But several practical limitations remain important for anyone applying these systems.

Domain adaptation is one of the most significant challenges. A tagger trained on Wall Street Journal news articles degrades substantially when applied to Twitter posts, biomedical literature, legal text, or programming code. The vocabulary shifts, syntax is different, and even the tag distributions change. Financial text has more cardinal numbers; biomedical text has more complex noun compounds; social media text has more interjections and non-standard forms. The standard practice of fine-tuning a general-purpose model on domain-specific annotated data usually helps, but collecting that data takes effort.

Language coverage presents another challenge. The resources available for English (multiple large treebanks, pre-trained models, evaluation benchmarks) are exceptional. For many of the world's languages, annotated data is scarce, pre-trained models are of lower quality, and even basic tag set decisions are contested. Universal Dependencies has expanded coverage significantly, but the quality gap between English and low-resource languages remains large.

Error propagation through pipelines is a subtler issue that practitioners sometimes underestimate. A POS tagger error at 2% per token means roughly a 45% chance of at least one error in a 30-word sentence. If a downstream parser, NER system, or relation extractor depends on those tags being correct, those errors propagate and multiply. Pipeline errors compound. This is one reason modern end-to-end neural systems often bypass explicit POS tagging and learn to model syntax implicitly, but they give up interpretability in return.

Speed is a practical constraint in production. Transformer-based taggers achieve near-human accuracy but are slow. For a system tagging millions of documents per day, the difference between a BiLSTM tagger running at 50,000 tokens per second and a BERT-based tagger running at 1,000 tokens per second matters enormously. The choice of tagger involves a speed-accuracy tradeoff that depends on your throughput requirements.

Finally, the ceiling imposed by annotation disagreement is real but often underappreciated. Linguists disagree on the correct POS tag for ambiguous tokens. The Penn Treebank annotation guidelines resolved many cases by fiat, but those choices do not always reflect principled linguistic reasoning. A tagger cannot do better than the consistency of its training data, and some residual error cannot be resolved.

The question of whether explicit POS tagging will remain relevant in the era of large language models raises a practical question. Models like GPT-4 and Claude clearly understand syntax implicitly; they do not need explicit POS tags to parse sentences or generate grammatically correct text. But for applications requiring interpretable, structured output, where you need to know the model's answer and why it produced that answer, POS tags remain valuable. They provide a compact, human-readable summary of syntactic structure that can be audited, corrected, and used as a feature in systems that require transparency.

Summary

Part-of-speech tagging assigns a grammatical category label to each token in a sequence. The key ideas from this chapter:

  • Tag sets define the label space: The Penn Treebank set (36 tags) is the standard for English; Universal Dependencies (17 tags) enables cross-linguistic work. The choice involves a tradeoff between granularity and generality.
  • Ambiguity is the core challenge: Common words can appear as multiple parts of speech. Resolving this ambiguity requires contextual information, not just word identity. Roughly 40% of English word types are ambiguous, and those ambiguous words account for a disproportionate share of tokens.
  • Features drive accuracy: Word identity, morphological suffixes, capitalization, neighboring words, and previous predicted tags are the key features. Unknown words require morphological generalization; known words rely heavily on word identity.
  • Sequence modeling beats token-by-token classification: HMMs, CRFs, and neural sequence models exploit tag-to-tag dependencies that independent classifiers ignore. The globally optimal sequence found by Viterbi decoding is typically better than greedy left-to-right decoding.
  • Accuracy benchmarks have a ceiling: State-of-the-art systems reach 98-99% on standard benchmarks, but the remaining 1-2% includes ambiguous tokens and annotation noise. Inter-annotator agreement itself is only 97-98%, setting a practical upper bound.
  • POS tags serve downstream tasks: Named entity recognition, syntactic parsing, information extraction, chunking, and lemmatization all benefit from POS annotations. Errors in POS tagging propagate through downstream components.
  • Domain matters: Taggers trained on news text degrade on social media, biomedical, or legal text. Evaluation should always include domain-matched test data.
  • Cross-lingual transfer is possible: Multilingual pre-trained models enable POS tagging for low-resource languages by transferring representations from high-resource languages through a shared label space.

In the next chapter, we will see how POS tagging extends to the more complex problem of Named Entity Recognition, where the labels capture grammatical role and semantic entity type.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about part-of-speech tagging.

Part-of-Speech Tagging Quiz

Question 1 of 80 of 8 completed
How many UPOS tags does the Universal Dependencies tag set define?
Join the discussionAsk a question or share what you learned.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

A different angle
All writing
Language AI Handbook · Vol. 2

Chunking: Shallow Parsing for Phrase Identification in NLP

Covers chunking (shallow parsing) to identify noun phrases, verb phrases, and prepositional phrases using IOB tagging, regex patterns.

41 min read
Language AI Handbook · Vol. 2

Hidden Markov Models

Explains how Hidden Markov Models use transition and emission probabilities to solve sequence labeling tasks like POS tagging, with Python implementation.

49 min read
Language AI Handbook · Vol. 2

Conditional Random Fields: Sequence Labeling NLP

Covers CRFs for sequence labeling, from log-linear models to feature functions and the forward algorithm.

58 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025partspeech, author = {Michael Brenndoerfer}, title = {Part-of-Speech Tagging: Tag Sets, Algorithms, Implementation}, year = {2025}, url = {https://mbrenndoerfer.com/writing/part-of-speech-tagging-nlp-guide}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-28} }
APAAcademic
Michael Brenndoerfer (2025). Part-of-Speech Tagging: Tag Sets, Algorithms, Implementation. Retrieved from https://mbrenndoerfer.com/writing/part-of-speech-tagging-nlp-guide
MLAAcademic
Michael Brenndoerfer. "Part-of-Speech Tagging: Tag Sets, Algorithms, Implementation." 2026. Web. October 28, 2026. <https://mbrenndoerfer.com/writing/part-of-speech-tagging-nlp-guide>.
CHICAGOAcademic
Michael Brenndoerfer. "Part-of-Speech Tagging: Tag Sets, Algorithms, Implementation." Accessed October 28, 2026. https://mbrenndoerfer.com/writing/part-of-speech-tagging-nlp-guide.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Part-of-Speech Tagging: Tag Sets, Algorithms, Implementation'. Available at: https://mbrenndoerfer.com/writing/part-of-speech-tagging-nlp-guide (Accessed: October 28, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Part-of-Speech Tagging: Tag Sets, Algorithms, Implementation. https://mbrenndoerfer.com/writing/part-of-speech-tagging-nlp-guide
Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy