├── .gitignore ├── setup.cfg ├── pymcmc ├── __init__.py ├── _symmetric_proposal.py ├── _tunable_proposal_concept.py ├── _utils.py ├── _assign_priors_to_gpy_model.py ├── _simple_proposal.py ├── _mala_proposal.py ├── _random_walk_proposal.py ├── _grad_proposal.py ├── _proposal.py ├── _gpy_model.py ├── _model.py ├── _priors.py ├── _database.py ├── _mean_function.py ├── _metropolis_hastings.py └── _single_parameter_tunable_proposal_concept.py ├── unittests ├── test_GPyModel.py ├── test_mcmc.py └── table_tests.py ├── setup.py ├── demos ├── demo1.py ├── demo4.py ├── demo2.py └── demo3.py ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | build 4 | *.h5 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /pymcmc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A generic Python module for MCMC. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | from _priors import * 10 | from _model import * 11 | from _mean_function import * 12 | from _assign_priors_to_gpy_model import * 13 | from _gpy_model import * 14 | from _proposal import * 15 | from _simple_proposal import * 16 | from _symmetric_proposal import * 17 | from _tunable_proposal_concept import * 18 | from _single_parameter_tunable_proposal_concept import * 19 | from _random_walk_proposal import * 20 | from _grad_proposal import * 21 | from _mala_proposal import * 22 | from _utils import * 23 | from _database import * 24 | from _metropolis_hastings import * 25 | -------------------------------------------------------------------------------- /pymcmc/_symmetric_proposal.py: -------------------------------------------------------------------------------- 1 | """ 2 | The base class for symmetric MCMC proposals. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['SymmetricProposal'] 10 | 11 | 12 | from . import SimpleProposal 13 | 14 | 15 | class SymmetricProposal(SimpleProposal): 16 | 17 | """ 18 | The base class for all symmetric proposals. 19 | 20 | """ 21 | 22 | def __init__(self, **kwargs): 23 | """ 24 | Initialize the object. 25 | """ 26 | super(SymmetricProposal, self).__init__(**kwargs) 27 | 28 | def __call__(self, new_params, old_params): 29 | """ 30 | Since the proposal is symmetric, it does not really matter what this 31 | probability is as soon as it is symetric with respect to ``new_params`` 32 | and ``old_params``. 33 | """ 34 | return 0. 35 | -------------------------------------------------------------------------------- /unittests/test_GPyModel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the GPyModel class. 3 | """ 4 | 5 | import sys 6 | import os 7 | sys.path.insert(0, os.path.abspath(os.path.split(__file__)[0])) 8 | import GPy 9 | import pymcmc 10 | 11 | 12 | if __name__ == '__main__': 13 | model = GPy.examples.regression.olympic_marathon_men(optimize=True, plot=False) 14 | mcmc_model = pymcmc.GPyModel(model, compute_grad=True) 15 | print str(mcmc_model) 16 | print str(model) 17 | quit() 18 | print mcmc_model.log_likelihood 19 | print mcmc_model.log_prior 20 | print mcmc_model.num_params 21 | print mcmc_model.params 22 | mcmc_model.params = mcmc_model.params 23 | print mcmc_model.param_names 24 | print mcmc_model.grad_log_likelihood 25 | print mcmc_model.grad_log_prior 26 | proposal = pymcmc.MALAProposal() 27 | print str(mcmc_model) 28 | new_state, log_p = proposal.propose(mcmc_model) 29 | print new_state 30 | print log_p 31 | -------------------------------------------------------------------------------- /unittests/test_mcmc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the GPyModel class. 3 | """ 4 | 5 | import sys 6 | import os 7 | sys.path.insert(0, os.path.abspath(os.path.split(__file__)[0])) 8 | import GPy 9 | import pymcmc as pm 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | 13 | 14 | if __name__ == '__main__': 15 | model = GPy.examples.regression.olympic_marathon_men(optimize=False, plot=False) 16 | noise_prior = pm.UninformativeScalePrior() 17 | variance_prior = pm.UninformativeScalePrior() 18 | length_scale_prior = pm.UninformativeScalePrior() 19 | model.set_prior('rbf_variance', variance_prior) 20 | model.set_prior('noise_variance', noise_prior) 21 | model.set_prior('rbf_lengthscale', length_scale_prior) 22 | #print str(model) 23 | #model.optimize() 24 | print str(model) 25 | mcmc_model = pm.GPyModel(model, compute_grad=True) 26 | mcmc = pm.MetropolisHastings(mcmc_model, db_filename='test_db.h5') 27 | mcmc.sample(1000) 28 | # model_state, prop_state = mcmc.db.get_states(-1, -1) 29 | # mcmc.sample(1000, num_thin=100, init_model_state=model_state, 30 | # init_proposal_state=prop_state) 31 | -------------------------------------------------------------------------------- /pymcmc/_tunable_proposal_concept.py: -------------------------------------------------------------------------------- 1 | """ 2 | A tunable proposal concept. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 3/22/2014 9 | 10 | """ 11 | 12 | 13 | __all__ = ['TunableProposalConcept'] 14 | 15 | 16 | class TunableProposalConcept(object): 17 | 18 | """ 19 | The tunable proposal concept should be inherited by any proposal that can 20 | tune itself. It is not a proper proposal, because it should be inherited 21 | in addition to a proper proposal class. 22 | """ 23 | 24 | def __init__(self, **kwargs): 25 | """ 26 | Initialize the object. 27 | """ 28 | pass 29 | 30 | def tune(self, ac, **kwargs): 31 | """ 32 | Tune the proposal. 33 | 34 | :param ac: The acceptance rate. 35 | :type ac: float 36 | :param kwargs: Any other parameters that are required to tune the 37 | proposal. 38 | """ 39 | raise NotImplementedError('Implement this.') 40 | 41 | def __getstate__(self): 42 | """ 43 | Get the state of the object. 44 | """ 45 | return {} 46 | 47 | def __setstate__(self, state): 48 | """ 49 | Set the state of the object. 50 | """ 51 | pass 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | setup.py file for SWIG example 5 | """ 6 | 7 | from setuptools import setup 8 | 9 | with open('README.md', 'r') as fd: 10 | long_description = fd.read() 11 | 12 | setup (name = 'pymcmc', 13 | version = '0.0a1', 14 | author = 'Ilias Bilionis', 15 | author_email = 'ibilion@purdue.edu', 16 | license = 'LGPL', 17 | description = 'A python module implementing some generic MCMC routines', 18 | long_description = long_description, 19 | packages = ['pymcmc'], 20 | package_dir={'pymcmc': 'pymcmc'}, 21 | py_modules = ['pymcmc.__init__'], 22 | classifiers = ['Development Status :: 3 - Alpha', 23 | 'Intended Audience :: Science/Research', 24 | 'Topic :: Scientific/Engineering :: Mathematics', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 2.6', 27 | 'Programming Language :: Python :: 2.7'], 28 | keywords = 'Markov-Chain-Monte-Carlo MCMC Metrpolis-Adjusted-Langevin-Dynamics MALA GPy', 29 | install_requires = ['GPy>=0.6.0'], 30 | url = 'https://github.com/ebilionis/pymcmc', 31 | download_url = 'https://github.com/ebilionis/pymcmc/tarball/0.0a1' 32 | ) 33 | -------------------------------------------------------------------------------- /pymcmc/_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various utility functions. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | import tables as pt 10 | import numpy as np 11 | 12 | 13 | __all__ = ['UnknownTypeException', 'state_to_table_dtype'] 14 | 15 | 16 | class UnknownTypeException(Exception): 17 | 18 | """ 19 | This exception is raise when we have to deal with an unknown type. 20 | """ 21 | 22 | pass 23 | 24 | 25 | DTYPE_STR_BUFFER_SAFETY_FACTOR = 2 26 | 27 | 28 | def state_to_table_dtype(state, 29 | str_buffer_safety_factor=DTYPE_STR_BUFFER_SAFETY_FACTOR): 30 | """ 31 | Get a state of an object represented as a dictionary and derive the 32 | appropriate type of a tables.Table. 33 | 34 | :param state: The state of an object. 35 | :type state: dict 36 | :raises: :class:`pymc.UnknownTypeException` 37 | """ 38 | dtype_dict = {} 39 | for name in state.keys(): 40 | if isinstance(state[name], int): 41 | dtype = pt.UInt32Col() 42 | elif isinstance(state[name], float): 43 | dtype = pt.Float64Col() 44 | elif isinstance(state[name], str): 45 | dtype = pt.StringCol(itemsize=len(state[name]) * 46 | str_buffer_safety_factor) 47 | elif isinstance(state[name], np.ndarray): 48 | dtype = pt.Float64Col(shape=state[name].shape) 49 | else: 50 | raise UnknownTypeException('I cannot deal with the type of %s (%s)' 51 | %(name, type(state[name]))) 52 | dtype_dict[name] = dtype 53 | return dtype_dict 54 | -------------------------------------------------------------------------------- /demos/demo1.py: -------------------------------------------------------------------------------- 1 | """ 2 | This demo demonstrates how to train a GPy model using the pymcmc module. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 3/20/2014 9 | 10 | """ 11 | 12 | import GPy 13 | import pymcmc as pm 14 | import numpy as np 15 | import matplotlib.pyplot as plt 16 | 17 | 18 | # Construct a GPy Model (anything really..., here we are using a regression 19 | # example) 20 | model = GPy.examples.regression.olympic_marathon_men(optimize=False, plot=False) 21 | # Look at the model before it is trained: 22 | print 'Model before training:' 23 | print str(model) 24 | # Pick a proposal for MCMC (here we pick a Metropolized Langevin Proposal 25 | proposal = pm.MALAProposal(dt=1.) 26 | # Construct a Metropolis Hastings object 27 | mcmc = pm.MetropolisHastings(model, # The model you want to train 28 | proposal=proposal, # The proposal you want to use 29 | db_filename='demo_1_db.h5')# The HDF5 database to write the results 30 | # Look at the model now: We have automatically added uninformative priors 31 | # by looking at the constraints of the parameters 32 | print 'Model after adding priors:' 33 | print str(model) 34 | # Now we can sample it: 35 | mcmc.sample(100000, # Number of MCMC steps 36 | num_thin=100, # Number of steps to skip 37 | num_burn=1000, # Number of steps to burn initially 38 | verbose=True) # Be verbose or not 39 | # Here is the model at the last MCMC step: 40 | print 'Model after training:' 41 | print str(model) 42 | # Let's plot the results: 43 | model.plot(plot_limits=(1850, 2050)) 44 | a = raw_input('press enter...') 45 | -------------------------------------------------------------------------------- /unittests/table_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Experimenting in order to construct the database support for the MCMC chains. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | import tables as tb 10 | import numpy as np 11 | 12 | 13 | if __name__ == '__main__': 14 | num_params = 10 15 | params = np.random.randn(num_params) 16 | filters = tb.Filters(complevel=9) 17 | fd = tb.open_file('test_db.h5', mode='a', filters=filters) 18 | fd.create_group('/', 'mcmc', 'Metropolis-Hastings Algorithm') 19 | # Data type for a single record in the chain 20 | single_record_dtype = {'step': tb.UInt16Col(), 21 | 'params': tb.Float32Col(shape=(num_params,)), 22 | 'proposal': tb.UInt16Col(), 23 | 'log_like': tb.Float32Col(), 24 | 'log_prior': tb.Float32Col(), 25 | 'grad_log_like': tb.Float32Col(shape=(num_params,)), 26 | 'grad_log_prior': tb.Float32Col(shape=(num_params,)), 27 | 'accepted': tb.UInt16Col()} 28 | table = fd.create_table('/mcmc', 'chain_000', single_record_dtype, 'Chain: 0') 29 | chain = table.row 30 | for i in xrange(1000): 31 | print i 32 | chain['step'] = i 33 | chain['params'] = np.random.randn(num_params) 34 | chain['proposal'] = 0 35 | chain['log_like'] = np.random.rand() 36 | chain['log_prior'] = np.random.rand() 37 | chain['grad_log_like'] = np.random.randn(num_params) 38 | chain['grad_log_prior'] = np.random.randn(num_params) 39 | chain['accepted'] = i % 2 40 | chain.append() 41 | table.flush() 42 | fd.close() 43 | -------------------------------------------------------------------------------- /pymcmc/_assign_priors_to_gpy_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automatically assign uninformative priors to a GPYModel. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 3/20/2014 9 | """ 10 | 11 | 12 | __all__ = ['assign_priors_to_gpy_model'] 13 | 14 | 15 | import GPy 16 | from GPy import priors 17 | POSITIVE = priors._POSITIVE 18 | REAL = priors._REAL 19 | import itertools 20 | from . import UninformativeScalePrior 21 | from . import UninformativePrior 22 | 23 | 24 | def assign_priors_to_gpy_model(model): 25 | """ 26 | Automatically assign uninformative priors to a GPYModel. 27 | 28 | It only assigns priors to variables that do not have one already. 29 | 30 | The assignmnent of priors is done as follows: 31 | + If a parameter is constrained to be POSITIVE, then it is assigned a 32 | :class:`pymcmc.UninformativeScalePrior`. 33 | + If a parameter is constrained to be REAL, then it is assigned a 34 | :class:`pymcmc.UninformativePrior` with infinite bounds. 35 | + If a parameter is constrained to be REAL, then it is assigned a 36 | :class:`pymcmc.UninformativePrior` with (semi)-finite bounds. 37 | 38 | :param model: The GPy model you want to assign uninformative priors to. 39 | Upon exit, the model will contain priors. 40 | """ 41 | assert isinstance(model, GPy.core.Model) 42 | param_names = model._get_param_names() 43 | if model.priors is None: 44 | model.priors = [None] * len(param_names) 45 | # Loop over the parameters of the model 46 | for mp in model.flattened_parameters: 47 | print mp._constraints_str 48 | if mp._constraints_str[0] == '+ve': 49 | mp.set_prior(UninformativeScalePrior()) 50 | else: 51 | print 'there' 52 | mp.set_prior(UninformativePrior()) 53 | -------------------------------------------------------------------------------- /pymcmc/_simple_proposal.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple MCMC proposal is one that is based only on the current state of the 3 | model, e.g. a Random Walk proposal. 4 | 5 | Author: 6 | Ilias Bilionis 7 | """ 8 | 9 | 10 | __all__ = ['SimpleProposal'] 11 | 12 | 13 | from . import Proposal 14 | 15 | 16 | class SimpleProposal(Proposal): 17 | 18 | """ 19 | The base class for simple proposals (proposals based only on the current 20 | parameters of a model. 21 | 22 | """ 23 | 24 | def __init__(self, **kwargs): 25 | """ 26 | Initialize the object. 27 | """ 28 | super(SimpleProposal, self).__init__(**kwargs) 29 | 30 | def _do_propose(self, model): 31 | """ 32 | Do the actual proposal and change the state of the model to contain 33 | the new parameters. 34 | 35 | :param model: The model. 36 | :returns: The ratio of the forward and backward moves. 37 | 38 | Upon return, the model shall contain the new parameters. 39 | """ 40 | old_params = model.params 41 | new_params = self._sample(old_params) 42 | log_p_new_cond_old = self(new_params, old_params) 43 | log_p_old_cond_new = self(old_params, new_params) 44 | model.params = new_params 45 | return log_p_old_cond_new - log_p_new_cond_old 46 | 47 | def _sample(self, old_params): 48 | """ 49 | Sample the proposal given the ``old_params``. 50 | 51 | :param old_params: The old parameters of the model. 52 | :returns: The new parameters of the model. 53 | """ 54 | raise NotImplementedError('Implement this.') 55 | 56 | def __call__(self, new_params, old_params): 57 | """ 58 | Evaluate the proposal at the new parameters given the old parameters. 59 | :param new_params: The new parameters. 60 | :param old_params: The old parameters. We are assuming that we 61 | are conditioning on them. 62 | """ 63 | raise NotImplementedError('Implement this.') 64 | -------------------------------------------------------------------------------- /pymcmc/_mala_proposal.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a Metropolis Adjusted Langevin Algorithm (MALA) proposal. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['MALAProposal'] 10 | 11 | 12 | import numpy as np 13 | from scipy.stats import norm 14 | from . import GradProposal 15 | from . import SingleParameterTunableProposalConcept 16 | 17 | 18 | class MALAProposal(GradProposal, SingleParameterTunableProposalConcept): 19 | 20 | """ 21 | A MALA proposal. 22 | 23 | :param dt: The time step. The larger you pick it, the bigger the steps 24 | you make and the acceptance rate will go down. 25 | :type dt: float 26 | 27 | The rest of the keyword arguments is what you would find in: 28 | + :class:`pymcmc.GradProposal` 29 | + :class:`pymcmc.SingleParameterTunableProposal` 30 | 31 | """ 32 | 33 | def __init__(self, dt=1., **kwargs): 34 | """ 35 | Initialize the object. 36 | """ 37 | self.dt = dt 38 | if not kwargs.has_key('name'): 39 | kwargs['name'] = 'MALA Proposal' 40 | kwargs['param_name'] = 'dt' 41 | GradProposal.__init__(self, **kwargs) 42 | SingleParameterTunableProposalConcept.__init__(self, **kwargs) 43 | 44 | def _sample(self, old_params, old_grad_params): 45 | return (old_params + 46 | 0.5 * self.dt ** 2 * old_grad_params + 47 | self.dt * np.random.randn(old_params.shape[0])) 48 | 49 | def __call__(self, new_params, old_params, old_grad_params): 50 | return np.sum(norm.logpdf(new_params, 51 | loc=(old_params + 0.5 * self.dt ** 2 * old_grad_params), 52 | scale=self.dt)) 53 | 54 | def __getstate__(self): 55 | state = GradProposal.__getstate__(self) 56 | state['dt'] = self.dt 57 | tuner_state = SingleParameterTunableProposalConcept.__getstate__(self) 58 | return dict(state.items() + tuner_state.items()) 59 | 60 | def __setstate__(self, state): 61 | GradProposal.__setstate__(self, state) 62 | self.dt = state['dt'] 63 | SingleParameterTunableProposalConcept.__setstate__(self, state['tuner']) 64 | -------------------------------------------------------------------------------- /pymcmc/_random_walk_proposal.py: -------------------------------------------------------------------------------- 1 | """ 2 | A random walk proposal. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['RandomWalkProposal'] 10 | 11 | 12 | import numpy as np 13 | from . import SymmetricProposal 14 | from . import SingleParameterTunableProposalConcept 15 | 16 | 17 | class RandomWalkProposal(SymmetricProposal, SingleParameterTunableProposalConcept): 18 | 19 | """ 20 | A random walk proposal. 21 | 22 | :param name: A name for the object. 23 | :type name: str 24 | :param cov: A covariance matrix (must have the same dimensions 25 | as the model we are going to use). 26 | :type cov: 2D numpy array 27 | :param scale: The scale of the proposal. 28 | :type scale: float 29 | """ 30 | 31 | def __init__(self, cov=None, scale=1., **kwargs): 32 | """ 33 | Initialize the object. 34 | """ 35 | if cov is None: 36 | cov = 1. 37 | self.cov = cov 38 | self.scale = scale 39 | if not kwargs.has_key('name'): 40 | kwargs['name'] = 'Random Walk Proposal' 41 | kwargs['param_name'] = 'scale' 42 | SymmetricProposal.__init__(self, **kwargs) 43 | SingleParameterTunableProposalConcept.__init__(self, **kwargs) 44 | 45 | def _sample(self, old_params): 46 | if isinstance(self.cov, float): 47 | self.cov = self.cov * np.eye(old_params.shape[0]) 48 | new_params = np.random.multivariate_normal(old_params, 49 | self.cov * self.scale ** 2) 50 | return new_params 51 | 52 | def __getstate__(self): 53 | """ 54 | Get the state of the object. 55 | """ 56 | state = SymmetricProposal.__getstate__(self) 57 | state['cov'] = self.cov 58 | state['scale'] = self.scale 59 | tuner_state = SingleParameterTunableProposalConcept.__getstate__() 60 | return dict(state.items() + tuner_state.items()) 61 | 62 | def __setstate__(self, state): 63 | """ 64 | Set the state of the object. 65 | """ 66 | SymmetricProposal.__setstate__(self, state) 67 | self.cov = state['cov'] 68 | self.scale = state['scale'] 69 | SingleParameterTunableProposalConcept.__setstate__(state) 70 | -------------------------------------------------------------------------------- /pymcmc/_grad_proposal.py: -------------------------------------------------------------------------------- 1 | """ 2 | A proposal for MCMC that depends on the gradients. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['GradProposal'] 10 | 11 | 12 | from . import Proposal 13 | 14 | 15 | class GradProposal(Proposal): 16 | 17 | """ 18 | The base class for proposals that depend on the gradient of the target 19 | probability distribution with respect to the parameters. 20 | 21 | The keyword arguments are the same as in :class:`pymcmc.Proposal`. 22 | """ 23 | 24 | def __init__(self, **kwargs): 25 | """ 26 | Initialize the object. 27 | """ 28 | if not kwargs.has_key('name'): 29 | kwargs['name'] = 'Grad Proposal' 30 | super(GradProposal, self).__init__(**kwargs) 31 | 32 | def _do_propose(self, model): 33 | """ 34 | Do the actual proposal and change the state of the model to contain 35 | the new parameters. 36 | 37 | :param model: The model. 38 | :returns: The ratio of the forward and backward moves. 39 | 40 | Upon return, the model shall contain the new parameters. 41 | """ 42 | old_params = model.params 43 | old_grad_params = model.grad_log_p 44 | new_params = self._sample(old_params, old_grad_params) 45 | log_p_new_cond_old = self(new_params, old_params, old_grad_params) 46 | model.params = new_params 47 | new_grad_params = model.grad_log_p 48 | log_p_old_cond_new = self(old_params, new_params, new_grad_params) 49 | return log_p_old_cond_new - log_p_new_cond_old 50 | 51 | def _sample(self, old_params, old_grad_params): 52 | """ 53 | Sample the proposal given the ``old_params`` and the gradient of the 54 | target probability with respect to them. 55 | 56 | :param old_params: The old parameters of the model. 57 | :param old_grad_params: The gradient of the target probability with 58 | respect to the parameters. 59 | """ 60 | raise NotImplementedError('Implement this.') 61 | 62 | def __call__(self, new_params, old_params, old_grad_params): 63 | """ 64 | Evaluate the proposal at the new parameters given the old parameters. 65 | :param new_params: The new parameters. 66 | :param old_params: The old parameters. We are assuming that we 67 | are conditioning on them. 68 | :param old_grad_params: The gradient of the target probability with 69 | respect to the parameters. 70 | """ 71 | raise NotImplementedError('Implement this.') 72 | -------------------------------------------------------------------------------- /pymcmc/_proposal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines the base proposal class for doing MCMC. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['Proposal'] 10 | 11 | 12 | import copy 13 | 14 | 15 | class Proposal(object): 16 | 17 | """ 18 | The base class of all MCMC proposals. 19 | 20 | :param name: A name for the object. 21 | :type name: str 22 | 23 | .. note:: 24 | It ignores all other keyword arguments. 25 | """ 26 | 27 | def __init__(self, **kwargs): 28 | """ 29 | Initialize the object. 30 | """ 31 | self.__name__ = kwargs['name'] if kwargs.has_key('name') else 'Proposal' 32 | 33 | def __str__(self): 34 | """ 35 | Return a string representation of the object. 36 | """ 37 | return 'Name:\t' + self.__name__ 38 | 39 | def __getstate__(self): 40 | """ 41 | Get the state of the object. 42 | """ 43 | state = {} 44 | state['name'] = self.__name__ 45 | return state 46 | 47 | def __setstate__(self, state): 48 | """ 49 | Set the state of the object. 50 | """ 51 | self.__name__ = state['name'] 52 | 53 | def propose(self, model): 54 | """ 55 | Propose a move. 56 | 57 | :param model: The model. 58 | :returns: A tuple of the following form: 59 | (new_state, log_p) 60 | where: 61 | + ``new_state`` is a new state for the model 62 | + ``log_p`` is the probability for acceptance 63 | 64 | The model shall remain the same after a call to this method. That is, 65 | no matter what happens to it, its parameters should remain the same. 66 | """ 67 | old_state = copy.deepcopy(model.__getstate__()) 68 | old_log_like = model.log_likelihood 69 | old_log_prior = model.log_prior 70 | log_a2 = self._do_propose(model) 71 | new_state = copy.deepcopy(model.__getstate__()) 72 | new_log_like = model.log_likelihood 73 | new_log_prior = model.log_prior 74 | log_a1 = (new_log_like - old_log_like) + (new_log_prior - old_log_prior) 75 | model.__setstate__(old_state) 76 | return new_state, log_a1 + log_a2 77 | 78 | def _do_propose(self, model): 79 | """ 80 | Actually propose a move. 81 | 82 | :param model: The model. 83 | 84 | This needs to be reimplemented by the deriving classes. 85 | Here, it is assumed that the model is left to the new state. 86 | """ 87 | raise NotImplementedError('Implement this.') 88 | -------------------------------------------------------------------------------- /pymcmc/_gpy_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | A generic wrapper for a GPy model. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | from . import Model 9 | from . import assign_priors_to_gpy_model 10 | import numpy as np 11 | 12 | 13 | __all__ = ['GPyModel'] 14 | 15 | 16 | class GPyModel(Model): 17 | 18 | """ 19 | A generic wrapper for GPy model. 20 | 21 | :param model: A GPy model. 22 | :type model: :class:`GPy.core.Model` 23 | :param name: A name for the model. 24 | :type name: str 25 | :param compute_grad: Compute gradients of the log probability or not. 26 | :type compute_grad: bool 27 | :param assign_priors: If ``True`` then uninformative priors are assigned 28 | to the underlying GPyModel. 29 | :type assign_priors: bool 30 | """ 31 | 32 | def __init__(self, model, name='GPy model wrapper', compute_grad=True, 33 | assign_priors=True): 34 | """ 35 | Initialize the object. 36 | """ 37 | if assign_priors: 38 | assign_priors_to_gpy_model(model) 39 | self.model = model 40 | super(GPyModel, self).__init__(name=name) 41 | self._compute_grad = compute_grad 42 | self._eval_state() 43 | 44 | def _eval_state(self): 45 | """ 46 | Evaluates the state of the model in order to avoid redundant calculations. 47 | """ 48 | self._state = {} 49 | self._state['log_likelihood'] = self.model.log_likelihood() 50 | self._state['log_prior'] = self.model.log_prior() 51 | if self._compute_grad: 52 | g = self.model._log_likelihood_gradients() 53 | self._state['grad_log_likelihood'] = self.model._transform_gradients(g) 54 | g = self.model._log_prior_gradients() 55 | if isinstance(g, float): 56 | g = np.array([g] * self.num_params) 57 | self._state['grad_log_prior'] = self.model._transform_gradients(g) 58 | self._state['params'] = self.model.optimizer_array.copy() 59 | 60 | def __getstate__(self): 61 | return self._state 62 | 63 | def __setstate__(self, state): 64 | self._state = state 65 | 66 | @property 67 | def log_likelihood(self): 68 | return self._state['log_likelihood'] 69 | 70 | @property 71 | def log_prior(self): 72 | return self._state['log_prior'] 73 | 74 | @property 75 | def num_params(self): 76 | return self.model.num_params_transformed() 77 | 78 | @property 79 | def params(self): 80 | return self._state['params'] 81 | 82 | @params.setter 83 | def params(self, value): 84 | self.model.optimizer_array = value 85 | self._eval_state() 86 | 87 | @property 88 | def param_names(self): 89 | return self.model._get_param_names() 90 | 91 | @property 92 | def grad_log_likelihood(self): 93 | return self._state['grad_log_likelihood'] 94 | 95 | @property 96 | def grad_log_prior(self): 97 | return self._state['grad_log_prior'] 98 | -------------------------------------------------------------------------------- /pymcmc/_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines the functionality of a model class. 3 | 4 | All models should simply inherit from this class. 5 | 6 | Author: 7 | Ilias Bilionis 8 | 9 | """ 10 | 11 | 12 | __all__ = ['Model'] 13 | 14 | 15 | class Model(object): 16 | 17 | """ 18 | A generic model class. 19 | 20 | :param name: A name for the model. 21 | :type name: str 22 | """ 23 | 24 | def __init__(self, name='Pymcmc Model'): 25 | """ 26 | Initialize the object. 27 | """ 28 | self.__name__ = name 29 | 30 | def __getstate__(self): 31 | """ 32 | Get the state of the model. 33 | 34 | This shoud return the state of the model. That is, return everything 35 | is necessary to avoid redundant computations. It is also used in oder 36 | to pickle the object or send it over the network. 37 | """ 38 | raise NotImplementedError('Implement this.') 39 | 40 | def __setstate__(self, state): 41 | """ 42 | Set the state of the model. 43 | 44 | This is supposed to take the return value of the 45 | :method:`Model.__getstate__`. 46 | """ 47 | raise NotImplementedError('Implement this.') 48 | 49 | @property 50 | def log_likelihood(self): 51 | """ 52 | Return the log likelihood of the model at the current state. 53 | """ 54 | raise NotImplementedError('Implement this.') 55 | 56 | @property 57 | def log_prior(self): 58 | """ 59 | Retutnr the log prior of the model at the current state. 60 | """ 61 | raise NotImplementedError('Implement this.') 62 | 63 | @property 64 | def num_params(self): 65 | """ 66 | Return the number of parameters. 67 | """ 68 | raise NotImplementedError('Implement this.') 69 | 70 | @property 71 | def params(self): 72 | """ 73 | Set/Get the parameters. 74 | """ 75 | raise NotImplementedError('Implement this.') 76 | 77 | @params.setter 78 | def params(self, value): 79 | raise NotImplementedError('Implement this.') 80 | 81 | @property 82 | def param_names(self): 83 | """ 84 | Return a list containing the names of the parameters. 85 | """ 86 | raise NotImplementedError('Implement this.') 87 | 88 | @property 89 | def grad_log_likelihood(self): 90 | """ 91 | Return the gradient of the log likelihood. 92 | """ 93 | raise NotImplementedError('Implement this.') 94 | 95 | @property 96 | def grad_log_prior(self): 97 | """ 98 | Return the gradient of the log prior. 99 | """ 100 | raise NotImplementedError('Implement this.') 101 | 102 | @property 103 | def log_p(self): 104 | """ 105 | Return the log probability of the model at the current parameters. 106 | """ 107 | return self.log_likelihood + self.log_prior 108 | 109 | @property 110 | def grad_log_p(self): 111 | """ 112 | Return the gradient of the model with respect to the current parameters. 113 | """ 114 | return self.grad_log_likelihood + self.grad_log_prior 115 | 116 | def __str__(self): 117 | """ 118 | Return a string representation of the object. 119 | """ 120 | s = 'Model name:\t' + self.__name__ + '\n' 121 | s += 'num_param:\t' + str(self.num_params) + '\n' 122 | s += 'param names:\t' + str(self.param_names) 123 | s += str(self._state) 124 | return s 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A python module implementing some generic MCMC routines 2 | ======================================================= 3 | 4 | The main purpose of this module is to serve as a simple MCMC framework for 5 | generic models. Probably the most useful contribution at the moment, is that 6 | it can be used to train Gaussian process (GP) models implemented in the 7 | [GPy package](http://sheffieldml.github.io/GPy/). 8 | 9 | 10 | Features 11 | -------- 12 | The code features the following things at the moment: 13 | + Fully object oriented. The models can be of any type as soon as they offer 14 | the right interface. 15 | + Random walk proposals. 16 | + Metropolis Adjusted Langevin Dynamics. 17 | + The MCMC chains are stored in fast [HDF5](http://www.hdfgroup.org/HDF5/) 18 | format using [PyTables](http://www.pytables.org/moin). 19 | + A mean function can be added to the (GP) models of the 20 | [GPy package](http://sheffieldml.github.io/GPy/). 21 | 22 | 23 | Installation 24 | ------------ 25 | Clone the package, get into its directory and do a: 26 | ``` 27 | python setup.py install 28 | ``` 29 | 30 | Related Packages 31 | ---------------- 32 | Probably, the most related package to what I am offering is the excellent 33 | [PyMC](https://github.com/pymc-devs/pymc) code. The reason I have departed from 34 | it is two-fold: 35 | + In the old versions (e.g. 36 | [PyMC 2.3](http://pymc-devs.github.io/pymc/index.html)), could not find an easy 37 | way to implement Metropolis Adjusted Langevin Dynamics. This was unfortunate 38 | because it is one of the most powerful sampling methods when derivatives are 39 | available. 40 | + In the new version (e.g. 41 | [PyMC 3](http://nbviewer.ipython.org/github/pymc-devs/pymc/blob/master/pymc/examples/tutorial.ipynb), 42 | which is based on [Theano](http://www.deeplearning.net/software/theano/) 43 | schemes with derivatives can be easily implemented but there are several issues 44 | when one tries to deal with existing models. In particular, it is not possible 45 | at the moment to deal in an easy way with a model that is not directly implemented 46 | using Theano (e.g. if it calls an external library or runs a complicated program). 47 | This is a tremendous limitation when it comes to solving realistic inverse 48 | problems. In addition, it is not easy to exploit the Gaussian process 49 | functionality of GPy in order to train these models with MCMC. 50 | 51 | Therefore, the purpose of this package is to fill the gap between PyMC 2.3 52 | and PyMC 3. When the programers of PyMC 3 fix the afforementioned problem, then 53 | the MCMC part of this code will become obsolete. 54 | 55 | 56 | Additional Useful Packages 57 | -------------------------- 58 | I have written some other packages that are useful in combination with py-mcmc: 59 | + [Py-ORTHPOL](https://github.com/ebilionis/py-orthpol): Construct orthogonal 60 | polynomials with respect to arbitrary weight functions. These can be useful 61 | as mean functions for the Gaussian processes discussed here. They can be used 62 | directly. 63 | + [Py-Design](https://github.com/ebilionis/py-design): Design of experiments for 64 | Python. This is extremely useful if you are trying to learn the output of a 65 | computer code and you want to a good design of points to evaluate it. 66 | 67 | 68 | Demos 69 | ----- 70 | I provide various demos demonstrating how the code can be used: 71 | + [demos/demo1.py](demos/demo1.py): Demonstrates how to train GPy model using MCMC. 72 | + [demos/demo2.py](demos/demo2.py): Demonstrates how a GP with a mean can be trained. 73 | This model is equivalent to Bayesian linear regression. 74 | + [demos/demo3.py](demos/demo3.py): Demonstrates how a GP with a mean using 75 | automatic relevance determination for the basis functions can be used. This is 76 | equivalent to a Relevance Vector Machine model. 77 | + [demos/demo4.py](demos/demo4.py): Demonstrates how a GP with a mean can be 78 | combined with a normal covariance kernel. 79 | 80 | 81 | Ilias Bilionis, 82 | December, 2014 83 | PredictiveScience Laboratory, 84 | School of Mechanical Engineering, 85 | Purdue University, 86 | West Lafayette, IN, USA 87 | -------------------------------------------------------------------------------- /pymcmc/_priors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some prior densities that are not GPy. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['Prior', 'GaussianPrior', 'LogGaussianPrior', 10 | 'MultivariateGaussianPrior', 'GammaPrior', 11 | 'InverseGammaPrior', 'UninformativeScalePrior', 12 | 'UninformativePrior'] 13 | 14 | 15 | from GPy import priors 16 | Prior = priors.Prior 17 | GaussianPrior = priors.Gaussian 18 | LogGaussianPrior = priors.LogGaussian 19 | MultivariateGaussianPrior = priors.MultivariateGaussian 20 | GammaPrior = priors.Gamma 21 | InverseGammaPrior = priors.InverseGamma 22 | import numpy as np 23 | 24 | 25 | class UninformativeScalePrior(Prior): 26 | 27 | """ 28 | An uninformative prior. 29 | """ 30 | 31 | domain = priors._POSITIVE 32 | 33 | def lnpdf(self, x): 34 | """ 35 | :param x: The value of the parameter. 36 | :type x: :class:`numpy.ndarray` 37 | :returns: The logarithm of the probability. 38 | """ 39 | return -np.log(x) 40 | 41 | def lnpdf_grad(self, x): 42 | """ 43 | :param x: The value of the parameter. 44 | :type x: :class:`numpy.ndarray` 45 | :returns: The gradient of the logarithm of the probability. 46 | """ 47 | return -1. / x 48 | 49 | def __str__(self): 50 | """ 51 | Return a string representation of the object. 52 | """ 53 | return 'Uninformative Scale Prior: p(x) = 1/x, x > 0' 54 | 55 | 56 | class UninformativePrior(Prior): 57 | 58 | """ 59 | An uninformative prior for bounded domains or the real line. 60 | The default is the real line. 61 | 62 | :param lower: The lower bound of the distribution (It can be 63 | ``-np.inf``). 64 | :type lower: float 65 | :param upper: The upper bound of the distribution (It can be 66 | ``np.inf``). 67 | """ 68 | 69 | domain = None 70 | 71 | def __init__(self, lower=-np.inf, upper=np.inf): 72 | """ 73 | Initialize the object. 74 | """ 75 | assert lower < upper 76 | if lower == -np.inf and upper == np.inf: 77 | self.domain = priors._REAL 78 | self.log_length = 0. 79 | elif lower == -np.inf or upper == np.inf: 80 | self.domain = priors._BOUNDED 81 | self.log_length = 0. 82 | else: 83 | self.domain = priors._BOUNDED 84 | self.log_length = np.log(upper - lower) 85 | self.lower = lower 86 | self.upper = upper 87 | 88 | def lnpdf(self, x): 89 | """ 90 | :param x: The value of the parameter. 91 | :type x: :class:`numpy.ndarray` 92 | :returns: The logarithm of the probability 93 | """ 94 | if x < self.lower or x > self.upper: 95 | return -np.inf 96 | else: 97 | return self.log_length 98 | 99 | def lnpdf_grad(self, x): 100 | """ 101 | :param x: The value of the parameter. 102 | :type x: :class:`numpy.ndarray` 103 | :returns: The gradient of the logarithm of the probability. 104 | """ 105 | return 0. 106 | 107 | def __str__(self): 108 | """ 109 | Return a string representation of the object. 110 | """ 111 | if (self.domain == priors._REAL or 112 | self.upper == np.inf or 113 | self.lower == -np.inf): 114 | return 'Uninformative Prior: p(x) = 1' 115 | else: 116 | return 'Uninformative Prior: p(x) = |D|*I_D(x)' 117 | 118 | def rvs(self, n): 119 | """ 120 | Draw random samples from the probability density. 121 | 122 | It works only for BOUNDED domains. 123 | 124 | :param n: The number of samples to draw. 125 | :type n: int 126 | """ 127 | if self.upper < np.inf and self.lower > -np.inf: 128 | return np.random.rand(n) * (self.upper - self.lower) + self.lower 129 | else: 130 | raise RuntimeError('Cannot draw samples from an improper ' 131 | ' probability distribution.') 132 | -------------------------------------------------------------------------------- /demos/demo4.py: -------------------------------------------------------------------------------- 1 | """ 2 | This demo demonstrates how to use a mean function in a GP and allow the model 3 | to discover the most important basis functions. 4 | This model is equivalent to a Relevance Vector Machine. 5 | 6 | Author: 7 | Ilias Bilionis 8 | 9 | Date: 10 | 3/20/2014 11 | 12 | """ 13 | 14 | 15 | import numpy as np 16 | import GPy 17 | import pymcmc as pm 18 | import matplotlib.pyplot as plt 19 | 20 | 21 | # Write a class that represents the mean you wish to use: 22 | class PolynomialBasis(object): 23 | """ 24 | A simple set of polynomials. 25 | 26 | :param degree: The degree of the polynomials. 27 | :type degree: int 28 | """ 29 | 30 | def __init__(self, degree): 31 | """ 32 | The constructor can do anything you want. The object should be 33 | constructed before doing anything with pymcmc in any case. 34 | Just make sure that inside the constructor you define the ``num_output`` 35 | attribute whose value should be equal to the number of basis functions. 36 | """ 37 | self.degree = degree 38 | self.num_output = degree + 1 # YOU HAVE TO DEFINE THIS ATTRIBUTE! 39 | 40 | def __call__(self, X): 41 | """ 42 | Evaluate the basis functions at ``X``. 43 | 44 | Now, you should assume that ``X`` is a 2D numpy array of size 45 | ``num_points x input_dim``. If ``input_dim`` is 1, then you still need 46 | to consider it as a 2D array because this is the kind of data that GPy 47 | requires. If you want to make the function work also with 1D arrays if 48 | ``input_dim`` is one the use the trick below. 49 | 50 | The output of this function should be the design matrix. That is, 51 | it should be the matrix ``phi`` of dimensions 52 | ``num_points x num_output``. In otherwors, ``phi[i, j]`` should be 53 | the value of basis function ``phi_j`` at ``X[i, :]``. 54 | """ 55 | if X.ndim == 1: 56 | X = X[:, None] # Trick for 1D arrays 57 | return np.hstack([X ** i for i in range(self.degree + 1)]) 58 | 59 | 60 | # Pick your degree 61 | degree = 5 62 | # Construct your basis 63 | poly_basis = PolynomialBasis(degree) 64 | # Let us generate some random data to play with 65 | # The number of input dimensions 66 | input_dim = 1 67 | # The number of observations 68 | num_points = 50 69 | # The noise level we are going to add to the observations 70 | noise = 0.1 71 | # Observed inputs 72 | X = 20. * np.random.rand(num_points, 1) - 10. 73 | # The observations we make 74 | Y = np.sin(X) / X + noise * np.random.randn(num_points, 1) - 0.1 * X + 0.1 * X ** 3 75 | # Let's construct a GP model with just a mean and a diagonal covariance 76 | # This is the mean (and at the same time the kernel) 77 | mean = pm.MeanFunction(input_dim, poly_basis, ARD=True) 78 | # Add an RBF kernel 79 | kernel = GPy.kern.RBF(input_dim) 80 | # Now, let's construct the model 81 | model = GPy.models.GPRegression(X, Y, kernel=mean + kernel) 82 | print 'Model before training:' 83 | print str(model) 84 | # You may just train the model by maximizing the likelihood: 85 | model.optimize_restarts(messages=True) 86 | print 'Trained model:' 87 | print str(model) 88 | print model.add.mean.variance 89 | # And just plot the predictions 90 | model.plot(plot_limits=(-10, 15)) 91 | # Let us also plot the full function 92 | x = np.linspace(-10, 15, 100)[:, None] 93 | y = np.sin(x) / x - 0.1 * x + 0.1 * x ** 3 94 | plt.plot(x, y, 'r', linewidth=2) 95 | plt.legend(['Mean of GP', '5% percentile of GP', '95% percentile of GP', 96 | 'Observations', 'Real Underlying Function'], loc='best') 97 | plt.title('Model trained by maximizing the likelihood') 98 | plt.show() 99 | a = raw_input('press enter to continue...') 100 | # Or you might want to do it using MCMC: 101 | new_mean = pm.MeanFunction(input_dim, poly_basis, ARD=True) 102 | new_kernel = GPy.kern.RBF(input_dim) 103 | new_model = GPy.models.GPRegression(X, Y, kernel=mean + new_kernel) 104 | proposal = pm.MALAProposal(dt=0.1) 105 | mcmc = pm.MetropolisHastings(new_model, proposal=proposal) 106 | mcmc.sample(50000, num_thin=100, num_burn=1000, verbose=True) 107 | print 'Model trained with MCMC:' 108 | print str(new_model) 109 | print new_model.add.mean.variance 110 | # Plot everything for this too: 111 | new_model.plot(plot_limits=(-10., 15.)) 112 | # Let us also plot the full function 113 | plt.plot(x, y, 'r', linewidth=2) 114 | plt.legend(['Mean of GP', '5% percentile of GP', '95% percentile of GP', 115 | 'Observations', 'Real Underlying Function'], loc='best') 116 | plt.title('Model trained by MCMC') 117 | plt.show() 118 | a = raw_input('press enter to continue...') 119 | -------------------------------------------------------------------------------- /demos/demo2.py: -------------------------------------------------------------------------------- 1 | """ 2 | This demo demonstrates how to construct a GPy model with a mean function 3 | and train it using the pymcmc module. This model is equivalent to Bayesian 4 | linear regression. 5 | 6 | Author: 7 | Ilias Bilionis 8 | 9 | Date: 10 | 3/20/2014 11 | 12 | """ 13 | 14 | 15 | import numpy as np 16 | import GPy 17 | import pymcmc as pm 18 | import matplotlib.pyplot as plt 19 | 20 | 21 | # Write a class that represents the mean you wish to use: 22 | class PolynomialBasis(object): 23 | """ 24 | A simple set of polynomials. 25 | 26 | :param degree: The degree of the polynomials. 27 | :type degree: int 28 | """ 29 | 30 | def __init__(self, degree): 31 | """ 32 | The constructor can do anything you want. The object should be 33 | constructed before doing anything with pymcmc in any case. 34 | Just make sure that inside the constructor you define the ``num_output`` 35 | attribute whose value should be equal to the number of basis functions. 36 | """ 37 | self.degree = degree 38 | self.num_output = degree + 1 # YOU HAVE TO DEFINE THIS ATTRIBUTE! 39 | 40 | def __call__(self, X): 41 | """ 42 | Evaluate the basis functions at ``X``. 43 | 44 | Now, you should assume that ``X`` is a 2D numpy array of size 45 | ``num_points x input_dim``. If ``input_dim`` is 1, then you still need 46 | to consider it as a 2D array because this is the kind of data that GPy 47 | requires. If you want to make the function work also with 1D arrays if 48 | ``input_dim`` is one the use the trick below. 49 | 50 | The output of this function should be the design matrix. That is, 51 | it should be the matrix ``phi`` of dimensions 52 | ``num_points x num_output``. In otherwors, ``phi[i, j]`` should be 53 | the value of basis function ``phi_j`` at ``X[i, :]``. 54 | """ 55 | if X.ndim == 1: 56 | X = X[:, None] # Trick for 1D arrays 57 | return np.hstack([X ** i for i in range(self.degree + 1)]) 58 | 59 | 60 | # Pick your degree 61 | degree = 10 62 | # Construct your basis 63 | poly_basis = PolynomialBasis(degree) 64 | # Let us generate some random data to play with 65 | # The number of input dimensions 66 | input_dim = 1 67 | # The number of observations 68 | num_points = 20 69 | # The noise level we are going to add to the observations 70 | noise = 0.1 71 | # Observed inputs 72 | X = np.random.rand(num_points, 1) 73 | # We are going to generate the outputs from the space that is spanned by 74 | # our basis functions and add some noise 75 | # The weights of the basis functions: 76 | weights = np.random.randn(poly_basis.num_output) 77 | weights[2] = 0. 78 | # The observations we make 79 | Y = np.dot(poly_basis(X), weights) + noise * np.random.randn(num_points) 80 | # The output need also be a 2D numpy array 81 | Y = Y[:, None] 82 | # Let's construct a GP model with just a mean and a diagonal covariance 83 | # This is the mean (and at the same time the kernel) 84 | mean = pm.MeanFunction(input_dim, poly_basis, ARD=True) 85 | # Now, let's construct the model 86 | model = GPy.models.GPRegression(X, Y, kernel=mean) 87 | print 'Model before training:' 88 | print str(model) 89 | # You may just train the model by maximizing the likelihood: 90 | model.optimize(messages=True) 91 | print 'Trained model:' 92 | print str(model) 93 | # And just plot the predictions 94 | model.plot(plot_limits=(0, 1)) 95 | # Let us also plot the full function 96 | x = np.linspace(0, 1, 50)[:, None] 97 | y = np.dot(poly_basis(x), weights) 98 | plt.plot(x, y, 'r', linewidth=2) 99 | plt.legend(['Mean of GP', '5\% percentile of GP', '95\% percentile of GP', 100 | 'Observations', 'Real Underlying Function'], loc='best') 101 | plt.title('Model trained by maximizing the likelihood') 102 | plt.show() 103 | a = raw_input('press enter to continue...') 104 | # Or you might want to do it using MCMC: 105 | new_model = GPy.models.GPRegression(X, Y, kernel=mean) 106 | proposal = pm.MALAProposal(dt=1.) 107 | mcmc = pm.MetropolisHastings(new_model, proposal=proposal) 108 | mcmc.sample(30000, num_thin=100, num_burn=1000, verbose=True) 109 | print 'Model trained with MCMC:' 110 | print str(new_model) 111 | # Plot everything for this too: 112 | new_model.plot(plot_limits=(0, 1)) 113 | # Let us also plot the full function 114 | x = np.linspace(0, 1, 50)[:, None] 115 | y = np.dot(poly_basis(x), weights) 116 | plt.plot(x, y, 'r', linewidth=2) 117 | plt.legend(['Mean of GP', '5% percentile of GP', '95% percentile of GP', 118 | 'Observations', 'Real Underlying Function'], loc='best') 119 | plt.title('Model trained by MCMC') 120 | plt.show() 121 | a = raw_input('press enter to continue...') 122 | -------------------------------------------------------------------------------- /demos/demo3.py: -------------------------------------------------------------------------------- 1 | """ 2 | This demo demonstrates how to use a mean function in a GP and allow the model 3 | to discover the most important basis functions. 4 | This model is equivalent to a Relevance Vector Machine. 5 | 6 | Author: 7 | Ilias Bilionis 8 | 9 | Date: 10 | 3/20/2014 11 | 12 | """ 13 | 14 | 15 | import numpy as np 16 | import GPy 17 | import pymcmc as pm 18 | import matplotlib.pyplot as plt 19 | 20 | 21 | # Write a class that represents the mean you wish to use: 22 | class PolynomialBasis(object): 23 | """ 24 | A simple set of polynomials. 25 | 26 | :param degree: The degree of the polynomials. 27 | :type degree: int 28 | """ 29 | 30 | def __init__(self, degree): 31 | """ 32 | The constructor can do anything you want. The object should be 33 | constructed before doing anything with pymcmc in any case. 34 | Just make sure that inside the constructor you define the ``num_output`` 35 | attribute whose value should be equal to the number of basis functions. 36 | """ 37 | self.degree = degree 38 | self.num_output = degree + 1 # YOU HAVE TO DEFINE THIS ATTRIBUTE! 39 | 40 | def __call__(self, X): 41 | """ 42 | Evaluate the basis functions at ``X``. 43 | 44 | Now, you should assume that ``X`` is a 2D numpy array of size 45 | ``num_points x input_dim``. If ``input_dim`` is 1, then you still need 46 | to consider it as a 2D array because this is the kind of data that GPy 47 | requires. If you want to make the function work also with 1D arrays if 48 | ``input_dim`` is one the use the trick below. 49 | 50 | The output of this function should be the design matrix. That is, 51 | it should be the matrix ``phi`` of dimensions 52 | ``num_points x num_output``. In otherwors, ``phi[i, j]`` should be 53 | the value of basis function ``phi_j`` at ``X[i, :]``. 54 | """ 55 | if X.ndim == 1: 56 | X = X[:, None] # Trick for 1D arrays 57 | return np.hstack([X ** i for i in range(self.degree + 1)]) 58 | 59 | 60 | # Pick your degree 61 | degree = 4 62 | # Construct your basis 63 | poly_basis = PolynomialBasis(degree) 64 | # Let us generate some random data to play with 65 | # The number of input dimensions 66 | input_dim = 1 67 | # The number of observations 68 | num_points = 20 69 | # The noise level we are going to add to the observations 70 | noise = 0.1 71 | # Observed inputs 72 | X = np.random.rand(num_points, 1) 73 | # We are going to generate the outputs from the space that is spanned by 74 | # our basis functions and add some noise 75 | # The weights of the basis functions: 76 | weights = np.random.randn(poly_basis.num_output) 77 | # Just make sure that some basis functions are missing 78 | weights[2] = 0. 79 | weights[3] = 0. 80 | # The observations we make 81 | Y = np.dot(poly_basis(X), weights) + noise * np.random.randn(num_points) 82 | # The output need also be a 2D numpy array 83 | Y = Y[:, None] 84 | # Let's construct a GP model with just a mean and a diagonal covariance 85 | # This is the mean (and at the same time the kernel) 86 | mean = pm.MeanFunction(input_dim, poly_basis, ARD=True) 87 | # Now, let's construct the model 88 | model = GPy.models.GPRegression(X, Y, kernel=mean) 89 | print 'Model before training:' 90 | print str(model) 91 | # You may just train the model by maximizing the likelihood: 92 | model.optimize(messages=True) 93 | print 'Trained model:' 94 | print str(model) 95 | # And just plot the predictions 96 | model.plot(plot_limits=(0, 1)) 97 | # Let us also plot the full function 98 | x = np.linspace(0, 1, 50)[:, None] 99 | y = np.dot(poly_basis(x), weights) 100 | plt.plot(x, y, 'r', linewidth=2) 101 | plt.legend(['Mean of GP', '5% percentile of GP', '95% percentile of GP', 102 | 'Observations', 'Real Underlying Function'], loc='best') 103 | plt.title('Model trained by maximizing the likelihood') 104 | plt.show() 105 | a = raw_input('press enter to continue...') 106 | # Or you might want to do it using MCMC: 107 | new_mean = pm.MeanFunction(input_dim, poly_basis, ARD=True) 108 | new_model = GPy.models.GPRegression(X, Y, kernel=new_mean) 109 | proposal = pm.MALAProposal(dt=0.1) 110 | mcmc = pm.MetropolisHastings(new_model, proposal=proposal) 111 | mcmc.sample(30000, num_thin=100, num_burn=1000, verbose=True) 112 | print 'Model trained with MCMC:' 113 | print str(new_model) 114 | # Plot everything for this too: 115 | new_model.plot(plot_limits=(0, 1)) 116 | # Let us also plot the full function 117 | x = np.linspace(0, 1, 50)[:, None] 118 | y = np.dot(poly_basis(x), weights) 119 | plt.plot(x, y, 'r', linewidth=2) 120 | plt.legend(['Mean of GP', '5% percentile of GP', '95% percentile of GP', 121 | 'Observations', 'Real Underlying Function'], loc='best') 122 | plt.title('Model trained by MCMC') 123 | plt.show() 124 | a = raw_input('press enter to continue...') 125 | -------------------------------------------------------------------------------- /pymcmc/_database.py: -------------------------------------------------------------------------------- 1 | """ 2 | A database to store the MCMC chains. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['DataBase'] 10 | 11 | 12 | import tables as pt 13 | import numpy as np 14 | import os 15 | from datetime import datetime 16 | import itertools 17 | from . import state_to_table_dtype 18 | from . import UnknownTypeException 19 | 20 | 21 | class DataBase(object): 22 | 23 | """ 24 | A database to store MCMC chains. 25 | 26 | :param filename: The filename of the database. 27 | :type filename: str 28 | :param model_state: The model. This is needed so that we know exactly 29 | how to store the state of a model. 30 | :type model_state: dict 31 | :param proposal_state: The MCMC proposal. This is needed so that we know 32 | exactly what data are required for the proposal. 33 | :type proposal_state: dict 34 | """ 35 | 36 | def __init__(self, filename, model_state, proposal_state): 37 | """ 38 | Initialize the object. 39 | """ 40 | self.filename = filename 41 | self.ChainRecordDType = state_to_table_dtype(model_state) 42 | self.ChainRecordDType['step'] = pt.UInt32Col() 43 | self.ChainRecordDType['accepted'] = pt.UInt32Col() 44 | self.ChainRecordDType['proposal'] = pt.UInt16Col() 45 | self.ProposalRecordDType = state_to_table_dtype(proposal_state) 46 | self.ChainCounterDType = {'id': pt.UInt16Col(), 47 | 'name': pt.StringCol(itemsize=32), 48 | 'date': pt.StringCol(itemsize=26) 49 | } 50 | if os.path.exists(filename) and pt.is_pytables_file(filename): 51 | self.fd = pt.open_file(filename, mode='a') 52 | else: 53 | self.fd = pt.open_file(filename, mode='w') 54 | self.fd.create_group('/', 'mcmc', 55 | 'Metropolis-Hastings Algorithm Data') 56 | self.fd.create_table('/mcmc', 'proposals', 57 | self.ProposalRecordDType, 58 | 'MCMC Proposals') 59 | self.fd.create_table('/mcmc', 'chain_counter', 60 | self.ChainCounterDType, 'Chain Counter') 61 | self.fd.create_group('/mcmc', 'data', 'Collection of Chains') 62 | 63 | @property 64 | def proposals(self): 65 | return self.fd.root.mcmc.proposals 66 | 67 | @property 68 | def chain_counter(self): 69 | return self.fd.root.mcmc.chain_counter 70 | 71 | @property 72 | def proposal_id(self): 73 | """ 74 | Get the id of the current proposal. 75 | """ 76 | return self.proposals.nrows - 1 77 | 78 | def add_proposal(self, state): 79 | """ 80 | Add a proposal to the database. 81 | """ 82 | row = self.proposals.row 83 | for name in state.keys(): 84 | row[name] = state[name] 85 | row.append() 86 | self.proposals.flush() 87 | 88 | def create_new_chain(self): 89 | """ 90 | Create a new chain. 91 | """ 92 | num_chains = self.chain_counter.nrows 93 | row = self.chain_counter.row 94 | row['id'] = num_chains 95 | row['name'] = 'chain_' + str(num_chains) 96 | row['date'] = str(datetime.now()) 97 | row.append() 98 | self.chain_counter.flush() 99 | self.current_chain = self.fd.create_table('/mcmc/data', 100 | 'chain_' + str(num_chains), 101 | self.ChainRecordDType, 102 | 'Chain Record ' + str(num_chains)) 103 | 104 | def add_chain_record(self, step, accepted, state): 105 | """ 106 | Add a chain record to the current state. 107 | """ 108 | row = self.current_chain.row 109 | for name in state.keys(): 110 | row[name] = state[name] 111 | row['step'] = step 112 | row['accepted'] = int(accepted) 113 | row['proposal'] = self.proposal_id 114 | row.append() 115 | self.current_chain.flush() 116 | 117 | def get_states(self, chain_num, step_num): 118 | """ 119 | Get the model state and the proposal state from the data base. 120 | """ 121 | model_state = {} 122 | proposal_state = {} 123 | chain_name = self.fd.root.mcmc.chain_counter.cols.name[chain_num] 124 | chain = self.fd.get_node('/mcmc/data', chain_name) 125 | step_data = chain[step_num] 126 | for name, data in itertools.izip(chain.colnames, step_data): 127 | model_state[name] = data 128 | proposals = self.fd.get_node('/mcmc/proposals') 129 | prop_data = proposals[model_state['proposal']] 130 | for name, data in itertools.izip(proposals.colnames, prop_data): 131 | proposal_state[name] = data 132 | return model_state, proposal_state 133 | -------------------------------------------------------------------------------- /pymcmc/_mean_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements various usefull kernels for GPy. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 3/20/2014 9 | 10 | """ 11 | 12 | 13 | __all__ = ['MeanFunction'] 14 | 15 | 16 | from GPy.kern import Kern 17 | from GPy.core.parameterization import Param 18 | from GPy.core.parameterization.transformations import Logexp 19 | from GPy.util.caching import Cache_this 20 | import numpy as np 21 | 22 | 23 | class MeanFunction(Kern): 24 | 25 | """ 26 | A kernel representing a mean function. 27 | 28 | :param input_dim: The number of input dimensions. 29 | :type input_dim: int 30 | :param basis: The basis. It should be a class that implements 31 | ``__call__(X)`` where X is a 2D numpy array with 32 | ``X.shape[1] == input_dim`` columns and returns the 33 | design matrix. In addition, it should at least have 34 | the attribute ``num_output`` which should store the 35 | number of basis functions. 36 | :type basis: any type that satisfies the consept of a basis function 37 | :param variance: The strength of the kernel. The smaller it is, the 38 | less important its contribution. 39 | :type variance: float 40 | :param kappa: The weight of each basis function. The closer it is to 41 | zero, the less important the corresponding basis 42 | function. 43 | :type kappa: :class:`numpy.ndarray` 44 | :param ARD: If ``ARD`` is ``True``, then the mean function behaves 45 | like a Relevance Vector Machine (RVM). If it is 46 | ``False`` then it is equivalent to a common mean 47 | function. 48 | :type ARD: bool 49 | """ 50 | 51 | # The basis functions 52 | _basis = None 53 | 54 | # The variance of the kernel 55 | _variance = None 56 | 57 | # The kappa of the kernel 58 | _kappa = None 59 | 60 | # Automatic Relevance Determination 61 | _ARD = None 62 | 63 | # The number of parameters of the kernel 64 | _num_params = None 65 | 66 | @property 67 | def basis(self): 68 | """ 69 | Get the basis functions. 70 | """ 71 | return self._basis 72 | 73 | @property 74 | def num_basis(self): 75 | """ 76 | Get the number of basis functions. 77 | """ 78 | return self.basis.num_output 79 | 80 | @property 81 | def num_params(self): 82 | """ 83 | Get the number of parameters of the object. 84 | """ 85 | return self._num_params 86 | 87 | @property 88 | def ARD(self): 89 | """ 90 | Get the ARD flag. 91 | """ 92 | return self._ARD 93 | 94 | def __init__(self, input_dim, basis, variance=None, ARD=False, 95 | active_dims=None, name='mean', useGP=False): 96 | """ 97 | Initialize the object. 98 | """ 99 | super(MeanFunction, self).__init__(input_dim, active_dims, name, 100 | useGP=useGP) 101 | self.input_dim = int(input_dim) 102 | self._ARD = ARD 103 | if not hasattr(basis, '__call__'): 104 | raise TypeError('The basis functions must implement the ' 105 | '\'__call__()\' method. This method should ' 106 | ' the basis functions given a 2D dimensional numpy' 107 | ' numpy array of \'num_points x input_dim\'' 108 | ' dimensions.') 109 | if not hasattr(basis, 'num_output'): 110 | raise TypeError('The basis functions must have an attribute ' 111 | ' \'num_output\' which should store the number of' 112 | ' basis functions it contains.') 113 | self._basis = basis 114 | self._num_params = basis.num_output 115 | if not ARD: 116 | if variance is None: 117 | variance = np.ones(1) 118 | else: 119 | variance = np.asarray(variance) 120 | assert variance.size == 1, 'Only 1 variance needed for a non-ARD kernel' 121 | else: 122 | if variance is not None: 123 | variance = np.asarray(variance) 124 | assert variance.size in [1, self.num_params], 'Bad number of variances' 125 | if variance.size != self.num_params: 126 | variance = np.ones(self.num_params) * variance 127 | else: 128 | variance = np.ones(self.num_params) 129 | self.variance = Param('variance', variance, Logexp()) 130 | self.link_parameters(self.variance) 131 | 132 | @Cache_this(limit=5, ignore_args=()) 133 | def K(self, X, X2=None): 134 | """ 135 | Evaluate the covariance (or cross covariance matrix) at ``X`` and ``X2``. 136 | If ``X2`` is ``None`` the covariance matrix is computed. The result is 137 | added to ``target``. 138 | """ 139 | # Compute the design matrix 140 | # TODO: This should happen only once if X does not change! 141 | phi_X = self.basis(X) 142 | phi_X2 = phi_X if X2 is None or X2 is X else self.basis(X2) 143 | return np.einsum('ij,j,kj', phi_X, self.variance, phi_X2) 144 | 145 | @Cache_this(limit=5, ignore_args=()) 146 | def Kdiag(self, X): 147 | """ 148 | Evaluate only the diagonal part of the covariance matrix at ``X`` and 149 | add it to ``target``. 150 | """ 151 | phi_X = self.basis(X) 152 | return np.einsum('ij,j,ij->i', phi_X, self.variance, phi_X) 153 | 154 | def update_gradients_full(self, dL_dK, X, X2=None): 155 | """ 156 | Given the derivative of the objective wrt the covariance matrix 157 | (dL_dK), compute the gradient wrt the parameters of this kernel, 158 | and store in the parameters object as e.g. self.variance.gradient 159 | """ 160 | phi_X = self.basis(X) 161 | phi_X2 = phi_X if X2 is None or X2 is X else self.basis(X2) 162 | i = 0 163 | self.variance.gradient = np.einsum('ij,ik,jk->k', dL_dK, phi_X, phi_X2) -------------------------------------------------------------------------------- /pymcmc/_metropolis_hastings.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple implementation of the Metropolis-Hastings algorithm. 3 | 4 | Author: 5 | Ilias Bilionis 6 | """ 7 | 8 | 9 | __all__ = ['MetropolisHastings'] 10 | 11 | 12 | from . import Model 13 | from . import GPyModel 14 | from . import Proposal 15 | from . import TunableProposalConcept 16 | from . import RandomWalkProposal 17 | from . import MALAProposal 18 | from . import DataBase 19 | import GPy 20 | import numpy as np 21 | import math 22 | import sys 23 | 24 | 25 | class MetropolisHastings(object): 26 | 27 | """ 28 | A simple implementation of the Metropolis-Hastings algorithm. 29 | 30 | :param model: The model to sample from. 31 | :type model: :class:`pymcmc.Model` 32 | :param proposal: The MCMC proposal. 33 | :type proposal: :class:`pymcmc.Proposal` 34 | :param db_filename: A filename to store the MCMC chains. If ``None``, then 35 | nothing is saved. 36 | :type db_filename: str 37 | """ 38 | 39 | def __init__(self, model, proposal=None, 40 | db_filename=None): 41 | """ 42 | Initialize the object. 43 | """ 44 | if isinstance(model, GPy.core.Model): 45 | model = GPyModel(model) 46 | else: 47 | assert isinstance(model, Model) 48 | self.model = model 49 | if proposal is None: 50 | try: 51 | y = model.grad_log_p 52 | proposal = MALAProposal() 53 | except: 54 | proposal = RandomWalkProposal() 55 | assert isinstance(proposal, Proposal) 56 | self.proposal = proposal 57 | self.db_filename = db_filename 58 | if self.has_db: 59 | self.db = DataBase(db_filename, model.__getstate__(), 60 | proposal.__getstate__()) 61 | 62 | @property 63 | def has_db(self): 64 | """ 65 | Return ``True`` if we are using a database, ``False`` otherwise. 66 | """ 67 | return self.db_filename is not None 68 | 69 | @property 70 | def acceptance_rate(self): 71 | """ 72 | Get the acceptance rate. 73 | """ 74 | return self.accepted / self.count 75 | 76 | def sample(self, num_samples, num_thin=1, num_burn=0, 77 | init_model_state=None, init_proposal_state=None, 78 | start_tuning_after=0, stop_tuning_after=None, 79 | tuning_frequency=1000, 80 | verbose=False): 81 | """ 82 | Take samples from the target. 83 | 84 | :param num_samples: The number of samples to take. 85 | :type num_samples: int 86 | :param num_thin: Record the samples every ``num_thin``. 87 | :type num_thin: int 88 | :param num_burn: Start collecting samples after ``num_burn`` 89 | samples have been burned. 90 | :type num_burn: int 91 | :param init_state: Set the initial state of the chain. If ``None``, 92 | then the initial state of the model is used. 93 | :type init_state: dict 94 | 95 | Tuning parameters: 96 | :param start_tuning_after: Start tuning after this sample. If you do 97 | not want tuning, then all you have to do 98 | is set this equal to ``None``, or to 99 | ``num_samples``. 100 | :type start_tuning_after: int 101 | :param stop_tuning_after: Stop tuning after this sample. If ``None``, then 102 | we never stop tuning. 103 | :type stop_tuning_after: int 104 | :param tuning_frequecny: Tune every so many samples. 105 | :type param: int 106 | """ 107 | # Set the initial state of the model. 108 | if init_model_state is not None: 109 | self.model.__setstate__(init_model_state) 110 | if init_proposal_state is not None: 111 | self.proposal.__setstate__(init_proposal_state) 112 | # Check the tuning parameters 113 | start_tuning_after = (num_samples if start_tuning_after is None 114 | else start_tuning_after) 115 | stop_tuning_after = (num_samples if stop_tuning_after is None 116 | else stop_tuning_after) 117 | # Initialize counters 118 | self.accepted = 0. 119 | self.count = 0. 120 | # Initialize the database 121 | if self.has_db: 122 | self.db.add_proposal(self.proposal.__getstate__()) 123 | self.db.create_new_chain() 124 | try: 125 | # Start sampling 126 | for i in xrange(num_samples): 127 | # MCMC Step 128 | new_state, log_p = self.proposal.propose(self.model) 129 | log_u = math.log(np.random.rand()) 130 | if log_u <= log_p: 131 | self.model.__setstate__(new_state) 132 | self.accepted += 1 133 | self.count += 1 134 | # Output 135 | if i > num_burn and i % num_thin == 0: 136 | # To database 137 | if self.has_db: 138 | self.db.add_chain_record(i + 1, self.accepted, 139 | self.model.__getstate__()) 140 | # To user 141 | if verbose: 142 | sys.stdout.write('sample ' + str(i + 1).zfill(len(str(num_samples))) 143 | + ' of ' + str(num_samples) 144 | + ', log_p: %.6f, acc. rate: %1.2f' 145 | % (self.model.log_p, self.acceptance_rate) 146 | + '\r') 147 | sys.stdout.flush() 148 | # Tuning 149 | if isinstance(self.proposal, TunableProposalConcept): 150 | if (i > 0 and 151 | i >= start_tuning_after and 152 | i % tuning_frequency == 0 and 153 | i <= stop_tuning_after): 154 | self.proposal.tune(self.acceptance_rate, verbose=verbose) 155 | except KeyboardInterrupt: 156 | if verbose: 157 | sys.stdout.flush() 158 | sys.stdout.write('\n') 159 | print '*** Interrupting sampling' 160 | 161 | if verbose: 162 | sys.stdout.write('\n') 163 | -------------------------------------------------------------------------------- /pymcmc/_single_parameter_tunable_proposal_concept.py: -------------------------------------------------------------------------------- 1 | """ 2 | A tunable proposal concept that tunes just one parameter. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 3/22/2014 9 | 10 | """ 11 | 12 | 13 | __all__ = ['SingleParameterTunableProposalConcept'] 14 | 15 | 16 | from . import TunableProposalConcept 17 | 18 | 19 | class SingleParameterTunableProposalConcept(TunableProposalConcept): 20 | 21 | """ 22 | A tunable proposal concept that tunes a single parameter based on the 23 | acceptance ratio only. 24 | 25 | :param param_name: The name of the parameter we are actually tuning. Note 26 | that we do test if a param_name parameter really 27 | exists in the object that inherits this concept. 28 | Therefore, the constructor to this class should be 29 | called **after** the parameter has been initialized 30 | by the proposal. 31 | :type param_name: str 32 | :param lowest_ac: The lowest allowed acceptance rate 33 | (``0 <= lowest_ac < highest_ac``). 34 | :type lowest_ac: float 35 | :param highest_ac: The highest allowed acceptance rate 36 | (``lowest_ac < highest_ac <= 1``). 37 | :type highest_ac: float 38 | :param inc_f: The factor by which we multiply the parameter if the 39 | acceptance rate is too big (``inc_f > 1``). 40 | :type inc_f: float 41 | :param dec_f: The factor by which we multiply the parameter if the 42 | acceptance rate is too low (``dec_f < 1``). 43 | """ 44 | 45 | # The name of the parameter we are tuning 46 | _param_name = None 47 | 48 | # Lowest allowed acceptance rate 49 | _lowest_ac = None 50 | 51 | # The highest allowed acceptance rate 52 | _highest_ac = None 53 | 54 | # The increase factor 55 | _inc_f = None 56 | 57 | # The decrease factor 58 | _dec_f = None 59 | 60 | @property 61 | def param_name(self): 62 | """ 63 | Set/Get the name of the parameter we are tuning. 64 | """ 65 | return self._param_name 66 | 67 | @param_name.setter 68 | def param_name(self, value): 69 | """ 70 | Set the name of the parameter we are tuning. 71 | """ 72 | value = str(value) 73 | if not hasattr(self, value): 74 | raise RuntimeError('The proposal does not contain any parameter named' 75 | ' `' + value + '`. This can probably be fixed by' 76 | ' calling the constructor of this object after' 77 | ' the parameter has been initializd in the' 78 | ' proposal. Of course, you might have the name' 79 | ' of the parameter wrong...') 80 | self._param_name = value 81 | 82 | @property 83 | def lowest_ac(self): 84 | """ 85 | Set/Get the lowest allowed acceptance rate. 86 | """ 87 | return self._lowest_ac 88 | 89 | @lowest_ac.setter 90 | def lowest_ac(self, value): 91 | """ 92 | Set the lowest ac. 93 | """ 94 | value = float(value) 95 | assert value >= 0. 96 | h_ac = self.highest_ac if self.highest_ac is not None else 1. 97 | assert value < h_ac 98 | self._lowest_ac = value 99 | 100 | @property 101 | def highest_ac(self): 102 | """ 103 | Set/Get the highest allowed acceptance rate. 104 | """ 105 | return self._highest_ac 106 | 107 | @highest_ac.setter 108 | def highest_ac(self, value): 109 | """ 110 | Set the highest ac. 111 | """ 112 | value = float(value) 113 | l_ac = self.lowest_ac if self.lowest_ac is not None else 0. 114 | assert value > l_ac 115 | assert value <= 1. 116 | self._highest_ac = value 117 | 118 | @property 119 | def inc_f(self): 120 | """ 121 | Set/Get the increase factor. 122 | """ 123 | return self._inc_f 124 | 125 | @inc_f.setter 126 | def inc_f(self, value): 127 | """ 128 | Set the increase factor. 129 | """ 130 | value = float(value) 131 | assert value > 1. 132 | self._inc_f = value 133 | 134 | @property 135 | def dec_f(self): 136 | """ 137 | Set/Get the decrease factor. 138 | """ 139 | return self._dec_f 140 | 141 | @dec_f.setter 142 | def dec_f(self, value): 143 | """ 144 | Set the decrease factor. 145 | """ 146 | value = float(value) 147 | assert value < 1. 148 | assert value > 0. 149 | self._dec_f = value 150 | 151 | def __init__(self, param_name, lowest_ac=0.2, highest_ac=0.6, 152 | inc_f=1.2, dec_f=0.7, **kwargs): 153 | """ 154 | Initialize the object. 155 | """ 156 | self.param_name = param_name 157 | self.lowest_ac = lowest_ac 158 | self.highest_ac = highest_ac 159 | self.inc_f = inc_f 160 | self.dec_f = dec_f 161 | super(SingleParameterTunableProposalConcept, self).__init__(**kwargs) 162 | 163 | def tune(self, ac, verbose=False, **kwargs): 164 | """ 165 | Tune the proposal. 166 | 167 | This really accepts just one parameter the ``ac`` and ignores any other 168 | parameter passed as ``kwargs``. 169 | """ 170 | m_f = 1. 171 | if ac < self.lowest_ac: 172 | m_f = self.dec_f 173 | elif ac > self.highest_ac: 174 | m_f = self.inc_f 175 | else: 176 | return 177 | old_param = getattr(self, self.param_name) 178 | setattr(self, self.param_name, m_f * old_param) 179 | if verbose: 180 | s = ('\nTuning parameter `' + self.param_name + 181 | '`: %2.6f -> %2.6f' % (old_param, getattr(self, self.param_name))) 182 | print s 183 | 184 | def __getstate__(self): 185 | """ 186 | Get the state of the object. 187 | """ 188 | state = {} 189 | state['lowest_ac'] = self.lowest_ac 190 | state['highest_ac'] = self.highest_ac 191 | state['inc_f'] = self.inc_f 192 | state['dec_f'] = self.dec_f 193 | state['param_name'] = self.param_name 194 | return state 195 | 196 | def __setstate__(self, state): 197 | """ 198 | Set the state of the object. 199 | """ 200 | self.lowest_ac = state['lowest_ac'] 201 | self.highest_ac = state['highest_ac'] 202 | self.inc_f = state['inc_f'] 203 | self.dec_f = state['dec_f'] 204 | self.param_name = state['param_name'] 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. --------------------------------------------------------------------------------