Skip to content

Commit ec61925

Browse files
authored
Merge pull request #970 from pints-team/adaptive-rao-blackwell
Rao-Blackwell adaptive MCMC.
2 parents 82fbce4 + e374ed5 commit ec61925

File tree

7 files changed

+421
-0
lines changed

7 files changed

+421
-0
lines changed

docs/source/mcmc_samplers/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface, that can be used to sample from an unknown
2525
metropolis_mcmc
2626
monomial_gamma_hamiltonian_mcmc
2727
population_mcmc
28+
rao_blackwell_ac_mcmc
2829
relativistic_mcmc
2930
slice_doubling_mcmc
3031
slice_stepout_mcmc
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
******************
2+
Rao-Blackwell ACMC
3+
******************
4+
5+
.. currentmodule:: pints
6+
7+
.. autoclass:: RaoBlackwellACMC

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ relevant code.
4444
- [Haario-Bardenet Adaptive Covariance MCMC](./sampling-adaptive-covariance-haario-bardenet.ipynb)
4545
- [Metropolis Random Walk MCMC](./sampling-metropolis-mcmc.ipynb)
4646
- [Population MCMC](./sampling-population-mcmc.ipynb)
47+
- [Rao-Blackwell Adaptive Covariance MCMC](./sampling-adaptive-covariance-rao-blackwell.ipynb)
4748
- [Slice Sampling: Stepout MCMC](./sampling-slice-stepout-mcmc.ipynb)
4849
- [Slice Sampling: Doubling MCMC](./sampling-slice-doubling-mcmc.ipynb)
4950
- [Slice Sampling: Overrelaxation MCMC](./sampling-slice-overrelaxation-mcmc.ipynb)

examples/sampling-adaptive-covariance-rao-blackwell.ipynb

Lines changed: 172 additions & 0 deletions
Large diffs are not rendered by default.

pints/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def version(formatted=False):
208208
from ._mcmc._metropolis import MetropolisRandomWalkMCMC
209209
from ._mcmc._monomial_gamma_hamiltonian import MonomialGammaHamiltonianMCMC
210210
from ._mcmc._population import PopulationMCMC
211+
from ._mcmc._rao_blackwell_ac import RaoBlackwellACMC
211212
from ._mcmc._relativistic import RelativisticMCMC
212213
from ._mcmc._slice_stepout import SliceStepoutMCMC
213214
from ._mcmc._slice_doubling import SliceDoublingMCMC

