mixle.stats.latent.structured_hmm module¶
Structured HMMs: a composable transition layer (dense / low-rank / combinators) + forward-backward.
A standard HMM stores a dense K x K transition matrix and the forward-backward does O(K^2) work per step.
Rich structure – a low-rank transition, a block of independent chains, a factorial (Kronecker) product –
is hard to express that way. This module factors the transition behind a small TransitionOperator
interface so the forward-backward only needs two primitives:
forward(alpha) = alpha @ A (push a state-belief forward one step) backward(v) = A @ v (pull an emission-weighted belief back one step)
and an M-step that re-estimates the operator from expected transition mass. Any operator that implements those plugs into the SAME forward-backward / EM. Implementations:
DenseTransition– the usual K x K matrix (O(K^2)).
LowRankTransition– A = G @ Phi with an inner rank r (K x r, r x K row-stochastic): each state mixes over r shared “transition profiles”. Forward/backward and the M-step are O(K r), and the parameter count drops from K^2 to 2 K r. (Combinators – block-diagonal, Kronecker/factorial – are the same interface; see TransitionOperator subclasses.)
The forgetting / mixing property of an ergodic chain (beliefs forget the distant past) is what lets the
forward-backward be split into chunks and run in parallel; see parallel in this package’s estimation.
- class TransitionOperator[source]
Bases:
objectA row-stochastic state-transition operator behind the HMM forward-backward.
Subclasses provide the two linear maps the recursions need plus an M-step from expected transition mass.
forward/backwardmust be consistent withas_matrix(forward(a) == a @ A,backward(v) == A @ v); the cheap operators never materializeA.- n_states: int
- accumulate(acc, alpha_t, w_next, scale)[source]
Add one transition’s expected mass.
alpha_tis the (normalized) forward belief at t,w_next = b_{t+1} * beta_{t+1}the emission-weighted backward belief at t+1,scalethe forward normalizerc_{t+1}(so the per-step posterior transition mass is exact).
- class DenseTransition(a, prior=None)[source]
Bases:
TransitionOperatorThe usual dense K x K row-stochastic transition (O(K^2) forward-backward).
prior(a K x K pseudocount matrix) is added to the expected counts before each M-step re-normalization – a Dirichlet/MAP transition. A diagonal prior is a sticky self-transition bias (seesticky_transition()); a flat prior is symmetric-Dirichlet smoothing.- Parameters:
a (np.ndarray)
prior (np.ndarray | None)
- forward(alpha)[source]
- backward(v)[source]
- as_matrix()[source]
- new_accumulator()[source]
- accumulate(acc, alpha_t, w_next, scale)[source]
Add one transition’s expected mass.
alpha_tis the (normalized) forward belief at t,w_next = b_{t+1} * beta_{t+1}the emission-weighted backward belief at t+1,scalethe forward normalizerc_{t+1}(so the per-step posterior transition mass is exact).
- estimate(acc)[source]
- sticky_transition(a, kappa)[source]
A dense transition with a STICKY self-transition prior:
kappapseudocounts on the diagonal favor staying in a state (longer dwell times, cleaner segmentation – the sticky-HMM idea).- Parameters:
kappa (float)
- Return type:
DenseTransition
- dirichlet_transition(a, alpha)[source]
A dense transition with a symmetric Dirichlet(
alpha) smoothing prior on every row (MAP).- Parameters:
alpha (float)
- Return type:
DenseTransition
- kron_initial(pi1, pi2)[source]
Factorized initial distribution
pi1 (x) pi2for a factorial (Kronecker) HMM – the two chains start independently. Matches aKroneckerTransitionso the joint initial respects the factors.- Return type:
- class LowRankTransition(g, phi)[source]
Bases:
TransitionOperatorA = G @ Phi: each state’s next-state distribution is a mix of
rshared transition profiles.Gis K x r row-stochastic (state -> profile mixing),Phiis r x K row-stochastic (profile -> next-state).A = G @ Phiis K x K row-stochastic with rank <= r. Forward ((alpha @ G) @ Phi), backward (G @ (Phi @ v)) and the M-step are all O(K r) – never forming A – and the parameter count is 2 K r instead of K^2.- Parameters:
g (np.ndarray)
phi (np.ndarray)
- forward(alpha)[source]
- backward(v)[source]
- as_matrix()[source]
- new_accumulator()[source]
- accumulate(acc, alpha_t, w_next, scale)[source]
Add one transition’s expected mass.
alpha_tis the (normalized) forward belief at t,w_next = b_{t+1} * beta_{t+1}the emission-weighted backward belief at t+1,scalethe forward normalizerc_{t+1}(so the per-step posterior transition mass is exact).
- estimate(acc)[source]
- class SparseTransition(n_states, edges, values=None)[source]
Bases:
TransitionOperatorOnly the given
(from, to)edges are allowed (left-to-right / banded HMMs). Forward, backward and the M-step are O(#edges) – transitions outside the edge set stay exactly zero through EM, so the structure is preserved. Build edges yourself or withleft_to_right_edges()/banded_edges().- Parameters:
n_states (int)
- forward(alpha)[source]
- backward(v)[source]
- as_matrix()[source]
- new_accumulator()[source]
- accumulate(acc, alpha_t, w_next, scale)[source]
Add one transition’s expected mass.
alpha_tis the (normalized) forward belief at t,w_next = b_{t+1} * beta_{t+1}the emission-weighted backward belief at t+1,scalethe forward normalizerc_{t+1}(so the per-step posterior transition mass is exact).
- estimate(acc)[source]
- left_to_right_edges(n_states, skip=1)[source]
Edges for a left-to-right (Bakis) HMM: each state may stay or advance up to
skipstates.
- banded_edges(n_states, bandwidth=1)[source]
Edges for a banded transition: state i connects to i-bandwidth .. i+bandwidth (local time-series).
- class FinalStateEnumeration(hmm, len_dist)[source]
Bases:
objectResult of
_final_state_enumerate():top_k(k)-> [(sequence, log_prob), …] descending.- top_k(k)[source]
- class StructuredHMM(emissions, pi, transition, emission_estimators=None, keys=(None, None), name=None, len_dist=None, terminal_states=None, final_states=None)[source]
Bases:
objectAn HMM whose transition is a
TransitionOperator(dense / low-rank / a combinator).emissionsis one observation distribution per state;pithe initial-state distribution;transitionanyTransitionOperator. The scaled forward-backward and EM call the operator’sforward/backward/accumulate/estimate, so a low-rank or factorial transition runs the SAME inference at its own cost (O(K r) for low-rank).emission_estimators(one per state) drives the emission M-step; default reusesemissions[k].estimator().- Parameters:
transition (TransitionOperator)
- viterbi(seq)[source]
Most-likely state path (Viterbi / max-product). Uses the transition matrix, so it works for any operator; O(T K^2) – a read-out, not the EM hot loop.
- posterior_decode(seq)[source]
Per-position MAP state argmax_k P(z_t = k | x) from the forward-backward posteriors gamma.
- enumerator()[source]
Enumerate observation sequences in descending marginal probability (top_k / rank / seek / nucleus / certified estimates). Enumeration depends only on pi, the transition MATRIX, the emissions and a length distribution – not on the operator’s internal structure – so it reuses the built-in HMM enumerator (an A*-style best-first search over the trellis) on the dense matrix. Requires
len_dist(a distribution over sequence length) and enumerable (discrete) emissions.
- dist_to_enumerator()[source]
- state_posteriors(seq)[source]
The full smoothing posteriors gamma[t,k] = P(z_t = k | x).
- sampler(seed=None)[source]
- fit(seqs, *, max_its=50, tol=1e-6, fast=True)[source]
EM (Baum-Welch) through the transition operator. Returns
(fitted_hmm, loglik_trace).fast=Trueuses the numba-jitted dense forward-backward (~30x over the numpy Python loop) when the transition is a plainDenseTransitionwith no terminal states; structured operators (low-rank / sparse / combinator) use the operator’s per-step accumulate as before.
- dist_to_encoder()
- estimator(pseudo_count=None)
- log_density(x)
- class BlockDiagonalTransition(blocks)[source]
Bases:
TransitionOperatorIndependent sub-chains: the states partition into blocks and transitions stay within a block.
A model whose initial state picks a block and then evolves inside it – a mixture of regimes that do not switch. Build it from any sub-operators (each block can itself be dense or low-rank). Exact, block-local forward-backward and M-step.
- forward(alpha)[source]
- backward(v)[source]
- as_matrix()[source]
- new_accumulator()[source]
- accumulate(acc, alpha_t, w_next, scale)[source]
Add one transition’s expected mass.
alpha_tis the (normalized) forward belief at t,w_next = b_{t+1} * beta_{t+1}the emission-weighted backward belief at t+1,scalethe forward normalizerc_{t+1}(so the per-step posterior transition mass is exact).
- estimate(acc)[source]
- class KroneckerTransition(op1, op2)[source]
Bases:
TransitionOperatorFactorial HMM: the state is the pair
(s1, s2)of two chains evolving in parallel, withA = A1 (x) A2(Kronecker). State index isi1 * K2 + i2.Forward-backward uses the reshape identity (
alpha @ (A1 (x) A2)reshapes toA1^T @ M @ A2), so a step is O(K1 K2 (K1 + K2)) instead of O((K1 K2)^2) – the whole point of a factorial HMM. The E-step is exact over the joint state; the M-step is the standard factorial marginal update (each factor re-estimated from the marginalized joint transition mass), verified to keep EM monotone.- Parameters:
op1 (TransitionOperator)
op2 (TransitionOperator)
- forward(alpha)[source]
- backward(v)[source]
- as_matrix()[source]
- new_accumulator()[source]
- accumulate(acc, alpha_t, w_next, scale)[source]
Add one transition’s expected mass.
alpha_tis the (normalized) forward belief at t,w_next = b_{t+1} * beta_{t+1}the emission-weighted backward belief at t+1,scalethe forward normalizerc_{t+1}(so the per-step posterior transition mass is exact).
- estimate(acc)[source]
- chunked_state_posteriors(hmm, seq, *, chunk, overlap)[source]
State posteriors gamma for one long sequence via overlapping chunks, each run INDEPENDENTLY (embarrassingly parallel). The first chunk uses the model’s pi; interior chunks start from the uniform belief and the
overlapcontext lets the chain forget that wrong boundary – so the kept interior matches the exact forward-backward up to an error that decays at the mixing rate inoverlap.
- fit_chunked(hmm, seqs, *, chunk, overlap, max_its=50, workers=0, tol=1e-6)[source]
Baum-Welch where each long sequence’s forward-backward is split into overlapping chunks run in PARALLEL (the forgetting property bounds the boundary error).
workers>0runs the per-chunk E-steps on a thread pool (NumPy releases the GIL in its array kernels);workers=0runs them serially. The interior suff-statistics are accumulated exactly as inStructuredHMM.fit(); this only changes how the E-step is computed, trading a small, overlap-controlled approximation for intra-sequence parallelism. Returns(fitted_hmm, loglik_trace)(LL is the chunk-summed approximation).
- class StructuredHMMDataEncoder[source]
Bases:
DataSequenceEncoderSequences pass through as lists – the structured forward-backward scores raw observations through the per-state emission
log_density(no flattened columnar encoding; composability over raw speed).- seq_encode(x)[source]
Encode the iid observation sequence x for vectorized evaluation.
- class StructuredHMMAccumulator(emission_accumulators, transition_proto, keys=(None, None))[source]
Bases:
SequenceEncodableStatisticAccumulatorBaum-Welch E-step accumulator: per-sequence forward-backward, accumulating initial-state mass, transition-operator mass, and per-state weighted emission statistics.
- update(x, weight, estimate)[source]
- seq_update(x, weights, estimate)[source]
- seq_initialize(x, weights, rng)[source]
- combine(suff_stat)[source]
- value()[source]
- from_value(x)[source]
- scale(factor)[source]
Multiply the running statistics by
factor– the decay primitive online/streaming Baum-Welch (StreamingEstimator) uses to fold a new batch into a forgetting running estimate.
- acc_to_encoder()[source]
- key_merge(store)[source]
Pool this accumulator’s statistics into
stats_dictunder its merge key.The structural default implements the common single-key pattern: store the accumulator under
self.keysthe first time the key is seen, elsecombineinto the one already there. Accumulators with several named keys (e.g. an HMM’s init/trans/state keys) or a non-accumulator stats payload override this. AkeysofNone(the default) is a no-op.
- key_replace(store)[source]
Replace this accumulator’s statistics from the pooled
stats_dictentry (see key_merge).
- class StructuredHMMAccumulatorFactory(emission_estimators, transition_proto, keys)[source]
Bases:
StatisticAccumulatorFactory- make()[source]
- class StructuredHMMEstimator(emission_estimators, transition_proto, keys=(None, None), name=None, len_dist=None, terminal_states=None)[source]
Bases:
ParameterEstimatorEstimator (M-step) for a
StructuredHMM: re-estimates pi, the transition OPERATOR (any structure – dense/low-rank/combinator), and each state’s emission from the Baum-Welch statistics.keys=(init_key, trans_key)tie the initial / transition parameters across HMMs that share them.- accumulator_factory()[source]
- estimate(nobs, suff_stat)[source]
- stationary_initial(op, *, iters=2000, tol=1e-13)[source]
The transition’s stationary distribution (pi @ A == pi), by power iteration through
op.forward– so it is O(K r) for a low-rank op, never forming A. Use it to COUPLE a StructuredHMM’s initial state to its transition (pi = stationary_initial(transition)): the chain starts in its long-run distribution instead of a free, separately-estimated pi. Answers “do the initial states match the transition?” – they can, by construction.
- class InputOutputHMM(emissions, pi, transitions, emission_estimators=None, name=None, terminal_states=None)[source]
Bases:
objectInput-output HMM (IOHMM): an exogenous discrete input
u_tselects which transition governs each step. Holds oneTransitionOperatorper input symbol; the emission is per-state. Data is(obs_seq, input_seq)pairs whereinput_seq[t]in {0..M-1} drives the transition from t to t+1.Lets a covariate steer the dynamics – regime switching driven by an observed control, the difference between a plain HMM and a controlled Markov model. (Input-dependent emissions are a natural extension; here emissions depend on state only.)
- seq_log_density(x, input_seqs=None)[source]
Per-sequence forward log-likelihood. Two call forms: -
seq_log_density(obs_seqs, input_seqs)– the explicit two-list API; or -seq_log_density(records)– one list of(obs, input)-pair sequences (the 5-part contract).
- log_density(seq)[source]
- dist_to_encoder()[source]
- estimator(pseudo_count=None)[source]
- class IOHMMDataEncoder[source]
Bases:
DataSequenceEncoderAn IOHMM record is one
(obs, input)sequence – a list of(observation, input_symbol)pairs.- seq_encode(x)[source]
Encode the iid observation sequence x for vectorized evaluation.
- class IOHMMAccumulator(emission_accumulators, transition_protos)[source]
Bases:
SequenceEncodableStatisticAccumulator- update(x, weight, estimate)[source]
- seq_update(x, weights, estimate)[source]
- seq_initialize(x, weights, rng)[source]
- combine(suff_stat)[source]
- value()[source]
- from_value(x)[source]
- acc_to_encoder()[source]
- class IOHMMAccumulatorFactory(emission_estimators, transition_protos)[source]
Bases:
StatisticAccumulatorFactory- make()[source]
- class IOHMMEstimator(emission_estimators, transition_protos, name=None)[source]
Bases:
ParameterEstimatorEstimator (M-step) for an
InputOutputHMM: re-estimates pi, one transition operator per input symbol (from the per-input expected counts), and each state’s emission.- accumulator_factory()[source]
- estimate(nobs, suff_stat)[source]
- class ExplicitDurationHMM(emissions, pi, transition_matrix, durations, max_duration, name=None)[source]
Bases:
objectHidden semi-Markov model (explicit-duration HMM): each state emits for a random duration drawn from a per-state duration distribution, then switches state (the transition matrix has a zero diagonal – dwell time is modeled explicitly, not as a self-loop). This captures non-geometric state durations a plain HMM cannot.
durationsis one length-max_durationprobability vector per state (over d = 1..max_duration). The forward variable alpha_t(j) = P(obs_1:t, a segment ends at t in state j); the likelihood is sum_j alpha_T(j). Forward/EM are O(T * K * max_duration). Verified against brute-force segmentation.- forward_loglik(seq)[source]
Total log-likelihood log sum_j alpha_T(j) via the scaled explicit-duration forward.
- fit(seqs, *, max_its=50, tol=1e-6)[source]
Baum-Welch (EM) for the explicit-duration HMM: re-estimates emissions, the per-state duration distributions, the (zero-diagonal) transition, and pi. Returns (fitted_hmm, loglik_trace).
- log_density(seq)[source]
- seq_log_density(x)[source]
- dist_to_encoder()[source]
- estimator(pseudo_count=None)[source]
- to_structured_hmm(len_dist=None)[source]
The HSMM as an EQUIVALENT StructuredHMM via the remaining-duration expansion: K*D sub-states (k, r) = “state k with r steps left in the segment”. The expanded chain emits from state k at every sub-state, decrements deterministically (k,r)->(k,r-1), and at (k,1) switches segment with A[k,k’]*dur[k’](d’).
final_states= the (k,1) sub-states require the last segment to COMPLETE, so the expanded forward log-likelihood EQUALS this EDHMM’s exactly. This hands the HSMM the full StructuredHMM read-out API – Viterbi (recover state+remaining-duration), posterior decoding, the standard forward – and, withlen_dist, enumeration. O(K*D) states.
- enumerator(len_dist)[source]
Enumerate observation sequences in descending marginal probability under this HSMM (complete final segment), given a
len_distover total sequence length. Built on the exact HMM expansion + the final-state best-first enumerator;.top_k(k)-> [(sequence, log_prob), …]. Needs discrete (Categorical) emissions and a Categorical-likelen_dist.
- state_posteriors(seq)[source]
Per-position smoothing posteriors gamma[t, j] = P(z_t = j | obs), marginalizing the durations (sum the posterior of every segment that covers position t). Rows sum to 1.
- posterior_decode(seq)[source]
Per-position MAP state argmax_j P(z_t = j | obs).
- viterbi_segments(seq)[source]
Most-likely segmentation (max-product over the segment lattice): a list of (state, start, duration) segments covering the sequence, O(T K D). The HSMM analog of Viterbi decoding.
- sampler(seed=None)[source]
- class EDHMMDataEncoder[source]
Bases:
DataSequenceEncoderAn ExplicitDurationHMM record is one observation sequence (the durations are latent).
- seq_encode(x)[source]
Encode the iid observation sequence x for vectorized evaluation.
- class EDHMMAccumulator(emission_accumulators, k, d)[source]
Bases:
SequenceEncodableStatisticAccumulatorE-step accumulator for an explicit-duration HMM: per-sequence segment posteriors -> initial / transition / per-state DURATION counts + emission occupancy statistics.
- update(x, weight, estimate)[source]
- seq_update(x, weights, estimate)[source]
- seq_initialize(x, weights, rng)[source]
- combine(suff_stat)[source]
- value()[source]
- from_value(x)[source]
- acc_to_encoder()[source]
- class EDHMMAccumulatorFactory(emission_estimators, k, d)[source]
Bases:
StatisticAccumulatorFactory- make()[source]
- class EDHMMEstimator(emission_estimators, k, d, name=None)[source]
Bases:
ParameterEstimatorEstimator (M-step) for an
ExplicitDurationHMM: re-estimates pi, the zero-diagonal transition, the per-state DURATION distributions, and each state’s emission from the segment-posterior statistics.- accumulator_factory()[source]
- estimate(nobs, suff_stat)[source]
- jit_forward_loglik(hmm)[source]
Compile the scaled forward log-likelihood recursion to a single jax.jit XLA program (lax.scan over time). Returns a callable
score(seq) -> float: emission log-densities are evaluated on the host (arbitrary emissions), then the forward scan runs jitted on the transition matrix. Works for any operator (usesas_matrix()); the win is large T / K. Requires the JAX optional extra.- Parameters:
hmm (StructuredHMM)