Source code for binf.samplers.hmc

'''
HMC sampler implementations
'''

from collections import namedtuple
from copy import deepcopy

import numpy as np

from csb.numeric import exp

HMCSampleStats = namedtuple('HMCSampleStats', 'accepted stepsize')


[docs]class HMCSampler(object): def __init__(self, pdf, state, timestep, nsteps, timestep_adaption_limit=0, adaption_uprate=1.05, adaption_downrate=0.95, variable_name=None): """ A Hamiltonian Monte Carlo implementation :param pdf: object representing the PDF this sampler is supposed to sample from :type pdf: :class:`.AbstractBinfPDF` :param state: initial state :type state: :class:`.BinfState` :param timestep: integration step size for leap frog integrator :type timestep: float :param nsteps: number of integration steps for leap frog integrator :type nsteps: int :param timestep_adaption_limit: # of samples after which to stop automatically adapting the timestep :type timestep_adaption_limit: int :param adaption_uprate: factor with which to multiply current time step in case of rejected move :type adaption_uprate: float :param adaption_downrate: factor with which to multiply current time step in case of accepted move :type adaption_downrate: float :param variable_name: name of the variable this sampler is supposed to draw random samples from :type variable_name: str """ self.pdf = pdf self.state = state self.timestep = timestep self.nsteps = nsteps self.timestep_adaption_limit = timestep_adaption_limit self.adaption_uprate = adaption_uprate self.adaption_downrate = adaption_downrate self._variable_name = variable_name self._last_move_accepted = 0 self.n_accepted = 0 self.counter = 0 @property def acceptance_rate(self): if self.counter > 0: return self.n_accepted / float(self.counter) else: return 0.0 @property def variable_name(self): """ Returns the name of the variable this sampler is supposed to draw random samples from :returns: variable name :rtype: str """ return 'HMC' if self._variable_name is None else self._variable_name @property def last_move_accepted(self): """ Returns whether the last move has been accepted or not :returns: whether the last move has been accepted or not :rtype: bool """ return self._last_move_accepted
[docs] def _leapfrog(self, q, p, timestep, nsteps): """ Performs leap frog integration of Hamiltonian dynamics guided by the gradient of the negative log-probability :param q: initial 'position' :type q: numpy.ndarray :param p: initial 'momentum' :type p: numpy.ndarray :param timestep: integration time step :type timestep: float :param nsteps: # of integration steps :type nsteps: int :returns: 'position' and 'momentum' at the end of the approximated MD trajectory :rtype: (numpy.ndarray, numpy.ndarray) """ gradient = lambda x: self.pdf.gradient(**{self._variable_name: x}) p -= 0.5 * timestep * gradient(q) for i in range(nsteps-1): q += p * timestep p -= timestep * gradient(q) q += p * timestep p -= 0.5 * timestep * gradient(q) return q, p
[docs] def _copy_state(self, state): """ Copies a state :param state: variable value to copy :type state: numpy.ndarray """ return deepcopy(state)
[docs] def sample(self): """ Draws a random sample :returns: a sample :rtype: numpy.ndarray """ V = lambda x: -self.pdf.log_prob(**{self._variable_name: x}) q = self._copy_state(self.state) p = np.random.normal(size=q.shape) E_before = V(q) + 0.5 * np.sum(p ** 2) q, p = self._leapfrog(q, p, self.timestep, self.nsteps) E_after = V(q) + 0.5 * np.sum(p ** 2) acc = np.random.uniform() < exp(-(E_after - E_before)) self._last_move_accepted = acc self.counter += 1 if self.counter < self.timestep_adaption_limit: self._adapt_timestep() if acc: self._state = q self.n_accepted += 1 return self._copy_state(q) else: return self._copy_state(self.state)
@property def last_draw_stats(self): """ Returns information about the most recently performed move This is usually used by a replica exchange scheme to log sampling statistics. :returns: whether the last move has been accepted and the current time step in the shape of a named tuple in a dictionary. This contrived is needed for Gibbs sampling / replica exchange statistics. :rtype: dict """ return {self.variable_name: HMCSampleStats(self.last_move_accepted, self.timestep)}
[docs] def _adapt_timestep(self): """ Increases / decreasese the leap frog time step depending on whether the last move has been rejected / accepted. """ if self.last_move_accepted: self.timestep *= self.adaption_uprate else: self.timestep *= self.adaption_downrate