Viterbi Algorithm › The Optimal Path Problem
Language AI HandbookAll chapters 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 and want to find the sequence of tags 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:
where:
- : the sequence of hidden states (POS tags) we want to infer
- : the observed sequence of words
- : the initial state probability, capturing how likely the sentence starts with tag
- : the transition probability from tag to tag , encoding grammatical patterns like "determiners often precede nouns"
- : the emission probability of word given tag , 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.
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 , greedy decoding selects:
where:
- : the greedily chosen tag at position
- : selects the tag that maximizes the following expression
- : the emission probability of the current word given candidate tag
- : the transition probability from the previously chosen tag to candidate tag
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.
# 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"]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.060This 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:
| Position | Greedy Path (DT→MD→VB) | Alternative Path (DT→NN→VB) |
|---|---|---|
| Position 2 (first "can") | 0.180 | 0.125 |
| Position 3 (second "can") | 0.0216 | 0.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 possible tags and a sequence of length , there are possible sequences. For a modest vocabulary of 45 tags and a 20-word sentence, that's sequences, far more than the number of atoms in the observable universe.
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
]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.