├── .gitignore ├── LICENSE ├── README.md ├── catalysis_data.txt ├── data.py ├── demos ├── __init__.py └── catalysis │ ├── __init__.py │ ├── _forward_model_dmnless.py │ ├── _utils.py │ ├── model_1.py │ ├── model_2.py │ └── system.py ├── pics ├── loss_eq.png └── scheme.png ├── plots.py ├── pydes ├── __init__.py ├── _cholesky.py ├── _core.py ├── _ei.py ├── _global_optimizer.py ├── _gp.py ├── _model_ensemble.py └── _prior.py ├── results ├── ei.png ├── final_fit.png ├── init_fit.png └── loss.png └── solve_inverse.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ilias Bilionis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Use Bayesian Global Optimization to Solve Inverse Problems 2 | ====================================================================== 3 | 4 | This package contains examples of application of Bayesian Global Optimization 5 | (BGO) to the solution of inverse problems. 6 | 7 | The code is developed by the 8 | [Predictive Science Laboratory (PSL)](http://www.predictivesciencelab.org) at 9 | Purdue University. 10 | 11 | Contents 12 | -------- 13 | 14 | We give a brief description of what is in each file/folder of this code. 15 | For more details, you are advised to look at the extensive comments. 16 | 17 | * [plots.py](./plots.py): 18 | Includes routines that make the plots you see. 19 | 20 | * [pydes](./pydes): 21 | This is a Python module that implements BGO. 22 | It will be a separate fully functional module in the near future. 23 | 24 | Dependencies 25 | ------------ 26 | 27 | Before trying to use the code, you should install the following dependencies: 28 | * [matplotlib](http://matplotlib.org) 29 | * [seaborn](http://stanford.edu/~mwaskom/software/seaborn/) 30 | * [GPy](https://github.com/SheffieldML/GPy) 31 | 32 | Installation 33 | ------------ 34 | 35 | There is nothing to install. You can just use the code once you enter the code 36 | directory. [pydes](./pydes) can be used as an independent python module if you 37 | add it to your PYTHONPATH. However, this is not necessary if all you want to 38 | do is run the demos. 39 | 40 | Runnings the demos 41 | ------------------ 42 | 43 | The demo is in [solve_inverse.py](./solve_inverse.py) which can be run as a 44 | common Python script. 45 | The demo calibrates the following catalysis model: 46 | ![Alt text](./pics/scheme.png) 47 | using this [data](./catalysis_data.txt) kindly provided by 48 | [Dr. Ioannis Katsounaros](http://casc.lic.leidenuniv.nl/people/katsounaros). 49 | The details of the model are explained in example 1 of our 50 | [paper](http://arxiv.org/abs/1410.5522). 51 | Contrary to the paper, here we pose the inverse problem as a minimization of 52 | a loss function: 53 | 54 | ![Alt text](./pics/loss_eq.png) 55 | 56 | The notation has the following meaning: 57 | * x: parameters that need calibrating 58 | * f(x): forward model 59 | * y: experimental data (presumably predicted by f(x)) 60 | * L(x, y): loss function 61 | 62 | The scripts produces detailed output of the BGO, and it creates the following 63 | figures: 64 | * [results/ei.png](./results/ei.png): 65 | Maximum of expected improvement at each iteration of the algorithm. 66 | ![Alt text](./results/ei.png) 67 | * [results/loss.png](./results/loss.png): 68 | Minimum observed loss function at each iteration of the BGO. 69 | ![Alt text](./results/loss.png) 70 | * [results/init_fit.png](./results/init_fit.png): 71 | The initial fit to the data. 72 | ![Alt text](./results/init_fit.png) 73 | * [results/final_fit.png](./results/final_fit.png): 74 | The final fit to the data. 75 | ![Alt text](./results/final_fit.png) 76 | -------------------------------------------------------------------------------- /catalysis_data.txt: -------------------------------------------------------------------------------- 1 | # Experimental data for NO3- --> N2 + bi-products reaction. 2 | # NO3- NO2- N2 NH3 N2O 3 | 500.00 0.00 0.00 0.00 0.00 4 | 250.95 107.32 18.51 3.33 4.98 5 | 123.66 132.33 74.85 7.34 20.14 6 | 84.47 98.81 166.19 13.14 42.10 7 | 30.24 38.74 249.78 19.54 55.98 8 | 27.94 10.42 292.32 24.07 60.65 9 | 13.54 6.11 309.50 27.26 62.54 10 | -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some utilities related to the data. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 7/19/2015 9 | 10 | """ 11 | 12 | 13 | __all__ = ['load_catalysis_data'] 14 | 15 | 16 | import numpy as np 17 | 18 | 19 | def load_catalysis_data(): 20 | """ 21 | Loads the catalysis data and casts them to the right format. 22 | """ 23 | data = np.loadtxt('catalysis_data.txt') 24 | return data.flatten() / 500. -------------------------------------------------------------------------------- /demos/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initialize the demos. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 6/6/2014 9 | 10 | """ 11 | 12 | 13 | import catalysis 14 | -------------------------------------------------------------------------------- /demos/catalysis/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initialize the module. 3 | 4 | Author: 5 | Panagiotis Tsilifis 6 | 7 | Date: 8 | 5/22/2014 9 | 10 | """ 11 | 12 | from _forward_model_dmnless import * 13 | -------------------------------------------------------------------------------- /demos/catalysis/_forward_model_dmnless.py: -------------------------------------------------------------------------------- 1 | """ 2 | Forward problem (dimensionless) 3 | 4 | Author: 5 | Panagiotis Tsilifis 6 | Date: 7 | 06/09/2014 8 | 9 | """ 10 | 11 | 12 | import numpy as np 13 | from model_1 import * 14 | from model_2 import * 15 | from _utils import * 16 | import sys 17 | 18 | 19 | 20 | class CatalysisModelDMNLESS(): 21 | """ 22 | A class representing the forward model of the catalysis problem. 23 | """ 24 | 25 | def __init__(self): 26 | """ 27 | Initialize the object 28 | """ 29 | self.num_input = 5 30 | self.num_output = 35 31 | 32 | def __call__(self, z): 33 | """ 34 | Solves the dynamical system for given parameters x. 35 | 36 | """ 37 | z = view_as_column(z) 38 | x = np.exp(z) 39 | # Points where the solution will be evaluated 40 | t = np.array([0., 1./6, 1./3, 1./2, 2./3, 5./6, 1.]) 41 | t = view_as_column(t) 42 | # Initial condition 43 | y0 = np.array([1., 0., 0., 0., 0., 0.]) 44 | y0 = view_as_column(y0) 45 | assert x.shape[0] == 5 46 | sol = f(x[:,0], y0[:,0], t[:,0]) 47 | J = df(x[:,0], y0[:,0], t[:,0]) 48 | H = df2(x[:,0], y0[:,0], t[:,0]) 49 | y = np.delete(sol.reshape((7,6)), 2, 1).flatten() # The 3rd species is unobservable 50 | dy = np.array([np.delete(J[:,i].reshape((7,6)), 2, 1).reshape(35) for i in range(J.shape[1])]) # Delete the 3rd species 51 | d2y = np.zeros((35, H.shape[1], H.shape[2])) 52 | for i in range(H.shape[1]): 53 | for j in range(H.shape[2]): 54 | d2y[:,i,j]= np.delete(H[:,i,j].reshape((7,6)), 2, 1).reshape(35) # Delete the 3rd species 55 | return y 56 | -------------------------------------------------------------------------------- /demos/catalysis/_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various utilities used throughout the code. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 5/19/2014 9 | 10 | """ 11 | 12 | 13 | __all__ = ['regularize_array', 'make_vector', 'call_many', 'view_as_column', 14 | 'euclidean_distance'] 15 | 16 | 17 | import numpy as np 18 | from scipy.spatial.distance import cdist 19 | 20 | 21 | def regularize_array(x): 22 | """ 23 | Regularize a numpy array. 24 | 25 | If x is 2D then nothing happens to it. If it is 1D then it is converted to 26 | 2D with 1 row and as many columns as the number of elements in x. 27 | 28 | :param x: The array to regularize. 29 | :type x: :class:`numpy.ndarray` 30 | :returns: Regularized version of x. 31 | :rtype: :class:`numpy.ndarray` 32 | 33 | .. note:: 34 | It does nothing if ``x`` is not a numpy array. 35 | """ 36 | if not isinstance(x, np.ndarray): 37 | return x 38 | if x.ndim == 1: 39 | x = x[None, :] 40 | return x 41 | 42 | 43 | def make_vector(x): 44 | """ 45 | Make a vector out of x. We will attempt to make an array out of x and then 46 | flatten it. 47 | """ 48 | return np.array(x).flatten() 49 | 50 | 51 | def call_many(x, func, return_numpy=True): 52 | """ 53 | Assuming the ``x`` is a 2D array, evaluate ``func(x[i, :])`` for each ``i`` 54 | and return the result as a numpy array. 55 | 56 | :param x: The evaluation points. 57 | :type x: :class:`numpy.ndarray` 58 | :param func: The function. 59 | :param return_numpy: If ``True``, then it puts all the outputs in a numpy array. 60 | Otherwise, it returns a list. 61 | """ 62 | x = regularize_array(x) 63 | out = [func(x[i, :]) for i in xrange(x.shape[0])] 64 | if return_numpy: 65 | return np.array(out) 66 | return out 67 | 68 | 69 | def view_as_column(x): 70 | """ 71 | View x as a column vector. 72 | """ 73 | if x.ndim == 1: 74 | x = x[:, None] 75 | elif x.ndim == 2 and x.shape[0] == 1: 76 | x = x.T 77 | return x 78 | 79 | 80 | def euclidean_distance(x, y): 81 | """ 82 | Returns the Euclidean distance between two numpy arrays. 83 | """ 84 | return cdist(regularize_array(x), regularize_array(y)) 85 | -------------------------------------------------------------------------------- /demos/catalysis/model_1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements the catalysis model as found in Yiannis paper. 3 | 4 | """ 5 | 6 | from system import * 7 | 8 | 9 | def make_A(kappa): 10 | """ 11 | Make the matrix of the dynamical system from ``kappa``. 12 | """ 13 | A = np.array([[-kappa[0], 0, 0, 0, 0, 0], 14 | [kappa[0], -kappa[1]-kappa[3]-kappa[4], 0, 0, 0, 0], 15 | [0, kappa[1], -kappa[2], 0, 0, 0], 16 | [0, 0, kappa[2], 0, 0, 0], 17 | [0, kappa[4], 0, 0, 0, 0], 18 | [0, kappa[3], 0, 0, 0, 0]]) 19 | # The derivative of A with respect to kappa 20 | d = A.shape[0] 21 | s = kappa.shape[0] 22 | dA = np.zeros((d, d, s)) 23 | dA[0, 0, 0] = -1. 24 | dA[1, 0, 0] = 1. 25 | dA[1, 1, 1] = -1. 26 | dA[1, 1, 4] = -1. 27 | dA[1, 1, 3] = -1. 28 | dA[2, 1, 1] = 1. 29 | dA[2, 2, 2] = -1. 30 | dA[3, 2, 2] = 1. 31 | dA[4, 1, 4] = 1. 32 | dA[5, 1, 3] = 1. 33 | return A, dA 34 | 35 | def make_full_A(kappa): 36 | """ 37 | Make the matrix of the dynamical system from ``kappa``. 38 | """ 39 | assert kappa.shape[0] == 36 40 | A = kappa.reshape((6,6)) 41 | dA = np.zeros((36,36)) 42 | for i in xrange(36): 43 | dA[i,i] = 1 44 | dA = dA.reshape((6,6,36)) 45 | return A, dA 46 | 47 | def f(kappa, y0, t): 48 | """ 49 | Evaluate the model at ``kappa`` and at times ``t`` with initial 50 | conditions ``y0``. 51 | 52 | It returns a flatten version of the system, i.e.: 53 | y_1(t_1) 54 | ... 55 | y_d(t_1) 56 | ... 57 | y_1(t_K) 58 | ... 59 | y_d(t_K) 60 | """ 61 | A = make_A(kappa)[0] 62 | r = ode(f0) 63 | r.set_initial_value(y0, 0).set_f_params(A) 64 | y_m = [y0[None, :]] 65 | for tt in t[1:]: 66 | r.integrate(tt) 67 | y_m.append(r.y[None, :]) 68 | y_m = np.vstack(y_m) 69 | return y_m.flatten() 70 | 71 | def f_full(kappa, y0, t): 72 | """ 73 | Evaluate the model at ``kappa`` and at times ``t`` with initial 74 | conditions ``y0``. 75 | 76 | It returns a flatten version of the system, i.e.: 77 | y_1(t_1) 78 | ... 79 | y_d(t_1) 80 | ... 81 | y_1(t_K) 82 | ... 83 | y_d(t_K) 84 | """ 85 | A = make_full_A(kappa)[0] 86 | r = ode(f0) 87 | r.set_initial_value(y0, 0).set_f_params(A) 88 | y_m = [y0[None, :]] 89 | for tt in t[1:]: 90 | r.integrate(tt) 91 | y_m.append(r.y[None, :]) 92 | y_m = np.vstack(y_m) 93 | return y_m.flatten() 94 | 95 | def df(kappa, y0, t): 96 | """ 97 | Evaluate the derivative of the model derivatives at ``kappa`` and at times ``t`` 98 | with initial conditions ``y0``. 99 | 100 | This returns a matrix of the following form: 101 | dy_1(t_1) / dkappa_1 ... dy_1(t_1) / dkappa_s 102 | ... 103 | dy_d(t_1) / dkappa_1 ... dy_d(t_1) / dkappa_s 104 | ... 105 | dy_1(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 106 | ... 107 | dy_d(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 108 | """ 109 | A, dA = make_A(kappa) 110 | d = A.shape[0] 111 | r = ode(f1) # Look at system.py: this should be the f of the adjoint 112 | r.set_initial_value(np.hstack([y0, np.zeros((d ** 3, ))])).set_f_params(A) 113 | y_m = [r.y] 114 | for tt in t[1:]: 115 | r.integrate(tt) 116 | y_m.append(r.y[None, :]) 117 | y_m = np.vstack(y_m) 118 | # This is the Jacobian with respect to the full matrix A 119 | J = y_m[:, d:].reshape((t.shape[0], d, d, d)) 120 | # Now we apply the chain rule to compute the jacobian wrt kappa 121 | J_kappa = np.einsum('ijkl,klr', J, dA) 122 | return J_kappa.reshape((d * t.shape[0], kappa.shape[0])) 123 | 124 | def df_full(kappa, y0, t): 125 | """ 126 | Evaluate the derivative of the model derivatives at ``kappa`` and at times ``t`` 127 | with initial conditions ``y0``. 128 | 129 | This returns a matrix of the following form: 130 | dy_1(t_1) / dkappa_1 ... dy_1(t_1) / dkappa_s 131 | ... 132 | dy_d(t_1) / dkappa_1 ... dy_d(t_1) / dkappa_s 133 | ... 134 | dy_1(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 135 | ... 136 | dy_d(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 137 | """ 138 | A, dA = make_full_A(kappa) 139 | d = A.shape[0] 140 | r = ode(f1) # Look at system.py: this should be the f of the adjoint 141 | r.set_initial_value(np.hstack([y0, np.zeros((d ** 3, ))])).set_f_params(A) 142 | y_m = [r.y] 143 | for tt in t[1:]: 144 | r.integrate(tt) 145 | y_m.append(r.y[None, :]) 146 | y_m = np.vstack(y_m) 147 | # This is the Jacobian with respect to the full matrix A 148 | J = y_m[:, d:].reshape((t.shape[0], d, d, d)) 149 | # Now we apply the chain rule to compute the jacobian wrt kappa 150 | J_kappa = np.einsum('ijkl,klr', J, dA) 151 | return J_kappa.reshape((d * t.shape[0], kappa.shape[0])) 152 | 153 | def ndf(kappa0, y0, t, h=1e-3): 154 | """ 155 | Numerically compute the Jacobian of f at x0. 156 | """ 157 | y = f(kappa0, y0, t) 158 | kappa = kappa0.copy() 159 | J = np.zeros((y.shape[0], kappa0.shape[0])) 160 | for i in xrange(kappa0.shape[0]): 161 | kappa[i] += h 162 | py = f(kappa, y0, t) 163 | kappa[i] -= h 164 | J[:, i] = (py - y) / h 165 | return J 166 | 167 | 168 | if __name__ == '__main__': 169 | # Test if what we are doing make sense 170 | import matplotlib.pyplot as plt 171 | # Some kappas to evaluate the model at: 172 | kappa = np.array([0.0216, 0.0292, 0.0219, 0.0021, 0.0048]) 173 | # The observed data (to read the initial conditions): 174 | obs_data = np.loadtxt('obs_data.txt') 175 | # Remember that the column of the data that has all zeros contains X 176 | # which is not observed. This is the 4th column. 177 | # The observed times 178 | t = obs_data[:, 0] 179 | # The intial points 180 | y0 = obs_data[0, 1:] 181 | # Here is how we evaluate the model 182 | y = f(kappa, y0, t) 183 | # Here is how we evaluate the derivatives of the model 184 | dy = df(kappa, y0, t) 185 | # Now evaluate the numerical derivatives at kappa 186 | J = ndf(kappa, y0, t) 187 | for i in xrange(kappa.shape[0]): 188 | plt.plot(dy[:, i], 'r', linewidth=2) 189 | plt.plot(J[:, i], '--g', linewidth=2) 190 | plt.legend(['Adjoint derivative', 'Numerical Derivative']) 191 | plt.show() 192 | -------------------------------------------------------------------------------- /demos/catalysis/model_2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements the catalysis model as found in Yiannis paper. 3 | 4 | """ 5 | 6 | from system import * 7 | 8 | 9 | def make_A(kappa): 10 | """ 11 | Make the matrix of the dynamical system from ``kappa``. 12 | """ 13 | A = np.array([[-kappa[0], 0, 0, 0, 0, 0], 14 | [kappa[0], -kappa[1]-kappa[3]-kappa[4], 0, 0, 0, 0], 15 | [0, kappa[1], -kappa[2], 0, 0, 0], 16 | [0, 0, kappa[2], 0, 0, 0], 17 | [0, kappa[4], 0, 0, 0, 0], 18 | [0, kappa[3], 0, 0, 0, 0]]) 19 | # The derivative of A with respect to kappa 20 | d = A.shape[0] 21 | s = kappa.shape[0] 22 | dA = np.zeros((d, d, s)) 23 | dA[0, 0, 0] = -1. 24 | dA[1, 0, 0] = 1. 25 | dA[1, 1, 1] = -1. 26 | dA[1, 1, 4] = -1. 27 | dA[1, 1, 3] = -1. 28 | dA[2, 1, 1] = 1. 29 | dA[2, 2, 2] = -1. 30 | dA[3, 2, 2] = 1. 31 | dA[4, 1, 4] = 1. 32 | dA[5, 1, 3] = 1. 33 | return A, dA 34 | 35 | def make_full_A(kappa): 36 | """ 37 | Make the matrix of the dynamical system from ``kappa``. 38 | """ 39 | assert kappa.shape[0] == 36 40 | A = kappa.reshape((6,6)) 41 | dA = np.zeros((36,36)) 42 | for i in xrange(36): 43 | dA[i,i] = 1 44 | dA = dA.reshape((6,6,36)) 45 | return A, dA 46 | 47 | def f(kappa, y0, t): 48 | """ 49 | Evaluate the model at ``kappa`` and at times ``t`` with initial 50 | conditions ``y0``. 51 | 52 | It returns a flatten version of the system, i.e.: 53 | y_1(t_1) 54 | ... 55 | y_d(t_1) 56 | ... 57 | y_1(t_K) 58 | ... 59 | y_d(t_K) 60 | """ 61 | A = make_A(kappa)[0] 62 | r = ode(f0) 63 | r.set_initial_value(y0, 0).set_f_params(A) 64 | y_m = [y0[None, :]] 65 | for tt in t[1:]: 66 | r.integrate(tt) 67 | y_m.append(r.y[None, :]) 68 | y_m = np.vstack(y_m) 69 | return y_m.flatten() 70 | 71 | def f_full(kappa, y0, t): 72 | """ 73 | Evaluate the model at ``kappa`` and at times ``t`` with initial 74 | conditions ``y0``. 75 | 76 | It returns a flatten version of the system, i.e.: 77 | y_1(t_1) 78 | ... 79 | y_d(t_1) 80 | ... 81 | y_1(t_K) 82 | ... 83 | y_d(t_K) 84 | """ 85 | A = make_full_A(kappa)[0] 86 | r = ode(f0) 87 | r.set_initial_value(y0, 0).set_f_params(A) 88 | y_m = [y0[None, :]] 89 | for tt in t[1:]: 90 | r.integrate(tt) 91 | y_m.append(r.y[None, :]) 92 | y_m = np.vstack(y_m) 93 | return y_m.flatten() 94 | 95 | 96 | def df2(kappa, y0, t): 97 | """ 98 | Evaluate the derivative of the model derivatives at ``kappa`` and at times ``t`` 99 | with initial conditions ``y0``. 100 | 101 | This returns a matrix of the following form: 102 | dy_1(t_1) / dkappa_1 ... dy_1(t_1) / dkappa_s 103 | ... 104 | dy_d(t_1) / dkappa_1 ... dy_d(t_1) / dkappa_s 105 | ... 106 | dy_1(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 107 | ... 108 | dy_d(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 109 | """ 110 | A, dA = make_A(kappa) 111 | d = A.shape[0] 112 | r = ode(f2) # Look at system.py: this should be the f of the adjoint 113 | r.set_initial_value(np.hstack([y0, np.zeros((d ** 3 + d ** 5, ))])).set_f_params(A) 114 | y_m = [r.y] 115 | for tt in t[1:]: 116 | r.integrate(tt) 117 | y_m.append(r.y[None, :]) 118 | y_m = np.vstack(y_m) 119 | # This is the Jacobian with respect to the full matrix A 120 | J = y_m[:, d:d+d**3].reshape((t.shape[0], d, d, d)) 121 | # Now we apply the chain rule to compute the jacobian wrt kappa 122 | J_kappa = np.einsum('ijkl,klr', J, dA) 123 | 124 | J2 = y_m[:,d+d**3:d+d**3+d**5].reshape((t.shape[0], d, d**2, d**2)) 125 | #print len(J2[0,0,0,:]) 126 | ## Trying something New 127 | JJ = np.zeros((t.shape[0],d,kappa.shape[0],d**2)) 128 | for j in range(t.shape[0]): 129 | for k in range(d): 130 | for i in range(d**2): 131 | JJ[j,k,:,i] = np.einsum('kl,klr',J2[j,k,i,:].reshape(d,d), dA) 132 | J_kappa2 = np.einsum('ijklr,lrs', JJ.reshape(t.shape[0],d,kappa.shape[0],d,d), dA) 133 | return J_kappa2.reshape((d * t.shape[0], kappa.shape[0], kappa.shape[0])) 134 | 135 | def df2_full(kappa, y0, t): 136 | """ 137 | Evaluate the derivative of the model derivatives at ``kappa`` and at times ``t`` 138 | with initial conditions ``y0``. 139 | 140 | This returns a matrix of the following form: 141 | dy_1(t_1) / dkappa_1 ... dy_1(t_1) / dkappa_s 142 | ... 143 | dy_d(t_1) / dkappa_1 ... dy_d(t_1) / dkappa_s 144 | ... 145 | dy_1(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 146 | ... 147 | dy_d(t_K) / dkappa_1 ... dy_d(t_K) / dkappa_s 148 | """ 149 | A, dA = make_full_A(kappa) 150 | d = A.shape[0] 151 | r = ode(f2) # Look at system.py: this should be the f of the adjoint 152 | r.set_initial_value(np.hstack([y0, np.zeros((d ** 3 + d ** 5, ))])).set_f_params(A) 153 | y_m = [r.y] 154 | for tt in t[1:]: 155 | r.integrate(tt) 156 | y_m.append(r.y[None, :]) 157 | y_m = np.vstack(y_m) 158 | # This is the Jacobian with respect to the full matrix A 159 | J = y_m[:, d:d+d**3].reshape((t.shape[0], d, d, d)) 160 | # Now we apply the chain rule to compute the jacobian wrt kappa 161 | J_kappa = np.einsum('ijkl,klr', J, dA) 162 | 163 | J2 = y_m[:,d+d**3:d+d**3+d**5].reshape((t.shape[0], d, d**2, d**2)) 164 | #print len(J2[0,0,0,:]) 165 | ## Trying something New 166 | JJ = np.zeros((t.shape[0],d,kappa.shape[0],d**2)) 167 | for j in range(t.shape[0]): 168 | for k in range(d): 169 | for i in range(d**2): 170 | JJ[j,k,:,i] = np.einsum('kl,klr',J2[j,k,i,:].reshape(d,d), dA) 171 | J_kappa2 = np.einsum('ijklr,lrs', JJ.reshape(t.shape[0],d,kappa.shape[0],d,d), dA) 172 | return J_kappa2.reshape((d * t.shape[0], kappa.shape[0], kappa.shape[0])) 173 | 174 | def ndf2(kappa0, y0 ,t, h=1e-3): 175 | 176 | y = f(kappa0, y0, t) 177 | kappa = kappa0.copy() 178 | J = np.zeros((y.shape[0], kappa0.shape[0], kappa0.shape[0])) 179 | for i in xrange(kappa0.shape[0]): 180 | for j in xrange(kappa0.shape[0]): 181 | kappa[i] += h 182 | kappa[j] += h 183 | py1 = f(kappa, y0, t) 184 | kappa[j] -= 2*h 185 | py2 = f(kappa, y0 ,t) 186 | kappa[j] += 2*h 187 | kappa[i] -= 2*h 188 | py3 = f(kappa, y0, t) 189 | kappa[j] -= 2*h 190 | py4 = f(kappa, y0, t) 191 | kappa[i] += h 192 | kappa[j] += h 193 | J[:, i, j] = (py1 - py2 - py3 + py4) / (4*h**2) 194 | return J 195 | 196 | if __name__ == '__main__': 197 | # Test if what we are doing make sense 198 | import matplotlib.pyplot as plt 199 | # Some kappas to evaluate the model at: 200 | kappa = np.array([0.0216, 0.0292, 0.0219, 0.0021, 0.0048]) 201 | # The observed data (to read the initial conditions): 202 | obs_data = np.loadtxt('obs_data.txt') 203 | # Remember that the column of the data that has all zeros contains X 204 | # which is not observed. This is the 4th column. 205 | # The observed times 206 | t = obs_data[:, 0] 207 | # The intial points 208 | y0 = obs_data[0, 1:] 209 | # Here is how we evaluate the model 210 | y = f(kappa, y0, t) 211 | # Here is how we evaluate the derivatives of the model 212 | ddy = df2(kappa, y0, t) 213 | # Now evaluate the numerical derivatives at kappa 214 | J = ndf2(kappa, y0, t) 215 | for i in xrange(kappa.shape[0]): 216 | for j in xrange(kappa.shape[0]): 217 | plt.plot(ddy[:, i, j], 'r', linewidth=2) 218 | plt.plot(J[:, i, j], '--g', linewidth=2) 219 | plt.legend(['Adjoint derivative', 'Numerical Derivative']) 220 | plt.show() 221 | -------------------------------------------------------------------------------- /demos/catalysis/system.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of a generic adjoint solver for linear dynamical systems. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 4/23/2014 9 | """ 10 | 11 | 12 | import numpy as np 13 | from scipy.integrate import ode 14 | 15 | 16 | def f0(t, y, A): 17 | """ 18 | The nomral dynamics. 19 | """ 20 | return np.dot(A, y) 21 | 22 | def f1(t, y, A): 23 | """ 24 | The normal dynamics and of the 1st derivatives wrt A. 25 | """ 26 | d = A.shape[0] 27 | y_0 = y[:d] 28 | f_y_0 = f0(t, y_0, A) 29 | y_1 = y[d:d + d**3].reshape((d, d, d)) 30 | f_y_1 = np.einsum('ij,jkl->ikl', A, y_1) 31 | for i in xrange(d): 32 | f_y_1[i, i, :] += y_0 33 | return np.hstack([f_y_0, f_y_1.flatten()]) 34 | 35 | def f2(t, y, A): 36 | """ 37 | The normal dynamics and of the 2st derivatives wrt A. 38 | """ 39 | d = A.shape[0] 40 | y_0 = y[:d] 41 | f_y_0 = f0(t, y_0, A) 42 | y_1 = y[d:d + d**3].reshape((d, d, d)) 43 | f_y_1 = np.einsum('ij,jkl->ikl', A, y_1) 44 | for i in xrange(d): 45 | f_y_1[i, i, :] += y_0 46 | #return np.hstack([f_y_0, f_y_1.flatten()]) 47 | y_2 = y[d+d**3:d+d**3+d**5].reshape((d,d**2,d**2)) 48 | f_y_2 = np.einsum('ij,jkl->ikl',A, y_2) 49 | for i in xrange(d): 50 | for j in xrange(i*d,(i+1)*d): 51 | f_y_2[i,j,:] += y_1[j-i*d,:,:].flatten() 52 | for i in xrange(d): 53 | for j in xrange(i*d,(i+1)*d): 54 | f_y_2[i,:,j] += y_1[j-i*d,:,:].flatten() 55 | return np.hstack([f_y_0, f_y_1.flatten(), f_y_2.flatten()]) 56 | 57 | 58 | def loss(A, t, y): 59 | r = ode(f0) 60 | r.set_initial_value(y[0, :], t[0]).set_f_params(A) 61 | y_m = np.zeros((t.shape[0], r.y.shape[0])) 62 | y_m[0, :] = r.y 63 | for i in xrange(1, t.shape[0]): 64 | r.integrate(t[i]) 65 | y_m[i, :] = r.y 66 | tmp = y_m - y 67 | return np.dot([1, 1, 0, 1, 1, 1], np.linalg.norm(tmp, axis=0) ** 2) 68 | 69 | 70 | def dloss(A, t, y): 71 | r = ode(f1) 72 | d = A.shape[0] 73 | r.set_initial_value(np.hstack([y[0, :], np.zeros((d ** 3, ))])).set_f_params(A) 74 | y_m = np.zeros((t.shape[0], r.y.shape[0])) 75 | y_m[0, :] = r.y 76 | for i in xrange(1, t.shape[0]): 77 | r.integrate(t[i]) 78 | y_m[i, :] = r.y 79 | y_m0 = y_m[:, :d] 80 | y_m1 = y_m[:, d:d + d**3].reshape((t.shape[0], d, d, d)) 81 | tmp = y_m0 - y 82 | return 2. * np.einsum('ij,j,ijkl->kl', tmp, np.array([1, 1, 0, 1, 1, 1]), y_m1) 83 | -------------------------------------------------------------------------------- /pics/loss_eq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/inverse-bgo/c0170394c6761f69ed91650649ae5925b61fe258/pics/loss_eq.png -------------------------------------------------------------------------------- /pics/scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/inverse-bgo/c0170394c6761f69ed91650649ae5925b61fe258/pics/scheme.png -------------------------------------------------------------------------------- /plots.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some functions that make useful plots. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 7/19/2015 9 | 10 | """ 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | from mpl_toolkits.mplot3d import Axes3D 15 | import seaborn as sns 16 | import itertools 17 | import os 18 | from data import * 19 | 20 | 21 | __all__ = ['plot_catalysis_output', 'make_plots', 'plot_1d_callback'] 22 | 23 | 24 | def plot_catalysis_output(fig_name, 25 | Y, 26 | y=load_catalysis_data(), 27 | t=np.array([0.0, 1./6, 1./3, 1./2, 2./3, 5./6, 1.]), 28 | colors=['b', 'r', 'g', 'k', 'm'], 29 | linestyles=['', '--', '-.', '--+', ':'], 30 | markerstyles=['o', 'v', 's', 'D', 'p'], 31 | legend=False, 32 | title=None): 33 | """ 34 | Draw the output of the catalysis problem. 35 | 36 | :param fig_name: A name for the figure. 37 | :param Y: The samples observed. 38 | :param y: The observations. 39 | :parma t: The times of observations. 40 | """ 41 | shape = (t.shape[0], Y.shape[0] / t.shape[0]) 42 | y = y.reshape(shape) 43 | Y = Y.reshape(shape) 44 | import matplotlib.pyplot as plt 45 | fig = plt.figure() 46 | ax = fig.add_subplot(111) 47 | for i in xrange(5): 48 | ax.plot(t, Y[:, i], colors[i] + linestyles[i], linewidth=2, markersize=5.) 49 | for i in xrange(5): 50 | ax.plot(t, y[:, i], colors[i] + markerstyles[i], markersize=10) 51 | ax.set_xlabel('Time ($\\tau$)', fontsize=26) 52 | ax.set_ylabel('Concentration', fontsize=26) 53 | ax.set_title(title, fontsize=26) 54 | plt.setp(ax.get_xticklabels(), fontsize=26) 55 | plt.setp(ax.get_yticklabels(), fontsize=26) 56 | if legend: 57 | leg = plt.legend(['$\operatorname{NO}_3^-$', '$\operatorname{NO}_2^-$', 58 | '$\operatorname{N}_2$', '$\operatorname{N}_2\operatorname{O}$', 59 | '$\operatorname{NH}_3$'], loc='best') 60 | plt.setp(leg.get_texts(), fontsize=26) 61 | ax.set_ylim([0., 1.5]) 62 | plt.tight_layout() 63 | if fig_name: 64 | print '> writing:', fig_name 65 | plt.savefig(fig_name) 66 | else: 67 | plt.show() 68 | 69 | 70 | def make_plots(bgo, to_file=False): 71 | """ 72 | Makes the demonstration plots. 73 | """ 74 | make_ei_plot(bgo, to_file) 75 | make_energy_plot(bgo, to_file) 76 | if not to_file: 77 | plt.show() 78 | 79 | def make_ei_plot(bgo, to_file): 80 | """ 81 | Plots the evolution of the expected improvement as BGO runs. 82 | """ 83 | fig, ax = plt.subplots() 84 | it = np.arange(1, len(bgo.af_values) + 1) 85 | ax.plot(it, bgo.af_values) 86 | ax.plot(it, [bgo.tol] * len(it), '--') 87 | ax.set_title('Evolution of expected improvement', fontsize=16) 88 | ax.set_xlabel('Iteration', fontsize=16) 89 | ax.set_ylabel('EI', fontsize=16) 90 | if to_file: 91 | figname = os.path.join('results', 'ei.png') 92 | print '> writing:', figname 93 | plt.savefig(figname) 94 | plt.close(fig) 95 | 96 | 97 | def make_energy_plot(bgo, to_file): 98 | """ 99 | Plots the evolution of the energy as BGO runs. 100 | """ 101 | fig, ax = plt.subplots() 102 | it = np.arange(1, len(bgo.current_best_value) + 1) 103 | ax.plot(it, bgo.current_best_value) 104 | ax.set_title('Evolution of minimum observed loss', fontsize=16) 105 | ax.set_xlabel('Iteration', fontsize=16) 106 | ax.set_ylabel('Loss', fontsize=16) 107 | if to_file: 108 | figname = os.path.join('results', 'loss.png') 109 | print '> writing:', figname 110 | plt.savefig(figname) 111 | plt.close(fig) 112 | 113 | 114 | __count_callback = 0 115 | def plot_1d_callback(bgo, molecule, interactive): 116 | """ 117 | Plots the evolution of BGO for the 1D case. 118 | """ 119 | global __count_callback 120 | __count_callback += 1 121 | fig, ax = plt.subplots() 122 | ax.set_ylabel('$V(r)$', fontsize=16) 123 | ax.set_xlabel('$r$', fontsize=16) 124 | ax.set_ylim([0, 20]) 125 | ax2 = ax.twinx() 126 | ax2.set_ylim([0, 1.5]) 127 | ax2.set_ylabel('EI$(r)$', color='g', fontsize=16) 128 | for tl in ax2.get_yticklabels(): 129 | tl.set_color('g') 130 | xx = np.linspace(bgo.X_design.min(), bgo.X_design.max(), 100) 131 | q = bgo.model.predict_quantiles(xx[:, None], quantiles=(50, 2.5, 97.5)) 132 | ax.plot(bgo.X, bgo.Y, 'kx', markersize=10, markeredgewidth=2) 133 | ax.plot(bgo.X[-1], bgo.Y[-1], 'go', markersize=10, markeredgewidth=2) 134 | ax.plot(xx, q[0], 'b', label='Mean prediction') 135 | ax.fill_between(xx, q[1].flatten(), q[2].flatten(), color='blue', alpha=0.25) 136 | ax2.plot(bgo.X_design, bgo.af / bgo.af_values[0], 'g.') 137 | if interactive: 138 | plt.show(block=True) 139 | else: 140 | figname = os.path.join('results', 'bgo_' + 141 | molecule.get_chemical_formula()+ '_' 142 | + str(__count_callback).zfill(2) 143 | + '.png') 144 | print '> writing:', figname 145 | plt.savefig(figname) 146 | -------------------------------------------------------------------------------- /pydes/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A package for optimizing expensive simulators and experiments. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 10/16/2014 9 | 10 | """ 11 | 12 | 13 | from _cholesky import * 14 | from _core import * 15 | from _ei import * 16 | from _prior import * 17 | from _model_ensemble import * 18 | from _global_optimizer import * -------------------------------------------------------------------------------- /pydes/_cholesky.py: -------------------------------------------------------------------------------- 1 | """Everything related to Cholesky decomposition. 2 | 3 | Author: 4 | Ilias Bilionis 5 | 6 | Date: 7 | 2/1/2013 8 | """ 9 | 10 | 11 | __all__ = ['update_cholesky', 'update_cholesky_linear_system'] 12 | 13 | 14 | import numpy as np 15 | import scipy.linalg 16 | 17 | 18 | def update_cholesky(L, B, C): 19 | """Updates the Cholesky decomposition of a matrix. 20 | 21 | We assume that L is the lower Cholesky decomposition of a matrix A, and 22 | we want to calculate the Cholesky decomposition of the matrix: 23 | A B 24 | B.T C. 25 | It can be easily shown that the new decomposition is given by: 26 | L 0 27 | D21 D22, 28 | where 29 | L * D21.T = B, 30 | and 31 | D22 * D22.T = C - D21 * D21.T. 32 | 33 | Arguments: 34 | L --- The Cholesky decomposition of the original 35 | n x n matrix. 36 | B --- The n x m upper right part of the new matrix. 37 | C --- The m x m bottom diagonal part of the new matrix. 38 | 39 | Return: 40 | The lower Cholesky decomposition of the new matrix. 41 | """ 42 | assert isinstance(L, np.ndarray) 43 | assert L.ndim == 2 44 | assert L.shape[0] == L.shape[1] 45 | assert isinstance(B, np.ndarray) 46 | assert B.ndim == 2 47 | assert B.shape[0] == L.shape[0] 48 | assert isinstance(C, np.ndarray) 49 | assert C.ndim == 2 50 | assert B.shape[1] == C.shape[0] 51 | assert C.shape[0] == C.shape[1] 52 | n = L.shape[0] 53 | m = B.shape[1] 54 | L_new = np.zeros((n + m, n + m)) 55 | L_new[:n, :n] = L 56 | D21 = L_new[n:, :n] 57 | D22 = L_new[n:, n:] 58 | D21[:] = scipy.linalg.solve_triangular(L, B, lower=True).T 59 | if m == 1: 60 | D22[:] = np.sqrt(C[0, 0] - np.dot(D21, D21.T)) 61 | else: 62 | D22[:] = scipy.linalg.cholesky(C - np.dot(D21, D21.T), lower=True) 63 | return L_new 64 | 65 | 66 | def update_cholesky_linear_system(x, L_new, z): 67 | """Updates the solution of a linear system involving a Cholesky factor. 68 | 69 | Assume that originally we had an n x n matrix lower triangular matrix L 70 | and that we have already solved the linear system: 71 | L * x = y. 72 | We wish now to solve the linear system: 73 | L_new * x_new = y_new, 74 | where L_new is again lower triangular but the fist n x n component is 75 | identical to L and y_new is (y, z). The solution is: 76 | x_new = (x, x_u), 77 | where x_u is the solution of the triangular system: 78 | D22 * x_u = z - D21 * x, 79 | where D22 is the lower m x m component of L_new and D21 is the m x n 80 | bottom left component of L_new. 81 | 82 | Arguments: 83 | x --- The original solution. It can be either a vector or a 84 | matrix. 85 | L_new --- The new lower Cholesky factor. 86 | z --- The new right hand side as described above. 87 | 88 | Return: 89 | The solution of the new linear system. 90 | """ 91 | assert isinstance(x, np.ndarray) 92 | assert x.ndim <= 2 93 | regularized_x = False 94 | if x.ndim == 1: 95 | regularized_x = True 96 | x = x.reshape((x.shape[0], 1)) 97 | assert isinstance(L_new, np.ndarray) 98 | assert L_new.shape[0] == L_new.shape[1] 99 | assert isinstance(z, np.ndarray) 100 | assert z.ndim <= 2 101 | regularized_z = False 102 | if z.ndim == 1: 103 | regularized_z = True 104 | z = z.reshape((z.shape[0], 1)) 105 | assert x.shape[1] == z.shape[1] 106 | assert L_new.shape[0] == x.shape[0] + z.shape[0] 107 | n = x.shape[0] 108 | D22 = L_new[n:, n:] 109 | D21 = L_new[n:, :n] 110 | x_u = scipy.linalg.solve_triangular(D22, z - np.dot(D21, x), lower=True) 111 | y = np.vstack([x, x_u]) 112 | if regularized_x or regularized_z: 113 | y = y.reshape((y.shape[0],)) 114 | return y 115 | -------------------------------------------------------------------------------- /pydes/_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global Optimization of Expensive Functions. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 10/15/2014 9 | 01/29/2015 10 | 11 | """ 12 | 13 | 14 | __all__ = ['expected_improvement', 15 | 'fb_expected_improvement', 16 | 'expected_information_gain', 17 | 'minimize', 'maximize', 18 | 'plot_summary', 'plot_summary_2d'] 19 | 20 | 21 | import GPy 22 | import GPy.inference.mcmc 23 | from GPy.inference.mcmc import HMC 24 | import numpy as np 25 | import math 26 | import scipy 27 | import scipy.stats as stats 28 | from scipy.integrate import quad 29 | #from choldate import choldowndate, cholupdate 30 | 31 | 32 | from statsmodels.sandbox.distributions.multivariate import mvnormcdf 33 | import math 34 | 35 | 36 | def remove(mu, S, i): 37 | """ 38 | Remove i element from mu and S. 39 | """ 40 | mu_ni = np.hstack([mu[:i], mu[i+1:]]) 41 | S_nini = np.array(np.bmat([[S[:i, :i], S[:i, i+1:]], 42 | [S[i+1:, :i], S[i+1:, i+1:]]])) 43 | return mu_ni, S_nini 44 | 45 | 46 | def maxpdf(x, mu, S): 47 | s = np.zeros(x.shape[0]) 48 | d = mu.shape[0] 49 | for i in xrange(d): 50 | mu_i = mu[i] 51 | S_ii = S[i, i] 52 | mu_ni, S_nini = remove(mu, S, i) 53 | S_ini = np.array(np.bmat([[S[:i, i], S[i+1:, i]]])) 54 | mu_nii = mu_ni[:, None] + np.dot(S_ini.T, x[None, :] - mu_i) / S_ii 55 | S_ninii = S_nini - np.dot(S_ini, S_ini.T) / S_ii 56 | phi_i = norm.pdf(x, loc=mu_i, scale=np.sqrt(S_ii)) 57 | Phi_i = np.array([mvnormcdf(x[j], mu_nii[:, j], S_ninii) 58 | for j in xrange(x.shape[0])]) 59 | s += phi_i * Phi_i 60 | return s 61 | 62 | 63 | def expected_improvement(X_design, model, mode='min'): 64 | """ 65 | Compute the Expected Improvement criterion at ``x``. 66 | """ 67 | y = model.Y.flatten() 68 | m_s, v_s = model.predict(X_design)[:2] 69 | m_s = m_s.flatten() 70 | v_s = v_s.flatten() 71 | s_s = np.sqrt(v_s) 72 | if mode == 'min': 73 | m_n = np.min(y) 74 | u = (m_n - m_s) / s_s 75 | elif mode == 'max': 76 | m_n = np.max(y) 77 | u = (m_s - m_n) / s_s 78 | else: 79 | raise NotImplementedError('I do not know what to do with mode %s' %mode) 80 | ei = s_s * (u * stats.norm.cdf(u) + stats.norm.pdf(u)) 81 | return ei 82 | 83 | 84 | def fb_expected_improvement(X_design, model, mode='min', stepsize=1e-2, 85 | num_samples=100): 86 | """ 87 | Compute the fully Bayesian expected improvement criterion. 88 | """ 89 | model.rbf.variance.set_prior(GPy.priors.LogGaussian(0., 1.)) 90 | model.rbf.lengthscale.set_prior(GPy.priors.LogGaussian(0., 0.1)) 91 | mcmc = HMC(model, stepsize=stepsize) 92 | params = mcmc.sample(num_samples=num_samples) 93 | ei_all = [] 94 | for i in xrange(params.shape[0]): 95 | model.rbf.variance = params[i, 0] 96 | model.rbf.lengthscale = params[i, 1] 97 | ei = expected_improvement(X_design, model, mode=mode) 98 | ei_all.append(ei) 99 | ei_all = np.array(ei_all) 100 | ei_fb = ei_all.mean(axis=0) 101 | return ei_fb 102 | 103 | 104 | def min_qoi(X_design, f): 105 | """ 106 | A QoI that corresponds to the min of the function. 107 | """ 108 | return np.argmin(f, axis=0) 109 | 110 | 111 | def kl_divergence(g1, g2): 112 | """ 113 | Compute the KL divergence. 114 | """ 115 | f = lambda(x): g1.evaluate([[x]]) * np.log(g1.evaluate([[x]]) / g2.evaluate([[x]])) 116 | return quad(f, 0, 6) 117 | 118 | 119 | def expected_information_gain(X_design, model, num_Omegas=1000, 120 | num_y=100, 121 | qoi=min_qoi, 122 | qoi_bins=None, 123 | qoi_num_bins=20): 124 | """ 125 | Compute the expected information gain criterion at ``x``. 126 | """ 127 | import matplotlib.pyplot as plt 128 | m_d, K_d = model.predict(X_design, full_cov=True)[:2] 129 | U_d = scipy.linalg.cholesky(K_d, lower=False) 130 | Omegas = np.random.randn(X_design.shape[0], num_Omegas) 131 | delta_y_i = np.random.randn(num_y) 132 | # Find the histogram of Q the current data 133 | S_d = m_d + np.dot(U_d.T, Omegas) 134 | Q_d = qoi(X_design, S_d) 135 | tmp = stats.itemfreq(Q_d) 136 | yy = model.posterior_samples(X_design, 10) 137 | plt.plot(X_design, yy, 'm', linewidth=2) 138 | plt.savefig('examples/samples.png') 139 | plt.clf() 140 | p_d = np.zeros((X_design.shape[0],)) 141 | p_d[np.array(tmp[:, 0], dtype='int')] = tmp[:, 1] / np.sum(tmp[:, 1]) 142 | if qoi_bins is None and qoi is min_qoi: 143 | #qoi_bins = np.linspace(np.min(Q_d), np.max(Q_d), qoi_num_bins)[None, :] 144 | qoi_bins = np.linspace(X_design[0, 0], X_design[-1, 0], qoi_num_bins)[None, :] 145 | H_d, e_d = np.histogramdd(Q_d, normed=True, bins=qoi_bins) 146 | delta_e_d = e_d[0][1] - e_d[0][0] 147 | #p_d = H_d * delta_e_d 148 | plt.plot(X_design, p_d) 149 | plt.plot(X_design, m_d) 150 | plt.plot(model.X, model.Y, 'ro', markersize=10) 151 | plt.hist(X_design[Q_d, 0], normed=True, alpha=0.5) 152 | plt.savefig('examples/kde_Q.png') 153 | plt.clf() 154 | print 'Entropy:', stats.entropy(p_d) 155 | G = np.zeros((X_design.shape[0],)) 156 | p_d += 1e-16 157 | for i in xrange(X_design.shape[0]): 158 | u_di = K_d[:, i] / math.sqrt(K_d[i, i]) 159 | u_di = u_di[:, None] 160 | #K_dd = K_d - np.dot(u_di, u_di.T) 161 | #K_dd += np.eye(K_d.shape[0]) * 1e-6 162 | choldowndate(U_d, u_di.flatten().copy()) 163 | #U_d = scipy.linalg.cholesky(K_dd, lower=False) 164 | # Pick a value for y: 165 | Omegas = np.random.randn(X_design.shape[0], num_Omegas) 166 | delta_y_i = np.random.randn(num_y) 167 | m_dgi = m_d + delta_y_i * u_di 168 | S_dgi = m_dgi[:, :, None] + np.dot(U_d.T, Omegas)[:, None, :] 169 | #for j in xrange(num_y): 170 | # print S_dgi[:, j, :] 171 | # plt.plot(X_design, S_dgi[:, j, :], 'm', linewidth=0.5) 172 | # plt.plot(model.X, model.likelihood.Y, 'ro', markersize=10) 173 | # plt.savefig('examples/ig_S_' + str(i).zfill(2) + '_' + str(j).zfill(2) + '.png') 174 | # plt.clf() 175 | Q_dgi = qoi(X_design, S_dgi) 176 | #print Q_dgi 177 | #quit() 178 | p_d_i = np.zeros((num_y, X_design.shape[0])) 179 | for j in xrange(num_y): 180 | tmp = stats.itemfreq(Q_dgi[j, :]) 181 | p_d_i[j, np.array(tmp[:, 0], dtype='int')] = tmp[:, 1] / np.sum(tmp[:, 1]) 182 | p_d_i += 1e-16 183 | G[i] = np.mean([stats.entropy(p_d_i[j, :], p_d) for j in xrange(num_y)]) 184 | #G[i] = np.mean([-stats.entropy(p_d_i[j, :]) for j in xrange(num_y)]) 185 | #plt.plot(X_design, S_dgi[:, :, 0], 'm', linewidth=0.5) 186 | #plt.plot(X_design, m_d, 'r', linewidth=2) 187 | plt.plot(model.X, np.zeros((model.X.shape[0], 1)), 'ro', markersize=10) 188 | plt.plot(X_design, np.mean(p_d_i, axis=0), 'g', linewidth=2) 189 | plt.savefig('examples/ig_S_' + str(i).zfill(2) + '.png') 190 | plt.clf() 191 | print X_design[i, 0], G[i] 192 | cholupdate(U_d, u_di.flatten().copy()) 193 | plt.plot(X_design, G) 194 | plt.savefig('examples/ig_KL.png') 195 | plt.clf() 196 | return G 197 | 198 | 199 | def plot_summary(f, X_design, model, prefix, G, Gamma_name): 200 | """ 201 | Plot a summary of the current iteration. 202 | """ 203 | import matplotlib.pyplot as plt 204 | X = model.X 205 | y = model.Y 206 | m_s, k_s = model.predict(X_design, full_cov=True) 207 | m_05, m_95 = model.predict_quantiles(X_design) 208 | fig, ax1 = plt.subplots() 209 | ax1.plot(X_design, f(X_design), 'b', linewidth=2) 210 | ax1.plot(X, y, 'go', linewidth=2, markersize=10, markeredgewidth=2) 211 | ax1.plot(X_design, m_s, 'r--', linewidth=2) 212 | ax1.fill_between(X_design.flatten(), m_05.flatten(), m_95.flatten(), 213 | color='grey', alpha=0.5) 214 | ax1.set_ylabel('$f(x)$', fontsize=16) 215 | ax2 = ax1.twinx() 216 | ax2.plot(X_design, G, 'g', linewidth=2) 217 | ax2.set_ylabel('$%s(x)$' % Gamma_name, fontsize=16, color='g') 218 | #ax2.set_ylim([0., 3.]) 219 | plt.setp(ax2.get_yticklabels(), color='g') 220 | png_file = prefix + '.png' 221 | print 'Writing:', png_file 222 | plt.savefig(png_file) 223 | plt.clf() 224 | 225 | 226 | def plot_summary_2d(f, X_design, model, prefix, G, Gamma_name): 227 | """ 228 | Plot a summary of the current iteration. 229 | """ 230 | import matplotlib.pyplot as plt 231 | n = np.sqrt(X_design.shape[0]) 232 | X1, X2 = (X_design[:, i].reshape((n, n)) for i in range(2)) 233 | GG = G.reshape((n, n)) 234 | fig = plt.figure() 235 | ax = fig.add_subplot(111) 236 | cax = ax.contourf(X1, X2, GG) 237 | fig.colorbar(cax) 238 | ax.set_xlabel('$x_1$', fontsize=16) 239 | ax.set_ylabel('$x_2$', fontsize=16) 240 | plt.savefig(prefix + '_' + Gamma_name + '.png') 241 | plt.clf() 242 | X = model.X 243 | m_s, k_s = model.predict(X_design) 244 | M_s = m_s.reshape((n, n)) 245 | S_s = np.sqrt(k_s.reshape((n, n))) 246 | fig = plt.figure() 247 | ax = fig.add_subplot(111) 248 | cax = ax.contourf(X1, X2, M_s) 249 | fig.colorbar(cax) 250 | ax.plot(X[:, 0], X[:, 1], 'k.', markersize=10) 251 | ax.set_xlabel('$x_1$', fontsize=16) 252 | ax.set_ylabel('$x_2$', fontsize=16) 253 | plt.savefig(prefix + '_mean.png') 254 | plt.clf() 255 | fig = plt.figure() 256 | ax = fig.add_subplot(111) 257 | cax = ax.contourf(X1, X2, S_s) 258 | fig.colorbar(cax) 259 | ax.plot(X[:, 0], X[:, 1], 'k.', markersize=10) 260 | ax.set_xlabel('$x_1$', fontsize=16) 261 | ax.set_ylabel('$x_2$', fontsize=16) 262 | plt.savefig(prefix + '_std.png') 263 | plt.clf() 264 | 265 | 266 | 267 | def minimize(f, X_init, X_design, prefix="minimize", Gamma=expected_improvement, 268 | Gamma_name='EI', max_it=10, tol=1e-1, callback=None): 269 | """ 270 | Optimize f using a limited number of evaluations. 271 | """ 272 | X = X_init 273 | y = np.array([f(X[i, :]) for i in xrange(X.shape[0])]) 274 | k = GPy.kern.RBF(X.shape[1], ARD=True) 275 | for count in xrange(max_it): 276 | model = GPy.models.GPRegression(X, y, k) 277 | model.Gaussian_noise.variance.constrain_fixed(1e-6) 278 | model.optimize() 279 | print str(model) 280 | G = Gamma(X_design, model) 281 | if callback is not None: 282 | callback(f, X_design, model, 283 | prefix + '_' + str(count).zfill(2), G, Gamma_name) 284 | i = np.argmax(G) 285 | if G[i] < tol: 286 | print '*** converged' 287 | break 288 | print 'I am adding:', X_design[i:(i+1), :] 289 | print 'which has a G of', G[i] 290 | X = np.vstack([X, X_design[i:(i+1), :]]) 291 | y = np.vstack([y, f(X_design[i, :])]) 292 | print 'it =', count+1, ', min =', np.min(y), ' arg min =', X[np.argmin(y), :] 293 | return X, y 294 | 295 | 296 | def maximize(f, X_init, X_design, prefix='maximize', Gamma=expected_improvement, 297 | Gamma_name='EI', max_it=10, tol=1e-1, callback=None): 298 | """ 299 | Maximize the function ``f``. 300 | """ 301 | f_minus = lambda(x) : -f(x) 302 | return minimize(f_minus, X_init, X_design, prefix=prefix, Gamma=Gamma, 303 | Gamma_name=Gamma_name, max_it=max_it, tol=tol) 304 | -------------------------------------------------------------------------------- /pydes/_ei.py: -------------------------------------------------------------------------------- 1 | """ 2 | An implmentation of the expected improvement data acquisition function. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 5/1/2015 9 | 10 | """ 11 | 12 | 13 | __all__ = ['expected_improvement'] 14 | 15 | 16 | 17 | import numpy as np 18 | import scipy.stats as stats 19 | 20 | 21 | def expected_improvement(Xd, model, mode='min', noise=None): 22 | """ 23 | Compute the expected improvement at ``Xd``. 24 | 25 | :param Xd: The design points on which to evaluate the improvement. 26 | :param model: The model of which we want to know the improvement. It must 27 | have a method called ``predict()`` that accepts a matrix 28 | representing the design points and returns a tuple representing 29 | the mean predictions and the predictive variance. 30 | :param noise: The variance of the measurement noise on each design point. If 31 | ``None``, then we attempt to get this noise from 32 | ``model.likelihood.noise``, if possible. 33 | :returns: The expected improvement on all design points. 34 | """ 35 | assert hasattr(model, 'predict') 36 | if noise is None: 37 | if hasattr(model, 'likelihood') and hasattr(model.likelihood, 'variance'): 38 | noise = float(model.likelihood.variance) 39 | else: 40 | noise = 0. 41 | X = model.X.copy() 42 | #m_obs = model.predict(X)[0].flatten() 43 | m_obs = model.Y.flatten() 44 | m_s, v_s = model.predict(Xd)[:2] 45 | m_s = m_s.flatten() 46 | v_s = v_s.flatten() - noise 47 | s_s = np.sqrt(v_s) 48 | idx = np.isnan(s_s) 49 | #print idx.any() 50 | s_s[np.isnan(s_s)] = 1e-10 51 | if mode == 'min': 52 | i_n = np.argmin(m_obs) 53 | m_n = m_obs[i_n] 54 | u = (m_n - m_s) / s_s 55 | elif mode == 'max': 56 | i_n = np.argmax(m_obs) 57 | m_n = m_obs[i_n] 58 | u = (m_s - m_n) / s_s 59 | else: 60 | raise NotImplementedError('I do not know what to do with mode %s' %mode) 61 | ei = s_s * (u * stats.norm.cdf(u) + stats.norm.pdf(u)) 62 | i0 = np.argmax(ei) 63 | #print 'EI: ', ei[i0], 'Pred.:', m_s[i0], 'Var.:', s_s[i0] 64 | return ei, i_n, m_n -------------------------------------------------------------------------------- /pydes/_global_optimizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | A myopic global optimizer class. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 5/1/2015 9 | 10 | """ 11 | 12 | 13 | __all__ = ['GlobalOptimizer'] 14 | 15 | 16 | import warnings 17 | warnings.filterwarnings('ignore') 18 | import numpy as np 19 | from collections import Iterable 20 | import math 21 | import GPy 22 | import matplotlib.pyplot as plt 23 | import seaborn 24 | from . import expected_improvement 25 | from . import ModelEnsemble 26 | from . import LogLogisticPrior 27 | from . import JeffreysPrior 28 | 29 | 30 | class GlobalOptimizer(object): 31 | 32 | """ 33 | A global optimizer class. 34 | 35 | It is essentially a myopic, sequential, global optimizer. 36 | 37 | :param func: The function you wish to optimize. 38 | :arapm args: Additional arguments for the function we optimize. 39 | :param afunc: The acquisition function you wish to use. 40 | :param afunc_args: Extra arguments to the optimization function. 41 | 42 | """ 43 | 44 | # The initial design 45 | _X_init = None 46 | 47 | _X_masked_init = None 48 | 49 | # The initial observations 50 | _Y_init = None 51 | 52 | # The total design we have available 53 | _X_design = None 54 | 55 | _X_masked_design = None 56 | 57 | # The indexes of the observations we have made so far (list of integers) 58 | _idx_X_obs = None 59 | 60 | # The objectives we have observed so far (list of whatever the observations are) 61 | _Y_obs = None 62 | 63 | # The function we wish to optimize 64 | _func = None 65 | 66 | # Extra arguments to func 67 | _args = None 68 | 69 | # The acquisition function we are going to use 70 | _acquisition_function = None 71 | 72 | # Extra arguments to the acquisition function 73 | _af_args = None 74 | 75 | @property 76 | def X_init(self): 77 | """ 78 | :getter: Get the initial design. 79 | """ 80 | return self._X_init 81 | 82 | @X_init.setter 83 | def X_init(self, value): 84 | """ 85 | :setter: Set the initial design. 86 | """ 87 | assert isinstance(value, Iterable) 88 | self._X_init = value 89 | 90 | @property 91 | def Y_init(self): 92 | """ 93 | :getter: Get the initial observations. 94 | """ 95 | return self._Y_init 96 | 97 | @Y_init.setter 98 | def Y_init(self, value): 99 | """ 100 | :setter: Set the initial observations. 101 | """ 102 | if value is not None: 103 | assert isinstance(value, Iterable) 104 | value = np.array(value) 105 | self._Y_init = value 106 | 107 | @property 108 | def X_design(self): 109 | """ 110 | :getter: Get the design. 111 | """ 112 | return self._X_design 113 | 114 | @X_design.setter 115 | def X_design(self, value): 116 | """ 117 | :setter: Set the design. 118 | """ 119 | assert isinstance(value, Iterable) 120 | self._X_design = value 121 | 122 | @property 123 | def idx_X_obs(self): 124 | """ 125 | :getter: The indexes of currently observed design points. 126 | """ 127 | return self._idx_X_obs 128 | 129 | @property 130 | def Y_obs(self): 131 | """ 132 | :getter: The values of the currently observed design points. 133 | """ 134 | return self._Y_obs 135 | 136 | @property 137 | def func(self): 138 | """ 139 | :getter: Get the function we are optimizing. 140 | """ 141 | return self._func 142 | 143 | @func.setter 144 | def func(self, value): 145 | """ 146 | :setter: Set the function we are optimizing. 147 | """ 148 | assert hasattr(value, '__call__') 149 | self._func = value 150 | 151 | @property 152 | def args(self): 153 | """ 154 | :getter: The extra arguments of func. 155 | """ 156 | return self._args 157 | 158 | @property 159 | def acquisition_function(self): 160 | """ 161 | :getter: Get the acquisition function. 162 | """ 163 | return self._acquisition_function 164 | 165 | @acquisition_function.setter 166 | def acquisition_function(self, value): 167 | """ 168 | :setter: Set the acquisition function. 169 | """ 170 | assert hasattr(value, '__call__') 171 | self._acquisition_function = value 172 | 173 | @property 174 | def af_args(self): 175 | """ 176 | :getter: The arguments of the acquisition function. 177 | """ 178 | return self._af_args 179 | 180 | @property 181 | def X(self): 182 | """ 183 | :getter: Get all the currently observed points. 184 | """ 185 | return np.vstack([self.X_init, self.X_design[self.idx_X_obs]]) 186 | 187 | @property 188 | def X_masked(self): 189 | """ 190 | :getter: Get all the currently observed points. 191 | """ 192 | return np.vstack([self._X_masked_init, self._X_masked_design[self.idx_X_obs]]) 193 | 194 | @property 195 | def Y(self): 196 | """ 197 | :getter: Get all the currently observed objectives. 198 | """ 199 | if len(self.Y_obs) == 0: 200 | return np.array(self.Y_init) 201 | return np.vstack([self.Y_init, self.Y_obs]) 202 | 203 | @property 204 | def best_value(self): 205 | """ 206 | :getter: Get the best value. 207 | """ 208 | return self.current_best_value[-1] 209 | 210 | @property 211 | def best_index(self): 212 | """ 213 | :getter: Get the current best index. 214 | """ 215 | return self.current_best_index[-1] 216 | 217 | @property 218 | def best_design(self): 219 | i = np.argmin(self.Y) 220 | return self.X[i, :] 221 | 222 | @property 223 | def best_masked_design(self): 224 | i = np.argmin(self.Y) 225 | return self.X_masked[i, :] 226 | 227 | def __init__(self, X_init, X_design, func, args=(), Y_init=None, 228 | af=expected_improvement, af_args=(), 229 | X_masked_init=None, 230 | X_masked_design=None): 231 | """ 232 | Initialize the object. 233 | """ 234 | self.X_init = X_init 235 | self.X_design = X_design 236 | self.Y_init = Y_init 237 | self.func = func 238 | self._args = args 239 | self.acquisition_function = af 240 | self._af_args = af_args 241 | self._idx_X_obs = [] 242 | self._Y_obs = [] 243 | self._X_masked_init = X_masked_init 244 | self._X_masked_design = X_masked_design 245 | 246 | def optimize(self, max_it=100, tol=1e-1, fixed_noise=1e-8, 247 | GPModelClass=GPy.models.GPRegression, 248 | verbose=True, 249 | add_at_least=10, 250 | callback_func=None, 251 | callback_func_args=(), 252 | **kwargs): 253 | """ 254 | Optimize the objective. 255 | 256 | :param callback_func: A function that should be called at each iteration. 257 | :param callback_func_args: Arguments to the callback function. 258 | """ 259 | assert add_at_least >= 1 260 | if self.Y_init is None: 261 | X = self.X_init if self._X_masked_init is None else self._X_masked_init 262 | self.Y_init = [self.func(x, *self.args) for x in X] 263 | self.tol = tol 264 | self.max_it = max_it 265 | self.af_values = [] 266 | self.selected_index = [] 267 | self.current_best_value = [] 268 | self.current_best_index = [] 269 | for it in xrange(max_it): 270 | kernel = GPy.kern.RBF(self.X_init.shape[1], ARD=True) 271 | model = GPModelClass(self.X, self.Y, kernel) 272 | self.model = model 273 | if fixed_noise is not None: 274 | model.Gaussian_noise.variance.unconstrain() 275 | model.Gaussian_noise.variance.constrain_fixed(fixed_noise) 276 | #model.kern.lengthscale.unconstrain() 277 | #model.kern.lengthscale.fix(.8) 278 | model.optimize_restarts(20, verbose=False) 279 | af, i_n, m_n = self.acquisition_function(self.X_design, model, *self.af_args) 280 | i = np.argmax(af) 281 | self.af = af 282 | self.selected_index.append(i) 283 | self.af_values.append(af[i]) 284 | self.current_best_value.append(self.Y.min()) 285 | if it >= add_at_least and af[i] / self.af_values[0] < tol: 286 | if verbose: 287 | print '*** Converged (af[i] / afmax0 = {0:1.7f}) < {1:e}'.format(af[i] / self.af_values[0], tol) 288 | break 289 | self.idx_X_obs.append(i) 290 | if self._X_masked_design is None: 291 | self.Y_obs.append(self.func(self.X_design[i], *self.args)) 292 | else: 293 | self.Y_obs.append(self.func(self._X_masked_design[i], *self.args)) 294 | if verbose: 295 | print '> Iter {0:d}, Selected struct.: {1:d}, Max EI = {2:1.4f}, Min seen energy: {4:1.3f}'.format(it, i, af[i] / self.af_values[0], self.Y_obs[-1][0], self.Y.min()) 296 | self.current_best_index.append(np.argmin(self.Y)) 297 | if callback_func is not None: 298 | callback_func(*callback_func_args) -------------------------------------------------------------------------------- /pydes/_gp.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/inverse-bgo/c0170394c6761f69ed91650649ae5925b61fe258/pydes/_gp.py -------------------------------------------------------------------------------- /pydes/_model_ensemble.py: -------------------------------------------------------------------------------- 1 | """ 2 | A model ensemble built by sampling from the posterior of a model using, presumably, 3 | MCMC. 4 | 5 | Author: 6 | Ilias Bilionis 7 | 8 | Date: 9 | 5/1/2015 10 | 11 | """ 12 | 13 | 14 | __all__ = ['ModelEnsemble'] 15 | 16 | 17 | import numpy as np 18 | from GPy import Model 19 | from GPy.inference.mcmc import HMC 20 | from . import expected_improvement 21 | 22 | 23 | class ModelEnsemble(object): 24 | 25 | """ 26 | A collection of models. 27 | 28 | :param model: The underlying model. 29 | :param param_list: The parameter list representing samples from the posterior 30 | of the model. (2D numpy array, rows are samples, columns are 31 | parameters) 32 | :param w: The weights corresponding to each parameter. 33 | """ 34 | 35 | # The underlying model 36 | _model = None 37 | 38 | # List of parameters 39 | _param_list = None 40 | 41 | # Weight corresponding to each model (normalized) 42 | _w = None 43 | 44 | @property 45 | def model(self): 46 | """ 47 | :getter: Get the model. 48 | """ 49 | return self._model 50 | 51 | @model.setter 52 | def model(self, value): 53 | """ 54 | :setter: The model. 55 | """ 56 | assert isinstance(value, Model) 57 | self._model = value 58 | 59 | @property 60 | def param_list(self): 61 | """ 62 | :getter: Get the parameter list. 63 | """ 64 | return self._param_list 65 | 66 | @param_list.setter 67 | def param_list(self, value): 68 | """ 69 | :setter: Set the parameter list. 70 | """ 71 | assert isinstance(value, np.ndarray) 72 | assert value.ndim == 2 73 | self._param_list = value 74 | 75 | @property 76 | def w(self): 77 | """ 78 | :getter: Get the weights. 79 | """ 80 | return self._w 81 | 82 | @w.setter 83 | def w(self, value): 84 | """ 85 | :setter: Set the weights. 86 | """ 87 | assert isinstance(value, np.ndarray) 88 | assert value.ndim == 1 89 | assert np.all(value >= 0.) 90 | self._w = value / np.sum(value) 91 | 92 | @property 93 | def num_particles(self): 94 | """ 95 | :getter: Get the number of particles in the ensemble. 96 | """ 97 | return self.param_list.shape[0] 98 | 99 | def get_model(self, i): 100 | """ 101 | Get the model with index ``i``. 102 | """ 103 | self.model.unfixed_param_array[:] = self.param_list[i, :] 104 | return self.model 105 | 106 | def __init__(self, model, param_list, w=None): 107 | """ 108 | Initialize the object. 109 | """ 110 | self.model = model 111 | self.param_list = param_list 112 | if w is None: 113 | w = np.ones(param_list.shape[0]) 114 | self.w = w 115 | 116 | def posterior_mean_samples(self, X): 117 | """ 118 | Return samples of the posterior mean. 119 | """ 120 | Y = [] 121 | for i in xrange(self.num_particles): 122 | model = self.get_model(i) 123 | y = model.predict(X)[0] 124 | Y.append(y[:, 0]) 125 | Y = np.array(Y) 126 | return Y 127 | 128 | def posterior_samples(self, X, size=10): 129 | """ 130 | Draw samples from the posterior of the ensemble. 131 | """ 132 | Y = [] 133 | for i in xrange(self.num_particles): 134 | model = self.get_model(i) 135 | y = model.posterior_samples(X, size=size).T 136 | Y.append(y) 137 | Y = np.vstack(Y) 138 | idx = np.arange(Y.shape[0]) 139 | return Y[np.random.permutation(idx), :] 140 | 141 | def predict_quantiles(self, X, quantiles=(50, 2.5, 97.5), size=1000): 142 | """ 143 | Get the predictive quantiles. 144 | """ 145 | if self.num_particles == 1: 146 | tmp = self.get_model(0).predict_quantiles(X, quantiles=quantiles) 147 | return np.array(tmp)[:, :, 0] 148 | else: 149 | Y = self.posterior_samples(X, size=size) 150 | return np.percentile(Y, quantiles, axis=0) 151 | 152 | def raw_predict(self, X): 153 | """ 154 | Return the prediction of each model at ``X``. 155 | """ 156 | Y = [] 157 | V = [] 158 | for i in xrange(self.num_particles): 159 | y, v = self.get_model(i).predict(X) 160 | Y.append(y) 161 | V.append(v) 162 | Y = np.array(Y) 163 | V = np.array(V) 164 | return Y, V 165 | 166 | def predict(self, X, **kwargs): 167 | """ 168 | Predict using the ensemble at ``X``. 169 | 170 | :returns: tuple containing the media, 0.025 quantile, 0.095 quantile 171 | """ 172 | return self.predict_quantiles(X) 173 | 174 | def eval_afunc(self, X, func, args=()): 175 | """ 176 | Evaluate an acquisition function at X using all models. 177 | """ 178 | res = [] 179 | X_ns = [] # Locations of max/min 180 | M_ns = [] # Values of max/min 181 | for i in xrange(self.num_particles): 182 | r, i_n, m_n = func(X, self.get_model(i), *args) 183 | res.append(r) 184 | X_ns.append(i_n) 185 | M_ns.append(m_n) 186 | res = np.array(res) 187 | X_ns = np.array(X_ns) 188 | M_ns = np.array(M_ns) 189 | return np.average(res, axis=0, weights=self.w), X_ns, M_ns 190 | 191 | def expected_improvement(self, X, **kwargs): 192 | """ 193 | Compute the expected improvement. 194 | """ 195 | return self.eval_afunc(X, expected_improvement, **kwargs) 196 | 197 | @staticmethod 198 | def train(model, num_samples=0, thin=1, burn=0, num_restarts=10, **kwargs): 199 | """ 200 | Train a Gaussian process model. 201 | 202 | :param model: The model to optimize. 203 | :param num_restarts: The number of restarts when maximizing the posterior. 204 | :param num_samples: The number of samples from the posterior. If zero, 205 | then we construct a single particle approximation to 206 | the posterior. If greater than zero, then we sample 207 | from the posterior of the hyper-parameters using Hybrid MC. 208 | :param thin: The number of samples to skip. 209 | :param burn: The number of samples to burn. 210 | :param **kwargs: Any parameters of GPy.inference.mcmc.HMC 211 | 212 | """ 213 | model.optimize_restarts(num_restarts=num_restarts, verbose=False) 214 | if num_samples == 0: 215 | param_list = np.array(model.unfixed_param_array)[None, :] 216 | else: 217 | hmc = HMC(model) 218 | tmp = hmc.sample(num_samples, **kwargs) 219 | param_list = tmp[burn::thin, :] 220 | w = np.ones(param_list.shape[0]) 221 | return ModelEnsemble(model, param_list, w=w) -------------------------------------------------------------------------------- /pydes/_prior.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some priors for GPy. 3 | 4 | Author: 5 | Ilias Bilionis 6 | 7 | Date: 8 | 5/5/2015 9 | 10 | """ 11 | 12 | 13 | __all__ = ['LogLogisticPrior', 'JeffreysPrior'] 14 | 15 | 16 | import GPy 17 | import numpy as np 18 | 19 | 20 | class LogLogisticPrior(GPy.priors.Prior): 21 | 22 | """ 23 | Log-Logistic prior suitable for lengthscale parameters. 24 | 25 | From Conti & O'Hagan (2010) 26 | """ 27 | 28 | domain = GPy.priors._POSITIVE 29 | 30 | def __init__(self): 31 | """ 32 | Initialize the object. 33 | """ 34 | pass 35 | 36 | def __str__(self): 37 | return 'LogLog' 38 | 39 | def lnpdf(self, x): 40 | return -np.log(1. + x ** 2) 41 | 42 | def lnpdf_grad(self, x): 43 | return -2. * x / (1. + x ** 2) 44 | 45 | def rvs(self, n): 46 | return np.exp(np.random.logistic(size=n)) 47 | 48 | 49 | class JeffreysPrior(GPy.priors.Prior): 50 | 51 | """ 52 | The uninformative Jeffrey's prior used for scale parameters. 53 | """ 54 | 55 | domain = GPy.priors._POSITIVE 56 | 57 | def __init__(self): 58 | """ 59 | Initialize the object. 60 | """ 61 | pass 62 | 63 | def __str__(self): 64 | return 'JeffreysPrior()' 65 | 66 | def lnpdf(self, x): 67 | return -np.log(x) 68 | 69 | def lnpdf_grad(self, x): 70 | return -1. / x 71 | 72 | def rvs(self, n): 73 | return np.ones(n) -------------------------------------------------------------------------------- /results/ei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/inverse-bgo/c0170394c6761f69ed91650649ae5925b61fe258/results/ei.png -------------------------------------------------------------------------------- /results/final_fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/inverse-bgo/c0170394c6761f69ed91650649ae5925b61fe258/results/final_fit.png -------------------------------------------------------------------------------- /results/init_fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/inverse-bgo/c0170394c6761f69ed91650649ae5925b61fe258/results/init_fit.png -------------------------------------------------------------------------------- /results/loss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictiveScienceLab/inverse-bgo/c0170394c6761f69ed91650649ae5925b61fe258/results/loss.png -------------------------------------------------------------------------------- /solve_inverse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstration of how Bayesian Global Optimization can be used to solve 3 | an inverse problem. 4 | 5 | Author: 6 | Ilias Bilionis 7 | 8 | Date: 9 | 7/19/2015 10 | 11 | """ 12 | 13 | 14 | from demos.catalysis import CatalysisModelDMNLESS 15 | import numpy as np 16 | import pydes 17 | from plots import * 18 | from data import * 19 | import os 20 | 21 | 22 | def loss_func(x, y, catal_model): 23 | """ 24 | The loss function we use in the formulation of the inverse problem. 25 | We want to minimize this. 26 | """ 27 | d = catal_model(x) - y 28 | return [np.dot(d, d)] 29 | 30 | 31 | if __name__ == '__main__': 32 | # Fix the random seed in order to ensure reproducibility of results: 33 | np.random.seed(13456) 34 | # Load the experimental data 35 | y = load_catalysis_data() 36 | # Construct the catalysis solver 37 | # Note that the solver accepts as input the logarithm of 38 | # the kinetic coefficients. 39 | catal_model = CatalysisModelDMNLESS() 40 | # Number of initial data pool (user) 41 | n_init = 20 42 | # Number of candidate test points (user) 43 | n_design = 10000 44 | # Maximum iterations for BGO (user) 45 | max_it = 100 46 | # Tolerance for BGO (user) 47 | tol = 1e-4 48 | # Minimum range for the kinetic coefficients (user) 49 | kappa_min = 0.2 50 | # Maximum range for the kinetic coefficients (user) 51 | kappa_max = 6. 52 | # Start the algorithm 53 | print 'SOLVING INVERSE PROBLEMS USING BGO'.center(80) 54 | print '=' * 80 55 | print '{0:20s}: {1:d}'.format('Init. pool size', n_init) 56 | print '{0:20s}: {1:d}'.format('Design. pool size', n_design) 57 | print '{0:20s}: {1:d}'.format('Max BGO iter.', max_it) 58 | print '{0:20s}: {1:e}'.format('Tol. of BGO:', tol) 59 | print '=' * 80 60 | print '> starting computations' 61 | # We work with the logarithms of the kinetic coefficients 62 | log_k_min = np.log(kappa_min) 63 | log_k_max = np.log(kappa_max) 64 | # The initial data pool 65 | X_init = log_k_min + \ 66 | (log_k_max - log_k_min) * np.random.rand(n_init, catal_model.num_input) 67 | # The design pool 68 | X_design = log_k_min + \ 69 | (log_k_max - log_k_min) * np.random.rand(n_design, catal_model.num_input) 70 | # Initialize the Bayesian Global Optimization 71 | bgo = pydes.GlobalOptimizer(X_init, X_design, loss_func, 72 | args=(y, catal_model)) 73 | print '> starting BGO' 74 | try: 75 | bgo.optimize(max_it=max_it, tol=tol) 76 | except KeyboardInterrupt: 77 | print '> keyboard interruption' 78 | print '> plotting the results' 79 | # Make the plots (set ``to_file`` to ``False`` for interactive plots) 80 | # It writes ``ei.png``, and ``loss.png``. 81 | # and puts them in the ``results`` directory. 82 | make_plots(bgo, to_file=True) 83 | init_log_kappa = X_init[np.argmin(bgo.Y_init), :] 84 | plot_catalysis_output(os.path.join('results', 'init_fit.png'), 85 | catal_model(init_log_kappa), title='Initial fit') 86 | final_log_kappa = bgo.best_design 87 | plot_catalysis_output(os.path.join('results', 'final_fit.png'), 88 | catal_model(final_log_kappa), title='Final fit') 89 | --------------------------------------------------------------------------------