technique
Baum-Welch
Fit the transition and emission tables of a hidden Markov model from observation sequences alone, by repeatedly computing expected counts and re-estimating.
Before this
This page assumes you are comfortable with:
- techniqueForward algorithmScore an observation sequence under a hidden Markov model, and read off the current hidden state along the way, in one left-to-right pass.
- techniqueViterbi algorithmRecover the single most likely hidden path behind an observation sequence by filling the forward table with max instead of sum and walking backpointers.
Why you need this
Hand-authoring a 6 by 6 transition matrix and a 6 by 12 emission matrix is possible. Hand-authoring them so the generator's output resembles the twenty levels a designer already approved is guesswork. Baum-Welch turns the approved levels into the tables. The same procedure fits a regime chain from play logs, once you have some.
The idea
You have one or more observation sequences (tile symbols per column) and a chosen number of hidden states . You want , , and : , , .
If the hidden states were visible, this would be counting. is the number of times was followed by , divided by the number of times occurred. is the number of times emitted , divided by the number of times occurred. But the states are hidden, so you cannot count them.
Expectation-maximization gets around this in plain words: guess the model, use it to compute how often each transition and emission probably happened (expected counts rather than actual counts), re-estimate the tables from those expected counts as if they were real, and repeat. Each round the model explains the data at least as well as the round before.
The backward variable
The forward algorithm gives , the probability of the observations up to ending in state . You also need the probability of the observations after given state :
It is computed right to left. At the end there is nothing left to observe, so for every . Then
From at time , move to , emit , and account for the rest with . It needs the same scaling as the forward pass to avoid underflow.
Two posterior quantities
Multiply forward and backward at the same time step and divide by , and you have the probability of being in state at time given the whole sequence:
Insert one transition and one emission between them, and you have the probability of the pair at times :
Summing over gives back, which is a useful check.
Re-estimation
Now treat and as soft counts.
is the expected number of times the sequence started in (with one sequence, just the probability).
is the expected number of transitions from to divided by the expected number of times in with a step still to come.
is the expected number of times state emitted symbol divided by the expected number of times in .
With several training sequences, add the numerators across sequences and the denominators across sequences before dividing. Then replace with the hats and go again.
Worked example
One iteration on the smallest useful case. Same model as the forward and Viterbi pages: states 1 Run and 2 Gap, symbols 1 flat and 2 pit,
and the single sequence , . The forward page computed and .
| 0.72 | 0.0528 | 0.133488 | |
| 0.04 | 0.1856 | 0.018016 | |
| 0.1971 | 0.69 | 1 | |
| 0.2398 | 0.62 | 1 | |
| 0.93669 | 0.24047 | 0.88109 | |
| 0.06331 | 0.75953 | 0.11891 |
Backward, right to left. (next observation is flat). . (next observation is pit). . Check: .
Gamma, for instance .
Xi, numerators then divided by :
| Run to Run | Run to Gap | Gap to Run | Gap to Gap | |
|---|---|---|---|---|
| (next is pit) | ||||
| (next is flat) |
Check one row: .
Re-estimate:
- .
- , and .
- , and .
- , so .
- , so .
Every row still sums to 1. Under the new tables the same sequence has (log ), up from (log ). It went up, as promised. It also shows the danger: from three observations the model has decided that Run almost always turns into Gap. That is the tiny dataset talking, not the world.
Convergence, local optima, initialization, and data
The log-likelihood never decreases from one iteration to the next. Stop when it improves by less than some small amount, or after a fixed number of iterations (a few dozen is typical).
It converges to a local optimum, not necessarily the best one. Different starting tables land in different places, and a poor start can converge to states that mean nothing. The cheap cure is a good start: decode your training levels with Viterbi under a hand-guessed model, count the labeled transitions and emissions, and use those normalized counts as the initial and . Run Baum-Welch from several starts and keep the one with the highest final log-likelihood.
Be honest about data size. Twenty approved levels of 100 columns is 2,000 observations, which sounds like a lot until you divide it among 36 transition entries and 72 emission entries. Some will be estimated from three or four soft occurrences. Regularize with pseudo-counts: add a small constant (0.5 or 1) to every expected count in the numerators, and the corresponding total to each denominator, before dividing. No entry goes to zero, and rare transitions stay possible instead of vanishing because they happened not to appear in the sample. In the worked example, adding 1 to each of the four expected transition counts pulls back toward uniform: instead of .
In a map generator
- Fitting the side-scroller HMM. The design tool holds levels the designer approved. Symbolize each column, initialize from Viterbi labels under a hand-guessed model, run Baum-Welch with pseudo-counts, and write into the parameter file. Generation then produces levels statistically like the approved ones. Re-run when the approved set grows.
- Fitting the regime chain. Play logs record which events a region produced each tick (spawns, resource pulls, floods). Treat the events as observations and the regime as hidden; the fitted replaces the hand-tuned one on the regime chains page.
- Validation. Hold out a few approved levels, and score them with the forward algorithm under the fitted model. If the held-out score is far below the training score, the model has memorized rather than learned. Add pseudo-counts or reduce .
Common mistakes
- Not scaling . The forward pass is scaled and the backward pass is not, so is garbage after 300 columns. Use the same factors for both.
- Zero entries that never recover. A zero in or stays zero forever, because its expected count is always zero. Initialize with no exact zeros unless the zero is a rule you mean to enforce.
- Too many hidden states. With and twenty levels, several states collapse into near-copies of each other and the log-likelihood barely moves. Start at the number of section types the designer can name.
- Fitting to one long sequence. Concatenating levels puts a fake transition from the end of one to the start of the next. Fit as separate sequences and sum the counts.
- Trusting one run. The first local optimum is not the best. Several restarts, compare log-likelihoods.
Cost
One iteration is a forward pass, a backward pass, and the sums, each per sequence, with the sequence length and the number of hidden states. Over sequences and iterations the total is ; with one sequence, . Memory is for the and tables of the sequence being processed. For , twenty levels of 100 columns, and 50 iterations, that is about 3.6 million multiplications, well under a second. It hurts when climbs past 50 or when the training set reaches tens of thousands of sequences, neither of which a level tool reaches.
Going further
- Expectation-maximization in general, of which Baum-Welch is the HMM special case.
- Viterbi training (hard EM), which replaces and with the counts from the decoded path: faster, cruder.
- Model selection, for choosing from held-out likelihood.
- Dirichlet priors, the principled version of pseudo-counts.