-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Adds probability of improvement as an acquisition function #458
Changes from 10 commits
eadb7bb
7ec1f89
c67083f
e0fc9d2
623181a
5d0a84c
3924fa9
ba5bbdc
b455c9a
1f67bfb
64aeb2c
b651e90
ceef630
58d7bd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# Copyright 2024 The JaxGaussianProcesses Contributors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# ============================================================================== | ||
from dataclasses import dataclass | ||
|
||
from beartype.typing import Mapping | ||
from jaxtyping import Num | ||
|
||
from gpjax.dataset import Dataset | ||
from gpjax.decision_making.utility_functions.base import ( | ||
AbstractSinglePointUtilityFunctionBuilder, | ||
SinglePointUtilityFunction, | ||
) | ||
from gpjax.decision_making.utils import ( | ||
OBJECTIVE, | ||
gaussian_cdf, | ||
) | ||
from gpjax.gps import ConjugatePosterior | ||
from gpjax.typing import ( | ||
Array, | ||
KeyArray, | ||
) | ||
|
||
|
||
@dataclass | ||
class ProbabilityOfImprovement(AbstractSinglePointUtilityFunctionBuilder): | ||
r""" | ||
An acquisition function which returns the probability of improvement | ||
of the objective function over the best observed value. | ||
|
||
More precisely, given a predictive posterior distribution of the objective | ||
function $`f`$, the probability of improvement at a test point $`x`$ is defined as: | ||
$$`\text{PI}(x) = \text{Prob}[f(x) < f(x_{\text{best}})]`$$ | ||
where $`x_{\text{best}}`$ is the minimizer of $`f`$ in the dataset. | ||
|
||
The probability of improvement can be easily computed using the | ||
cumulative distribution function of the standard normal distribution $`\Phi`$: | ||
$$`\text{PI}(x) = \Phi\left(\frac{f(x_{\text{best}}) - \mu}{\sigma}\right)`$$ | ||
where $`\mu`$ and $`\sigma`$ are the mean and standard deviation of the | ||
predictive distribution of the objective function at $`x`$. | ||
|
||
References | ||
---------- | ||
[1] Kushner, H. J. (1964). | ||
A new method of locating the maximum point of an arbitrary multipeak curve in the presence of noise. | ||
Journal of Basic Engineering, 86(1), 97-106. | ||
|
||
[2] Shahriari, B., Swersky, K., Wang, Z., Adams, R. P., & de Freitas, N. (2016). | ||
Taking the human out of the loop: A review of Bayesian optimization. | ||
Proceedings of the IEEE, 104(1), 148-175. doi: 10.1109/JPROC.2015.2494218 | ||
""" | ||
|
||
def build_utility_function( | ||
self, | ||
posteriors: Mapping[str, ConjugatePosterior], | ||
datasets: Mapping[str, Dataset], | ||
key: KeyArray, | ||
) -> SinglePointUtilityFunction: | ||
""" | ||
Constructs the probability of improvement utility function | ||
using the predictive posterior of the objective function. | ||
|
||
Args: | ||
posteriors (Mapping[str, AbstractPosterior]): Dictionary of posteriors to be | ||
used to form the utility function. One of the posteriors must correspond | ||
to the `OBJECTIVE` key, as we sample from the objective posterior to form | ||
the utility function. | ||
datasets (Mapping[str, Dataset]): Dictionary of datasets which may be used | ||
to form the utility function. Keys in `datasets` should correspond to | ||
keys in `posteriors`. One of the datasets must correspond | ||
to the `OBJECTIVE` key. | ||
key (KeyArray): JAX PRNG key used for random number generation. Since | ||
the probability of improvement is computed deterministically | ||
from the predictive posterior, the key is not used. | ||
|
||
Returns: | ||
SinglePointUtilityFunction: the probability of improvement utility function. | ||
""" | ||
self.check_objective_present(posteriors, datasets) | ||
|
||
objective_posterior = posteriors[OBJECTIVE] | ||
if not isinstance(objective_posterior, ConjugatePosterior): | ||
raise ValueError( | ||
"Objective posterior must be a ConjugatePosterior to draw an approximate sample." | ||
miguelgondu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
objective_dataset = datasets[OBJECTIVE] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably best to have something along the lines of
given that we use the objective dataset to find There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed! |
||
|
||
def probability_of_improvement(x_test: Num[Array, "N D"]): | ||
predictive_dist = objective_posterior.predict(x_test, objective_dataset) | ||
|
||
# Assuming that the goal is to minimize the objective function | ||
best_y = objective_dataset.y.min() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be useful to define There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. I've updated how we compute There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah - for instance this approach is mentioned in "A benchmark of kriging-based infill criteria for noisy |
||
|
||
return gaussian_cdf( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of implementing our own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another alternative would be to update |
||
(best_y - predictive_dist.mean()) / predictive_dist.stddev() | ||
).reshape(-1, 1) | ||
|
||
return probability_of_improvement |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Copyright 2023 The GPJax Contributors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# ============================================================================== | ||
from jax import config | ||
|
||
config.update("jax_enable_x64", True) | ||
|
||
import jax.numpy as jnp | ||
import jax.random as jr | ||
|
||
from gpjax.decision_making.test_functions.continuous_functions import Forrester | ||
from gpjax.decision_making.utility_functions.probability_of_improvement import ( | ||
ProbabilityOfImprovement, | ||
) | ||
from gpjax.decision_making.utils import ( | ||
OBJECTIVE, | ||
gaussian_cdf, | ||
) | ||
from tests.test_decision_making.utils import generate_dummy_conjugate_posterior | ||
|
||
|
||
def test_probability_of_improvement_gives_correct_value_for_a_seed(): | ||
key = jr.key(42) | ||
forrester = Forrester() | ||
dataset = forrester.generate_dataset(num_points=10, key=key) | ||
posterior = generate_dummy_conjugate_posterior(dataset) | ||
posteriors = {OBJECTIVE: posterior} | ||
datasets = {OBJECTIVE: dataset} | ||
|
||
pi_utility_builder = ProbabilityOfImprovement() | ||
pi_utility = pi_utility_builder.build_utility_function( | ||
posteriors=posteriors, datasets=datasets, key=key | ||
) | ||
|
||
test_X = forrester.generate_test_points(num_points=10, key=key) | ||
utility_values = pi_utility(test_X) | ||
|
||
# Computing the expected utility values | ||
predictive_dist = posterior.predict(test_X, train_data=dataset) | ||
predictive_mean = predictive_dist.mean() | ||
predictive_std = predictive_dist.stddev() | ||
|
||
expected_utility_values = gaussian_cdf( | ||
(dataset.y.min() - predictive_mean) / predictive_std | ||
).reshape(-1, 1) | ||
|
||
assert utility_values.shape == (10, 1) | ||
assert jnp.isclose(utility_values, expected_utility_values).all() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could update to something like
where $x_{\text{best}}$ is the minimiser of the posterior mean at previously observed values, to handle noisy observations
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated! Forgot to change this.