Source code for pints._mcmc._adaptive_covariance
#
# Base class for adaptive covariance MCMC methods
#
# This file is part of PINTS (https://github.com/pints-team/pints/) which is
# released under the BSD 3-clause license. See accompanying LICENSE.md for
# copyright notice and full license details.
#
import pints
import numpy as np
[docs]class AdaptiveCovarianceMC(pints.SingleChainMCMC):
"""
Base class for single chain MCMC methods that globally adapt a proposal
covariance matrix when running, in order to control the acceptance rate.
Each subclass should provide a method :meth:`_generate_proposal()` that
will be called by :meth:`ask()`.
Adaptation is implemented with three methods, which are called in
sequence, at the end of every ``tell()``: :meth:`_adapt_mu()`,
:meth:`_adapt_sigma()`, and :meth:`_adapt_internal()`.
A basic implementation is provided for each, which extending methods can
choose to override.
Extends :class:`SingleChainMCMC`.
"""
def __init__(self, x0, sigma0=None):
super(AdaptiveCovarianceMC, self).__init__(x0, sigma0)
# Current running status, used to initialise on first run and check
# that certain methods are only called before or during run.
self._running = False
# Adaptive mode: disabled during initial phase
self._adaptive = False
# Current point and its log PDF
self._current = None
self._current_log_pdf = None
# Proposed point
self._proposed = None
# Acceptance rate monitoring
self._iterations = 0
self._adaptations = 1
# Target acceptance rate
self._target_acceptance = None
self.set_target_acceptance_rate()
# Measured acceptance rate
self._acceptance_count = 0
self._acceptance_rate = 0
# Parameters used in setting the proposal distributions
# See update_mu() and update_sigma()
self._mu = np.array(self._x0, copy=True)
self._sigma = np.array(self._sigma0, copy=True)
# Determines decay rate in adaptation
self._eta = 0.6
# Initial decay rate in adaptation
self._gamma = 1
[docs] def acceptance_rate(self):
"""
Returns the current (measured) acceptance rate.
"""
return self._acceptance_rate
def _adapt_internal(self, accepted, log_ratio):
"""
Called at the end of every ``tell()`` to adapt any internal parameters.
Parameters
----------
accepted : boolean
Whether or not the proposal was accepted
log_ratio : float
The log of the ratio proposed log pdf / current log pdf
"""
pass
def _adapt_mu(self):
"""
Called at the end of every ``tell()`` to adapt the current running mean
used to calculate the sample covariance matrix of proposals.
"""
self._mu = (1 - self._gamma) * self._mu + self._gamma * self._current
def _adapt_sigma(self, log_ratio):
"""
Called at the end of every ``tell()`` to adapt the covariance matrix
used to generate proposals.
Parameters
----------
log_ratio
The log of the ratio proposed log pdf / current log pdf.
"""
dsigm = np.reshape(self._current - self._mu, (self._n_parameters, 1))
self._sigma = ((1 - self._gamma) * self._sigma +
self._gamma * np.dot(dsigm, dsigm.T))
[docs] def ask(self):
""" See :meth:`SingleChainMCMC.ask()`. """
# Initialise on first call
if not self._running:
self._running = True
# Store x0 as proposal, and set as read-only, so it can be passed
# to user if it gets accepted.
self._proposed = self._x0
self._proposed.setflags(write=False)
# Propose new point
if self._proposed is None:
# Let subclass generate proposal
self._proposed = self._generate_proposal()
# Set proposed as read-only, so it can be passed to user if it gets
# accepted.
self._proposed.setflags(write=False)
# Return proposed point
return self._proposed
[docs] def eta(self):
"""
Returns ``eta`` which controls the rate of adaptation decay
``adaptations**(-eta)``, where ``eta > 0`` to ensure asymptotic
ergodicity.
"""
return self._eta
def _generate_proposal(self):
"""
Should generate and return a proposed point.
"""
raise NotImplementedError
[docs] def in_initial_phase(self):
""" See :meth:`pints.MCMCSampler.in_initial_phase()`. """
return not self._adaptive
def _log_init(self, logger):
""" See :meth:`Loggable._log_init()`. """
logger.add_float('Accept.')
def _log_write(self, logger):
""" See :meth:`Loggable._log_write()`. """
logger.log(self._acceptance_rate)
[docs] def n_hyper_parameters(self):
""" See :meth:`TunableMethod.n_hyper_parameters()`. """
return 1
[docs] def needs_initial_phase(self):
""" See :meth:`pints.MCMCSampler.needs_initial_phase()`. """
return True
[docs] def replace(self, current, current_log_pdf, proposed=None):
""" See :meth:`pints.SingleChainMCMC.replace()`. """
# At least one round of ask-and-tell must have been run
if (not self._running) or self._current_log_pdf is None:
raise RuntimeError(
'Replace can only be used when already running.')
# Check position
current = pints.vector(current)
if len(current) != self._n_parameters:
raise ValueError('Point `current` has the wrong dimensions.')
current.setflags(write=False)
# Check log pdf
current_log_pdf = float(current_log_pdf)
# Check proposal
if proposed is not None:
proposed = pints.vector(proposed)
if len(proposed) != self._n_parameters:
raise ValueError('Point `proposed` has the wrong dimensions.')
proposed.setflags(write=False)
# Store
self._current = current
self._current_log_pdf = current_log_pdf
self._proposed = proposed
[docs] def set_eta(self, eta):
"""
Updates ``eta`` which controls the rate of adaptation decay
``adaptations**(-eta)``, where ``eta > 0`` to ensure asymptotic
ergodicity.
"""
if eta <= 0:
raise ValueError('eta should be greater than zero')
self._eta = eta
[docs] def set_hyper_parameters(self, x):
"""
The hyper-parameter vector is ``[eta]``.
See :meth:`TunableMethod.set_hyper_parameters()`.
"""
self.set_eta(x[0])
[docs] def set_initial_phase(self, initial_phase):
""" See :meth:`pints.MCMCSampler.set_initial_phase()`. """
# No adaptation during initial phase
self._adaptive = not bool(initial_phase)
[docs] def set_target_acceptance_rate(self, rate=0.234):
"""
Sets the target acceptance rate.
"""
rate = float(rate)
if rate <= 0:
raise ValueError('Target acceptance rate must be greater than 0.')
elif rate > 1:
raise ValueError('Target acceptance rate cannot exceed 1.')
self._target_acceptance = rate
[docs] def target_acceptance_rate(self):
"""
Returns the target acceptance rate.
"""
return self._target_acceptance
[docs] def tell(self, fx):
""" See :meth:`pints.SingleChainMCMC.tell()`. """
# Check if we had a proposal
if self._proposed is None:
raise RuntimeError('Tell called before proposal was set.')
# Ensure fx is a float
fx = float(fx)
# Increase iteration count
self._iterations += 1
# First point?
if self._current is None:
if not np.isfinite(fx):
raise ValueError(
'Initial point for MCMC must have finite logpdf.')
# Accept
self._current = self._proposed
self._current_log_pdf = fx
# Clear proposal
self._proposed = None
# Return first point for chain
return self._current, self._current_log_pdf, True
# Calculate log of the ratio of proposed and current log pdf
# Can be used in adaptation, regardless of acceptance
log_ratio = fx - self._current_log_pdf
# Accept or reject the point
accepted = False
if np.isfinite(fx):
u = np.log(np.random.uniform(0, 1))
if u < log_ratio:
accepted = True
self._acceptance_count += 1
# Update current point
self._current = self._proposed
self._current_log_pdf = fx
# Calculate acceptance rate
self._acceptance_rate = self._acceptance_count / self._iterations
# Clear proposal
self._proposed = None
# Adapt covariance matrix
if self._adaptive:
# Set gamma based on number of adaptive iterations
self._gamma = (self._adaptations + 1) ** -self._eta
# Update the number of adaptations
self._adaptations += 1
# Update the proposal distribution
self._adapt_mu()
self._adapt_sigma(log_ratio)
# Adapt
self._adapt_internal(accepted, log_ratio)
# Return current sample
return self._current, self._current_log_pdf, accepted