Skip to content

Commit d19ef28

Browse files
authored
Merge pull request #73 from GPflow/hyper_callback
Callback
2 parents 10da813 + 339b699 commit d19ef28

File tree

4 files changed

+152
-17
lines changed

4 files changed

+152
-17
lines changed

gpflowopt/acquisition/acquisition.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -396,27 +396,31 @@ class MCMCAcquistion(AcquisitionSum):
396396
"""
397397
Apply MCMC over the hyperparameters of an acquisition function (= over the hyperparameters of the contained models).
398398
399-
The models passed into an object of this class are optimized with MLE, and then further sampled with HMC.
400-
These hyperparameter samples are then set in copies of the acquisition.
399+
The models passed into an object of this class are optimized with MLE (fast burn-in), and then further sampled with
400+
HMC. These hyperparameter samples are then set in copies of the acquisition.
401401
402402
For evaluating the underlying acquisition function, the predictions of the acquisition copies are averaged.
403403
"""
404404
def __init__(self, acquisition, n_slices, **kwargs):
405405
assert isinstance(acquisition, Acquisition)
406406
assert n_slices > 0
407-
408-
copies = [copy.deepcopy(acquisition) for _ in range(n_slices - 1)]
409-
for c in copies:
410-
c.optimize_restarts = 0
411-
412407
# the call to the constructor of the parent classes, will optimize acquisition, so it obtains the MLE solution.
413-
super(MCMCAcquistion, self).__init__([acquisition] + copies)
408+
super(MCMCAcquistion, self).__init__([acquisition]*n_slices)
409+
self._needs_new_copies = True
414410
self._sample_opt = kwargs
415411

416412
def _optimize_models(self):
417413
# Optimize model #1
418414
self.operands[0]._optimize_models()
419415

416+
# Copy it again if needed due to changed free state
417+
if self._needs_new_copies:
418+
new_copies = [copy.deepcopy(self.operands[0]) for _ in range(len(self.operands) - 1)]
419+
for c in new_copies:
420+
c.optimize_restarts = 0
421+
self.operands = ParamList([self.operands[0]] + new_copies)
422+
self._needs_new_copies = False
423+
420424
# Draw samples using HMC
421425
# Sample each model of the acquisition function - results in a list of 2D ndarrays.
422426
hypers = np.hstack([model.sample(len(self.operands), **self._sample_opt) for model in self.models])
@@ -440,3 +444,13 @@ def set_data(self, X, Y):
440444
def build_acquisition(self, Xcand):
441445
# Average the predictions of the copies.
442446
return 1. / len(self.operands) * super(MCMCAcquistion, self).build_acquisition(Xcand)
447+
448+
def _kill_autoflow(self):
449+
"""
450+
Flag for recreation on next optimize.
451+
452+
Following the recompilation of models, the free state might have changed. This means updating the samples can
453+
cause inconsistencies and errors.
454+
"""
455+
super(MCMCAcquistion, self)._kill_autoflow()
456+
self._needs_new_copies = True

gpflowopt/bo.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,40 @@
1616

1717
import numpy as np
1818
from scipy.optimize import OptimizeResult
19+
import tensorflow as tf
20+
from gpflow.gpr import GPR
1921

2022
from .acquisition import Acquisition, MCMCAcquistion
21-
from .optim import Optimizer, SciPyOptimizer
22-
from .objective import ObjectiveWrapper
2323
from .design import Design, EmptyDesign
24+
from .objective import ObjectiveWrapper
25+
from .optim import Optimizer, SciPyOptimizer
2426
from .pareto import non_dominated_sort
27+
from .models import ModelWrapper
28+
29+
30+
def jitchol_callback(models):
31+
"""
32+
Increase the likelihood in case of Cholesky failures.
33+
34+
This is similar to the use of jitchol in GPy. Default callback for BayesianOptimizer.
35+
Only usable on GPR models, other types are ignored.
36+
"""
37+
for m in np.atleast_1d(models):
38+
if isinstance(m, ModelWrapper):
39+
jitchol_callback(m.wrapped) # pragma: no cover
40+
41+
if not isinstance(m, GPR):
42+
continue
43+
44+
s = m.get_free_state()
45+
eKdiag = np.mean(np.diag(m.kern.compute_K_symm(m.X.value)))
46+
for e in [0] + [10**ex for ex in range(-6,-1)]:
47+
try:
48+
m.likelihood.variance = m.likelihood.variance.value + e * eKdiag
49+
m.optimize(maxiter=5)
50+
break
51+
except tf.errors.InvalidArgumentError: # pragma: no cover
52+
m.set_state(s)
2553

2654

