def _updateTransitionMatrix(self): """ Updates the hidden-state transition matrix and the initial distribution """ # TRANSITION MATRIX C = self.model.count_matrix() + self.prior_C # posterior count matrix # check if we work with these options if self.reversible and not _tmatrix_disconnected.is_connected(C, strong=True): raise NotImplementedError( "Encountered disconnected count matrix with sampling option reversible:\n " + str(C) + "\nUse prior to ensure connectivity or use reversible=False." ) # ensure consistent sparsity pattern (P0 might have additional zeros because of underflows) # TODO: these steps work around a bug in msmtools. Should be fixed there P0 = msmest.transition_matrix(C, reversible=self.reversible, maxiter=10000, warn_not_converged=False) zeros = np.where(P0 + P0.T == 0) C[zeros] = 0 # run sampler Tij = msmest.sample_tmatrix( C, nsample=1, nsteps=self.transition_matrix_sampling_steps, reversible=self.reversible ) # INITIAL DISTRIBUTION if self.stationary: # p0 is consistent with P p0 = _tmatrix_disconnected.stationary_distribution(Tij, C=C) else: n0 = self.model.count_init().astype(float) p0 = np.random.dirichlet(n0 + self.prior_n0) # sample p0 from posterior # update HMM with new sample self.model.update(p0, Tij)
def stationary_distribution(self): r""" Compute stationary distribution of hidden states if possible. Raises ------ ValueError if the HMM is not stationary """ assert _tmatrix_disconnected.is_connected(self._Tij, strong=False), \ 'No unique stationary distribution because transition matrix is not connected' import msmtools.analysis as msmana return msmana.stationary_distribution(self._Tij)
def _updateTransitionMatrix(self): """ Updates the hidden-state transition matrix and the initial distribution """ # TRANSITION MATRIX C = self.model.count_matrix() + self.prior_C # posterior count matrix # check if we work with these options if self.reversible and not _tmatrix_disconnected.is_connected( C, strong=True): raise NotImplementedError( 'Encountered disconnected count matrix with sampling option reversible:\n ' + str(C) + '\nUse prior to ensure connectivity or use reversible=False.') # ensure consistent sparsity pattern (P0 might have additional zeros because of underflows) # TODO: these steps work around a bug in msmtools. Should be fixed there P0 = msmest.transition_matrix(C, reversible=self.reversible, maxiter=10000, warn_not_converged=False) zeros = np.where(P0 + P0.T == 0) C[zeros] = 0 # run sampler Tij = msmest.sample_tmatrix( C, nsample=1, nsteps=self.transition_matrix_sampling_steps, reversible=self.reversible) # INITIAL DISTRIBUTION if self.stationary: # p0 is consistent with P p0 = _tmatrix_disconnected.stationary_distribution(Tij, C=C) else: n0 = self.model.count_init().astype(float) first_timestep_counts_with_prior = n0 + self.prior_n0 positive = first_timestep_counts_with_prior > 0 p0 = np.zeros_like(n0) p0[positive] = np.random.dirichlet( first_timestep_counts_with_prior[positive] ) # sample p0 from posterior # update HMM with new sample self.model.update(p0, Tij)
def __init__(self, observations, nstates, initial_model=None, reversible=True, stationary=False, transition_matrix_sampling_steps=1000, p0_prior='mixed', transition_matrix_prior='mixed', output='gaussian'): """Initialize a Bayesian hidden Markov model sampler. Parameters ---------- observations : list of numpy arrays representing temporal data `observations[i]` is a 1d numpy array corresponding to the observed trajectory index `i` nstates : int The number of states in the model. initial_model : HMM, optional, default=None If specified, the given initial model will be used to initialize the BHMM. Otherwise, a heuristic scheme is used to generate an initial guess. reversible : bool, optional, default=True If True, a prior that enforces reversible transition matrices (detailed balance) is used; otherwise, a standard non-reversible prior is used. stationary : bool, optional, default=False If True, the stationary distribution of the transition matrix will be used as initial distribution. Only use True if you are confident that the observation trajectories are started from a global equilibrium. If False, the initial distribution will be estimated as usual from the first step of the hidden trajectories. transition_matrix_sampling_steps : int, optional, default=1000 number of transition matrix sampling steps per BHMM cycle p0_prior : None, str, float or ndarray(n) Prior for the initial distribution of the HMM. Will only be active if stationary=False (stationary=True means that p0 is identical to the stationary distribution of the transition matrix). Currently implements different versions of the Dirichlet prior that is conjugate to the Dirichlet distribution of p0. p0 is sampled from: .. math: p0 \sim \prod_i (p0)_i^{a_i + n_i - 1} where :math:`n_i` are the number of times a hidden trajectory was in state :math:`i` at time step 0 and :math:`a_i` is the prior count. Following options are available: | 'mixed' (default), :math:`a_i = p_{0,init}`, where :math:`p_{0,init}` is the initial distribution of initial_model. | 'uniform', :math:`a_i = 1` | ndarray(n) or float, the given array will be used as A. | None, :math:`a_i = 0`. This option ensures coincidence between sample mean an MLE. Will sooner or later lead to sampling problems, because as soon as zero trajectories are drawn from a given state, the sampler cannot recover and that state will never serve as a starting state subsequently. Only recommended in the large data regime and when the probability to sample zero trajectories from any state is negligible. transition_matrix_prior : str or ndarray(n, n) Prior for the HMM transition matrix. Currently implements Dirichlet priors if reversible=False and reversible transition matrix priors as described in [1]_ if reversible=True. For the nonreversible case the posterior of transition matrix :math:`P` is: .. math: P \sim \prod_{i,j} p_{ij}^{b_{ij} + c_{ij} - 1} where :math:`c_{ij}` are the number of transitions found for hidden trajectories and :math:`b_{ij}` are prior counts. | 'mixed' (default), :math:`b_{ij} = p_{ij,init}`, where :math:`p_{ij,init}` is the transition matrix of initial_model. That means one prior count will be used per row. | 'uniform', :math:`b_{ij} = 1` | ndarray(n, n) or broadcastable, the given array will be used as B. | None, :math:`b_ij = 0`. This option ensures coincidence between sample mean an MLE. Will sooner or later lead to sampling problems, because as soon as a transition :math:`ij` will not occur in a sample, the sampler cannot recover and that transition will never be sampled again. This option is not recommended unless you have a small HMM and a lot of data. output_model_type : str, optional, default='gaussian' Output model type. ['gaussian', 'discrete'] References ---------- .. [1] Trendelkamp-Schroer, B., H. Wu, F. Paul and F. Noe: Estimation and uncertainty of reversible Markov models. J. Chem. Phys. 143, 174101 (2015). """ # Sanity checks. if len(observations) == 0: raise Exception("No observations were provided.") # Store options. self.reversible = reversible self.stationary = stationary # Store the number of states. self.nstates = nstates # Store a copy of the observations. self.observations = copy.deepcopy(observations) self.nobs = len(observations) self.Ts = [len(o) for o in observations] self.maxT = np.max(self.Ts) # initial model if initial_model: # Use user-specified initial model, if provided. self.model = copy.deepcopy(initial_model) else: # Generate our own initial model. self.model = self._generateInitialModel(output) # prior initial vector if p0_prior is None or p0_prior == 'sparse': self.prior_n0 = np.zeros(self.nstates) elif isinstance(p0_prior, np.ndarray): if len(p0_prior.shape) == 1 and p0_prior.shape[0] == self.nstates: self.prior_n0 = np.array(p0_prior) else: raise ValueError( 'initial distribution prior must have dimension ' + str(nstates)) elif p0_prior == 'mixed': self.prior_n0 = np.array(self.model.initial_distribution) elif p0_prior == 'uniform': self.prior_n0 = np.ones(nstates) else: raise ValueError('initial distribution prior mode undefined: ' + str(p0_prior)) # prior count matrix if transition_matrix_prior is None or p0_prior == 'sparse': self.prior_C = np.zeros((self.nstates, self.nstates)) elif isinstance(transition_matrix_prior, np.ndarray): if np.array_equal(transition_matrix_prior.shape, (self.nstates, self.nstates)): self.prior_C = np.array(transition_matrix_prior) elif transition_matrix_prior == 'mixed': self.prior_C = np.array(self.model.transition_matrix) elif p0_prior == 'uniform': self.prior_C = np.ones((nstates, nstates)) else: raise ValueError('transition matrix prior mode undefined: ' + str(transition_matrix_prior)) # check if we work with these options if reversible: if not _tmatrix_disconnected.is_connected( self.model.transition_matrix + self.prior_C, strong=True): raise NotImplementedError( 'Trying to sample disconnected HMM with option reversible:\n ' + str(self.model.transition_matrix) + '\nUse prior to connect, select connected subset, or use reversible=False.' ) # sampling options self.transition_matrix_sampling_steps = transition_matrix_sampling_steps # implementation options hidden.set_implementation(config.kernel) self.model.output_model.set_implementation(config.kernel) # pre-construct hidden variables self.alpha = np.zeros((self.maxT, self.nstates), config.dtype, order='C') self.pobs = np.zeros((self.maxT, self.nstates), config.dtype, order='C') return
def __init__( self, observations, nstates, initial_model=None, reversible=True, stationary=False, transition_matrix_sampling_steps=1000, p0_prior="mixed", transition_matrix_prior="mixed", output="gaussian", ): """Initialize a Bayesian hidden Markov model sampler. Parameters ---------- observations : list of numpy arrays representing temporal data `observations[i]` is a 1d numpy array corresponding to the observed trajectory index `i` nstates : int The number of states in the model. initial_model : HMM, optional, default=None If specified, the given initial model will be used to initialize the BHMM. Otherwise, a heuristic scheme is used to generate an initial guess. reversible : bool, optional, default=True If True, a prior that enforces reversible transition matrices (detailed balance) is used; otherwise, a standard non-reversible prior is used. stationary : bool, optional, default=False If True, the stationary distribution of the transition matrix will be used as initial distribution. Only use True if you are confident that the observation trajectories are started from a global equilibrium. If False, the initial distribution will be estimated as usual from the first step of the hidden trajectories. transition_matrix_sampling_steps : int, optional, default=1000 number of transition matrix sampling steps per BHMM cycle p0_prior : None, str, float or ndarray(n) Prior for the initial distribution of the HMM. Will only be active if stationary=False (stationary=True means that p0 is identical to the stationary distribution of the transition matrix). Currently implements different versions of the Dirichlet prior that is conjugate to the Dirichlet distribution of p0. p0 is sampled from: .. math: p0 \sim \prod_i (p0)_i^{a_i + n_i - 1} where :math:`n_i` are the number of times a hidden trajectory was in state :math:`i` at time step 0 and :math:`a_i` is the prior count. Following options are available: | 'mixed' (default), :math:`a_i = p_{0,init}`, where :math:`p_{0,init}` is the initial distribution of initial_model. | 'uniform', :math:`a_i = 1` | ndarray(n) or float, the given array will be used as A. | None, :math:`a_i = 0`. This option ensures coincidence between sample mean an MLE. Will sooner or later lead to sampling problems, because as soon as zero trajectories are drawn from a given state, the sampler cannot recover and that state will never serve as a starting state subsequently. Only recommended in the large data regime and when the probability to sample zero trajectories from any state is negligible. transition_matrix_prior : str or ndarray(n, n) Prior for the HMM transition matrix. Currently implements Dirichlet priors if reversible=False and reversible transition matrix priors as described in [1]_ if reversible=True. For the nonreversible case the posterior of transition matrix :math:`P` is: .. math: P \sim \prod_{i,j} p_{ij}^{b_{ij} + c_{ij} - 1} where :math:`c_{ij}` are the number of transitions found for hidden trajectories and :math:`b_{ij}` are prior counts. | 'mixed' (default), :math:`b_{ij} = p_{ij,init}`, where :math:`p_{ij,init}` is the transition matrix of initial_model. That means one prior count will be used per row. | 'uniform', :math:`b_{ij} = 1` | ndarray(n, n) or broadcastable, the given array will be used as B. | None, :math:`b_ij = 0`. This option ensures coincidence between sample mean an MLE. Will sooner or later lead to sampling problems, because as soon as a transition :math:`ij` will not occur in a sample, the sampler cannot recover and that transition will never be sampled again. This option is not recommended unless you have a small HMM and a lot of data. output_model_type : str, optional, default='gaussian' Output model type. ['gaussian', 'discrete'] References ---------- .. [1] Trendelkamp-Schroer, B., H. Wu, F. Paul and F. Noe: Estimation and uncertainty of reversible Markov models. J. Chem. Phys. 143, 174101 (2015). """ # Sanity checks. if len(observations) == 0: raise Exception("No observations were provided.") # Store options. self.reversible = reversible self.stationary = stationary # Store the number of states. self.nstates = nstates # Store a copy of the observations. self.observations = copy.deepcopy(observations) self.nobs = len(observations) self.Ts = [len(o) for o in observations] self.maxT = np.max(self.Ts) # initial model if initial_model: # Use user-specified initial model, if provided. self.model = copy.deepcopy(initial_model) else: # Generate our own initial model. self.model = self._generateInitialModel(output) # prior initial vector if p0_prior is None or p0_prior == "sparse": self.prior_n0 = np.zeros(self.nstates) elif isinstance(p0_prior, np.ndarray): if np.array_equal(p0_prior.shape, self.nstates): self.prior_n0 = np.array(p0_prior) elif p0_prior == "mixed": self.prior_n0 = np.array(self.model.initial_distribution) elif p0_prior == "uniform": self.prior_n0 = np.ones(nstates) else: raise ValueError("initial distribution prior mode undefined: " + str(p0_prior)) # prior count matrix if transition_matrix_prior is None or p0_prior == "sparse": self.prior_C = np.zeros((self.nstates, self.nstates)) elif isinstance(transition_matrix_prior, np.ndarray): if np.array_equal(transition_matrix_prior.shape, (self.nstates, self.nstates)): self.prior_C = np.array(transition_matrix_prior) elif transition_matrix_prior == "mixed": self.prior_C = np.array(self.model.transition_matrix) elif p0_prior == "uniform": self.prior_C = np.ones((nstates, nstates)) else: raise ValueError("transition matrix prior mode undefined: " + str(transition_matrix_prior)) # check if we work with these options if reversible: if not _tmatrix_disconnected.is_connected(self.model.transition_matrix + self.prior_C, strong=True): raise NotImplementedError( "Trying to sample disconnected HMM with option reversible:\n " + str(self.model.transition_matrix) + "\nUse prior to connect, select connected subset, or use reversible=False." ) # sampling options self.transition_matrix_sampling_steps = transition_matrix_sampling_steps # implementation options hidden.set_implementation(config.kernel) self.model.output_model.set_implementation(config.kernel) # pre-construct hidden variables self.alpha = np.zeros((self.maxT, self.nstates), config.dtype, order="C") self.pobs = np.zeros((self.maxT, self.nstates), config.dtype, order="C") return
def is_weakly_connected(self): r""" Whether the HMM transition matrix is weakly connected """ return _tmatrix_disconnected.is_connected(self._Tij, strong=False)
def is_strongly_connected(self): r""" Whether the HMM transition matrix is strongly connected """ return _tmatrix_disconnected.is_connected(self._Tij, strong=True)