179 lines
5.5 KiB
Python
Executable File
179 lines
5.5 KiB
Python
Executable File
import numpy as np
|
|
|
|
|
|
class HiddenMarkovModel(object):
|
|
"""
|
|
Base class of Hidden Markov models
|
|
"""
|
|
|
|
def __init__(self, initial_proba, transition_proba):
|
|
"""
|
|
construct hidden markov model
|
|
|
|
Parameters
|
|
----------
|
|
initial_proba : (n_hidden,) np.ndarray
|
|
initial probability of each hidden state
|
|
transition_proba : (n_hidden, n_hidden) np.ndarray
|
|
transition probability matrix
|
|
(i, j) component denotes the transition probability from i-th to j-th hidden state
|
|
|
|
Attribute
|
|
---------
|
|
n_hidden : int
|
|
number of hidden state
|
|
"""
|
|
self.n_hidden = initial_proba.size
|
|
self.initial_proba = initial_proba
|
|
self.transition_proba = transition_proba
|
|
|
|
def fit(self, seq, iter_max=100):
|
|
"""
|
|
perform EM algorithm to estimate parameter of emission model and hidden variables
|
|
|
|
Parameters
|
|
----------
|
|
seq : (N, ndim) np.ndarray
|
|
observed sequence
|
|
iter_max : int
|
|
maximum number of EM steps
|
|
|
|
Returns
|
|
-------
|
|
posterior : (N, n_hidden) np.ndarray
|
|
posterior distribution of each latent variable
|
|
"""
|
|
params = np.hstack(
|
|
(self.initial_proba.ravel(), self.transition_proba.ravel()))
|
|
for i in range(iter_max):
|
|
p_hidden, p_transition = self.expect(seq)
|
|
self.maximize(seq, p_hidden, p_transition)
|
|
params_new = np.hstack(
|
|
(self.initial_proba.ravel(), self.transition_proba.ravel()))
|
|
if np.allclose(params, params_new):
|
|
break
|
|
else:
|
|
params = params_new
|
|
return self.forward_backward(seq)
|
|
|
|
def expect(self, seq):
|
|
"""
|
|
estimate posterior distributions of hidden states and
|
|
transition probability between adjacent latent variables
|
|
|
|
Parameters
|
|
----------
|
|
seq : (N, ndim) np.ndarray
|
|
observed sequence
|
|
|
|
Returns
|
|
-------
|
|
p_hidden : (N, n_hidden) np.ndarray
|
|
posterior distribution of each hidden variable
|
|
p_transition : (N - 1, n_hidden, n_hidden) np.ndarray
|
|
posterior transition probability between adjacent latent variables
|
|
"""
|
|
likelihood = self.likelihood(seq)
|
|
|
|
f = self.initial_proba * likelihood[0]
|
|
constant = [f.sum()]
|
|
forward = [f / f.sum()]
|
|
for like in likelihood[1:]:
|
|
f = forward[-1] @ self.transition_proba * like
|
|
constant.append(f.sum())
|
|
forward.append(f / f.sum())
|
|
forward = np.asarray(forward)
|
|
constant = np.asarray(constant)
|
|
|
|
backward = [np.ones(self.n_hidden)]
|
|
for like, c in zip(likelihood[-1:0:-1], constant[-1:0:-1]):
|
|
backward.insert(0, self.transition_proba @ (like * backward[0]) / c)
|
|
backward = np.asarray(backward)
|
|
|
|
p_hidden = forward * backward
|
|
p_transition = self.transition_proba * likelihood[1:, None, :] * backward[1:, None, :] * forward[:-1, :, None]
|
|
return p_hidden, p_transition
|
|
|
|
def forward_backward(self, seq):
|
|
"""
|
|
estimate posterior distributions of hidden states
|
|
|
|
Parameters
|
|
----------
|
|
seq : (N, ndim) np.ndarray
|
|
observed sequence
|
|
|
|
Returns
|
|
-------
|
|
posterior : (N, n_hidden) np.ndarray
|
|
posterior distribution of hidden states
|
|
"""
|
|
likelihood = self.likelihood(seq)
|
|
|
|
f = self.initial_proba * likelihood[0]
|
|
constant = [f.sum()]
|
|
forward = [f / f.sum()]
|
|
for like in likelihood[1:]:
|
|
f = forward[-1] @ self.transition_proba * like
|
|
constant.append(f.sum())
|
|
forward.append(f / f.sum())
|
|
|
|
backward = [np.ones(self.n_hidden)]
|
|
for like, c in zip(likelihood[-1:0:-1], constant[-1:0:-1]):
|
|
backward.insert(0, self.transition_proba @ (like * backward[0]) / c)
|
|
|
|
forward = np.asarray(forward)
|
|
backward = np.asarray(backward)
|
|
posterior = forward * backward
|
|
return posterior
|
|
|
|
def filtering(self, seq):
|
|
"""
|
|
bayesian filtering
|
|
|
|
Parameters
|
|
----------
|
|
seq : (N, ndim) np.ndarray
|
|
observed sequence
|
|
|
|
Returns
|
|
-------
|
|
posterior : (N, n_hidden) np.ndarray
|
|
posterior distributions of each latent variables
|
|
"""
|
|
likelihood = self.likelihood(seq)
|
|
p = self.initial_proba * likelihood[0]
|
|
posterior = [p / np.sum(p)]
|
|
for like in likelihood[1:]:
|
|
p = posterior[-1] @ self.transition_proba * like
|
|
posterior.append(p / np.sum(p))
|
|
posterior = np.asarray(posterior)
|
|
return posterior
|
|
|
|
def viterbi(self, seq):
|
|
"""
|
|
viterbi algorithm (a.k.a. max-sum algorithm)
|
|
|
|
Parameters
|
|
----------
|
|
seq : (N, ndim) np.ndarray
|
|
observed sequence
|
|
|
|
Returns
|
|
-------
|
|
seq_hid : (N,) np.ndarray
|
|
the most probable sequence of hidden variables
|
|
"""
|
|
nll = -np.log(self.likelihood(seq))
|
|
cost_total = nll[0]
|
|
from_list = []
|
|
for i in range(1, len(seq)):
|
|
cost_temp = cost_total[:, None] - np.log(self.transition_proba) + nll[i]
|
|
cost_total = np.min(cost_temp, axis=0)
|
|
index = np.argmin(cost_temp, axis=0)
|
|
from_list.append(index)
|
|
seq_hid = [np.argmin(cost_total)]
|
|
for source in from_list[::-1]:
|
|
seq_hid.insert(0, source[seq_hid[0]])
|
|
return seq_hid
|