2026-02-09 16:15:41 +01:00
# Hidden Markov Models
2026-02-09 18:58:45 +01:00
Hidden Markov Models (HMMs) are probabilistic models for sequential data where observations
are generated by a system that transitions between **hidden (latent) states** . Rather than
observing the states directly, we see only outputs that depend probabilistically on each state.
2026-02-09 16:15:41 +01:00
2026-02-09 18:58:45 +01:00
This module provides a high-performance **Gaussian HMM** implementation with a Rust backend,
featuring the Forward-Backward algorithm, Viterbi decoding, and Baum-Welch parameter learning.
---
## Mathematical Foundations
### Model Definition
A Hidden Markov Model $\lambda$ is defined by three components:
#### 1. States
- **Number of states:** $N$
- **State at time $t$:** $q_t \in \{1, 2, \ldots, N\}$
- **State sequence:** $Q = q_1, q_2, \ldots, q_T$
#### 2. Observations
- **Observation at time $t$:** $o_t \in \mathbb{R}$ (continuous)
- **Observation sequence:** $O = o_1, o_2, \ldots, o_T$
#### 3. Parameters
**Initial State Distribution:**
$$
\pi_i = P(q_1 = i), \quad 1 \leq i \leq N
$$
$$
\sum_{i=1}^N \pi_i = 1
$$
**State Transition Matrix:**
$$
a_{ij} = P(q_{t+1} = j \mid q_t = i), \quad 1 \leq i,j \leq N
$$
$$
\sum_{j=1}^N a_{ij} = 1 \quad \text{for all } i
$$
**Emission Distribution** (Gaussian):
$$
b_j(o_t) = P(o_t \mid q_t = j) = \mathcal{N}(o_t; \mu_j, \sigma_j^2)
$$
$$
b_j(o_t) = \frac{1}{\sigma_j\sqrt{2\pi}} \exp\left(-\frac{(o_t - \mu_j)^2}{2\sigma_j^2}\right)
$$
**Complete model:** $\lambda = (\boldsymbol{\pi}, \mathbf{A}, \mathbf{B})$
---
### The Markov Property
#### First-Order Markov Assumption
The future state depends only on the current state, not the history:
$$
P(q_{t+1} \mid q_1, q_2, \ldots, q_t) = P(q_{t+1} \mid q_t)
$$
#### Output Independence
Observations are conditionally independent given the state:
$$
P(o_t \mid q_1, \ldots, q_T, o_1, \ldots, o_{t-1}, o_{t+1}, \ldots, o_T) = P(o_t \mid q_t)
$$
---
## The Three Fundamental Problems
HMMs are used to solve three fundamental problems:
| Problem | Given | Find | Solution |
|---------|-------|------|----------|
| **Evaluation** | Model $\lambda$, observations $O$ | $P(O \mid \lambda)$ | Forward Algorithm |
| **Decoding** | Model $\lambda$, observations $O$ | Most likely state sequence $Q^*$ | Viterbi Algorithm |
| **Learning** | Observations $O$ | Optimal parameters $\lambda^*$ | Baum-Welch Algorithm |
---
## Forward Algorithm
Computes $P(O \mid \lambda)$ efficiently using dynamic programming.
### Forward Variable
$$
\alpha_t(i) = P(o_1, o_2, \ldots, o_t, q_t = i \mid \lambda)
$$
The probability of observing the first $t$ observations AND being in state $i$ at time $t$.
### Algorithm
**Initialization** ($t = 1$):
$$
\alpha_1(i) = \pi_i \cdot b_i(o_1), \quad 1 \leq i \leq N
$$
**Recursion** ($1 \leq t < T$):
$$
\alpha_{t+1}(j) = \left[\sum_{i=1}^N \alpha_t(i) \cdot a_{ij}\right] \cdot b_j(o_{t+1})
$$
**Termination:**
$$
P(O \mid \lambda) = \sum_{i=1}^N \alpha_T(i)
$$
### Complexity
| Metric | Value |
|--------|-------|
| Time | $O(N^2 T)$ |
| Space | $O(N T)$ |
| Without DP | $O(N^T)$ — exponential! |
---
## Backward Algorithm
Alternative computation for completeness and use in Baum-Welch.
### Backward Variable
$$
\beta_t(i) = P(o_{t+1}, o_{t+2}, \ldots, o_T \mid q_t = i, \lambda)
$$
**Initialization** ($t = T$):
$$
\beta_T(i) = 1, \quad 1 \leq i \leq N
$$
**Recursion** ($t = T-1, T-2, \ldots, 1$):
$$
\beta_t(i) = \sum_{j=1}^N a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)
$$
---
## Viterbi Algorithm
Finds the single **most likely state sequence** given observations.
### Objective
$$
Q^* = \arg\max_Q P(Q \mid O, \lambda) = \arg\max_Q P(Q, O \mid \lambda)
$$
### Viterbi Variable
$$
\delta_t(i) = \max_{q_1, \ldots, q_{t-1}} P(q_1, \ldots, q_{t-1}, q_t = i, o_1, \ldots, o_t \mid \lambda)
$$
The maximum probability of any path ending in state $i$ at time $t$.
### Algorithm
**Initialization** ($t = 1$):
$$
\delta_1(i) = \pi_i \cdot b_i(o_1)
$$
$$
\psi_1(i) = 0
$$
**Recursion** ($2 \leq t \leq T$):
$$
\delta_t(j) = \max_{1 \leq i \leq N} \left[\delta_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)
$$
$$
\psi_t(j) = \arg\max_{1 \leq i \leq N} \left[\delta_{t-1}(i) \cdot a_{ij}\right]
$$
**Termination:**
$$
P^* = \max_{1 \leq i \leq N} \delta_T(i)
$$
$$
q_T^* = \arg\max_{1 \leq i \leq N} \delta_T(i)
$$
**Backtracking** ($t = T-1, T-2, \ldots, 1$):
$$
q_t^* = \psi_{t+1}(q_{t+1}^*)
$$
---
## Baum-Welch Algorithm
An **Expectation-Maximization (EM)** algorithm for learning HMM parameters from data.
### Auxiliary Variables
**State occupation probability:**
$$
\gamma_t(i) = P(q_t = i \mid O, \lambda) = \frac{\alpha_t(i) \cdot \beta_t(i)}{\sum_{j=1}^N \alpha_t(j) \cdot \beta_t(j)}
$$
**Transition probability:**
$$
\xi_t(i,j) = P(q_t = i, q_{t+1} = j \mid O, \lambda)
$$
$$
= \frac{\alpha_t(i) \cdot a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)}{\sum_{i=1}^N \sum_{j=1}^N \alpha_t(i) \cdot a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)}
$$
### E-Step
Compute $\gamma_t(i)$ and $\xi_t(i,j)$ for all $t$, $i$, $j$ using current parameters.
### M-Step
Update parameters to maximize expected log-likelihood:
**Initial state probabilities:**
$$
\bar{\pi}_i = \gamma_1(i)
$$
**Transition probabilities:**
$$
\bar{a}_{ij} = \frac{\sum_{t=1}^{T-1} \xi_t(i,j)}{\sum_{t=1}^{T-1} \gamma_t(i)}
$$
**Emission parameters (Gaussian):**
$$
\bar{\mu}_j = \frac{\sum_{t=1}^T \gamma_t(j) \cdot o_t}{\sum_{t=1}^T \gamma_t(j)}
$$
$$
\bar{\sigma}_j^2 = \frac{\sum_{t=1}^T \gamma_t(j) \cdot (o_t - \bar{\mu}_j)^2}{\sum_{t=1}^T \gamma_t(j)}
$$
### Convergence
Iterate E-step and M-step until:
$$
|L(\lambda^{(k+1)}) - L(\lambda^{(k)})| < \epsilon
$$
where $L(\lambda) = \log P(O \mid \lambda)$ is the log-likelihood.
**Properties:**
- Guaranteed to converge to a **local maximum**
- May converge to different solutions depending on initialization
- Multiple random restarts recommended
---
## Numerical Stability
### Scaling
Raw probabilities underflow for long sequences. Use **scaling factors** :
$$
c_t = \frac{1}{\sum_{i=1}^N \alpha_t(i)}
$$
Scaled forward variables:
$$
\hat{\alpha}_t(i) = c_t \cdot \alpha_t(i)
$$
### Log-Space Computation
For Viterbi, work in log-space:
$$
\log \delta_t(j) = \max_{1 \leq i \leq N} \left[\log \delta_{t-1}(i) + \log a_{ij}\right] + \log b_j(o_t)
$$
Use log-sum-exp for stable additions:
$$
\log(e^a + e^b) = \max(a,b) + \log(1 + e^{-|a-b|})
$$
---
## Model Selection
### Number of States
Choose the number of states $N$ using information criteria:
| Criterion | Formula | Description |
|-----------|---------|-------------|
| **AIC** | $-2\log L + 2k$ | Akaike Information Criterion |
| **BIC** | $-2\log L + k\log n$ | Bayesian Information Criterion |
where $k$ is the number of parameters and $n$ is the sample size.
**Lower values indicate better models** (penalized for complexity).
---
## Python API
### Basic Usage
2026-02-09 16:15:41 +01:00
```python
import numpy as np
from optimizr import HMM
2026-02-09 18:58:45 +01:00
# Generate synthetic regime data
np . random . seed ( 42 )
2026-02-09 16:15:41 +01:00
returns = np . concatenate ([
2026-02-09 18:58:45 +01:00
np . random . normal ( 0.01 , 0.02 , 500 ), # Bull regime
np . random . normal ( - 0.015 , 0.03 , 500 ), # Bear regime
2026-02-09 16:15:41 +01:00
])
2026-02-09 18:58:45 +01:00
# Fit a 2-state HMM
2026-02-09 16:15:41 +01:00
model = HMM ( n_states = 2 )
model . fit ( returns , n_iterations = 100 )
2026-02-09 18:58:45 +01:00
# Decode the most likely state sequence
2026-02-09 16:15:41 +01:00
states = model . predict ( returns )
2026-02-09 18:58:45 +01:00
print ( "State counts:" , np . unique ( states , return_counts = True ))
```
**Expected output:**
```
State counts: (array([0, 1]), array([498, 502]))
2026-02-09 16:15:41 +01:00
```
2026-02-09 18:58:45 +01:00
### Model Inspection
```python
# Examine learned parameters
print ( "Initial distribution:" , model . pi )
print ( "Transition matrix: \n " , model . A )
print ( "Emission means:" , model . means )
print ( "Emission stds:" , model . stds )
# Compute log-likelihood
ll = model . score ( returns )
print ( f "Log-likelihood: { ll : .2f } " )
```
**Expected output:**
```
Initial distribution: [0.95 0.05]
Transition matrix:
[[0.992 0.008]
[0.010 0.990]]
Emission means: [ 0.0098 -0.0152]
Emission stds: [0.0198 0.0301]
Log-likelihood: 2847.31
```
### Feature Engineering for HMM
2026-02-09 18:31:14 +01:00
```python
from optimizr import prepare_for_hmm_py
2026-02-09 18:58:45 +01:00
# Prepare features from price data with rolling statistics
features = prepare_for_hmm_py (
prices ,
lag_periods = [ 1 , 5 , 20 ], # multi-scale lookback
)
# Fit HMM with richer features
2026-02-09 18:31:14 +01:00
hmm = HMM ( n_states = 3 ) . fit ( features , n_iterations = 120 )
```
2026-02-09 18:58:45 +01:00
Use rolling statistics from `timeseries_utils` as additional features for richer
regime classification:
```python
from optimizr import rolling_hurst , rolling_halflife
hurst = rolling_hurst ( prices , window = 50 )
halflife = rolling_halflife ( prices , window = 50 )
features = np . column_stack ([ returns , hurst , halflife ])
hmm = HMM ( n_states = 3 ) . fit ( features , n_iterations = 100 )
```
---
## Visualization
### State Sequence Plot
```python
import matplotlib.pyplot as plt
fig , axes = plt . subplots ( 2 , 1 , figsize = ( 12 , 6 ), sharex = True )
# Price/returns
axes [ 0 ] . plot ( returns , alpha = 0.7 )
axes [ 0 ] . set_ylabel ( 'Returns' )
axes [ 0 ] . set_title ( 'Returns Time Series' )
# State sequence
axes [ 1 ] . plot ( states , drawstyle = 'steps-post' , color = 'orange' )
axes [ 1 ] . set_ylabel ( 'State' )
axes [ 1 ] . set_xlabel ( 'Time' )
axes [ 1 ] . set_title ( 'Decoded State Sequence' )
axes [ 1 ] . set_yticks ([ 0 , 1 ])
axes [ 1 ] . set_yticklabels ([ 'Bull' , 'Bear' ])
plt . tight_layout ()
plt . savefig ( 'hmm_states.png' , dpi = 150 )
```
### Transition Diagram
```python
import networkx as nx
G = nx . DiGraph ()
for i in range ( model . n_states ):
for j in range ( model . n_states ):
if model . A [ i , j ] > 0.01 : # threshold small probabilities
G . add_edge ( f "State { i } " , f "State { j } " , weight = model . A [ i , j ])
pos = nx . spring_layout ( G )
edge_labels = {( u , v ): f " { d [ 'weight' ] : .2f } " for u , v , d in G . edges ( data = True )}
plt . figure ( figsize = ( 8 , 6 ))
nx . draw ( G , pos , with_labels = True , node_size = 2000 , node_color = 'lightblue' )
nx . draw_networkx_edge_labels ( G , pos , edge_labels = edge_labels )
plt . title ( 'HMM Transition Diagram' )
plt . savefig ( 'hmm_transitions.png' , dpi = 150 )
```
---
## Applications
### 1. Financial Regime Detection
HMMs identify market regimes (bull/bear/sideways) from returns:
```python
from optimizr import HMM
# Daily returns
hmm = HMM ( n_states = 3 ) # bull, bear, high-volatility
hmm . fit ( daily_returns )
regimes = hmm . predict ( daily_returns )
regime_labels = [ 'Bull' , 'Bear' , 'High Vol' ]
current_regime = regime_labels [ regimes [ - 1 ]]
print ( f "Current market regime: { current_regime } " )
```
### 2. Mean Reversion Trading
Use regime as a filter for mean-reversion strategies:
```python
# Only trade mean-reversion when in low-volatility regime
regime = hmm . predict ( recent_returns )[ - 1 ]
if regime == 0 : # low-vol regime
# Execute mean-reversion strategy
pass
else :
# Hold or reduce position
pass
```
### 3. Risk Management
Adjust position sizing based on detected regime:
```python
regime_volatilities = {
0 : 0.02 , # low vol
1 : 0.03 , # medium vol
2 : 0.05 , # high vol
}
current_regime = hmm . predict ( returns )[ - 1 ]
position_size = base_size * ( target_vol / regime_volatilities [ current_regime ])
```
---
## Performance
Benchmarks on Apple M1 (1000 observations):
| States | Fit Time | Predict Time | Memory |
|--------|----------|--------------|--------|
| 2 | 45 ms | 2 ms | 0.5 MB |
| 3 | 68 ms | 3 ms | 0.8 MB |
| 5 | 124 ms | 6 ms | 1.4 MB |
| 10 | 412 ms | 18 ms | 4.2 MB |
**Complexity:** $O(N^2 T)$ for both forward-backward and Viterbi.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| States not separating | Too few iterations | Increase `n_iterations` to 150– 200 |
| Converges to bad solution | Local optimum | Run multiple restarts with different seeds |
| Underflow errors | Long sequences | Enable log-space computation (automatic in Rust backend) |
| All observations in one state | Poor initialization | Try more states or normalize input data |
---
## Tips
2026-02-09 18:31:14 +01:00
2026-02-09 18:58:45 +01:00
- **Normalize inputs:** Scale features to similar ranges for better learning.
- **Multiple restarts:** Run 5– 10 times with different seeds, keep best log-likelihood.
- **Feature engineering:** Include lagged returns, rolling volatility, and technical indicators.
- **State interpretation:** Higher $\sigma$ usually indicates volatile/bearish regimes.
---
## Implementation Notes
- Uses Rust backend when available; falls back to Python if unavailable.
2026-02-09 16:15:41 +01:00
- `fit` runs Baum-Welch; `predict` runs Viterbi.
2026-02-09 18:58:45 +01:00
- Call `score(X)` to compute log-likelihood for model comparison.
- Scaling is applied automatically to prevent numerical underflow.
---
## References
1. Rabiner, L.R. (1989). "A tutorial on hidden Markov models and selected applications in speech recognition." *Proceedings of the IEEE* , 77(2):257– 286.
2. Baum, L.E. & Petrie, T. (1966). "Statistical inference for probabilistic functions of finite state Markov chains." *Annals of Mathematical Statistics* , 37(6):1554– 1563.
3. Viterbi, A. (1967). "Error bounds for convolutional codes and an asymptotically optimum decoding algorithm." *IEEE Trans. Info. Theory* , 13(2):260– 269.
4. Hamilton, J.D. (1989). "A new approach to the economic analysis of nonstationary time series and the business cycle." *Econometrica* , 57(2):357– 384.
---
## Related Topics
- [MCMC ](mcmc.md ) – Alternative inference for more complex latent variable models
- [Differential Evolution ](differential_evolution.md ) – Global optimization for HMM initialization
- [Mean Field Games ](mean_field_games.md ) – Population dynamics with strategic interactions