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:

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 O1,…,OTO_1, \dots, O_T (tile symbols per column) and a chosen number of hidden states NN. You want π\pi, AA, and BB: πi=P(X1=i)\pi_i = P(X_1 = i), Aij=P(Xt+1=j∣Xt=i)A_{ij} = P(X_{t+1} = j \mid X_t = i), Bjk=P(Ot=k∣Xt=j)B_{jk} = P(O_t = k \mid X_t = j).

If the hidden states were visible, this would be counting. AijA_{ij} is the number of times ii was followed by jj, divided by the number of times ii occurred. BjkB_{jk} is the number of times jj emitted kk, divided by the number of times jj 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 αt(i)=P(O1..Ot,Xt=i)\alpha_t(i) = P(O_1..O_t, X_t = i), the probability of the observations up to tt ending in state ii. You also need the probability of the observations after tt given state ii:

βt(i)=P(Ot+1,…,OT∣Xt=i).\beta_t(i) = P(O_{t+1}, \dots, O_T \mid X_t = i).

It is computed right to left. At the end there is nothing left to observe, so βT(i)=1\beta_T(i) = 1 for every ii. Then

βt(i)=∑j=1NAij BjOt+1 βt+1(j).\beta_t(i) = \sum_{j=1}^{N} A_{ij} \, B_{j O_{t+1}} \, \beta_{t+1}(j).

From ii at time tt, move to jj, emit Ot+1O_{t+1}, and account for the rest with βt+1(j)\beta_{t+1}(j). 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 P(O)=∑jαT(j)P(O) = \sum_j \alpha_T(j), and you have the probability of being in state ii at time tt given the whole sequence:

γt(i)=P(Xt=i∣O)=αt(i) βt(i)P(O).\gamma_t(i) = P(X_t = i \mid O) = \frac{\alpha_t(i) \, \beta_t(i)}{P(O)}.

Insert one transition and one emission between them, and you have the probability of the pair (i,j)(i, j) at times (t,t+1)(t, t+1):

ξt(i,j)=P(Xt=i,Xt+1=j∣O)=αt(i) Aij BjOt+1 βt+1(j)P(O).\xi_t(i, j) = P(X_t = i, X_{t+1} = j \mid O) = \frac{\alpha_t(i) \, A_{ij} \, B_{j O_{t+1}} \, \beta_{t+1}(j)}{P(O)}.

Summing ξt(i,j)\xi_t(i, j) over jj gives γt(i)\gamma_t(i) back, which is a useful check.

Re-estimation

Now treat γ\gamma and ξ\xi as soft counts.

π^i=γ1(i)\hat{\pi}_i = \gamma_1(i)

is the expected number of times the sequence started in ii (with one sequence, just the probability).

A^ij=∑t=1T−1ξt(i,j)∑t=1T−1γt(i)\hat{A}_{ij} = \frac{\sum_{t=1}^{T-1} \xi_t(i, j)}{\sum_{t=1}^{T-1} \gamma_t(i)}

is the expected number of transitions from ii to jj divided by the expected number of times in ii with a step still to come.

B^jk=∑t:Ot=kγt(j)∑t=1Tγt(j)\hat{B}_{jk} = \frac{\sum_{t : O_t = k} \gamma_t(j)}{\sum_{t=1}^{T} \gamma_t(j)}

is the expected number of times state jj emitted symbol kk divided by the expected number of times in jj.

With several training sequences, add the numerators across sequences and the denominators across sequences before dividing. Then replace (π,A,B)(\pi, A, B) 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,

π=(0.8,0.2),A=(0.70.30.60.4),B=(0.90.10.20.8),\pi = (0.8, 0.2), \qquad A = \begin{pmatrix} 0.7 & 0.3 \\ 0.6 & 0.4 \end{pmatrix}, \qquad B = \begin{pmatrix} 0.9 & 0.1 \\ 0.2 & 0.8 \end{pmatrix},

and the single sequence O=(flat,pit,flat)O = (\text{flat}, \text{pit}, \text{flat}), T=3T = 3. The forward page computed α\alpha and P(O)=0.151504P(O) = 0.151504.

t=1t = 1 t=2t = 2 t=3t = 3
αt(Run)\alpha_t(\text{Run}) 0.72 0.0528 0.133488
αt(Gap)\alpha_t(\text{Gap}) 0.04 0.1856 0.018016
βt(Run)\beta_t(\text{Run}) 0.1971 0.69 1
βt(Gap)\beta_t(\text{Gap}) 0.2398 0.62 1
γt(Run)\gamma_t(\text{Run}) 0.93669 0.24047 0.88109
γt(Gap)\gamma_t(\text{Gap}) 0.06331 0.75953 0.11891

Backward, right to left. β2(Run)=0.7⋅0.9⋅1+0.3⋅0.2⋅1=0.69\beta_2(\text{Run}) = 0.7 \cdot 0.9 \cdot 1 + 0.3 \cdot 0.2 \cdot 1 = 0.69 (next observation is flat). β2(Gap)=0.6⋅0.9+0.4⋅0.2=0.62\beta_2(\text{Gap}) = 0.6 \cdot 0.9 + 0.4 \cdot 0.2 = 0.62. β1(Run)=0.7⋅0.1⋅0.69+0.3⋅0.8⋅0.62=0.0483+0.1488=0.1971\beta_1(\text{Run}) = 0.7 \cdot 0.1 \cdot 0.69 + 0.3 \cdot 0.8 \cdot 0.62 = 0.0483 + 0.1488 = 0.1971 (next observation is pit). β1(Gap)=0.6⋅0.1⋅0.69+0.4⋅0.8⋅0.62=0.0414+0.1984=0.2398\beta_1(\text{Gap}) = 0.6 \cdot 0.1 \cdot 0.69 + 0.4 \cdot 0.8 \cdot 0.62 = 0.0414 + 0.1984 = 0.2398. Check: 0.72⋅0.1971+0.04⋅0.2398=0.151504=P(O)0.72 \cdot 0.1971 + 0.04 \cdot 0.2398 = 0.151504 = P(O).

Gamma, for instance γ2(Run)=0.0528⋅0.69/0.151504=0.036432/0.151504=0.24047\gamma_2(\text{Run}) = 0.0528 \cdot 0.69 / 0.151504 = 0.036432 / 0.151504 = 0.24047.

Xi, numerators then divided by P(O)P(O):

ξt(i,j)\xi_t(i, j) Run to Run Run to Gap Gap to Run Gap to Gap
t=1t = 1 (next is pit) 0.72⋅0.7⋅0.1⋅0.69=0.034776→0.229540.72 \cdot 0.7 \cdot 0.1 \cdot 0.69 = 0.034776 \to 0.22954 0.72⋅0.3⋅0.8⋅0.62=0.107136→0.707150.72 \cdot 0.3 \cdot 0.8 \cdot 0.62 = 0.107136 \to 0.70715 0.04⋅0.6⋅0.1⋅0.69=0.001656→0.010930.04 \cdot 0.6 \cdot 0.1 \cdot 0.69 = 0.001656 \to 0.01093 0.04⋅0.4⋅0.8⋅0.62=0.007936→0.052380.04 \cdot 0.4 \cdot 0.8 \cdot 0.62 = 0.007936 \to 0.05238
t=2t = 2 (next is flat) 0.0528⋅0.7⋅0.9⋅1=0.033264→0.219560.0528 \cdot 0.7 \cdot 0.9 \cdot 1 = 0.033264 \to 0.21956 0.0528⋅0.3⋅0.2⋅1=0.003168→0.020910.0528 \cdot 0.3 \cdot 0.2 \cdot 1 = 0.003168 \to 0.02091 0.1856⋅0.6⋅0.9⋅1=0.100224→0.661530.1856 \cdot 0.6 \cdot 0.9 \cdot 1 = 0.100224 \to 0.66153 0.1856⋅0.4⋅0.2⋅1=0.014848→0.098000.1856 \cdot 0.4 \cdot 0.2 \cdot 1 = 0.014848 \to 0.09800

