Struct Tpe
pub struct Tpe { /* private fields */ }Expand description
A Tree-structured Parzen Estimator sampler.
Construct one with Tpe::new (the defaults it documents) and hand it to a
study through StudyBuilder::sampler.
The knobs below are the only configuration; everything else is derived from
the study’s history on each decision. See the module-level documentation
for the algorithm.
use atune_core::sampler::Tpe;
use atune_core::space::{Distribution, ParamSpec, SpaceSchema};
use atune_core::study::{Budget, Study, StudyConfig};
use std::sync::Arc;
let space = SpaceSchema::try_new([
ParamSpec::try_new("x", Distribution::try_new_with_float(-5.0, 5.0).unwrap()).unwrap(),
ParamSpec::try_new("y", Distribution::try_new_with_float(-5.0, 5.0).unwrap()).unwrap(),
])
.unwrap();
let study = Study::builder()
.sampler(Arc::new(Tpe::new()))
.budget(Budget::new_with_trials(60))
.create(StudyConfig::new("sphere").with_seed(0).with_space(space))
.unwrap();
study
.optimize(|ctx| {
let x = ctx.suggest_f64("x", -5.0..=5.0, atune_core::space::Scale::Linear)?;
let y = ctx.suggest_f64("y", -5.0..=5.0, atune_core::space::Scale::Linear)?;
Ok((x * x + y * y).into())
})
.unwrap();
// TPE concentrates near the origin, so the best is well below a random draw.
let best = study.best_trial().unwrap().unwrap();
assert!(best.single_objective_value().unwrap() < 1.0);Implementations§
§impl Tpe
impl Tpe
pub const fn new() -> Self
pub const fn new() -> Self
A TPE sampler with the default settings.
n_startup_trials = 10, n_ei_candidates = 24, gamma = 0.25 (read
only by the sub-linear split), prior_weight = 1.0, constant liar
on, the multivariate kernel, discrete kernels, log scoring, the
linear split, Optuna bandwidths and sharpening on, and constraints
and MOTPE/HSSP off.
The six modelling settings became defaults together; before that each
was off. Pass false to their builders
(with_multivariate,
with_discrete_kernels,
with_log_scoring,
with_linear_split,
with_optuna_bandwidth,
with_sharpening) to reproduce the earlier
default’s seeded output bit for bit.
pub const fn with_startup_trials(self, n: usize) -> Self
pub const fn with_startup_trials(self, n: usize) -> Self
Sets how many initial trials are sampled uniformly before TPE engages.
Too few and the first density models are fit on noise; the default of 10 matches Optuna.
pub const fn with_ei_candidates(self, n: usize) -> Self
pub const fn with_ei_candidates(self, n: usize) -> Self
Sets how many candidate points are scored per decision.
More candidates sharpen the argmax at linear cost; the default is 24.
pub const fn with_gamma(self, gamma: f64) -> Self
pub const fn with_gamma(self, gamma: f64) -> Self
Sets the split quantile factor gamma in n_good = ceil(gamma·√n).
Larger gamma widens the “good” set. The raw value is retained by this
infallible builder; it must be finite and in (0, 1] when sampling.
Read only with with_linear_split(false):
the default linear split ignores it.
pub const fn with_prior_weight(self, weight: f64) -> Self
pub const fn with_prior_weight(self, weight: f64) -> Self
Sets the prior weight used in every Parzen estimator.
pub const fn with_constant_liar(self, on: bool) -> Self
pub const fn with_constant_liar(self, on: bool) -> Self
Turns the pending-aware constant liar on or off.
On by default. With it off, running trials are ignored and parallel workers no longer repel from one another — useful only for isolating the effect in a test.
pub const fn with_constraints(self, on: bool) -> Self
pub const fn with_constraints(self, on: bool) -> Self
Turns the feasibility-first split over recorded constraints on or off.
Off by default, which keeps the objective-only split and a raw ask-time
view. On, trials whose recorded constraints all hold (≤ 0
satisfied) are ordered by objective ahead of every violating trial,
and violating trials use the order of
compare_violations, so the good
set l(x) comes from feasible trials while any exist. Representable
f64 totals keep their existing order; finite constraint sums that
overflow are ordered by their exact positive sums, and NaN or positive
infinity inputs rank above finite violations. The scalar
total_violation can still return
+∞ for an overflowing finite sum; TPE keeps the richer rank internally.
A trial that recorded no constraints counts as feasible. The study then
attaches constraint values to the ask-time view
(reads_constraints), at
the cost of one state read per newly synced finished trial.
pub const fn constraints(&self) -> bool
pub const fn constraints(&self) -> bool
Whether the split is feasibility-first over recorded constraints.
pub const fn with_multivariate(self, on: bool) -> Self
pub const fn with_multivariate(self, on: bool) -> Self
Turns multivariate TPE — one mixture of product kernels per group — on or off.
On by default. Off keeps the joint split with a product of
per-parameter estimators, l(x) = Πᵈ lᵈ(xᵈ), the earlier default.
On, each co-occurrence
group is modelled by one mixture with a component per weighted trial
(plus the prior), each component a product of per-parameter kernels:
l(x) = Σₖ wₖ Πᵈ Kₖ,ᵈ(xᵈ), and candidates are drawn jointly from it.
This is Optuna’s multivariate=True (_MixtureOfProductDistribution
in optuna/samplers/_tpe/probability_distributions.py; Falkner et al.,
2018). It captures correlation between parameters, at a cost linear in
trials times parameters per candidate. Categorical parameters join the
mixture with indicator kernels, or with Aitchison–Aitken kernels under
with_discrete_kernels.
use atune_core::sampler::Tpe;
assert!(Tpe::new().multivariate());
assert!(!Tpe::new().with_multivariate(false).multivariate());pub const fn multivariate(&self) -> bool
pub const fn multivariate(&self) -> bool
Whether each group is one mixture of product kernels.
pub const fn with_discrete_kernels(self, on: bool) -> Self
pub const fn with_discrete_kernels(self, on: bool) -> Self
Turns Optuna’s discrete and categorical kernels on or off.
On by default. Off (the earlier default), an integer or stepped parameter is modelled by the continuous unit-interval density and snapped to its grid afterwards, and a categorical one by recency-weighted counts plus uniform prior mass. On:
- an integer or stepped parameter’s kernel is the Gaussian’s mass
on each grid point’s cell,
[v − step/2, v + step/2](for a log integer[ln(v − ½), ln(v + ½)]), truncated to the union of the cells — Optuna’s_BatchedDiscreteTruncNormDistributionsand its log variant. Probability lives on the grid, so a candidate is scored by the mass of the value it will become; - a categorical or boolean parameter’s kernel is Aitchison–Aitken,
(ε + [c = cᵢ]) / (1 + kε)withε = prior_weight / n_kernels, Optuna’s categorical kernel (Aitchison and Aitken, 1976), which spreads every observation’s mass a little over all choices.
use atune_core::sampler::Tpe;
assert!(Tpe::new().discrete_kernels());
assert!(!Tpe::new().with_discrete_kernels(false).discrete_kernels());pub const fn discrete_kernels(&self) -> bool
pub const fn discrete_kernels(&self) -> bool
Whether discrete and categorical parameters use their own kernels.
pub const fn with_log_scoring(self, on: bool) -> Self
pub const fn with_log_scoring(self, on: bool) -> Self
Turns log-space scoring on or off.
On by default. Off (the earlier default), a mixture’s density is the
linear sum of its components, floored at 1e-12 before the
logarithm. On, it is
logsumexp over the components’ log terms with no floor, as Optuna
computes it, so log l(x) − log g(x) stays finite and keeps its order
where every component underflows — far from all observations, or over
many dimensions of a multivariate mixture — instead of collapsing to
a tie at the floor. Where nothing underflows the two agree to
rounding.
use atune_core::sampler::Tpe;
assert!(Tpe::new().log_scoring());
assert!(!Tpe::new().with_log_scoring(false).log_scoring());pub const fn log_scoring(&self) -> bool
pub const fn log_scoring(&self) -> bool
Whether mixtures are scored in log space.
pub const fn with_hssp(self, on: bool) -> Self
pub const fn with_hssp(self, on: bool) -> Self
Turns Optuna 5-style multi-objective TPE splitting on or off.
Off by default: a multi-objective study uses uniform random sampling, preserving the legacy behavior. On, complete and pruned observations are ranked by nondomination fronts; HSSP chooses the required subset of the last partial front. The good-set Parzen components receive uniform weights, as in Optuna 5.0.0, while the bad set keeps the usual recency weights. The split and all weights are deterministic. Single-objective studies keep their existing split and seeded output.
Exact hypervolume calculations within HSSP use the existing WFG path for objective counts above three.
use atune_core::sampler::Tpe;
assert!(Tpe::new().with_hssp(true).hssp());
assert!(!Tpe::new().hssp());pub const fn with_linear_split(self, on: bool) -> Self
pub const fn with_linear_split(self, on: bool) -> Self
Turns Optuna’s linear-capped split on or off.
On by default: the good set holds min(ceil(0.1·n), 25) of the n
ranked observations, the default_gamma that Optuna’s TPESampler
uses unless told otherwise, and with_gamma is not
read (it is still validated). Off (the earlier default), it holds
ceil(gamma·√n): Optuna’s hyperopt_default_gamma without its cap at
25, so the two agree up to n = (25 / gamma)². Either rule is clamped
so both sets stay non-empty, and the multi-objective HSSP split takes
its size from the same rule.
use atune_core::sampler::Tpe;
assert!(Tpe::new().linear_split());
assert!(!Tpe::new().with_linear_split(false).linear_split());pub const fn linear_split(&self) -> bool
pub const fn linear_split(&self) -> bool
Whether the split is min(ceil(0.1·n), 25) instead of ceil(gamma·√n).
pub const fn with_optuna_bandwidth(self, on: bool) -> Self
pub const fn with_optuna_bandwidth(self, on: bool) -> Self
Turns Optuna’s numeric bandwidth rule on or off.
On by default. Off (the earlier default), every numeric kernel’s sigma
is the larger gap to its neighbours, the prior’s mean among them,
clamped below by one floor, 1/n of the unit width for n split
observations (within [0.005, 1]), shared by l and g. On, each
estimator follows
Optuna’s _ParzenEstimator with its default consider_endpoints=False
and consider_magic_clip=True: gaps among its own observations only,
the outermost taking their inner gap, clamped below by
width / min(100, n + 2) for its own n observations, where an integer
or stepped axis under the discrete kernels has its cells’ extended
width.
use atune_core::sampler::Tpe;
assert!(Tpe::new().optuna_bandwidth());
assert!(!Tpe::new().with_optuna_bandwidth(false).optuna_bandwidth());pub const fn optuna_bandwidth(&self) -> bool
pub const fn optuna_bandwidth(&self) -> bool
Whether numeric bandwidths follow Optuna’s per-estimator rule.
pub const fn with_sharpening(self, on: bool) -> Self
pub const fn with_sharpening(self, on: bool) -> Self
Turns sharpening of the multivariate kernel on or off.
On by default (atune’s own; off is the earlier default). On, and with
with_multivariate, each decision scores
twice n_ei_candidates, drawn alternately
from l and from a sharpened l′, and keeps the argmax of
log l′ − log g′. Once a group has 30 observations (three default
startup phases), l′ and g′ are its mixtures rebuilt with Optuna’s
neighbour gaps and a lower floor, the smaller of Optuna’s magic clip
and 0.08 · width · (n + 2)^(−1/d) for n observations over d
parameters; before that they are l and g. The floor scales as the
observations’ nearest-neighbour spacing, so the kernels sharpen most
in few dimensions, and the unsharpened proposals keep exploring.
Without the multivariate kernel the setting has no effect, and it
stands aside for a group once any observation drew a parameter from a
different distribution than the one modelled — an open range that
grew, whose optimum lies outside the region sharpening would refine.
use atune_core::sampler::Tpe;
assert!(Tpe::new().sharpening());
assert!(!Tpe::new().with_sharpening(false).sharpening());pub const fn sharpening(&self) -> bool
pub const fn sharpening(&self) -> bool
Whether the multivariate kernel adds sharpened candidates and scoring.
Trait Implementations§
impl Copy for Tpe
§impl Sampler for Tpe
impl Sampler for Tpe
§fn reads_constraints(&self) -> bool
fn reads_constraints(&self) -> bool
with_constraints: only a feasibility-first
split needs the recorded constraint values on its view.
§fn infer_relative_space(&self, study: &StudyView) -> Result<SpaceSchema>
fn infer_relative_space(&self, study: &StudyView) -> Result<SpaceSchema>
The study’s declared space, or the intersection of the visible trials’ parameters for a define-by-run study.
Identical in shape to Random: a declared
space is returned verbatim, and an inferred one keeps only the
parameters every visible trial recorded compatibly, name-ordered. The
group decomposition then runs inside
sample_relative; for a
co-occurring space it is a single group.
§Errors
Error::InvalidSpace if a recorded
parameter name is empty, which no writer of this crate produces, or if
one of the sampler’s configuration knobs is invalid.
§fn sample_relative(
&self,
study: &StudyView,
trial: &TrialMeta,
space: &SpaceSchema,
) -> Result<Assignment>
fn sample_relative( &self, study: &StudyView, trial: &TrialMeta, space: &SpaceSchema, ) -> Result<Assignment>
Samples the joint space with multivariate, group-decomposed TPE.
Each co-occurrence group is modelled jointly and the constant liar folds
in the pending trials; a group short of n_startup_trials completed
observations (including the whole study during startup) is drawn
uniformly. Every returned value lies in its declared support, because it
is produced through Distribution::from_unit or a valid choice index.
§Errors
Error::InvalidSpace if a declared
distribution is malformed or one of the sampler’s configuration knobs
is invalid.
§fn sample_independent(
&self,
study: &StudyView,
trial: &TrialMeta,
name: &str,
dist: &Distribution,
) -> Result<ParamValue>
fn sample_independent( &self, study: &StudyView, trial: &TrialMeta, name: &str, dist: &Distribution, ) -> Result<ParamValue>
Samples one parameter with univariate TPE — the fallback for a parameter the relative space did not cover, or one outside any joint group.
Uses the same joint split (by trial objective) and the same constant
liar as the relative path, over the trials that recorded this parameter.
Below the startup threshold it is the uniform
Random draw.
§Errors
Error::InvalidSpace if dist is
malformed or one of the sampler’s configuration knobs is invalid.
§fn reseed(&self, _seed: u64)
fn reseed(&self, _seed: u64)
A no-op.
TPE derives its candidate RNG from
TrialMeta::sampler_seed, so
there is no auxiliary randomness for a reseed to move — the same argument
Random::reseed documents. Change the study
seed to change what a study samples.