pints/_mcmc/_rao_blackwell_ac.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#
2+
# Rao-Blackwell Adaptive MCMC method
3+
#
4+
# This file is part of PINTS.
5+
# Copyright (c) 2017-2019, University of Oxford.
6+
# For licensing information, see the LICENSE file distributed with the PINTS
7+
# software package.
8+
#
9+
from __future__ import absolute_import, division
10+
from __future__ import print_function, unicode_literals
11+
import pints
12+
import numpy as np
13+
14+
15+
class RaoBlackwellACMC(pints.GlobalAdaptiveCovarianceMC):
16+
"""
17+
Rao-Blackwell adaptive MCMC, as described by Algorithm 3 in [1]_.
18+
After initialising mu0 and sigma0, in each iteration after initial
19+
phase (t), the following steps occur::
20+
21+
theta* ~ N(theta_t, lambda * sigma0)
22+
alpha(theta_t, theta*) = min(1, p(theta*|data) / p(theta_t|data))
23+
u ~ uniform(0, 1)
24+
if alpha(theta_t, theta*) > u:
25+
theta_t+1 = theta*
26+
else:
27+
theta_t+1 = theta_t
28+
mu_t+1 = mu_t + gamma_t+1 * (theta_t+1 - mu_t)
29+
sigma_t+1 = sigma_t + gamma_t+1 *
30+
(bar((theta_t+1 - mu_t)(theta_t+1 - mu_t)') - sigma_t)
31+
32+
where::
33+
34+
bar(theta_t+1) = alpha(theta_t, theta*) theta* +
35+
(1 - alpha(theta_t, theta*)) theta_t
36+
37+
Note that we deviate from the paper in two places::
38+
39+
gamma_t = t^-eta
40+
Y_t+1 ~ N(theta_t, lambda * sigma0) rather than
41+
Y_t+1 ~ N(theta_t, sigma0)
42+
43+
Extends :class:`GlobalAdaptiveCovarianceMC`.
44+
45+
References
46+
----------
47+
.. [1] A tutorial on adaptive MCMC
48+
Christophe Andrieu and Johannes Thoms, Statistical Computing, 2008,
49+
18: 343-373.
50+
https://doi.org/10.1007/s11222-008-9110-y
51+
"""
52+
def __init__(self, x0, sigma0=None):
53+
super(RaoBlackwellACMC, self).__init__(x0, sigma0)
54+
55+
# heuristic based on normal approximation
56+
self._lambda = (2.38**2) / self._n_parameters
57+
58+
self._X = None
59+
self._Y = None
60+
61+
def ask(self):
62+
""" See :meth:`SingleChainMCMC.ask()`. """
63+
super(RaoBlackwellACMC, self).ask()
64+
65+
# Propose new point
66+
if self._proposed is None:
67+
self._proposed = np.random.multivariate_normal(
68+
self._current, self._lambda * self._sigma)
69+
70+
# Set as read-only
71+
self._proposed.setflags(write=False)
72+
73+
# Return proposed point
74+
return self._proposed
75+
76+
def n_hyper_parameters(self):
77+
""" See :meth:`TunableMethod.n_hyper_parameters()`. """
78+
return 1
79+
80+
def name(self):
81+
""" See :meth:`pints.MCMCSampler.name()`. """
82+
return 'Rao-Blackwell adaptive covariance MCMC'
83+
84+
def tell(self, fx):
85+
""" See :meth:`pints.AdaptiveCovarianceMCMC.tell()`. """
86+
self._Y = np.copy(self._proposed)
87+
self._X = np.copy(self._current)
88+
89+
super(RaoBlackwellACMC, self).tell(fx)
90+
91+
return self._current
92+
93+
def _update_sigma(self):
94+
"""
95+
Updates sigma using Rao-Blackwellised formula::
96+
97+
sigma_t+1 = sigma_t + gamma_t+1 *
98+
(bar((theta_t+1 - mu_t)(theta_t+1 - mu_t)') - sigma_t)
99+
100+
where::
101+
102+
bar(X_t+1) = alpha(X_t, Y_t+1) * Y_t+1 +
103+
(1 - alpha(X_t, Y_t+1)) * X_t
104+
"""
105+
acceptance_prob = (
106+
np.minimum(1, np.exp(self._log_acceptance_ratio)))
107+
X_bar = acceptance_prob * self._Y + (1.0 - acceptance_prob) * self._X
108+
dsigm = np.reshape(X_bar - self._mu, (self._n_parameters, 1))
109+
self._sigma = ((1 - self._gamma) * self._sigma +
110+
self._gamma * np.dot(dsigm, dsigm.T))
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python
2+
#
3+
# Tests the basic methods of the Rao-Blackwel adaptive covariance MCMC routine.
4+
#
5+
# This file is part of PINTS.
6+
# Copyright (c) 2017-2019, University of Oxford.
7+
# For licensing information, see the LICENSE file distributed with the PINTS
8+
# software package.
9+
#
10+
import pints
11+
import pints.toy as toy
12+
import unittest
13+
import numpy as np
14+
15+
from shared import StreamCapture
16+
17+
# Consistent unit testing in Python 2 and 3
18+
try:
19+
unittest.TestCase.assertRaisesRegex
20+
except AttributeError:
21+
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
22+
23+
24+
class TestRaoBlackwellACMC(unittest.TestCase):
25+
"""
26+
Tests the basic methods of the Rao-Blackwell AC routine.
27+
"""
28+
29+
@classmethod
30+
def setUpClass(cls):
31+
""" Set up problem for tests. """
32+
33+
# Create toy model
34+
cls.model = toy.LogisticModel()
35+
cls.real_parameters = [0.015, 500]
36+
cls.times = np.linspace(0, 1000, 1000)
37+
cls.values = cls.model.simulate(cls.real_parameters, cls.times)
38+
39+
# Add noise
40+
cls.noise = 10
41+
cls.values += np.random.normal(0, cls.noise, cls.values.shape)
42+
cls.real_parameters.append(cls.noise)
43+
cls.real_parameters = np.array(cls.real_parameters)
44+
45+
# Create an object with links to the model and time series
46+
cls.problem = pints.SingleOutputProblem(
47+
cls.model, cls.times, cls.values)
48+
49+
# Create a uniform prior over both the parameters and the new noise
50+
# variable
51+
cls.log_prior = pints.UniformLogPrior(
52+
[0.01, 400, cls.noise * 0.1],
53+
[0.02, 600, cls.noise * 100]
54+
)
55+
56+
# Create a log likelihood
57+
cls.log_likelihood = pints.GaussianLogLikelihood(cls.problem)
58+
59+
# Create an un-normalised log-posterior (log-likelihood + log-prior)
60+
cls.log_posterior = pints.LogPosterior(
61+
cls.log_likelihood, cls.log_prior)
62+
63+
def test_method(self):
64+
65+
# Create mcmc
66+
x0 = self.real_parameters * 1.1
67+
mcmc = pints.RaoBlackwellACMC(x0)
68+
69+
# Configure
70+
mcmc.set_target_acceptance_rate(0.3)
71+
mcmc.set_initial_phase(True)
72+
73+
# Perform short run
74+
rate = []
75+
chain = []
76+
for i in range(100):
77+
x = mcmc.ask()
78+
fx = self.log_posterior(x)
79+
sample = mcmc.tell(fx)
80+
if i == 20:
81+
mcmc.set_initial_phase(False)
82+
if i >= 50:
83+
chain.append(sample)
84+
rate.append(mcmc.acceptance_rate())
85+
if np.all(sample == x):
86+
self.assertEqual(mcmc.current_log_pdf(), fx)
87+
88+
chain = np.array(chain)
89+
rate = np.array(rate)
90+
self.assertEqual(chain.shape[0], 50)
91+
self.assertEqual(chain.shape[1], len(x0))
92+
self.assertEqual(rate.shape[0], 100)
93+
94+
def test_options(self):
95+
96+
# Test setting acceptance rate
97+
x0 = self.real_parameters
98+
mcmc = pints.RaoBlackwellACMC(x0)
99+
self.assertNotEqual(mcmc.target_acceptance_rate(), 0.5)
100+
mcmc.set_target_acceptance_rate(0.5)
101+
self.assertEqual(mcmc.target_acceptance_rate(), 0.5)
102+
mcmc.set_target_acceptance_rate(1)
103+
self.assertRaises(ValueError, mcmc.set_target_acceptance_rate, 0)
104+
self.assertRaises(ValueError, mcmc.set_target_acceptance_rate, -1e-6)
105+
self.assertRaises(ValueError, mcmc.set_target_acceptance_rate, 1.00001)
106+
107+
# test hyperparameter setters and getters
108+
self.assertEqual(mcmc.n_hyper_parameters(), 1)
109+
self.assertRaises(ValueError, mcmc.set_hyper_parameters, [-0.1])
110+
mcmc.set_hyper_parameters([0.3])
111+
self.assertEqual(mcmc.eta(), 0.3)
112+
113+
self.assertEqual(mcmc.name(), 'Rao-Blackwell adaptive covariance MCMC')
114+
115+
def test_logging(self):
116+
117+
# Test logging includes name.
118+
x = [self.real_parameters] * 3
119+
mcmc = pints.MCMCController(
120+
self.log_posterior, 3, x, method=pints.RaoBlackwellACMC)
121+
mcmc.set_max_iterations(5)
122+
with StreamCapture() as c:
123+
mcmc.run()
124+
text = c.text()
125+
self.assertIn('Rao-Blackwell adaptive covariance MCMC', text)
126+
127+
128+
if __name__ == '__main__':
129+
unittest.main()

0 commit comments

Comments
 (0)