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

Tutorial on Transfer Learning Bayesian Optimisation #439

Merged
merged 15 commits into from
Sep 19, 2024

Conversation

jpfolch
Copy link
Contributor

@jpfolch jpfolch commented Sep 10, 2024

Added a tutorial on how to use BoFire for Transfer Learning Bayesian Optimisation. Also made two small modifications for the code to run:

  • Inverse transform for ordinal encodings transforms into int type

  • When predicting within the task using MultiTaskGPs we must add likelihood noise manually since the observation_noise flag is not supported by BoTorch for MultiTaskGPs

Next step would be to incorporate a step for task selection for multi-fidelity problems.

@jduerholt jduerholt self-requested a review September 11, 2024 10:10
@jduerholt
Copy link
Contributor

Thanks @jpfolch. I will provide a review tomorrow. Best, Johannes

Copy link
Contributor

@jduerholt jduerholt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jpfolch,

looks very nice. Thank you!

I let one comment inline, and here are two other ones:

Best,

Johannes

@@ -170,13 +170,21 @@ def _predict(self, transformed: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
# input and further transform it to a torch tensor
X = torch.from_numpy(transformed.values).to(**tkwargs)
with torch.no_grad():
posterior = self.model.posterior(X=X, observation_noise=True) # type: ignore
# observation noise is not implemented for MultiTaskGPSurrogate, has to be treated differently
if self.surrogate_specs.surrogates[0].type == "MultiTaskGPSurrogate":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a validator to BotorchSurrogates that checks that in case one MultiTaskGP is present, all surrogates are of type MultiTaskGP. Else, we will get trouble with the different required encodings. You have to add it here: https://github.com/experimental-design/bofire/blob/main/bofire/data_models/surrogates/botorch_surrogates.py

Copy link
Contributor Author

@jpfolch jpfolch Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were correct and the code actually crashed when using multiple models (even if all were MultiTaskGPs). I sent a fix for it and it seems to work, but I am not sure it is entirely correct; I do not really understand when we expect to have len(posterior.mean.shape) == 2 and when len(posterior.mean.shape) == 3? Any guidance would be appreciated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, also important to mention that when I tested with a single surrogate model, or two of them, in all cases it goes through len(posterior.mean.shape) == 2 which caused my confusion...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a posterior mean of shape 3 in case of using fully basesian GPs. Have a look here: https://github.com/experimental-design/bofire/blob/main/bofire/surrogates/fully_bayesian.py

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking, at this and how this gets really messy here, I want to propose the following solution:

If the model can handle posterior noise, we take it, else we do not take it.

try:
    posterior = self.model.posterior(X=X, observation_noise=True)
except: # the error which is thorwn for MultiTask
    posterior = self.model.posterior(X=X, observation_noise=False)

And the rest we keep as it is. And I will in a later PR refactor this whole part. Ok for you?

@jpfolch
Copy link
Contributor Author

jpfolch commented Sep 13, 2024

Hi @jduerholt,

I have made the requested changes, I am a little unsure about the _predict method in bofire/strategies/predictives/botorch.pybofire/strategies/predictives/botorch.py being correct, I left some more details in the comment above.

I also added some testing, checking that the sobo and qehvi work okay with MultiTask surrogates, they seem to do! Let me know if I should move the testing to a specs file somewhere, I searched for it but couldn't really pinpoint exactly where they should go so I just tested in a separate file.

@jduerholt
Copy link
Contributor

Thanks, I will have a look the latest on Monday, but I hope to get it done tomorrow!

Copy link
Contributor

@jduerholt jduerholt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jpfolch,

thank you very much. Looks very good. I think we are almost there. Have a look at my inline comments.

Regarding the notebook, only two things:

  • Can you also add a "SMOKE_TEST" limitation for the iterations that you run for the notebook? So that in case of the automatest test pipleine, only two iterations per optimiuation campagin are run?
  • There is a variable called regrets_single_task_mean. Can you rename it to regrets_single_task_median as it is actually the median?
  • Optional one could think of creating a benchmark class for you multitask benchmark that you created and then use the automatic benchmark excutors. But this is only optional.

Best,

Johannes

@@ -170,13 +170,21 @@ def _predict(self, transformed: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
# input and further transform it to a torch tensor
X = torch.from_numpy(transformed.values).to(**tkwargs)
with torch.no_grad():
posterior = self.model.posterior(X=X, observation_noise=True) # type: ignore
# observation noise is not implemented for MultiTaskGPSurrogate, has to be treated differently
if self.surrogate_specs.surrogates[0].type == "MultiTaskGPSurrogate":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a posterior mean of shape 3 in case of using fully basesian GPs. Have a look here: https://github.com/experimental-design/bofire/blob/main/bofire/surrogates/fully_bayesian.py

@@ -170,13 +170,21 @@ def _predict(self, transformed: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
# input and further transform it to a torch tensor
X = torch.from_numpy(transformed.values).to(**tkwargs)
with torch.no_grad():
posterior = self.model.posterior(X=X, observation_noise=True) # type: ignore
# observation noise is not implemented for MultiTaskGPSurrogate, has to be treated differently
if self.surrogate_specs.surrogates[0].type == "MultiTaskGPSurrogate":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking, at this and how this gets really messy here, I want to propose the following solution:

If the model can handle posterior noise, we take it, else we do not take it.

try:
    posterior = self.model.posterior(X=X, observation_noise=True)
except: # the error which is thorwn for MultiTask
    posterior = self.model.posterior(X=X, observation_noise=False)

And the rest we keep as it is. And I will in a later PR refactor this whole part. Ok for you?

@@ -227,3 +229,17 @@ def _generate_surrogate_specs(
surrogate_specs.surrogates = _surrogate_specs
surrogate_specs._check_compability(inputs=domain.inputs, outputs=domain.outputs)
return surrogate_specs

@model_validator(mode="after")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be moved to data_models/strategies/predictives/botorch.py, as it is just a requirement for optimization.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corresponding test would go then into tests/bofire/data_models/specs/strategies.py and you would add an invalid spec for SOBO for example as it inherits from the base class.

Copy link
Contributor Author

@jpfolch jpfolch Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be moved to data_models/strategies/predictives/botorch.py, as it is just a requirement for optimization.

Is it not already in the correct file?

I am adding the test now though.

@@ -131,4 +131,10 @@ def validate_surrogates(cls, v, values):
raise ValueError(
f"Preprocessing steps for features with {key} are incompatible."
)
# check that if any surrogate is a MultiTaskGPSurrogate, all have to be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be tested in tests/bofire/data_models/specs/surrogates.py. You can add there invalid configurations, and the error that should be thrown. This is the new default way of validating data models.

),
],
)
def test_qehvi_with_multitask(task_input, surrogate_2):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this to tests/bofire/strategies/test_mobo.py? qehvi is deprecated and only included vor legacy reasons.

surrogate_data_2 = surrogate_2(inputs=inputs, outputs=outputs_2)
surrogate_data = [surrogate_data_1, surrogate_data_2]

# test for error if both models are not multi-task
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would go in the specs part, as mentioned above.

surrogate_specs = BotorchSurrogates(surrogates=surrogate_data)

# test for error if task input has more than 1 allowed category
if sum(task_input.allowed) > 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also go in the specs, as mentioned above

surrogate_specs=surrogate_specs,
)

