Causality APIs 04: An HMM Is Just a Dynamic Bayesian Network

Once you unroll time, an HMM is just an ordinary Bayesian network with repeated structure. That makes temporal reasoning much less mysterious and much more programmable.

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:

Unrolled three-step HMM as a dynamic Bayesian network

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 = umbrella
  • O2 = umbrella
  • O3 = 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

Discover more from Rocket Vector Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading