10. Dynamic Bayesian Network, Markov Chain

Let’s see how we can represent a Markov Chain (MC) as a Dynamic Bayesian Network (DBN). We will verify our results with the stationary distribution or steady state.

10.1. Model

The model, \(P\), has two states, sunny and rainy. Our initial state, \(s\), will be sunny.

[1]:
import numpy as np

P = np.array([[0.9, 0.1], [0.5, 0.5]])
s = np.array([1.0, 0.0])

10.2. Power Method

We can use the Power Method to find the steady state.

\(s^n = s^0P^n\)

We will continue to compute \(s^n\) until \(||P^n - P^{n-1}|| \lt \delta\).

[2]:
s.dot(P)
[2]:
array([0.9, 0.1])
[3]:
s.dot(P).dot(P)
[3]:
array([0.86, 0.14])
[4]:
import pandas as pd

P_curr = s.dot(P)
trace_df = [list(P_curr) + [np.nan]]

for it in range(100):
    P_next = P_curr.dot(P)
    d = np.linalg.norm(P_next - P_curr, 1)

    if d < 0.0001:
        break

    trace_df.append(list(P_next) + [d])
    P_curr = P_next

trace_df = pd.DataFrame(trace_df, columns=['sunny', 'rainy', 'delta'])
P_next
[4]:
array([0.83335081, 0.16664919])

Here is the trace of \(s^n\) until convergence.

[5]:
trace_df
[5]:
sunny rainy delta
0 0.900000 0.100000 NaN
1 0.860000 0.140000 0.080000
2 0.844000 0.156000 0.032000
3 0.837600 0.162400 0.012800
4 0.835040 0.164960 0.005120
5 0.834016 0.165984 0.002048
6 0.833606 0.166394 0.000819
7 0.833443 0.166557 0.000328
8 0.833377 0.166623 0.000131

10.3. Eigen decomposition

We can also find the steady state directly through eigen decomposition. The steady state is the eigenvector associated with the eigenvalue closest to 1.

[6]:
def get_stationary_p(X):
    S, U = np.linalg.eig(X.T)
    r = (U[:,np.isclose(S, 1)][:,0] / U[:,np.isclose(S, 1)][:,0].sum()).real
    return r

get_stationary_p(P)
[6]:
array([0.83333333, 0.16666667])

10.4. Dynamic Bayesian Network

Another way to represent a MC is with a DBN. In general, the DBN structure consists of an endless number of nodes corresponding to time slices. However, in a DBN, we can unroll the DBN structure to just 2 consecutive time slices. It is assumed that the intra- (within a time slice) and inter-relationships (between two consecutive time slices) do not change over time.

[7]:
from pybbn.graph.dag import Bbn
from pybbn.graph.edge import Edge, EdgeType
from pybbn.graph.jointree import EvidenceBuilder
from pybbn.graph.node import BbnNode
from pybbn.graph.variable import Variable
from pybbn.pptc.inferencecontroller import InferenceController

# create the nodes
w_curr = BbnNode(Variable(0, 'weather', ['rainy', 'sunny']), [1.0, 0.0])
w_next = BbnNode(Variable(2, 'weather_next', ['rainy', 'sunny']), [0.9, 0.1, 0.5, 0.5])

# create the network structure
bbn = Bbn() \
    .add_node(w_curr) \
    .add_node(w_next) \
    .add_edge(Edge(w_curr, w_next, EdgeType.DIRECTED))

# convert the BBN to a join tree
join_tree = InferenceController.apply(bbn)

for node in [w_curr, w_next]:
    potential = join_tree.get_bbn_potential(node)
    print(node)
    print(potential)
    print('-' * 15)
0|weather|rainy,sunny
0=rainy|1.00000
0=sunny|0.00000
---------------
2|weather_next|rainy,sunny
2=rainy|0.90000
2=sunny|0.10000
---------------

We can perform inference on the DBN and use the probability vector of the next state as input to the following time slice.

[8]:
import pandas as pd

def get_probs(jt, node):
    entries = jt.get_bbn_potential(node).entries
    return np.array([e.value for e in entries])

P_curr = get_probs(join_tree, w_curr)
jt = InferenceController.reapply(join_tree, {w_curr.id: list(P_curr)})

trace_df = [list(P_curr) + [np.nan]]

for _ in range(20):
    P_next = get_probs(jt, w_next)
    d = np.linalg.norm(P_next - P_curr, 1)

    if d < 0.0001:
        break

    trace_df.append(list(P_next) + [d])
    P_curr = P_next

    jt = InferenceController.reapply(jt, {w_curr.id: list(P_curr)})

trace_df = pd.DataFrame(trace_df, columns=['sunny', 'rainy', 'delta'])
P_next
[8]:
array([0.83335081, 0.16664919])
[9]:
trace_df
[9]:
sunny rainy delta
0 1.000000 0.000000 NaN
1 0.900000 0.100000 0.200000
2 0.860000 0.140000 0.080000
3 0.844000 0.156000 0.032000
4 0.837600 0.162400 0.012800
5 0.835040 0.164960 0.005120
6 0.834016 0.165984 0.002048
7 0.833606 0.166394 0.000819
8 0.833443 0.166557 0.000328
9 0.833377 0.166623 0.000131