strategy = strategies.map(strategy_data_model)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be real functionally tested in test_mobo

acquisition_function=qLogEI(),
)

strategy = strategies.map(strategy_data_model)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep only this, the rest would be in the specs as it is part of the data model validators.

@jpfolch
Copy link
Contributor Author

jpfolch commented Sep 18, 2024

Hi @jduerholt,

I have made the requested changes. A few points:

  • I have made the try and except changes for prediction using MultiTaskGPs both in bofire/surrogates/multi_task_gp.py and bofire/strategies/predictives/botorch.py for consistency.

  • I was unable to test that if a list of surrogates is used that includes a MultiTaskGP, then all models should be MultiTaskGPs using the specs; I kept getting errors. I noticed that none of the specs in tests/bofire/data_models/specs/surrogates.py have validation testing for the BotorchSurrogates surrogate and that these are instead tested in tests/bofire/surrogates/test_torch_models.py so that is where I added the testing. Let me know if this is okay, or if you have a guess of what was causing the errors.

  • The "SMOKE_TEST" variable in the notebook was included in a cell further down, let me know if you'd prefer it at the beginning.

  • I am happy to write MultiTask / Multifidelity benchmarks in a future PR that are more complete, rather than my current silly examples, I will have a think of which ones would be best to add.

Thanks!
Best,
Jose

@jduerholt
Copy link
Contributor

Looks very good for me. Thank you very much!

@jduerholt jduerholt merged commit 4b34e75 into experimental-design:main Sep 19, 2024
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants