Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ProbabilisticModel a pure interface #797

Merged
merged 6 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions docs/notebooks/code_overview.pct.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,13 @@ class HasGizmoReparamSamplerAndObservationNoise(
AcquisitionFunction,
SingleModelAcquisitionBuilder,
)
from trieste.models.interfaces import SupportsPredictY
from trieste.data import Dataset


class ProbabilityOfValidity(SingleModelAcquisitionBuilder[ProbabilisticModel]):
class ProbabilityOfValidity(SingleModelAcquisitionBuilder[SupportsPredictY]):
def prepare_acquisition_function(
self, model: ProbabilisticModel, dataset: Optional[Dataset] = None
self, model: SupportsPredictY, dataset: Optional[Dataset] = None
) -> AcquisitionFunction:
def acquisition(at: TensorType) -> TensorType:
mean, _ = model.predict_y(tf.squeeze(at, -2))
Expand All @@ -217,9 +218,9 @@ def acquisition(at: TensorType) -> TensorType:


# %%
class ProbabilityOfValidity2(SingleModelAcquisitionBuilder[ProbabilisticModel]):
class ProbabilityOfValidity2(SingleModelAcquisitionBuilder[SupportsPredictY]):
def prepare_acquisition_function(
self, model: ProbabilisticModel, dataset: Optional[Dataset] = None
self, model: SupportsPredictY, dataset: Optional[Dataset] = None
) -> AcquisitionFunction:
@tf.function
def acquisition(at: TensorType) -> TensorType:
Expand All @@ -231,7 +232,7 @@ def acquisition(at: TensorType) -> TensorType:
def update_acquisition_function(
self,
function: AcquisitionFunction,
model: ProbabilisticModel,
model: SupportsPredictY,
dataset: Optional[Dataset] = None,
) -> AcquisitionFunction:
return function # no need to update anything
Expand Down
4 changes: 3 additions & 1 deletion docs/notebooks/expected_improvement.pct.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ def build_model(data):

# %%
# save the model to a given path, exporting just the predict method
module = result.try_get_final_model().get_module_with_variables()
from trieste.models.utils import get_module_with_variables

module = get_module_with_variables(result.try_get_final_model())
module.predict = tf.function(
model.predict,
input_signature=[tf.TensorSpec(shape=[None, 2], dtype=tf.float64)],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
check_and_extract_fidelity_query_points,
split_dataset_by_fidelity,
)
from trieste.models import TrainableProbabilisticModel
from trieste.models.gpflow import GaussianProcessRegression
from trieste.models.gpflow.builders import (
build_gpr,
Expand Down Expand Up @@ -119,7 +120,7 @@ def test_multifidelity_nonlinear_autoregressive_results_better_than_linear() ->
observer = mk_observer(noisy_nonlinear_multifidelity)
initial_data = observer(initial_sample)

nonlinear_model = MultifidelityNonlinearAutoregressive(
nonlinear_model: TrainableProbabilisticModel = MultifidelityNonlinearAutoregressive(
build_multifidelity_nonlinear_autoregressive_models(
initial_data, n_fidelities, input_search_space
)
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/acquisition/function/test_entropy.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
MinValueEntropySearch,
MUMBOModelType,
SupportsCovarianceObservationNoiseTrajectory,
SupportsCovarianceWithTopFidelityPredictY,
gibbon_quality_term,
gibbon_repulsion_term,
min_value_entropy_search,
Expand All @@ -48,7 +49,6 @@
ThompsonSamplerFromTrajectory,
)
from trieste.data import Dataset, add_fidelity_column
from trieste.models import SupportsCovarianceWithTopFidelity
from trieste.objectives import Branin
from trieste.space import Box
from trieste.types import TensorType
Expand Down Expand Up @@ -612,7 +612,7 @@ def test_mumbo_raises_when_use_trajectory_sampler_and_model_without_trajectories
)
def test_mumbo_builder_builds_min_value_samples(
mocked_mves: MagicMock,
min_value_sampler: ThompsonSampler[SupportsCovarianceWithTopFidelity],
min_value_sampler: ThompsonSampler[SupportsCovarianceWithTopFidelityPredictY],
) -> None:
dataset = Dataset(tf.zeros([3, 2], dtype=tf.float64), tf.ones([3, 2], dtype=tf.float64))
search_space = Box([0, 0], [1, 1])
Expand All @@ -638,7 +638,7 @@ def test_mumbo_builder_builds_min_value_samples(
[ExactThompsonSampler(sample_min_value=True), GumbelSampler(sample_min_value=True)],
)
def test_mumbo_builder_updates_acquisition_function(
min_value_sampler: ThompsonSampler[SupportsCovarianceWithTopFidelity],
min_value_sampler: ThompsonSampler[SupportsCovarianceWithTopFidelityPredictY],
) -> None:
search_space = Box([0.0, 0.0], [1.0, 1.0])
model = MultiFidelityQuadraticMeanAndRBFKernel(
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/models/gpflow/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from __future__ import annotations

from typing import Any
from typing import Any, Optional

import gpflow
import numpy.testing as npt
Expand All @@ -37,6 +37,9 @@ def optimize(self, dataset: Dataset) -> None:
def update(self, dataset: Dataset) -> None:
return

def log(self, dataset: Optional[Dataset] = None) -> None:
return


class _QuadraticGPModel(GPModel):
def __init__(self) -> None:
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/models/gpflow/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
RandomFourierFeatureTrajectorySampler,
)
from trieste.models.optimizer import BatchOptimizer, DatasetTransformer, Optimizer
from trieste.models.utils import get_last_optimization_result, optimize_model_and_save_result
from trieste.space import Box
from trieste.types import TensorType
from trieste.utils import DEFAULTS
Expand Down Expand Up @@ -150,13 +151,14 @@ def test_gpflow_wrappers_default_optimize(
args = {}

loss = internal_model.training_loss(**args)
model.optimize_and_save_result(Dataset(*data))
optimize_model_and_save_result(model, Dataset(*data))

new_loss = internal_model.training_loss(**args)
assert new_loss < loss
if not isinstance(internal_model, SVGP):
assert model.last_optimization_result is not None
npt.assert_allclose(new_loss, model.last_optimization_result.fun)
optimization_result = get_last_optimization_result(model)
assert optimization_result is not None
npt.assert_allclose(new_loss, optimization_result.fun)


def test_gpflow_wrappers_ref_optimize(gpflow_interface_factory: ModelFactoryType) -> None:
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/models/gpflow/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from trieste.models.interfaces import HasTrajectorySampler
from trieste.models.optimizer import BatchOptimizer, Optimizer
from trieste.models.utils import get_module_with_variables
from trieste.types import TensorType


Expand Down Expand Up @@ -92,7 +93,7 @@ def test_gaussian_process_tf_saved_model(gpflow_interface_factory: ModelFactoryT
trajectory = trajectory_sampler.get_trajectory()

# generate client model with predict and sample methods
module = model.get_module_with_variables(trajectory_sampler, trajectory)
module = get_module_with_variables(model, trajectory_sampler, trajectory)
module.predict = tf.function(
model.predict, input_signature=[tf.TensorSpec(shape=[None, 1], dtype=tf.float64)]
)
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/models/gpflux/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from __future__ import annotations

from typing import Optional

import gpflow
import numpy.testing as npt
import pytest
Expand Down Expand Up @@ -70,6 +72,9 @@ def sample(self, query_points: TensorType, num_samples: int) -> TensorType:
def update(self, dataset: Dataset) -> None:
return

def log(self, dataset: Optional[Dataset] = None) -> None:
return


class _QuadraticGPModel(DeepGP):
def __init__(
Expand Down
14 changes: 10 additions & 4 deletions tests/unit/models/gpflux/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
from trieste.models.gpflux import DeepGaussianProcess
from trieste.models.interfaces import HasTrajectorySampler
from trieste.models.optimizer import KerasOptimizer
from trieste.models.utils import (
get_last_optimization_result,
get_module_with_variables,
optimize_model_and_save_result,
)
from trieste.types import TensorType


Expand Down Expand Up @@ -297,10 +302,11 @@ def test_deep_gaussian_process_with_lr_scheduler(
optimizer = KerasOptimizer(tf.optimizers.Adam(lr_schedule), fit_args)
model = DeepGaussianProcess(two_layer_model(x), optimizer)

model.optimize_and_save_result(Dataset(x, y))
optimize_model_and_save_result(model, Dataset(x, y))

assert model.last_optimization_result is not None
assert len(model.last_optimization_result.history["loss"]) == epochs
optimization_result = get_last_optimization_result(model)
assert optimization_result is not None
assert len(optimization_result.history["loss"]) == epochs


def test_deep_gaussian_process_default_optimizer_is_correct(
Expand Down Expand Up @@ -395,7 +401,7 @@ def test_deepgp_tf_saved_model() -> None:
trajectory = trajectory_sampler.get_trajectory()

# generate client model with predict and sample methods
module = model.get_module_with_variables(trajectory_sampler, trajectory)
module = get_module_with_variables(model, trajectory_sampler, trajectory)
module.predict = tf.function(
model.predict, input_signature=[tf.TensorSpec(shape=[None, 1], dtype=tf.float64)]
)
Expand Down
14 changes: 10 additions & 4 deletions tests/unit/models/keras/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
sample_with_replacement,
)
from trieste.models.optimizer import KerasOptimizer, TrainingData
from trieste.models.utils import (
get_last_optimization_result,
get_module_with_variables,
optimize_model_and_save_result,
)
from trieste.types import TensorType

_ENSEMBLE_SIZE = 3
Expand Down Expand Up @@ -215,10 +220,11 @@ def scheduler(epoch: int, lr: float) -> float:

npt.assert_allclose(model.model.optimizer.lr.numpy(), init_lr, rtol=1e-6)

model.optimize_and_save_result(example_data)
optimize_model_and_save_result(model, example_data)

assert model.last_optimization_result is not None
npt.assert_allclose(model.last_optimization_result.history["lr"], [0.5, 0.25])
optimization_result = get_last_optimization_result(model)
assert optimization_result is not None
npt.assert_allclose(optimization_result.history["lr"], [0.5, 0.25])
npt.assert_allclose(model.model.optimizer.lr.numpy(), init_lr, rtol=1e-6)


Expand Down Expand Up @@ -568,7 +574,7 @@ def test_deep_ensemble_tf_saved_model() -> None:
trajectory = trajectory_sampler.get_trajectory()

# generate client model with predict and sample methods
module = model.get_module_with_variables(trajectory_sampler, trajectory)
module = get_module_with_variables(model, trajectory_sampler, trajectory)
module.predict = tf.function(
model.predict, input_signature=[tf.TensorSpec(shape=[None, 3], dtype=tf.float64)]
)
Expand Down
25 changes: 5 additions & 20 deletions tests/unit/models/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
from trieste.models import TrainableModelStack, TrainableProbabilisticModel
from trieste.models.interfaces import (
TrainablePredictJointReparamModelStack,
TrainablePredictYModelStack,
TrainableSupportsPredictJoint,
TrainableSupportsPredictJointHasReparamSampler,
)
from trieste.models.utils import get_last_optimization_result, optimize_model_and_save_result
from trieste.types import TensorType


Expand Down Expand Up @@ -114,28 +116,11 @@ def test_model_stack_predict_joint() -> None:
npt.assert_allclose(cov[..., 3:, :, :], cov3)


def test_model_missing_predict_y() -> None:
model = _QuadraticModel([-1.0], [0.1])
x_predict = tf.constant([[0]], gpflow.default_float())
with pytest.raises(NotImplementedError):
model.predict_y(x_predict)


def test_model_stack_missing_predict_y() -> None:
x = tf.constant(np.arange(5).reshape(-1, 1), dtype=gpflow.default_float())
model1 = gpr_model(x, fnc_3x_plus_10(x))
model2 = _QuadraticModel([1.0], [2.0])
stack = TrainableModelStack((model1, 1), (model2, 1))
x_predict = tf.constant([[0]], gpflow.default_float())
with pytest.raises(NotImplementedError):
stack.predict_y(x_predict)


def test_model_stack_predict_y() -> None:
x = tf.constant(np.arange(5).reshape(-1, 1), dtype=gpflow.default_float())
model1 = gpr_model(x, fnc_3x_plus_10(x))
model2 = sgpr_model(x, fnc_2sin_x_over_3(x))
stack = TrainableModelStack((model1, 1), (model2, 1))
stack = TrainablePredictYModelStack((model1, 1), (model2, 1))
mean, variance = stack.predict_y(x)
npt.assert_allclose(mean[:, 0:1], model1.predict_y(x)[0])
npt.assert_allclose(mean[:, 1:2], model2.predict_y(x)[0])
Expand Down Expand Up @@ -193,8 +178,8 @@ def _assert_data(self, dataset: Dataset) -> None:
stack = TrainableModelStack((model01, 2), (model2, 1), (model3, 1))
data = Dataset(tf.random.uniform([5, 7, 3]), tf.random.uniform([5, 7, 4]))
stack.update(data)
stack.optimize_and_save_result(data)
assert stack.last_optimization_result == [None] * 3
optimize_model_and_save_result(stack, data)
assert get_last_optimization_result(stack) == [None] * 3


def test_model_stack_reparam_sampler_raises_for_submodels_without_reparam_sampler() -> None:
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_bayesian_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,9 @@ def update(self, dataset: Dataset) -> NoReturn:
def optimize(self, dataset: Dataset) -> NoReturn:
assert False

def log(self, dataset: Optional[Dataset] = None) -> None:
return

class _UnusableRule(AcquisitionRule[NoReturn, Box, ProbabilisticModel]):
def acquire(
self,
Expand Down
17 changes: 15 additions & 2 deletions tests/util/models/gpflow/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
SupportsGetKernel,
SupportsGetObservationNoise,
SupportsPredictJoint,
SupportsPredictY,
)
from trieste.models.optimizer import Optimizer
from trieste.types import TensorType
Expand Down Expand Up @@ -120,6 +121,9 @@ def covariance_between_points(
]
return tf.concat(covs, axis=-3)

def log(self, dataset: Optional[Dataset] = None) -> None:
return


class GaussianProcessWithoutNoise(GaussianMarginal, SupportsPredictJoint, HasReparamSampler):
"""A (static) Gaussian process over a vector random variable with independent reparam sampler
Expand Down Expand Up @@ -160,6 +164,9 @@ def reparam_sampler(
) -> ReparametrizationSampler[GaussianProcessWithoutNoise]:
return IndependentReparametrizationSampler(num_samples, self)

def log(self, dataset: Optional[Dataset] = None) -> None:
return


class GaussianProcessWithSamplers(GaussianProcess, HasReparamSampler):
"""A (static) Gaussian process over a vector random variable with independent reparam sampler"""
Expand Down Expand Up @@ -259,7 +266,7 @@ def optimize(self, dataset: Dataset) -> None:


class MultiFidelityQuadraticMeanAndRBFKernel(
QuadraticMeanAndRBFKernel, SupportsCovarianceWithTopFidelity
QuadraticMeanAndRBFKernel, SupportsPredictY, SupportsCovarianceWithTopFidelity
):
r"""
A Gaussian process with scalar quadratic mean, an RBF kernel and
Expand Down Expand Up @@ -293,7 +300,7 @@ def predict_y(self, query_points: TensorType) -> tuple[TensorType, TensorType]:


class MultiFidelityQuadraticMeanAndRBFKernelWithSamplers(
QuadraticMeanAndRBFKernelWithSamplers, SupportsCovarianceWithTopFidelity
QuadraticMeanAndRBFKernelWithSamplers, SupportsPredictY, SupportsCovarianceWithTopFidelity
):
r"""
A Gaussian process with scalar quadratic mean, an RBF kernel and
Expand Down Expand Up @@ -323,6 +330,12 @@ def covariance_with_top_fidelity(self, x: TensorType) -> TensorType:
mean, _ = self.predict(x)
return tf.ones_like(mean, dtype=mean.dtype) # dummy covariances of correct shape

@inherit_check_shapes
def predict_y(self, query_points: TensorType) -> tuple[TensorType, TensorType]:
fmean, fvar = self.predict(query_points)
yvar = fvar + tf.constant(1.0, dtype=fmean.dtype) # dummy noise variance
return fmean, yvar


class QuadraticMeanAndRBFKernelWithBatchSamplers(
QuadraticMeanAndRBFKernel, HasTrajectorySampler, HasReparamSampler
Expand Down
Loading
Loading