← All tutorials

Hidden Markov Models (HMM)

Inferring hidden states from what you can observe.

On this page

1 From a chain you see to a chain you guess 2 In a Markov chain, tomorrow depends only on today 3 Hide the state and you get the dishonest casino 4 How likely are these rolls? The forward pass 5 Which die when? Viterbi keeps the best path into each cell 6 How sure at each roll? Forward times backward 7 Baum-Welch learns the numbers with EM 8 HMMs in practice 9 HMMs in speech 10 Common traps 11 What if the structure is a direction, not a hidden state? 12 References

A Markov chain is a sequence of states where each step picks the next state by fixed chances. Hide the state, show only what it emits, and you have an HMM, the model behind decades of speech recognition.

1 From a chain you see to a chain you guess

A hidden Markov model (HMM) is a Markov chain whose state you never see, only what each state emits. Given a sequence it answers three questions, as Rabiner (1989) organizes them: how likely the sequence is (the forward pass), which hidden state made each step (Viterbi), and which chances make the sequence most likely (Baum-Welch, which is EM). Baum-Welch fits once, offline, over the training sequences; after that, scoring or decoding a new sequence is one pass over it.

Markov chainstates seenhidden Markovstates hiddenhow likelyis the data?forwardwhich statewhen?Viterbilearn thechancesBaum-Welchthree questions
Figure 1. A chain you see, a chain you guess, and three questions.

2 In a Markov chain, tomorrow depends only on today

Take a toy weather model with made-up numbers. A sunny day stays sunny 80% of the time; a rainy day stays rainy 60% of the time. Nothing before today matters, which is the Markov assumption (the independence idea from Probability and Bayes Rule).

0.8sunny0.6rainy0.20.4
Figure 2. Arrows are chances for tomorrow; those out of a state add to 1.
A Markov chain is fully described by one table of next-step chances, and that table alone fixes the long-run share of days in each state.
Aij = P(zt+1 = j | zt = i)
zt is the state on day t. Aij is the chance that state i today becomes state j tomorrow. Only today’s state sits right of the bar, which is the Markov assumption written out. Row i lists everywhere i can go next, so each row adds to 1, and a large number on the diagonal means a sticky state.
s = s A
s is a row of shares, the fraction of days in each state. Multiplying it by A gives the next day’s shares: each new share adds today’s shares times the chance of moving there. The long-run row is the one that one more step leaves unchanged, so flow out of each state equals flow in. Sunny loses 0.2 of its share each day and rainy loses 0.4 of its own, so sunny must hold twice as much, and s = (2/3, 1/3) whatever the first day was.

Figure 3. Stickier weather, longer runs; the rainy share stays a third.

This snippet simulates the chain from A alone and prints the share of rainy days; the rainy share comes out near one third.

import numpy as np
SUNNY, RAINY = 0, 1
A = np.array([[0.8, 0.2], [0.4, 0.6]])
rng = np.random.default_rng(1)
days = [SUNNY]
for _ in range(999):
    days.append(rng.choice(2, p=A[days[-1]]))
print(np.mean(np.array(days) == RAINY))  # about 0.33

3 Hide the state and you get the dishonest casino

Hide which die is in use, and you get an HMM. A dealer mostly rolls a fair die but sometimes swaps in a loaded one that rolls six half the time (the occasionally dishonest casino, Durbin et al. 1998). You see the rolls, never the die.

0.95fair0.90loaded0.050.10fair rolls 1/6 each; loaded rolls six 1/2
Figure 4. The hidden dice form a Markov chain; each die has its own chances.
An HMM keeps the hidden chain and adds two tables: what each hidden state emits, and where the chain starts.
Bj(o) = P(roll o | zt = j)
o is a die face. B is the emission table, and Bj(o) is the chance that die j rolls face o. Bfair(6) = 1/6 and Bloaded(6) = 1/2, so each six is three times more likely under the loaded die. Ratios like that are what let the model tell the dice apart.
πj = P(z1 = j)
π is the start. πj is the chance the first roll uses die j, one half each here. It shapes only the first roll; after that A takes over.
314253666FLFLFLFLFLFLFLFLFLrolls on top; fair and loaded below each
Figure 5. In a trellis, every path is one guess about which die made each roll.

4 How likely are these rolls? The forward pass

Adding up every possible die sequence is hopeless: 40 rolls have 240, about 1012, of them.