Check one row: 0.22954+0.70715=0.93669=γ1(Run)0.22954 + 0.70715 = 0.93669 = \gamma_1(\text{Run}).

Re-estimate:

  • π^=(0.93669,0.06331)\hat{\pi} = (0.93669, 0.06331).
  • A^Run,Run=(0.22954+0.21956)/(0.93669+0.24047)=0.44910/1.17716=0.3815\hat{A}_{\text{Run},\text{Run}} = (0.22954 + 0.21956) / (0.93669 + 0.24047) = 0.44910 / 1.17716 = 0.3815, and A^Run,Gap=0.72806/1.17716=0.6185\hat{A}_{\text{Run},\text{Gap}} = 0.72806 / 1.17716 = 0.6185.
  • A^Gap,Run=(0.01093+0.66153)/(0.06331+0.75953)=0.67246/0.82284=0.8172\hat{A}_{\text{Gap},\text{Run}} = (0.01093 + 0.66153) / (0.06331 + 0.75953) = 0.67246 / 0.82284 = 0.8172, and A^Gap,Gap=0.1828\hat{A}_{\text{Gap},\text{Gap}} = 0.1828.
  • B^Run,flat=(0.93669+0.88109)/(0.93669+0.24047+0.88109)=1.81778/2.05825=0.8832\hat{B}_{\text{Run},\text{flat}} = (0.93669 + 0.88109) / (0.93669 + 0.24047 + 0.88109) = 1.81778 / 2.05825 = 0.8832, so B^Run,pit=0.1168\hat{B}_{\text{Run},\text{pit}} = 0.1168.
  • B^Gap,flat=(0.06331+0.11891)/0.94175=0.1935\hat{B}_{\text{Gap},\text{flat}} = (0.06331 + 0.11891) / 0.94175 = 0.1935, so B^Gap,pit=0.8065\hat{B}_{\text{Gap},\text{pit}} = 0.8065.

Every row still sums to 1. Under the new tables the same sequence has P(O)=0.331P(O) = 0.331 (log −1.105-1.105), up from 0.15150.1515 (log −1.887-1.887). 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 log⁡P(O∣π,A,B)\log P(O \mid \pi, A, B) 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 AA and BB. 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 A^\hat{A} back toward uniform: A^Run,Run=(0.4491+1)/(1.1772+2)=0.456\hat{A}_{\text{Run},\text{Run}} = (0.4491 + 1) / (1.1772 + 2) = 0.456 instead of 0.38150.3815.

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 (π,A,B)(\pi, A, B) 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 AA 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 NN.

Common mistakes

  • Not scaling β\beta. The forward pass is scaled and the backward pass is not, so γ\gamma is garbage after 300 columns. Use the same ctc_t factors for both.
  • Zero entries that never recover. A zero in AA or BB 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 N=12N = 12 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 ξ\xi sums, each O(T⋅N2)O(T \cdot N^2) per sequence, with TT the sequence length and NN the number of hidden states. Over SS sequences and II iterations the total is O(I⋅S⋅T⋅N2)O(I \cdot S \cdot T \cdot N^2); with one sequence, O(I⋅T⋅N2)O(I \cdot T \cdot N^2). Memory is O(T⋅N)O(T \cdot N) for the α\alpha and β\beta tables of the sequence being processed. For N=6N = 6, twenty levels of 100 columns, and 50 iterations, that is about 3.6 million multiplications, well under a second. It hurts when NN 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 γ\gamma and ξ\xi with the counts from the decoded path: faster, cruder.
  • Model selection, for choosing NN from held-out likelihood.
  • Dirichlet priors, the principled version of pseudo-counts.

Back to Dynamic map generation