hyperopt_samplers/
tpe_sampler.rs1use hyperopt_core::{Direction, Distribution, Sampler, StudyState, Trial, Value};
2use rand::rngs::StdRng;
3use rand::{RngExt, SeedableRng};
4use tpe::density_estimation::DefaultEstimatorBuilder;
5use tpe::{categorical_range, histogram_estimator, parzen_estimator, range, TpeOptimizer};
6
7use crate::random::sample_value;
8
9pub struct TpeSampler {
41 rng: StdRng,
42 n_startup_trials: usize,
43}
44
45impl TpeSampler {
46 pub fn new() -> Self {
49 let mut seeder = rand::rng();
50 TpeSampler {
51 rng: StdRng::seed_from_u64(seeder.random()),
52 n_startup_trials: 10,
53 }
54 }
55
56 pub fn seeded(seed: u64) -> Self {
58 TpeSampler {
59 rng: StdRng::seed_from_u64(seed),
60 n_startup_trials: 10,
61 }
62 }
63
64 pub fn n_startup_trials(mut self, n: usize) -> Self {
66 self.n_startup_trials = n;
67 self
68 }
69
70 fn observations(
73 study_state: &StudyState,
74 name: &str,
75 distribution: &Distribution,
76 ) -> Vec<(f64, f64)> {
77 let direction = study_state.direction();
78 let mut obs = Vec::new();
79 for t in study_state.completed_trials() {
80 let (Some(record), Some(value)) = (
81 t.params.iter().find(|p| p.name == name),
82 t.value,
83 ) else {
84 continue;
85 };
86 if let Some(coord) = encode(distribution, &record.value) {
87 let objective = match direction {
88 Direction::Minimize => value,
89 Direction::Maximize => -value,
90 };
91 obs.push((coord, objective));
92 }
93 }
94 obs
95 }
96}
97
98impl Default for TpeSampler {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl Sampler for TpeSampler {
105 fn suggest(
106 &mut self,
107 study_state: &StudyState,
108 _trial: &Trial,
109 param_name: &str,
110 distribution: &Distribution,
111 ) -> Value {
112 let obs = Self::observations(study_state, param_name, distribution);
113
114 if obs.len() < self.n_startup_trials {
116 return sample_value(&mut self.rng, distribution);
117 }
118
119 let Some((estimator, tpe_range)) = build_estimator(distribution) else {
122 return sample_value(&mut self.rng, distribution);
123 };
124 let mut optim: TpeOptimizer<DefaultEstimatorBuilder> =
125 TpeOptimizer::new(estimator, tpe_range);
126
127 for (coord, value) in &obs {
128 let _ = optim.tell(*coord, *value);
131 }
132
133 match optim.ask(&mut self.rng) {
134 Ok(raw) => decode(distribution, raw),
135 Err(_) => sample_value(&mut self.rng, distribution),
136 }
137 }
138}
139
140fn build_estimator(
143 distribution: &Distribution,
144) -> Option<(DefaultEstimatorBuilder, tpe::range::Range)> {
145 match distribution {
146 Distribution::Uniform { low, high } => {
147 Some((parzen_estimator(), range(*low, *high).ok()?))
148 }
149 Distribution::LogUniform { low, high } => {
150 if *low <= 0.0 {
151 return None;
152 }
153 Some((parzen_estimator(), range(low.ln(), high.ln()).ok()?))
154 }
155 Distribution::IntUniform { low, high } => {
156 Some((parzen_estimator(), range(*low as f64, *high as f64 + 1.0).ok()?))
158 }
159 Distribution::Categorical { choices } => {
160 Some((histogram_estimator(), categorical_range(choices.len()).ok()?))
161 }
162 }
163}
164
165fn encode(distribution: &Distribution, value: &Value) -> Option<f64> {
167 match distribution {
168 Distribution::Uniform { .. } => value.as_float(),
169 Distribution::LogUniform { .. } => value.as_float().map(f64::ln),
170 Distribution::IntUniform { .. } => value.as_int().map(|i| i as f64),
171 Distribution::Categorical { choices } => {
172 let label = value.as_categorical()?;
173 choices.iter().position(|c| c == label).map(|i| i as f64)
174 }
175 }
176}
177
178fn decode(distribution: &Distribution, raw: f64) -> Value {
180 match distribution {
181 Distribution::Uniform { .. } => Value::Float(raw),
182 Distribution::LogUniform { .. } => Value::Float(raw.exp()),
183 Distribution::IntUniform { low, high } => {
184 let i = (raw.floor() as i64).clamp(*low, *high);
185 Value::Int(i)
186 }
187 Distribution::Categorical { choices } => {
188 if choices.is_empty() {
189 return Value::Categorical(String::new());
190 }
191 let idx = (raw.floor() as usize).min(choices.len() - 1);
192 Value::Categorical(choices[idx].clone())
193 }
194 }
195}