Viterbi Algorithm › The Optimal Path Problem

Michael BrenndoerferUpdated April 28, 20256 min read
Language AI HandbookAll chapters
Part 6 of 67Sequence Labeling
Chapter 41Viterbi Algorithm › The Optimal Path Problem
Reading section 2 of 12 · The Optimal Path Problem · 6 minSection 2 of 12 · 6 min

Book contents

Section 2 of 12 · 6 min read

Viterbi Algorithm › The Optimal Path ProblemOne section · Sections in this chapter2 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Consider a hidden Markov model for part-of-speech tagging. You observe a sequence of words w1,w2,…,wnw_1, w_2, \ldots, w_n and want to find the sequence of tags t1,t2,…,tnt_1, t_2, \ldots, t_n that maximizes the joint probability of seeing both the tags and the words together. The HMM factors this joint probability into a product of simpler terms:

P(t1,…,tn,w1,…,wn)=P(t1)∏i=2nP(ti∣ti−1)∏i=1nP(wi∣ti)P(t_1, \ldots, t_n, w_1, \ldots, w_n) = P(t_1) \prod_{i=2}^{n} P(t_i | t_{i-1}) \prod_{i=1}^{n} P(w_i | t_i)

where:

  • t1,…,tnt_1, \ldots, t_n: the sequence of hidden states (POS tags) we want to infer
  • w1,…,wnw_1, \ldots, w_n: the observed sequence of words
  • P(t1)P(t_1): the initial state probability, capturing how likely the sentence starts with tag t1t_1
  • P(ti∣ti−1)P(t_i | t_{i-1}): the transition probability from tag ti−1t_{i-1} to tag tit_i, encoding grammatical patterns like "determiners often precede nouns"
  • P(wi∣ti)P(w_i | t_i): the emission probability of word wiw_i given tag tit_i, capturing which words are typical for each tag

The first product captures transition probabilities: how likely is each tag given the previous one? The second product captures emission probabilities: how likely is each word given its tag? This factorization embodies the Markov assumption: each tag depends only on its immediate predecessor, not on the full history.

Optimal Path Problem

Given a sequence of observations and a model with transition and emission probabilities, find the sequence of hidden states that maximizes the probability of generating those observations.

Why Greedy Decoding Fails

A greedy approach picks the best tag at each position independently, considering only the previous tag. At each position ii, greedy decoding selects:

ti∗=arg⁡max⁡tiP(wi∣ti)⋅P(ti∣ti−1∗)t_i^* = \arg\max_{t_i} P(w_i | t_i) \cdot P(t_i | t_{i-1}^*)

where:

  • ti∗t_i^*: the greedily chosen tag at position ii
  • arg⁡max⁡ti\arg\max_{t_i}: selects the tag tit_i that maximizes the following expression
  • P(wi∣ti)P(w_i | t_i): the emission probability of the current word given candidate tag tit_i
  • P(ti∣ti−1∗)P(t_i | t_{i-1}^*): the transition probability from the previously chosen tag ti−1∗t_{i-1}^* to candidate tag tit_i

This seems reasonable but can produce suboptimal sequences. The problem is that a locally good choice might force bad choices later. Consider tagging "The can can hold water." A greedy tagger might assign MODAL to the first "can" because modals are common after determiners. But this makes the second "can" difficult: modals rarely follow modals. The globally optimal solution assigns NOUN to the first "can" and VERB to the second, even though NOUN isn't the highest-probability tag for "can" in isolation.

In[3]:
Code
# Demonstration of greedy vs. optimal tagging

# Simple transition probabilities (log space for clarity)
transitions = {
    ("<START>", "DT"): 0.3,
    ("<START>", "NN"): 0.1,
    ("DT", "NN"): 0.5,
    ("DT", "MD"): 0.3,
    ("DT", "VB"): 0.1,
    ("NN", "VB"): 0.4,
    ("NN", "MD"): 0.2,
    ("MD", "VB"): 0.8,
    ("MD", "MD"): 0.05,  # Rare: modal after modal
    ("VB", "NN"): 0.3,
    ("VB", "DT"): 0.2,
}

