Abstract
The original motivation can be found here.
HPO is often iterative: a range that looked reasonable at the start can turn out to be too narrow once a few trials complete. For example, if reg_alpha for an XGBoost model is first searched over [0, 1] and the best trials keep landing near 0.99, the natural next step is to widen it to, say, [0, 2] and keep optimizing on the same study.
Optuna’s built-in TPESampler(multivariate=True) (and other samplers with the same requirement, e.g. CmaEsSampler) cannot make use of the trials collected before such a change. Internally, the relative search space is computed as the intersection of the distributions seen so far, so a parameter whose bounds ever changed is dropped from that intersection entirely; it then falls back to independent (univariate) sampling for that parameter, losing the joint/multivariate model between it and the rest of the parameters, even though every previous trial is still a perfectly valid observation once the range is only ever grown, never narrowed.
TPESampler in this package special-cases this scenario:
infer_relative_search_spacealways returns the most recently completed trial’s distributions as the current search space, instead of computing an intersection across all trials.- Before fitting the below/above Parzen estimators, every historical trial is checked against this current search space: a trial is kept if each of its
FloatDistribution/IntDistributionparameter values still falls inside the current[low, high]bounds (CategoricalDistributionchoices are assumed immutable within a study, matching Optuna’s own assumption, so those trials are always kept).
As long as a numerical parameter’s range is only ever grown (the new bounds are a superset of every previous range), every past observation for it remains inside the new bounds and keeps contributing to the joint multivariate model. If a range is instead narrowed, trials whose recorded value now falls outside the new bounds are simply excluded from that round’s model rather than raising an error.
This sampler is a fork of Optuna 4.9.0’s optuna.samplers.TPESampler (kept under _tpe_v4_9_0/) with the deprecated, experimental multivariate/group constructor arguments removed: multivariate is fixed to True and group-decomposed conditional search spaces (group=True) are not supported.
APIs
TPESampler(*, n_startup_trials: int = 10, n_ei_candidates: int = 24, seed: int | None = None, constant_liar: bool = True, constraints_func: Callable[[FrozenTrial], Sequence[float]] | None = None)n_startup_trials: The random sampling is used instead of the TPE algorithm until the given number of trials finish in the same study.n_ei_candidates: Number of candidate samples used to calculate the expected improvement.seed: Seed for random number generator.constant_liar: IfTrue, penalize running trials to avoid suggesting parameter configurations nearby. Defaults toTruehere, unlike Optuna’s built-inTPESampler, which defaults toFalse.constraints_func: An optional function that computes the objective constraints. It must take aoptuna.trial.FrozenTrialand return the constraints. The return value must be a sequence offloats. A value strictly larger than 0 means that a constraint is violated. A value equal to or smaller than 0 is considered feasible. Ifconstraints_funcreturns more than one value for a trial, that trial is considered feasible if and only if all values are equal to 0 or smaller. The function is evaluated after each successful trial and is not called when trials fail or are pruned.
Installation
$ pip install -r https://hub.optuna.org/samplers/multivariate_tpe_flex/requirements.txt
Example
import optuna
import optunahub
def objective(trial: optuna.Trial) -> float:
x = trial.suggest_float("x", -5, 5)
y = trial.suggest_float("y", -5, 5)
return x**2 + y**2
module = optunahub.load_module(package="samplers/multivariate_tpe_flex")
sampler = module.TPESampler()
study = optuna.create_study(sampler=sampler)
study.optimize(objective, n_trials=100)
Growing the search space across optimize calls
The example below mirrors the motivating scenario: reg_alpha is first searched over [0, 1], and once the sampler keeps favoring values near the upper bound, the same study is resumed with reg_alpha widened to [0, 2]. All trials from the first round remain valid observations for the second round because their reg_alpha values fall inside the new, wider bound.
import optuna
import optunahub
def objective(trial: optuna.Trial, reg_alpha_high: float) -> float:
reg_alpha = trial.suggest_float("reg_alpha", 0.0, reg_alpha_high)
lr = trial.suggest_float("lr", 1e-4, 1e-1, log=True)
return (reg_alpha - 0.95) ** 2 + (lr - 1e-2) ** 2
module = optunahub.load_module(package="samplers/multivariate_tpe_flex")
sampler = module.TPESampler(seed=0)
study = optuna.create_study(sampler=sampler)
# Round 1: reg_alpha is initially assumed to live in [0, 1].
study.optimize(lambda trial: objective(trial, reg_alpha_high=1.0), n_trials=30)
# Round 2: reg_alpha kept landing near 1.0, so the range is widened to [0, 2] and
# optimization continues on the same study/sampler, still using the round 1 trials.
study.optimize(lambda trial: objective(trial, reg_alpha_high=2.0), n_trials=30)
print(study.best_params, study.best_value)
- Package
- samplers/multivariate_tpe_flex
- Author
- Shuhei Watanabe
- License
- MIT License
- Verified Optuna version
- 4.9.0
- Dependencies (.txt)
- optuna>=4.9.0
- Last update
- 2026-07-09
- Discussions & Issues
- Create a discussion
- Create a bug report