Skip to content
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

Custom resamplers #8

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
93 changes: 16 additions & 77 deletions optbayesexpt/particlepdf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numpy as np
import warnings
from optbayesexpt.samplers import sample,Liu_West_resampler

GOT_NUMBA = False
# GOT_NUMBA = True
Expand Down Expand Up @@ -40,25 +41,6 @@ class ParticlePDF:
different ``n_particles`` sizes to assure consistent results.

Keyword Args:
a_param: (float) In resampling, determines the scale of random
diffusion relative to the distribution covariance. After
weighted sampling, some parameter values may have been
chosen multiple times. To make the new distribution smoother,
the parameters are given small 'nudges', random displacements
much smaller than the overall parameter distribution, but with
the same shape as the overall distribution. More precisely,
the covariance of the nudge distribution is :code:`(1 -
a_param ** 2)` times the covariance of the parameter distribution.
Default ``0.98``.

scale (:obj:`bool`): determines whether resampling includes a
contraction of the parameter distribution toward the
distribution mean. The idea of this contraction is to
compensate for the overall expansion of the distribution
that is a by-product of random displacements. If true,
parameter samples (particles) move a fraction ``a_param`` of
the distance to the distribution mean. Default is ``True``,
but ``False`` is recommended.

resample_threshold (:obj:`float`): Sets a threshold for automatic
resampling. Resampling is triggered when the effective fraction of
Expand All @@ -76,27 +58,21 @@ class ParticlePDF:
**Attributes:**
"""

def __init__(self, prior, a_param=0.98, resample_threshold=0.5,
auto_resample=True, scale=True, use_jit=True):
def __init__(self, prior, resampler=Liu_West_resampler, resample_threshold=0.5,
auto_resample=True, use_jit=True, **kwargs):

#: dict: A package of parameters affecting the resampling algorithm
#:
#: - ``'a_param'`` (:obj:`float`): Initially, the value of the
#: ``a_param`` keyword argument. Default ``0.98``
#:
#: - ``'scale'`` (:obj:`bool`): Initially, the value of the
#: ``scale`` keyword argument. Default ``True``
#:
#: - ``'resample_threshold'`` (:obj:`float`): Initially,
#: the value of the ``resample_threshold`` keyword argument.
#: Default ``0.5``.
#:
#: - ``'auto_resample'`` (:obj:`bool`): Initially, the value of the
#: ``auto_resample`` keyword argument. Default ``True``.
self.tuning_parameters = {'a_param': a_param,
'resample_threshold': resample_threshold,
'auto_resample': auto_resample,
'scale': scale}
self.tuning_parameters = {'resample_threshold': resample_threshold,
'auto_resample': auto_resample}
self.resampler = resampler
self.resampler_params = kwargs

#: ``n_dims x n_particles ndarray`` of ``float64``: Together with
#: ``particle_weights``,#: these ``n_particles`` points represent
Expand Down Expand Up @@ -258,7 +234,8 @@ def resample_test(self):
self.just_resampled = False

def resample(self):
"""Performs a resampling of the distribution.
"""Performs a resampling of the distribution as specified by
and self.resampler and self.resampler_params.

Resampling refreshes the random draws that represent the probability
distribution. As Bayesian updates are made, the weights of
Expand All @@ -283,31 +260,12 @@ def resample(self):
*supposed* to change the distribution, just refresh its
representation.
"""
coords = self.randdraw(self.n_particles)
# coords is n_dims x n_particles
origin = np.zeros(self.n_dims)

covar = self.covariance()
old_center = self.mean().reshape((self.n_dims, 1))
# a_param is typically close to but less than 1
a_param = self.tuning_parameters['a_param']
# newcover is a small version of covar that determines the size of
# the nudge.
newcovar = (1 - a_param ** 2) * covar

# multivariate normal returns n_particles x n_dims array. ".T"
# transposes to match coords shape.
nudged = coords + self.rng.multivariate_normal(origin, newcovar,
self.n_particles).T

if self.tuning_parameters['scale']:
scaled = nudged * a_param + old_center * (1 - a_param)
self.particles = scaled
else:
self.particles = nudged