314253666FLFLFLFLFLFLFLFLFLadd both paths in
Figure 6. Each cell adds the paths into it from the column before.
The forward pass gives the chance of all the rolls by keeping one running total per die per roll, so the work grows with the number of rolls, not with the number of die sequences.
α1(j) = πj Bj(o1)
ot is the face shown at roll t. αt(j) is the chance of the rolls up to t, ending with die j at roll t. The first roll has no previous die, so it is just the start chance times the chance that die j rolls o1.
αt(j) = [Σi αt−1(i) Aij] Bj(ot)
To end in j at roll t, the chain sat in some die i at roll t − 1 and stepped to j. Adding those incoming paths, each times its step chance Aij, gathers every path into j at once, and Bj(ot) then scores the new roll. Each cell needs only the column before, so 40 rolls take 40 columns of two cells instead of 240 paths. With N hidden states and T rolls that is about N2T steps.
P(o1, …, oT) = Σj αT(j)
T is the last roll. Adding the last column over both dice gives the chance of all the rolls, with every die sequence included.

CTC, the speech recognition loss that adds up every way to line a transcript up with the audio, fills the same kind of table over label positions. The totals shrink by roughly a factor of 6 per roll, so real code works in logs (see Maximum Likelihood and EM) or rescales each column.

This snippet runs the forward pass in logs on nine rolls and prints the log chance of all of them.

import numpy as np
from scipy.special import logsumexp
logA = np.log([[0.95, 0.05], [0.10, 0.90]])
logB = np.log([[1/6] * 6, [0.1] * 5 + [0.5]])
pi = np.array([0.5, 0.5])
o = np.array([3, 1, 4, 2, 5, 3, 6, 6, 6]) - 1
log_a = np.log(pi) + logB[:, o[0]]
for t in range(1, len(o)):
    log_a = logsumexp(log_a[:, None] + logA, axis=0) + logB[:, o[t]]
print(logsumexp(log_a))  # log P(rolls), about -15.41
  1. 1Sets A, B, π of the casino in Figure 4 and the nine rolls.
  2. 2Starts the first column, eq card in Section 4, line 1.
  3. 3Fills each next column from the last, line 2, in logs.
  4. 4Adds the last column to get the chance of the rolls, line 3.

5 Which die when? Viterbi keeps the best path into each cell

The Viterbi algorithm (Viterbi 1967) answers the second question, which die made each roll. It fills the same table as the forward pass but keeps only the best path into each cell, a max in place of the sum, and remembers which cell that path came from. At the last roll it walks those remembered choices back and reads off one die per roll.

Figure 7. Darker cell, better die so far; three sixes flip the path at roll 7.

Viterbi finds the single most likely die sequence by filling the same table with max in place of the sum.
δ1(j) = πj Bj(o1)
δt(j) (delta) is the chance of the single best path that ends with die j at roll t. It starts exactly like α, since one roll has only one path into each die.
δt(j) =
maxi [δt−1(i) Aij] Bj(ot)
Keep only the best incoming path, from whichever die i gives the largest value, and remember that i. A sum lets many weak paths add up; max keeps one, so δ answers which path while α answers how likely overall. At the end, follow the remembered choices back from the best last cell.

This snippet runs Viterbi on the same nine rolls and prints the best die per roll.

import numpy as np
logA = np.log([[0.95, 0.05], [0.10, 0.90]])
logB = np.log([[1/6] * 6, [0.1] * 5 + [0.5]])
o = np.array([3, 1, 4, 2, 5, 3, 6, 6, 6]) - 1
log_delta = np.log([0.5, 0.5]) + logB[:, o[0]]
back = []
for t in range(1, len(o)):
    scores = log_delta[:, None] + logA
    back.append(scores.argmax(axis=0))
    log_delta = scores.max(axis=0) + logB[:, o[t]]
path = [int(log_delta.argmax())]
for ptr in reversed(back):
    path.append(int(ptr[path[-1]]))
print(path[::-1])  # [0, 0, 0, 0, 0, 0, 1, 1, 1], 0 = fair
  1. 1Sets the same casino and rolls.
  2. 2Starts δ1, eq card in Section 5, line 1.
  3. 3Keeps the best path into each cell and its back pointer, lines 2 and 3.
  4. 4Follows the back pointers from the last roll, the path drawn in Figure 7.

6 How sure at each roll? Forward times backward

Viterbi gives one best path. Sometimes you want a chance for every roll instead: how likely is it that the loaded die made roll t? Run the table backward as well and combine the two.

A forward and a backward pass together give, for every roll, the chance that each die made it, using the rolls both before and after.
γt(j) = αt(j) βt(j)P(o1, …, oT)
γt(j) is the chance die j was in use at roll t, given every roll. α covers the rolls up to t. βt(j), from a backward pass, is the chance of the rolls after t given die j at t. Their product is the joint chance of die j at t and all the rolls, and dividing by the chance of all the rolls is the same flip as Bayes rule. A run of sixes just after t raises β for loaded, so a roll can look loaded because of what follows it, which the forward pass alone cannot see.

On 30 rolls, the model’s switch chance matters. Fair goes to loaded with chance q and back with 2q; the dealer’s real q is 0.05.

