Rabiner 1989 - Problem 1 - Forward and Backward Algorithms
Summary
The likelihood needs about operations by direct enumeration. The forward variable satisfies and gives in time. The backward variable is the symmetric recursion used for Problems 2 and 3.
Overview
For a fixed state sequence , observations are conditionally independent: (eq. 13). The sequence probability is (eq. 14).
Main Content
Direct enumeration (eqs 15-17)
. There are sequences, each needing about operations: multiplications and additions, infeasible even for , ().
Forward variable (eq. 18)
.
Forward algorithm (eqs 19-21)
- Initialisation: , .
- Induction: , , .
- Termination: . Why it works. is the probability of observing and being in at then moving to at ; summing over merges all paths into the nodes of a lattice (trellis, Fig. 4b), so cost is : multiplications and additions. For , about 3000 operations vs , a saving of about 69 orders of magnitude.
Backward variable (eq. 23) and recursion (eqs 24-25)
. Initialisation (arbitrary definition). Induction for : . Also (Fig. 5). The backward part is not needed for Problem 1 alone but is needed for Problem 3.
Identity linking them (derived)
For any : . At : , which numerically equals (verified in the test below: both ways on a simulated 200-step 2-state Poisson HMM).
import numpy as np
from scipy.special import logsumexp
from scipy.stats import poisson
def forward(logB, logA, logpi): # logB: (T,N) emission log-probs
T, N = logB.shape; la = np.zeros((T, N)); la[0] = logpi + logB[0]
for t in range(1, T):
la[t] = logB[t] + logsumexp(la[t-1][:, None] + logA, axis=0) # eq. 20 in log space
return la # log P(O|lam) = logsumexp(la[-1])
def backward(logB, logA):
T, N = logB.shape; lb = np.zeros((T, N))
for t in range(T-2, -1, -1):
lb[t] = logsumexp(logA + logB[t+1][None, :] + lb[t+1][None, :], axis=1) # eq. 25
return lb
# Tested: matches brute-force enumeration over all 2^6 paths for T=6 (-7.8808), and la/lb identity above.Examples (PyMC-style)
The forward recursion is exactly what a PyMC HMM writes as the marginal likelihood, so the discrete state is never sampled (E24; untested sketch):
import pymc as pm, pytensor, pytensor.tensor as pt
def hmm_logp(y, A, pi, lam):
logB = pm.logp(pm.Poisson.dist(lam), y[:, None]) # (T, N)
def step(logb_t, la_prev, logA):
return logb_t + pt.logsumexp(la_prev[:, None] + logA, axis=0)
la0 = pt.log(pi) + logB[0]
la, _ = pytensor.scan(step, sequences=logB[1:], outputs_info=la0, non_sequences=pt.log(A))
return pt.logsumexp(la[-1])
# pm.Potential("loglik", hmm_logp(y, A, pi, lam)) inside a model with A rows ~ Dirichlet, ordered lamSee Bayesian HMMs - Label Switching, Non-Homogeneous Transitions and Dwell Times.
Connections
The Kalman filter is the same recursion with Gaussian state: The Kalman Filter, Marginal Likelihood via the Kalman Filter. Numerical stability of for long series requires scaling or log space: Rabiner 1989 - Implementation Issues - Scaling, Multiple Sequences and Initialisation.
See Also
Rabiner 1989 - HMM Tutorial - Overview, Rabiner 1989 - Problem 2 - State Posteriors and the Viterbi Algorithm