self.particle_weights = np.full_like(self.particle_weights,
1.0 / self.n_particles)
# Call the resampler function to get a new set of particles
# and overwrite the current particles in-place
self.particles = self.resampler(self.particles, self.particle_weights, **self.resampler_params)
# Re-fill the current particle weights with 1/n_particles
self.particle_weights.fill( 1.0 / self.n_particles)

def randdraw(self, n_draws=1):
"""Provides random parameter draws from the distribution
Expand All @@ -322,27 +280,8 @@ def randdraw(self, n_draws=1):
Returns:
An ``n_dims`` x ``N_DRAWS`` :obj:`ndarray` of parameter draws.
"""
# draws = self.rng.choice(self.particles, size=n_draws,
# p=self.particle_weights, axis=1)
draws = np.zeros((self.n_dims, n_draws))

try:
indices = self.rng.choice(self._particle_indices, size=n_draws,
p=self.particle_weights)
for i, param in enumerate(self.particles):
# for j, selected_index in enumerate(indices):
# draws[i,j] = param[selected_index]
draws[i] = param[indices]
except ValueError:
print('weights: ', self.particle_weights)
print('weight sum = ', np.sum(self.particle_weights))

for i, param in enumerate(self.particles):
# for j, selected_index in enumerate(indices):
# draws[i,j] = param[selected_index]
draws[i] = param[indices]

return draws

return sample(self.particles,self.particle_weights,n=n_draws)

@staticmethod
def _normalized_product(weight_array, likelihood_array):
Expand Down
76 changes: 76 additions & 0 deletions optbayesexpt/samplers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import numpy as np

def sample(particles, weights, n=1):
"""Provides random samples from a particle distribution.

Particles are selected randomly with probabilities given by
``weights``.

Args:
particles (`ndarray`): The location of particles
weights (`ndarray`): The probability weights
n_draws (:obj:`int`): the number of samples requested. Default
``1``.

Returns:
An ``n_dims`` x ``N_DRAWS`` :obj:`ndarray` of parameter draws.
"""
num_particles = particles.shape[1]
rng = np.random.default_rng()
I = rng.choice(num_particles,size=n,p=weights)
return particles[:,I]


def Liu_West_resampler(particles, weights, a=0.98, scale=True):
"""Resamples a particle distribution according to the Liu-West algorithm.

Particles (``particles``) are selected randomly with probabilities given by
``weights``.

Args:
particles (`ndarray`): The location of particles

weights (`ndarray`): The probability weights

a_param (`float`): In resampling, determines the scale of random
diffusion relative to the distribution covariance. After
weighted sampling, some parameter values may have been
chosen multiple times. To make the new distribution smoother,
the parameters are given small 'nudges', random displacements
much smaller than the overall parameter distribution, but with
the same shape as the overall distribution. More precisely,
the covariance of the nudge distribution is :code:`(1 -
a_param ** 2)` times the covariance of the parameter distribution.
Default ``0.98``.

scale (:obj:`bool`): determines whether resampling includes a
contraction of the parameter distribution toward the
distribution mean. The idea of this contraction is to
compensate for the overall expansion of the distribution
that is a by-product of random displacements. If true,
parameter samples (particles) move a fraction ``a_param`` of
the distance to the distribution mean. Default is ``True``,
but ``False`` is recommended.

Returns:
new_particles (`ndarray`): The new set of particles
"""
rng = np.random.default_rng()
ndim, num_particles = particles.shape
origin = np.zeros(ndim)
# coords is n_dims x n_particles
coords = sample(particles, weights, n=num_particles)
old_center = np.average(particles, axis=1, weights=weights)
# newcovar is a small version of covar that determines the size of
# the nudge.
newcovar = (1-a**2)*np.cov(particles, aweights=weights, ddof=0)
# multivariate normal returns n_particles x n_dims array. ".T"
# transposes to match coords shape.
nudged = coords + rng.multivariate_normal(origin, newcovar,
num_particles).T

if scale:
nudged = nudged * a
nudged = nudged + old_center * (1 - a)

return nudged