-
Notifications
You must be signed in to change notification settings - Fork 34
Integrate Slice Sampling: Covariance Matching MCMC #896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lorcandelaney
wants to merge
14
commits into
main
Choose a base branch
from
slice-sampling-covariance-matching
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
5fd7d68
Add Slice Sampling Covariance Matching method.
lorcandelaney 675f8fe
Add small fixes to covariance-matching tests.
lorcandelaney 34da85e
Add Notebook.
lorcandelaney 26d1e0f
started to update docstrings
ben18785 484fdc3
Merge branch 'master' into slice-sampling-covariance-matching
ben18785 3b267b1
addec cached variables and corrected docstrings
ben18785 03288e2
Merge branch 'master' into slice-sampling-covariance-matching
MichaelClerx ceca654
updated docstrings
ben18785 12fbf11
further docstrings work
ben18785 1f87900
updated docstrings
ben18785 0d618d7
Merge branch 'master' into slice-sampling-covariance-matching
ben18785 76f3d96
simplified notebook
ben18785 c7344e6
No idea why this isn't working but something is blowing up the proposal
ben18785 dbc221e
Merge branch 'master' into slice-sampling-covariance-matching
MichaelClerx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
************************************************** | ||
Slice Sampling - Covariance Matching MCMC | ||
************************************************** | ||
|
||
.. currentmodule:: pints | ||
|
||
.. autoclass:: SliceCovarianceMatchingMCMC |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,354 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Slice Sampling - Covariance Adaptive: Covariance Matching MCMC Method | ||
# | ||
# This file is part of PINTS. | ||
# Copyright (c) 2017-2019, University of Oxford. | ||
# For licensing information, see the LICENSE file distributed with the PINTS | ||
# software package. | ||
# | ||
from __future__ import absolute_import, division | ||
from __future__ import print_function, unicode_literals | ||
import pints | ||
import numpy as np | ||
|
||
|
||
class SliceCovarianceMatchingMCMC(pints.SingleChainMCMC): | ||
""" | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Implements Covariance-Adaptive Slice Sampling by Covariance Matching, | ||
as described in [1]. This is an adaptive multivariate method which | ||
uses additional points, called "crumbs", and rejected proposals to | ||
guide the selection of samples. | ||
It generates samples by sampling uniformly from the volume underneath the | ||
posterior (``f``). It does so by introducing an auxiliary variable (``y``) | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
and by definying a Markov chain. | ||
Sampling follows: | ||
1. Calculate the pdf (``f(x_0)``) of the current sample (``x_0``). | ||
2. Draw a real value (``y``) uniformly from (0, f(x0)), defining a | ||
horizontal “slice”: S = {x: y < f(x)}. Note that ``x_0`` is | ||
always within S. | ||
3. Draw the first crumb (``c_1``) from a Gaussian distribution with | ||
mean ``x_0`` and precision matrix ``W_1``. | ||
4. Draw a new point (``x_1``) from a Gaussian distribution with mean | ||
``c_1`` and precision matrix ``W_2``. | ||
New crumbs are drawn until a new proposal is accepted. In particular, | ||
after sampling ``k`` crumbs from Gaussian distributions with mean ``x0`` | ||
and precision matrices (``W_1``, ..., ``W_k``), the distribution for the | ||
``k`` th proposal sample is: | ||
1. ``x_k \sim Normal(\bar{c}_k, \Lambda^{-1}_k)`` | ||
where: | ||
2. ``\Lambda_k = W_1 + ... + W_k`` | ||
3. ``\bar{c}_k = \Lambda^{-1}_k * (W_1 * c_1 + ... + W_k * c_k)`` | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
This method attempts to find the ``k + 1`` th crumb precision matrix | ||
(``W_{k + 1}``) so that the distribution for the ``k + 1`` th proposal | ||
point has the same conditional variance as uniform sampling from the slice | ||
``S`` in the direction of the gradient of ``f(x)`` evaluated at the last | ||
rejected proposal (``x_k``). | ||
To avoid floating-point underflow, we implement the suggestion advanced | ||
in [1] pp.712. We use the log pdf of the un-normalised posterior | ||
(``log f(x)``) instead of ``f(x)``. In doing so, we use an | ||
auxiliary variable ``z = log(y) - \epsilon``, where | ||
``\epsilon \sim \text{exp}(1)`` and define the slice as | ||
S = {x : z < log f(x)}. | ||
[1] Thompson, M. and Neal, R.M., 2010. Covariance-adaptive slice sampling. | ||
arXiv preprint arXiv:1003.3201. | ||
*Extends:* :class:`SingleChainMCMC` | ||
""" | ||
|
||
def __init__(self, x0, sigma0=None): | ||
super(SliceCovarianceMatchingMCMC, self).__init__(x0, sigma0) | ||
|
||
# Set initial state | ||
self._x0 = np.asarray(x0, dtype=float) | ||
self._running = False | ||
self._ready_for_tell = False | ||
self._current = None | ||
self._proposed_pdf = None | ||
self._current_log_y = None | ||
self._proposed = None | ||
self._log_fx_u = None | ||
self._calculate_fx_u = False | ||
self._sent_proposal = False | ||
|
||
# Standard deviation of initial crumb | ||
self._sigma_c = 1 | ||
|
||
# Crumb | ||
self._c = None | ||
|
||
# Estimate of the density at the mode | ||
self._M = None | ||
|
||
# Initialise un-normalised crumb mean | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._c_bar_star = np.zeros(self._n_parameters) | ||
|
||
# Define Cholesky upper triangles: F for W_k and R_k for Lambda_k | ||
self._R = None | ||
self._F = None | ||
|
||
# Un-normalised gradient calculated at rejected proposal | ||
self._G = None | ||
|
||
# Normalised gradient calculated at rejected proposal | ||
self._g = None | ||
|
||
# Distance between rejected proposal and crumb | ||
self._delta = None | ||
|
||
# Parameter to control variance precision | ||
self._theta = 1 | ||
|
||
# Function which calculates Cholesky rank one updates | ||
def _chud(self, matrix, vector): | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
V = np.dot(matrix.transpose(), matrix) + np.outer(vector, vector) | ||
chol = np.linalg.cholesky(V).transpose() | ||
return np.array(chol, copy=True) | ||
|
||
def ask(self): | ||
""" See :meth:`SingleChainMCMC.ask()`. """ | ||
|
||
# Check ask/tell pattern | ||
if self._ready_for_tell: | ||
raise RuntimeError('Ask() called when expecting call to tell().') | ||
|
||
# Initialise on first call | ||
if not self._running: | ||
self._running = True | ||
|
||
# Very first iteration | ||
if self._current is None: | ||
|
||
# Ask for the log pdf of x0 | ||
self._ready_for_tell = True | ||
return np.array(self._x0, copy=True) | ||
|
||
# If the proposal has been rejected, we need to evaluate ``f(u)`` | ||
# in order to find the covariance matrix for the next proposal | ||
if self._calculate_fx_u: | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Ask for the log pdf of ``u`` | ||
self._ready_for_tell = True | ||
self._calculate_fx_u = False | ||
return np.array(self._u, copy=True) | ||
|
||
# Draw first p-variate | ||
mean = np.zeros(self._n_parameters) | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
cov = np.identity(self._n_parameters) | ||
z = np.random.multivariate_normal(mean, cov) | ||
|
||
# Draw crumb c | ||
self._c = self._current + np.dot(np.linalg.inv(self._F), z) | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Compute un-normalised proposal mean | ||
self._c_bar_star = self._c_bar_star + np.dot(np.dot(np.transpose( | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._F), self._F), self._c) | ||
|
||
# Compute normalised proposal mean | ||
c_bar = np.dot(np.dot(np.linalg.inv(self._R), np.transpose( | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
np.linalg.inv(self._R))), self._c_bar_star) | ||
|
||
# Draw second p-variate | ||
z = np.random.multivariate_normal(mean, cov) | ||
|
||
# Draw sample | ||
self._proposed = c_bar + np.dot(np.linalg.inv(self._R), z) | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Set flag indicating we have created a new proposal. This is used to | ||
# store the value of the log_pdf of the proposed point in the tell() | ||
# method | ||
self._sent_proposal = True | ||
|
||
# Set flag that will be used if the proposal is rejected | ||
self._calculate_fx_u = True | ||
|
||
# Send trial point for checks | ||
self._ready_for_tell = True | ||
return np.array(self._proposed, copy=True) | ||
|
||
def current_log_pdf(self): | ||
""" See :meth:`SingleChainMCMC.current_log_pdf()`. """ | ||
return np.copy(self._current_log_pdf) | ||
|
||
def current_slice_height(self): | ||
""" | ||
Returns current log_y used to define the current slice. | ||
""" | ||
return self._current_log_y | ||
|
||
def name(self): | ||
""" See :meth:`pints.MCMCSampler.name()`. """ | ||
return 'Slice Sampling - Covariance-Adaptive: Covariance Matching' | ||
|
||
def n_hyper_parameters(self): | ||
""" See :meth:`TunableMethod.n_hyper_parameters()`. """ | ||
return 2 | ||
|
||
def needs_sensitivities(self): | ||
""" See :meth:`pints.MCMCSampler.needs_sensitivities()`. """ | ||
return True | ||
|
||
def set_hyper_parameters(self, x): | ||
""" | ||
The hyper-parameter vector is ``[sigma_c, theta]``. | ||
See :meth:`TunableMethod.set_hyper_parameters()`. | ||
""" | ||
self.set_sigma_c(x[0]) | ||
self.set_theta(x[1]) | ||
|
||
def set_sigma_c(self, sigma_c): | ||
""" | ||
Sets standard deviation of initial crumb. | ||
""" | ||
sigma_c = float(sigma_c) | ||
if sigma_c < 0: | ||
raise ValueError('Inital crumb standard deviation' | ||
'must be positive.') | ||
self._sigma_c = sigma_c | ||
|
||
def set_theta(self, theta): | ||
""" | ||
Sets parameter to control how fast the precision of the | ||
proposal distribution increases. | ||
""" | ||
self._theta = float(theta) | ||
|
||
def sigma_c(self): | ||
""" | ||
Returns standard deviation of initial crumb. | ||
""" | ||
return self._sigma_c | ||
|
||
def tell(self, reply): | ||
""" See :meth:`pints.SingleChainMCMC.tell()`. """ | ||
|
||
# Check ask/tell pattern | ||
if not self._ready_for_tell: | ||
raise RuntimeError('Tell called before proposal was set.') | ||
self._ready_for_tell = False | ||
|
||
# Unpack reply | ||
fx, grad = reply | ||
|
||
# Check reply, copy gradient | ||
fx = float(fx) | ||
grad = pints.vector(grad) | ||
|
||
# If this is the log_pdf of a new point, save the value and use it | ||
# to check ``f(x_1) >= y`` | ||
if self._sent_proposal: | ||
self._proposed_pdf = fx | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
self._sent_proposal = False | ||
|
||
# Very first call | ||
if self._current is None: | ||
# Check first point is somewhere sensible | ||
if not np.isfinite(fx): | ||
raise ValueError( | ||
'Initial point for MCMC must have finite logpdf.') | ||
|
||
# Set current sample, log pdf of current sample and initialise | ||
# proposed sample for next iteration | ||
self._current = np.array(self._x0, copy=True) | ||
self._proposed = np.array(self._current, copy=True) | ||
self._current_log_pdf = fx | ||
self._M = fx | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Define Cholesky upper triangles: F_k for W_k and R_k for Lambda_k | ||
self._R = self._sigma_c ** (-1) * np.identity(self._n_parameters) | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
self._F = self._sigma_c ** (-1) * np.identity(self._n_parameters) | ||
|
||
# Sample height of the slice log_y for next iteration | ||
e = np.random.exponential(1) | ||
self._current_log_y = self._M - e | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Return first point in chain, which is x0 | ||
return np.array(self._current, copy=True) | ||
|
||
# Acceptance check | ||
if self._proposed_pdf >= self._current_log_y: | ||
# The accepted sample becomes the new current sample | ||
self._current = np.array(self._proposed, copy=True) | ||
|
||
# Update estimate of mode density | ||
self._M = fx | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Sample new log_y used to define the next slice | ||
e = np.random.exponential(1) | ||
self._current_log_y = self._M - e | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Reset parameters | ||
self._c_bar_star = np.zeros(self._n_parameters) | ||
self._F = self._sigma_c ** (-1) * np.identity(self._n_parameters) | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
self._R = self._sigma_c ** (-1) * np.identity(self._n_parameters) | ||
self._calculate_fx_u = False | ||
|
||
# Return accepted sample | ||
return np.array(self._proposed, copy=True) | ||
|
||
# If proposal is rejected, we a define new covariance matrix for | ||
# next proposal distribution | ||
else: | ||
if self._calculate_fx_u: | ||
# Gradient evaluated at rejected proposal | ||
self._G = grad | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Calculate normalised gradient | ||
self._g = self._G / np.linalg.norm(self._G) | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Calculate distance between proposal and crumb | ||
self._delta = np.linalg.norm(self._proposed - self._c) | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Generate new point ``u`` | ||
self._u = self._proposed + self._delta * self._g | ||
ben18785 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return None | ||
|
||
else: | ||
self._log_fx_u = fx | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
# Calculate ``\kappa`` | ||
kappa = (-2.) * self._delta ** (-2.) * ( | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
self._log_fx_u - self._proposed_pdf - self._delta * | ||
np.linalg.norm(self._G)) | ||
|
||
# Peak of parabolic cut through ``x1`` and ``u`` | ||
lxu = (0.5 * (np.linalg.norm(self._G) ** 2 / kappa) + | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
self._proposed_pdf) | ||
|
||
# Update ``M`` | ||
self._M = max(self._M, lxu) | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
# Calculate conditional variance of new distribution | ||
sigma_squared = 2 / 3 * (self._M - self._current_log_y) / kappa | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
alpha = max( | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
0, sigma_squared ** (-1) - (( | ||
1 + self._theta) * np.dot( | ||
np.transpose(self._g), np.dot( | ||
np.transpose(self._R), np.dot( | ||
self._R, self._g))))) | ||
|
||
# Update F and R | ||
self._F = self._chud( | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
np.sqrt(self._theta) * self._R, np.sqrt(alpha) * self._g) | ||
self._R = self._chud( | ||
ben18785 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
np.sqrt(1 + self._theta) * self._R, np.sqrt(alpha) * | ||
self._g) | ||
|
||
return None | ||
|
||
def theta(self): | ||
""" | ||
Returns the parameter used to control how fast the precision of | ||
the proposal distribution increases. | ||
""" | ||
return self._theta |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.