2755
class BayesianOptimizer(Optimizer):
@@ -32,7 +60,8 @@ class BayesianOptimizer(Optimizer):
3260
Additionally, it is configured with a separate optimizer for the acquisition function.
3361
"""
3462

35-
def __init__(self, domain, acquisition, optimizer=None, initial=None, scaling=True, hyper_draws=None):
63+
def __init__(self, domain, acquisition, optimizer=None, initial=None, scaling=True, hyper_draws=None,
64+
callback=jitchol_callback):
3665
"""
3766
:param Domain domain: The optimization space.
3867
:param Acquisition acquisition: The acquisition function to optimize over the domain.
@@ -51,6 +80,12 @@ def __init__(self, domain, acquisition, optimizer=None, initial=None, scaling=Tr
5180
are obtained using Hamiltonian MC.
5281
(see `GPflow documentation <https://gpflow.readthedocs.io/en/latest//>`_ for details) for each model.
5382
The acquisition score is computed for each draw, and averaged.
83+
:param callable callback: (optional) this function or object will be called, after the
84+
data of all models has been updated with all models as retrieved by acquisition.models as argument without
85+
the wrapping model handling any scaling . This allows custom model optimization strategies to be implemented.
86+
All manipulations of GPflow models are permitted. Combined with the optimize_restarts parameter of
87+
:class:`~.Acquisition` this allows several scenarios: do the optimization manually from the callback
88+
(optimize_restarts equals 0), or choose the starting point + some random restarts (optimize_restarts > 0).
5489
"""
5590
assert isinstance(acquisition, Acquisition)
5691
assert hyper_draws is None or hyper_draws > 0
@@ -69,6 +104,8 @@ def __init__(self, domain, acquisition, optimizer=None, initial=None, scaling=Tr
69104
initial = initial or EmptyDesign(domain)
70105
self.set_initial(initial.generate())
71106

107+
self._model_callback = callback
108+
72109
@Optimizer.domain.setter
73110
def domain(self, dom):
74111
assert self.domain.size == dom.size
@@ -86,6 +123,8 @@ def _update_model_data(self, newX, newY):
86123
assert self.acquisition.data[0].shape[1] == newX.shape[-1]
87124
assert self.acquisition.data[1].shape[1] == newY.shape[-1]
88125
assert newX.shape[0] == newY.shape[0]
126+
if newX.size == 0:
127+
return
89128
X = np.vstack((self.acquisition.data[0], newX))
90129
Y = np.vstack((self.acquisition.data[1], newY))
91130
self.acquisition.set_data(X, Y)
@@ -174,7 +213,6 @@ def _optimize(self, fx, n_iter):
174213
:param n_iter: number of iterations to run
175214
:return: OptimizeResult object
176215
"""
177-
178216
assert isinstance(fx, ObjectiveWrapper)
179217

180218
# Evaluate and add the initial design (if any)
@@ -190,6 +228,10 @@ def inverse_acquisition(x):
190228

191229
# Optimization loop
192230
for i in range(n_iter):
231+
# If a callback is specified, and acquisition has the setup flag enabled (indicating an upcoming
232+
# compilation), run the callback.
233+
if self._model_callback and self.acquisition._needs_setup:
234+
self._model_callback([m.wrapped for m in self.acquisition.models])
193235
result = self.optimizer.optimize(inverse_acquisition)
194236
self._update_model_data(result.x, fx(result.x))
195237

testing/test_acquisition.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ def test_object_integrity(self, acquisition):
146146
for oper in acquisition.operands:
147147
self.assertTrue(isinstance(oper, gpflowopt.acquisition.Acquisition),
148148
msg="All operands should be an acquisition object")
149-
150149
self.assertTrue(all(isinstance(m, gpflowopt.models.ModelWrapper) for m in acquisition.models))
151150

152151
@parameterized.expand(list(zip(aggregations)))
@@ -218,9 +217,23 @@ def test_marginalized_score(self, acquisition):
218217
ei_mcmc = acquisition.evaluate(Xt)
219218
np.testing.assert_almost_equal(ei_mle, ei_mcmc, decimal=5)
220219

221-
@parameterized.expand(list(zip([aggregations[2]])))
222-
def test_mcmc_acq_models(self, acquisition):
220+
def test_mcmc_acq(self):
221+
acquisition = gpflowopt.acquisition.MCMCAcquistion(
222+
gpflowopt.acquisition.ExpectedImprovement(create_parabola_model(domain)), 10)
223+
for oper in acquisition.operands:
224+
self.assertListEqual(acquisition.models, oper.models)
225+
self.assertEqual(acquisition.operands[0], oper)
226+
self.assertTrue(acquisition._needs_new_copies)
227+
acquisition._optimize_models()
223228
self.assertListEqual(acquisition.models, acquisition.operands[0].models)
229+
for oper in acquisition.operands[1:]:
230+
self.assertNotEqual(acquisition.operands[0], oper)
231+
self.assertFalse(acquisition._needs_new_copies)
232+
acquisition._setup()
233+
Xt = np.random.rand(20, 2) * 2 - 1
234+
ei_mle = acquisition.operands[0].evaluate(Xt)
235+
ei_mcmc = acquisition.evaluate(Xt)
236+
np.testing.assert_almost_equal(ei_mle, ei_mcmc, decimal=5)
224237

225238

226239
class TestJointAcquisition(unittest.TestCase):

testing/test_optimizers.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,8 @@ def test_optimize_multi_objective(self):
214214
result = optimizer.optimize(vlmop2, n_iter=2)
215215
self.assertTrue(result.success)
216216
self.assertEqual(result.nfev, 2, "Only 2 evaluations permitted")
217-
self.assertTupleEqual(result.x.shape, (9, 2))
218-
self.assertTupleEqual(result.fun.shape, (9, 2))
217+
self.assertTupleEqual(result.x.shape, (7, 2))
218+
self.assertTupleEqual(result.fun.shape, (7, 2))
219219
_, dom = gpflowopt.pareto.non_dominated_sort(result.fun)
220220
self.assertTrue(np.all(dom==0))
221221

@@ -288,6 +288,71 @@ def test_mcmc(self):
288288
self.assertTrue(np.allclose(result.x, 0), msg="Optimizer failed to find optimum")
289289
self.assertTrue(np.allclose(result.fun, 0), msg="Incorrect function value returned")
290290

291+
def test_callback(self):
292+
class DummyCallback(object):
293+
def __init__(self):
294+
self.counter = 0
295+
296+
def __call__(self, models):
297+
self.counter += 1
298+
299+
c = DummyCallback()
300+
optimizer = gpflowopt.BayesianOptimizer(self.domain, self.acquisition, callback=c)
301+
result = optimizer.optimize(lambda X: parabola2d(X)[0], n_iter=2)
302+
self.assertEqual(c.counter, 2)
303+
304+
def test_callback_recompile(self):
305+
class DummyCallback(object):
306+
def __init__(self):
307+
self.recompile = False
308+
309+
def __call__(self, models):
310+
c = np.random.randint(2, 10)
311+
models[0].kern.variance.prior = gpflow.priors.Gamma(c, 1./c)
312+
self.recompile = models[0]._needs_recompile
313+
314+
c = DummyCallback()
315+
optimizer = gpflowopt.BayesianOptimizer(self.domain, self.acquisition, callback=c)
316+
self.acquisition.evaluate(np.zeros((1,2))) # Make sure its run and setup to skip
317+
result = optimizer.optimize(lambda X: parabola2d(X)[0], n_iter=1)
318+
self.assertFalse(c.recompile)
319+
result = optimizer.optimize(lambda X: parabola2d(X)[0], n_iter=1)
320+
self.assertTrue(c.recompile)
321+
self.assertFalse(self.acquisition.models[0]._needs_recompile)
322+
323+
def test_callback_recompile_mcmc(self):
324+
class DummyCallback(object):
325+
def __init__(self):
326+
self.no_models = 0
327+
328+
def __call__(self, models):
329+
c = np.random.randint(2, 10)
330+
models[0].kern.variance.prior = gpflow.priors.Gamma(c, 1. / c)
331+
self.no_models = len(models)
332+
333+
c = DummyCallback()
334+
optimizer = gpflowopt.BayesianOptimizer(self.domain, self.acquisition, hyper_draws=5, callback=c)
335+
opers = optimizer.acquisition.operands
336+
result = optimizer.optimize(lambda X: parabola2d(X)[0], n_iter=1)
337+
self.assertEqual(c.no_models, 1)
338+
self.assertEqual(id(opers[0]), id(optimizer.acquisition.operands[0]))
339+
for op1, op2 in zip(opers[1:], optimizer.acquisition.operands[1:]):
340+
self.assertNotEqual(id(op1), id(op2))
341+
opers = optimizer.acquisition.operands
342+
result = optimizer.optimize(lambda X: parabola2d(X)[0], n_iter=1)
343+
self.assertEqual(id(opers[0]), id(optimizer.acquisition.operands[0]))
344+
for op1, op2 in zip(opers[1:], optimizer.acquisition.operands[1:]):
345+
self.assertNotEqual(id(op1), id(op2))
346+
347+
def test_nongpr_model(self):
348+
design = gpflowopt.design.LatinHyperCube(16, self.domain)
349+
X, Y = design.generate(), parabola2d(design.generate())[0]
350+
m = gpflow.vgp.VGP(X, Y, gpflow.kernels.RBF(2, ARD=True), likelihood=gpflow.likelihoods.Gaussian())
351+
acq = gpflowopt.acquisition.ExpectedImprovement(m)
352+
optimizer = gpflowopt.BayesianOptimizer(self.domain, acq)
353+
result = optimizer.optimize(lambda X: parabola2d(X)[0], n_iter=1)
354+
self.assertTrue(result.success)
355+
291356

292357
class TestSilentOptimization(unittest.TestCase):
293358
@contextmanager
@@ -323,3 +388,4 @@ def _optimize(self, objective):
323388
opt.optimize(None)
324389
output = out.getvalue().strip()
325390
self.assertEqual(output, '')
391+

0 commit comments

Comments
 (0)