# Emission probabilities for "can"
emissions_can = {
    "MD": 0.6,  # Modal is most common usage
    "NN": 0.25,  # Container noun
    "VB": 0.15,  # Preserve/put in cans
}

sentence = ["The", "can", "can", "hold", "water"]
Out[4]:
Console
The Greedy Decoding Problem
=======================================================

Sentence: The can can hold water

At position 2 (first 'can'), greedy considers:
  P('can'|MD) × P(MD|DT) = 0.60 × 0.30 = 0.180
  P('can'|NN) × P(NN|DT) = 0.25 × 0.50 = 0.125
  P('can'|VB) × P(VB|DT) = 0.15 × 0.10 = 0.015

Greedy picks MD (highest score: 0.180)

But now at position 3 (second 'can'):
  P('can'|VB) × P(VB|MD) = 0.15 × 0.80 = 0.120
  P('can'|MD) × P(MD|MD) = 0.60 × 0.05 = 0.030

The greedy choice of MD at position 2 forces a bad situation at position 3.
If we had chosen NN at position 2 instead:
  P('can'|VB) × P(VB|NN) = 0.15 × 0.40 = 0.060

This example demonstrates why greedy decoding fails. The locally optimal choice of MD for the first "can" yields score 0.180, beating NN's 0.125. But this decision creates a problem: the second "can" must follow a modal, and modals rarely follow modals (P(MD|MD) = 0.05). The alternative path through NN enables a much higher score at the next position (0.060 vs 0.030), potentially yielding a better overall sequence probability.

Let's trace the cumulative scores for both paths:

Cumulative path scores comparing greedy vs alternative decoding.
PositionGreedy Path (DT→MD→VB)Alternative Path (DT→NN→VB)
Position 2 (first "can")0.1800.125
Position 3 (second "can")0.02160.0075

The greedy path starts stronger (0.180 vs 0.125) because MD has higher emission probability for "can." But this local advantage creates a problem: the MD→VB transition at position 3 yields only 0.0216, while NN→VB would have enabled a score of 0.0075. The globally optimal path requires looking ahead, something greedy decoding cannot do.

The Exponential Search Space

If greedy decoding doesn't work, why not try all possible tag sequences? With kk possible tags and a sequence of length nn, there are knk^n possible sequences. For a modest vocabulary of 45 tags and a 20-word sentence, that's 4520≈103345^{20} \approx 10^{33} sequences, far more than the number of atoms in the observable universe.

In[5]:
Code
def count_sequences(num_tags, seq_length):
    """Calculate the number of possible tag sequences."""
    return num_tags**seq_length


# Common scenarios
scenarios = [
    ("Simple (5 tags, 10 words)", 5, 10),
    ("Penn Treebank (45 tags, 15 words)", 45, 15),
    ("Penn Treebank (45 tags, 20 words)", 45, 20),
    ("Penn Treebank (45 tags, 30 words)", 45, 30),
]

sequence_counts = [
    (name, count_sequences(tags, length)) for name, tags, length in scenarios
]
Out[6]:
Console
Exhaustive Search is Intractable
============================================================

Scenario                                          Sequences
------------------------------------------------------------
Simple (5 tags, 10 words)                             ~10^6
Penn Treebank (45 tags, 15 words)                    ~10^24
Penn Treebank (45 tags, 20 words)                    ~10^33
Penn Treebank (45 tags, 30 words)                    ~10^49

For comparison:
  Atoms in the universe: ~10^80
  Nanoseconds since Big Bang: ~10^26

Even with a modest 45-tag vocabulary, a 20-word sentence produces more possible sequences than atoms in the observable universe. Checking each sequence individually would take longer than the age of the universe on any computer. The key insight is that we don't need to enumerate every sequence. Many sequences share common prefixes, and we can avoid recomputing scores for these shared subsequences. This is where dynamic programming enters.