Figure 8. A low q almost never switches dice. At q = 0.3 the path switches 5 to 12 times in 30 rolls, jumping to loaded at lone sixes.

7 Baum-Welch learns the numbers with EM

If A and B are unknown, learn them from the rolls with EM, Expectation-Maximization, here called Baum-Welch (Baum et al. 1970). The E step runs forward and backward to get soft counts; the M step divides counts. The updates are essentially EM’s, so the likelihood never drops and the fit can stop at a local peak.

314253666FLFLFLFLFLFLFLFLFLthicker arrows carry more expected steps
Figure 9. Arrow thickness is the soft count each arrow adds to the new A.
Each Baum-Welch round re-estimates A as the expected share of steps out of i that go to j.
Aij ←
expected steps from i to jexpected steps out of i
Both counts come from the forward and backward tables: each possible step is counted at its chance, not as 0 or 1. It is the counting formula of maximum likelihood with soft counts in place of seen ones, so if the tables say the chain mostly stayed put, the stay chances in A come out large.

The official hmmlearn casino example fits 50 times from random starts and keeps the best fit.

0.950.940.900.92fair staysloaded staystruefitted, best of 501
Figure 10. True and fitted stay chances; 1the fit calls loaded state 0, a label swap.

8 HMMs in practice

With hmmlearn the three questions are three calls on rolls stored as one column, and the fit runs from several starts as Section 10 advises; several sequences are stacked into one array, and lengths says where each one ends, so no step is counted across the join.

import numpy as np
from hmmlearn import hmm
casino = hmm.CategoricalHMM(n_components=2)
casino.startprob_ = np.array([0.5, 0.5])
casino.transmat_ = np.array([[0.95, 0.05], [0.10, 0.90]])
casino.emissionprob_ = np.array([[1/6] * 6, [0.1] * 5 + [0.5]])
rs = np.random.RandomState(1)
X1, _ = casino.sample(300, random_state=rs)
X2, _ = casino.sample(200, random_state=rs)
X, lengths = np.concatenate([X1, X2]), [len(X1), len(X2)]
best_score, model = -np.inf, None
for seed in range(10):
    candidate = hmm.CategoricalHMM(n_components=2, n_iter=50, random_state=seed)
    candidate.fit(X, lengths)
    score = candidate.score(X, lengths)
    if score > best_score:
        best_score, model = score, candidate
log_prob, path = model.decode(X2)
gamma = model.predict_proba(X2)
print(model.transmat_.round(2), log_prob)
  1. 1Builds the true casino of Figure 4 to make toy data.
  2. 2Samples two different nights from one shared generator rs and records their lengths.
  3. 3Fits A, B and π with Baum-Welch from 10 random starts and keeps the one with the best training log-likelihood, eq card in Section 7.
  4. 4Decodes the best die per roll with Viterbi, eq card in Section 5, and gets γt(j) per roll, eq card in Section 6.
  5. 5Prints the fitted A and the log chance of the best path.
stepwhen, on what, and its cost
fitOnce, in Baum-Welch rounds; each round runs the forward and backward pass over every sequence.
decode, predict_proba, scorePer new sequence, one Viterbi pass, or one forward and backward pass.
stateπ, A and B (startprob_, transmat_, emissionprob_).
settingraise it, lower itdefault
n_components, the hidden states j in A and BMore states fit finer patterns and soon overfit; 1 state means no hidden switching at all, one die for every roll.1
n_iter, tol, the Baum-Welch roundsFit stops after n_iter rounds or when a round gains less than tol in total log-likelihood. A smaller tol runs more rounds and lands closer to the peak; too few rounds stop short of it.10, 0.01
init_params, the starting π, A and BSays which of π, A and B (letters s, t, e) fit sets fresh before the first Baum-Welch round, so it picks where the climb starts. With all three letters the random_state seed decides the start, so 10 seeds give 10 different starts. Fewer letters keep the values you set yourself; an empty string starts every seed from your own π, A and B."ste"
lengthsWithout it, separate sequences are read as one chain with a false step at every join.one sequence

Defaults are from the hmmlearn API reference. Gotchas are in Section 10.

9 HMMs in speech

Classic speech recognizers model each word or sound unit as a chain of left-to-right states that can stay or move on, never back. A Gaussian mixture under each state scores the sound frames, often MFCCs (see MFCC and Other Audio Features), and Viterbi finds the best path. Hinton et al. (2012) showed deep neural networks (see Multilayer Perceptrons) beating those mixtures as the frame scorers on many benchmarks, and CTC later dropped the explicit HMM while keeping a forward table.

123a GMM under each state scores the sound
Figure 11. Three left-to-right states; each loops on itself or moves on.

10 Common traps

11 What if the structure is a direction, not a hidden state?

Mixtures and HMMs look for hidden groups. Principal Component Analysis looks for the few directions along which the data stretches most.

12 References