From d14c3c29d075c67c2b62aac3834d1d9922d918f6 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 29 Oct 2025 09:34:26 +0100 Subject: [PATCH 1/8] Add safety guard for const noisy --- package/samplers/value_at_risk/sampler.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index fd54324e..9218f7a5 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -106,6 +106,7 @@ def __init__( warn_independent_sampling: bool = True, uniform_input_noise_rads: dict[str, float] | None = None, normal_input_noise_stdevs: dict[str, float] | None = None, + const_noisy_input_param_names: list[str] | None = None, ) -> None: if uniform_input_noise_rads is None and normal_input_noise_stdevs is None: raise ValueError( @@ -117,8 +118,25 @@ def __init__( "Only one of `uniform_input_noise_rads` and `normal_input_noise_stdevs` " "can be specified." ) + if const_noisy_input_param_names is not None: + if uniform_input_noise_rads is not None and len( + const_noisy_input_param_names & uniform_input_noise_rads.keys() + ): + raise ValueError( + "noisy parameters can be specified only in one of " + "`const_noisy_input_param_names` and `uniform_input_noise_rads`." + ) + if normal_input_noise_stdevs is not None and len( + const_noisy_input_param_names & normal_input_noise_stdevs.keys() + ): + raise ValueError( + "noisy parameters can be specified only in one of " + "`const_noisy_input_param_names` and `normal_input_noise_stdevs`." + ) + self._uniform_input_noise_rads = uniform_input_noise_rads self._normal_input_noise_stdevs = normal_input_noise_stdevs + self._const_noisy_input_param_names = const_noisy_input_param_names self._rng = LazyRandomState(seed) self._independent_sampler = independent_sampler or optuna.samplers.RandomSampler(seed=seed) self._intersection_search_space = optuna.search_space.IntersectionSearchSpace() From 6dae50770775230da01dce452521dbadf9ca4542 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 29 Oct 2025 10:11:34 +0100 Subject: [PATCH 2/8] Add const_noise_inds to samplers --- package/samplers/value_at_risk/_gp/acqf.py | 7 +++++++ package/samplers/value_at_risk/sampler.py | 24 +++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/package/samplers/value_at_risk/_gp/acqf.py b/package/samplers/value_at_risk/_gp/acqf.py index e52a6632..15616424 100644 --- a/package/samplers/value_at_risk/_gp/acqf.py +++ b/package/samplers/value_at_risk/_gp/acqf.py @@ -117,11 +117,13 @@ def __init__( threshold_list: list[float], n_input_noise_samples: int, qmc_seed: int | None, + const_noise_param_inds: list[int] | None = None, uniform_input_noise_rads: torch.Tensor | None = None, normal_input_noise_stdevs: torch.Tensor | None = None, stabilizing_noise: float = 1e-12, ) -> None: self._gpr_list = gpr_list + self._const_noise_param_inds = const_noise_param_inds self._threshold_list = threshold_list rng = np.random.RandomState(qmc_seed) self._input_noise = _sample_input_noise( @@ -167,6 +169,7 @@ def __init__( n_qmc_samples: int, qmc_seed: int | None, acqf_type: str, + const_noise_param_inds: list[int] | None = None, uniform_input_noise_rads: torch.Tensor | None = None, normal_input_noise_stdevs: torch.Tensor | None = None, ) -> None: @@ -174,6 +177,7 @@ def __init__( self._gpr = gpr self._confidence_level = confidence_level rng = np.random.RandomState(qmc_seed) + self._const_noise_param_inds = const_noise_param_inds or [] self._input_noise = _sample_input_noise( n_input_noise_samples, uniform_input_noise_rads, @@ -226,6 +230,7 @@ def __init__( n_qmc_samples: int, qmc_seed: int | None, acqf_type: str, + const_noise_param_inds: list[int] | None = None, uniform_input_noise_rads: torch.Tensor | None = None, normal_input_noise_stdevs: torch.Tensor | None = None, stabilizing_noise: float = 1e-12, @@ -238,6 +243,7 @@ def __init__( n_qmc_samples=n_qmc_samples, qmc_seed=qmc_seed, acqf_type=acqf_type, + const_noise_param_inds=const_noise_param_inds, uniform_input_noise_rads=uniform_input_noise_rads, normal_input_noise_stdevs=normal_input_noise_stdevs, ) @@ -248,6 +254,7 @@ def __init__( threshold_list=constraints_threshold_list, n_input_noise_samples=n_input_noise_samples, qmc_seed=qmc_seed, + const_noise_param_inds=const_noise_param_inds, uniform_input_noise_rads=uniform_input_noise_rads, normal_input_noise_stdevs=normal_input_noise_stdevs, stabilizing_noise=stabilizing_noise, diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index 9218f7a5..f9c5eeac 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -136,7 +136,7 @@ def __init__( self._uniform_input_noise_rads = uniform_input_noise_rads self._normal_input_noise_stdevs = normal_input_noise_stdevs - self._const_noisy_input_param_names = const_noisy_input_param_names + self._const_noisy_input_param_names = const_noisy_input_param_names or [] self._rng = LazyRandomState(seed) self._independent_sampler = independent_sampler or optuna.samplers.RandomSampler(seed=seed) self._intersection_search_space = optuna.search_space.IntersectionSearchSpace() @@ -190,16 +190,13 @@ def infer_relative_search_space( return search_space - def _optimize_acqf( - self, acqf: acqf_module.BaseAcquisitionFunc, best_params: np.ndarray | None - ) -> np.ndarray: + def _optimize_acqf(self, acqf: acqf_module.BaseAcquisitionFunc) -> np.ndarray: # Advanced users can override this method to change the optimization algorithm. # However, we do not make any effort to keep backward compatibility between versions. # Particularly, we may remove this function in future refactoring. - assert best_params is None or len(best_params.shape) == 2 normalized_params, _acqf_val = optim_mixed.optimize_acqf_mixed( acqf, - warmstart_normalized_params_array=best_params, + warmstart_normalized_params_array=None, n_preliminary_samples=self._n_preliminary_samples, n_local_search=self._n_local_search, tol=self._tol, @@ -286,15 +283,23 @@ def _get_scaled_input_noise_params( return scaled_input_noise_params noise_kwargs: dict[str, torch.Tensor] = {} + const_noise_param_inds = [ + i + for i, param_name in enumerate(search_space) + if param_name in self._const_noisy_input_param_names + ] if self._uniform_input_noise_rads is not None: scaled_input_noise_params = _get_scaled_input_noise_params( self._uniform_input_noise_rads, "uniform_input_noise_rads" ) + scaled_input_noise_params[const_noise_param_inds] = 0.5 noise_kwargs["uniform_input_noise_rads"] = scaled_input_noise_params elif self._normal_input_noise_stdevs is not None: scaled_input_noise_params = _get_scaled_input_noise_params( self._normal_input_noise_stdevs, "normal_input_noise_stdevs" ) + # NOTE(nabenabe): \pm 2 sigma will cover the domain. + scaled_input_noise_params[const_noise_param_inds] = 0.25 noise_kwargs["normal_input_noise_stdevs"] = scaled_input_noise_params else: assert False, "Should not reach here." @@ -308,6 +313,7 @@ def _get_scaled_input_noise_params( n_qmc_samples=self._n_qmc_samples, qmc_seed=self._rng.rng.randint(1 << 30), acqf_type=acqf_type, + const_noise_param_inds=const_noise_param_inds, **noise_kwargs, ) else: @@ -322,6 +328,7 @@ def _get_scaled_input_noise_params( n_qmc_samples=self._n_qmc_samples, qmc_seed=self._rng.rng.randint(1 << 30), acqf_type=acqf_type, + const_noise_param_inds=const_noise_param_inds, **noise_kwargs, ) @@ -397,7 +404,6 @@ def sample_relative( return {} gprs_list = self._get_gpr_list(study, search_space) - best_params: np.ndarray | None acqf: acqf_module.BaseAcquisitionFunc assert len(gprs_list) == 1 internal_search_space = gp_search_space.SearchSpace(search_space) @@ -409,7 +415,6 @@ def sample_relative( search_space, acqf_type="mean", ) - best_params = None else: constraint_vals, _ = _get_constraint_vals_and_feasibility(study, trials) constr_gpr_list, constr_threshold_list = self._get_constraints_acqf_args( @@ -426,9 +431,8 @@ def sample_relative( constraints_gpr_list=constr_gpr_list, constraints_threshold_list=constr_threshold_list, ) - best_params = None - normalized_param = self._optimize_acqf(acqf, best_params) + normalized_param = self._optimize_acqf(acqf) return internal_search_space.get_unnormalized_param(normalized_param) def get_robust_trial(self, study: Study) -> FrozenTrial: From 69c08c3cd002dfcc7d07acbb36938492f022375f Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 5 Nov 2025 03:00:59 +0100 Subject: [PATCH 3/8] Make it runnable --- package/samplers/value_at_risk/sampler.py | 31 +++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index f9c5eeac..2d3fa41a 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -106,7 +106,7 @@ def __init__( warn_independent_sampling: bool = True, uniform_input_noise_rads: dict[str, float] | None = None, normal_input_noise_stdevs: dict[str, float] | None = None, - const_noisy_input_param_names: list[str] | None = None, + const_noisy_param_names: list[str] | None = None, ) -> None: if uniform_input_noise_rads is None and normal_input_noise_stdevs is None: raise ValueError( @@ -118,25 +118,25 @@ def __init__( "Only one of `uniform_input_noise_rads` and `normal_input_noise_stdevs` " "can be specified." ) - if const_noisy_input_param_names is not None: + if const_noisy_param_names is not None: if uniform_input_noise_rads is not None and len( - const_noisy_input_param_names & uniform_input_noise_rads.keys() + const_noisy_param_names & uniform_input_noise_rads.keys() ): raise ValueError( "noisy parameters can be specified only in one of " - "`const_noisy_input_param_names` and `uniform_input_noise_rads`." + "`const_noisy_param_names` and `uniform_input_noise_rads`." ) if normal_input_noise_stdevs is not None and len( - const_noisy_input_param_names & normal_input_noise_stdevs.keys() + const_noisy_param_names & normal_input_noise_stdevs.keys() ): raise ValueError( "noisy parameters can be specified only in one of " - "`const_noisy_input_param_names` and `normal_input_noise_stdevs`." + "`const_noisy_param_names` and `normal_input_noise_stdevs`." ) self._uniform_input_noise_rads = uniform_input_noise_rads self._normal_input_noise_stdevs = normal_input_noise_stdevs - self._const_noisy_input_param_names = const_noisy_input_param_names or [] + self._const_noisy_param_names = const_noisy_param_names or [] self._rng = LazyRandomState(seed) self._independent_sampler = independent_sampler or optuna.samplers.RandomSampler(seed=seed) self._intersection_search_space = optuna.search_space.IntersectionSearchSpace() @@ -244,6 +244,14 @@ def _get_constraints_acqf_args( self._constraints_gprs_cache_list = constraints_gprs return constraints_gprs, constraints_threshold_list + def _get_internal_search_space_with_fixed_params( + self, search_space: dict[str, BaseDistribution] + ) -> gp_search_space.SearchSpace: + search_space_with_fixed_params = search_space.copy() + for param_name in self._const_noisy_param_names: + search_space_with_fixed_params[param_name] = optuna.distributions.IntDistribution(0, 0) + return gp_search_space.SearchSpace(search_space_with_fixed_params) + def _get_value_at_risk( self, gpr: gp.GPRegressor, @@ -286,7 +294,7 @@ def _get_scaled_input_noise_params( const_noise_param_inds = [ i for i, param_name in enumerate(search_space) - if param_name in self._const_noisy_input_param_names + if param_name in self._const_noisy_param_names ] if self._uniform_input_noise_rads is not None: scaled_input_noise_params = _get_scaled_input_noise_params( @@ -304,10 +312,13 @@ def _get_scaled_input_noise_params( else: assert False, "Should not reach here." + search_space_with_fixed_params = self._get_internal_search_space_with_fixed_params( + search_space + ) if constraints_gpr_list is None or constraints_threshold_list is None: return acqf_module.ValueAtRisk( gpr=gpr, - search_space=internal_search_space, + search_space=search_space_with_fixed_params, confidence_level=self._objective_confidence_level, n_input_noise_samples=self._n_input_noise_samples, n_qmc_samples=self._n_qmc_samples, @@ -319,7 +330,7 @@ def _get_scaled_input_noise_params( else: return acqf_module.ConstrainedLogValueAtRisk( gpr=gpr, - search_space=internal_search_space, + search_space=search_space_with_fixed_params, constraints_gpr_list=constraints_gpr_list, constraints_threshold_list=constraints_threshold_list, objective_confidence_level=self._objective_confidence_level, From 56160566ee7b08350dd8d868b0e61c196487c233 Mon Sep 17 00:00:00 2001 From: Masahiro Sakai Date: Thu, 6 Nov 2025 17:23:43 +0900 Subject: [PATCH 4/8] do not pass unnecessary const_noise_param_inds information to acquisition functions --- package/samplers/value_at_risk/_gp/acqf.py | 7 ------- package/samplers/value_at_risk/sampler.py | 2 -- 2 files changed, 9 deletions(-) diff --git a/package/samplers/value_at_risk/_gp/acqf.py b/package/samplers/value_at_risk/_gp/acqf.py index 15616424..e52a6632 100644 --- a/package/samplers/value_at_risk/_gp/acqf.py +++ b/package/samplers/value_at_risk/_gp/acqf.py @@ -117,13 +117,11 @@ def __init__( threshold_list: list[float], n_input_noise_samples: int, qmc_seed: int | None, - const_noise_param_inds: list[int] | None = None, uniform_input_noise_rads: torch.Tensor | None = None, normal_input_noise_stdevs: torch.Tensor | None = None, stabilizing_noise: float = 1e-12, ) -> None: self._gpr_list = gpr_list - self._const_noise_param_inds = const_noise_param_inds self._threshold_list = threshold_list rng = np.random.RandomState(qmc_seed) self._input_noise = _sample_input_noise( @@ -169,7 +167,6 @@ def __init__( n_qmc_samples: int, qmc_seed: int | None, acqf_type: str, - const_noise_param_inds: list[int] | None = None, uniform_input_noise_rads: torch.Tensor | None = None, normal_input_noise_stdevs: torch.Tensor | None = None, ) -> None: @@ -177,7 +174,6 @@ def __init__( self._gpr = gpr self._confidence_level = confidence_level rng = np.random.RandomState(qmc_seed) - self._const_noise_param_inds = const_noise_param_inds or [] self._input_noise = _sample_input_noise( n_input_noise_samples, uniform_input_noise_rads, @@ -230,7 +226,6 @@ def __init__( n_qmc_samples: int, qmc_seed: int | None, acqf_type: str, - const_noise_param_inds: list[int] | None = None, uniform_input_noise_rads: torch.Tensor | None = None, normal_input_noise_stdevs: torch.Tensor | None = None, stabilizing_noise: float = 1e-12, @@ -243,7 +238,6 @@ def __init__( n_qmc_samples=n_qmc_samples, qmc_seed=qmc_seed, acqf_type=acqf_type, - const_noise_param_inds=const_noise_param_inds, uniform_input_noise_rads=uniform_input_noise_rads, normal_input_noise_stdevs=normal_input_noise_stdevs, ) @@ -254,7 +248,6 @@ def __init__( threshold_list=constraints_threshold_list, n_input_noise_samples=n_input_noise_samples, qmc_seed=qmc_seed, - const_noise_param_inds=const_noise_param_inds, uniform_input_noise_rads=uniform_input_noise_rads, normal_input_noise_stdevs=normal_input_noise_stdevs, stabilizing_noise=stabilizing_noise, diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index 2d3fa41a..782da73e 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -324,7 +324,6 @@ def _get_scaled_input_noise_params( n_qmc_samples=self._n_qmc_samples, qmc_seed=self._rng.rng.randint(1 << 30), acqf_type=acqf_type, - const_noise_param_inds=const_noise_param_inds, **noise_kwargs, ) else: @@ -339,7 +338,6 @@ def _get_scaled_input_noise_params( n_qmc_samples=self._n_qmc_samples, qmc_seed=self._rng.rng.randint(1 << 30), acqf_type=acqf_type, - const_noise_param_inds=const_noise_param_inds, **noise_kwargs, ) From 960e5e30fdebc7c586a458fd082b927876c06c87 Mon Sep 17 00:00:00 2001 From: Masahiro Sakai Date: Thu, 6 Nov 2025 17:18:07 +0900 Subject: [PATCH 5/8] change RobustGPSampler.sample_relative to return randomly sampled value for constant noisy parameters --- package/samplers/value_at_risk/sampler.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index 782da73e..01fbb165 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -442,7 +442,14 @@ def sample_relative( ) normalized_param = self._optimize_acqf(acqf) - return internal_search_space.get_unnormalized_param(normalized_param) + params = internal_search_space.get_unnormalized_param(normalized_param) + + for name in self._const_noisy_param_names: + dist = search_space[name] + assert isinstance(dist, optuna.distributions.FloatDistribution) + params[name] = self._rng.rng.uniform(dist.low, dist.high) + + return params def get_robust_trial(self, study: Study) -> FrozenTrial: states = (TrialState.COMPLETE,) From 46f4bc8c4495bfef7795f69c3b7b7734d2c2d818 Mon Sep 17 00:00:00 2001 From: Masahiro Sakai Date: Thu, 6 Nov 2025 18:09:41 +0900 Subject: [PATCH 6/8] factor out RobustGPSampler._optimize_params from RobustGPSampler.sample_relative --- package/samplers/value_at_risk/sampler.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index 01fbb165..ee043f8b 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -401,16 +401,13 @@ def _get_gpr_list( self._gprs_cache_list = gprs_list return gprs_list - def sample_relative( - self, study: Study, trial: FrozenTrial, search_space: dict[str, BaseDistribution] + def _optimize_params( + self, study: Study, trials: list[FrozenTrial], search_space: dict[str, BaseDistribution] ) -> dict[str, Any]: if search_space == {}: return {} self._verify_search_space(search_space) - trials = study._get_trials(deepcopy=False, states=(TrialState.COMPLETE,), use_cache=True) - if len(trials) < self._n_startup_trials: - return {} gprs_list = self._get_gpr_list(study, search_space) acqf: acqf_module.BaseAcquisitionFunc @@ -442,8 +439,18 @@ def sample_relative( ) normalized_param = self._optimize_acqf(acqf) - params = internal_search_space.get_unnormalized_param(normalized_param) + return internal_search_space.get_unnormalized_param(normalized_param) + + def sample_relative( + self, study: Study, trial: FrozenTrial, search_space: dict[str, BaseDistribution] + ) -> dict[str, Any]: + trials = study._get_trials(deepcopy=False, states=(TrialState.COMPLETE,), use_cache=True) + if len(trials) < self._n_startup_trials: + return {} + + params = self._optimize_params(study, trials, search_space) + # Perturb constant noisy parameter uniformly for name in self._const_noisy_param_names: dist = search_space[name] assert isinstance(dist, optuna.distributions.FloatDistribution) From 87c47599d5d86b5816a6632c1211a8f19b36709e Mon Sep 17 00:00:00 2001 From: Masahiro Sakai Date: Thu, 6 Nov 2025 18:10:04 +0900 Subject: [PATCH 7/8] add RobustGPSampler.get_robust_params --- package/samplers/value_at_risk/sampler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index ee043f8b..8acd4fa8 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -490,6 +490,12 @@ def get_robust_trial(self, study: Study) -> FrozenTrial: best_idx = np.argmax(acqf.eval_acqf_no_grad(X_train)).item() return trials[best_idx] + def get_robust_params(self, study: Study) -> dict[str, Any]: + states = (TrialState.COMPLETE,) + trials = study._get_trials(deepcopy=False, states=states, use_cache=True) + search_space = self.infer_relative_search_space(study, trials[0]) + return self._optimize_params(study, trials, search_space) + def sample_independent( self, study: Study, From 895b998a2ff7854dd1307ca63ac6c3f2d7517497 Mon Sep 17 00:00:00 2001 From: Masahiro Sakai Date: Fri, 7 Nov 2025 10:23:45 +0900 Subject: [PATCH 8/8] add docstring about const_noisy_param_names --- package/samplers/value_at_risk/sampler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package/samplers/value_at_risk/sampler.py b/package/samplers/value_at_risk/sampler.py index bb391bcb..d65c9c1a 100644 --- a/package/samplers/value_at_risk/sampler.py +++ b/package/samplers/value_at_risk/sampler.py @@ -101,6 +101,10 @@ class RobustGPSampler(BaseSampler): The input noise standard deviations for each parameter. For example, when `{"x": 0.1, "y": 0.2}` is given, the sampler assumes that the input noise of `x` and `y` follows `N(0, 0.1**2)` and `N(0, 0.2**2)`, respectively. + const_noisy_param_names: + The list of parameters determined externally rather than being decision variables. + For these parameters, `suggest_float` samples random values instead of searching + values that optimize the objective function. """ def __init__(