Say “hidden Markov model” in a room full of engineers and people often treat it like a separate species of machinery. That is a problem if you want sequence reasoning to feel like a natural extension of a causal API rather than a separate system with separate mental models.
The useful simplification is that once you unroll time, a lot of that mystery disappears.
One of the fastest ways to demystify an HMM is to stop talking about it as a special object.
It is a dynamic Bayesian network.
More concretely: once you unroll time, it becomes an ordinary DAG with repeated structure.
That matters because it means temporal reasoning is not some separate intellectual continent. It is the same factorization game with an extra axis.
Unroll three time steps and it is just a BBN
We can build a three-step weather model with hidden states H1, H2, H3 and observation nodes O1, O2, O3.
The only edges are the ones you would expect: hidden state to observation at each time step, and hidden state to hidden state across time. If you draw that unrolled model explicitly, the structure stops looking exotic very quickly:

The whole construction is small enough to write directly:
for t in range(1, horizon + 1):
hidden = f"H{t}"
observed = f"O{t}"
dag["nodes"].extend([hidden, observed])
dag["edges"].append((hidden, observed))
if t > 1:
dag["edges"].append((f"H{t - 1}", hidden))
params[hidden] = transition_cpt(f"H{t - 1}", hidden)
else:
params[hidden] = start_cpt(hidden)
params[observed] = emission_cpt(hidden, observed)
That is it. Hidden state transitions become edges between hidden nodes. Emissions become edges from hidden state to observation. After unrolling, the model can be handed to the same exact discrete reasoning machinery as any other BBN.
Query the unrolled graph
Suppose the observation sequence is:
O1 = umbrellaO2 = umbrellaO3 = no_umbrella
Then the query is ordinary:
model = build_unrolled_model(3)
evidence = model.e({"O1": "umbrella", "O2": "umbrella", "O3": "no_umbrella"})
sequence_probability = model.pevidence(evidence)
posterior_h2 = model.pquery(nodes=["H2"], evidences=evidence)["H2"]
And the output is completely standard:
P(O1=umbrella, O2=umbrella, O3=no_umbrella) = 0.120445
P(H2=rain | umbrella, umbrella, no_umbrella) = 0.799161
P(H2=sun | umbrella, umbrella, no_umbrella) = 0.200839
The factorization behind that query is just:
P(H1) P(O1 | H1) P(H2 | H1) P(O2 | H2) P(H3 | H2) P(O3 | H3)
That is why the slogan is useful:
An HMM is not a different species.
It is a repeated Bayesian network template over time.
So what
On its own, an HMM is still mostly a probabilistic state-estimation object, not an intervention engine.
But the modeling move is the same one you need for temporal causality:
- keep the hidden state
- keep the observation model
- add action nodes when you want to represent control
- query the resulting unrolled graph with the same reasoning surface
It takes a familiar sequential model and strips away the mystique. Once that click happens, “dynamic Bayesian network” stops sounding like a new ontology and starts sounding like what it is: a reusable graph template whose time-unrolled version is just another causal model you can query.

Leave a Reply