sklears_svm/hyperparameter_optimization/
bayesian_optimization.rs1use std::time::Instant;
4
5use scirs2_core::ndarray::{Array1, Array2};
6use scirs2_core::random::Random;
7
8use crate::kernels::KernelType;
9use crate::svc::SVC;
10use sklears_core::error::{Result, SklearsError};
11use sklears_core::traits::{Fit, Predict};
12
13use super::{
14 OptimizationConfig, OptimizationResult, ParameterSet, ParameterSpec, ScoringMetric, SearchSpace,
15};
16
17pub struct BayesianOptimizationCV {
19 config: OptimizationConfig,
20 search_space: SearchSpace,
21 rng: Random<scirs2_core::random::rngs::StdRng>,
22}
23
24impl BayesianOptimizationCV {
25 pub fn new(config: OptimizationConfig, search_space: SearchSpace) -> Self {
27 let rng = if let Some(seed) = config.random_state {
28 Random::seed(seed)
29 } else {
30 Random::seed(42) };
32
33 Self {
34 config,
35 search_space,
36 rng,
37 }
38 }
39
40 pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
42 let start_time = Instant::now();
43
44 if self.config.verbose {
45 println!(
46 "Bayesian optimization with {} iterations",
47 self.config.n_iterations
48 );
49 }
50
51 let n_initial = (self.config.n_iterations / 5).clamp(5, 20);
53 let mut evaluated_params: Vec<(ParameterSet, f64)> = Vec::new();
54 let mut best_score = -f64::INFINITY;
55 let mut best_params = ParameterSet::new();
56 let mut score_history = Vec::new();
57 let mut iterations_without_improvement = 0;
58
59 if self.config.verbose {
61 println!("Phase 1: Random exploration ({} samples)", n_initial);
62 }
63
64 for i in 0..n_initial {
65 let params = self.sample_random_params()?;
66 let score = self.evaluate_params(¶ms, x, y)?;
67
68 evaluated_params.push((params.clone(), score));
69 score_history.push(score);
70
71 if score > best_score {
72 best_score = score;
73 best_params = params.clone();
74 iterations_without_improvement = 0;
75 } else {
76 iterations_without_improvement += 1;
77 }
78
79 if self.config.verbose {
80 println!("Sample {}/{}: Score {:.6}", i + 1, n_initial, score);
81 }
82 }
83
84 if self.config.verbose {
86 println!("Phase 2: Bayesian optimization");
87 }
88
89 for iteration in n_initial..self.config.n_iterations {
90 let next_params = self.select_next_point(&evaluated_params)?;
93
94 let score = self.evaluate_params(&next_params, x, y)?;
96
97 evaluated_params.push((next_params.clone(), score));
98 score_history.push(score);
99
100 if score > best_score {
101 best_score = score;
102 best_params = next_params.clone();
103 iterations_without_improvement = 0;
104
105 if self.config.verbose {
106 println!(
107 "Iteration {}/{}: NEW BEST Score {:.6}",
108 iteration + 1,
109 self.config.n_iterations,
110 score
111 );
112 }
113 } else {
114 iterations_without_improvement += 1;
115
116 if self.config.verbose && (iteration + 1) % 10 == 0 {
117 println!(
118 "Iteration {}/{}: Score {:.6} (best: {:.6})",
119 iteration + 1,
120 self.config.n_iterations,
121 score,
122 best_score
123 );
124 }
125 }
126
127 if let Some(patience) = self.config.early_stopping_patience {
129 if iterations_without_improvement >= patience {
130 if self.config.verbose {
131 println!("Early stopping at iteration {}", iteration + 1);
132 }
133 break;
134 }
135 }
136 }
137
138 if self.config.verbose {
139 println!("Best score: {:.6}", best_score);
140 println!("Best params: {:?}", best_params);
141 }
142
143 Ok(OptimizationResult {
144 best_params,
145 best_score,
146 cv_results: evaluated_params,
147 n_iterations: score_history.len(),
148 optimization_time: start_time.elapsed().as_secs_f64(),
149 score_history,
150 })
151 }
152
153 fn sample_random_params(&mut self) -> Result<ParameterSet> {
155 let c_spec = self.search_space.c.clone();
157 let kernel_spec = self.search_space.kernel.clone();
158 let tol_spec = self.search_space.tol.clone();
159 let max_iter_spec = self.search_space.max_iter.clone();
160
161 let c = self.sample_value(&c_spec)?;
162
163 let kernel = if let Some(ref spec) = kernel_spec {
164 self.sample_kernel(spec)?
165 } else {
166 KernelType::Rbf { gamma: 1.0 }
167 };
168
169 let tol = if let Some(ref spec) = tol_spec {
170 self.sample_value(spec)?
171 } else {
172 1e-3
173 };
174
175 let max_iter = if let Some(ref spec) = max_iter_spec {
176 self.sample_value(spec)? as usize
177 } else {
178 1000
179 };
180
181 Ok(ParameterSet {
182 c,
183 kernel,
184 tol,
185 max_iter,
186 })
187 }
188
189 fn select_next_point(&mut self, evaluated: &[(ParameterSet, f64)]) -> Result<ParameterSet> {
191 let n_candidates = 100;
193 let mut candidates = Vec::with_capacity(n_candidates);
194
195 for _ in 0..n_candidates {
196 candidates.push(self.sample_random_params()?);
197 }
198
199 let best_observed = evaluated
201 .iter()
202 .map(|(_, score)| *score)
203 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
204 .unwrap_or(0.0);
205
206 let mut best_ei = -f64::INFINITY;
207 let mut best_candidate = candidates[0].clone();
208
209 for candidate in &candidates {
210 let ei = self.expected_improvement(candidate, evaluated, best_observed)?;
211 if ei > best_ei {
212 best_ei = ei;
213 best_candidate = candidate.clone();
214 }
215 }
216
217 Ok(best_candidate)
218 }
219
220 fn expected_improvement(
222 &self,
223 candidate: &ParameterSet,
224 evaluated: &[(ParameterSet, f64)],
225 best_observed: f64,
226 ) -> Result<f64> {
227 let (mean, std) = self.gp_predict(candidate, evaluated)?;
232
233 if std < 1e-10 {
234 return Ok(0.0);
235 }
236
237 let z = (mean - best_observed - 0.01) / std; let ei = (mean - best_observed - 0.01) * self.normal_cdf(z) + std * self.normal_pdf(z);
240
241 Ok(ei.max(0.0))
242 }
243
244 fn gp_predict(
246 &self,
247 candidate: &ParameterSet,
248 evaluated: &[(ParameterSet, f64)],
249 ) -> Result<(f64, f64)> {
250 if evaluated.is_empty() {
251 return Ok((0.0, 1.0));
252 }
253
254 let length_scale = 1.0;
256 let mut total_weight = 0.0;
257 let mut weighted_mean = 0.0;
258
259 for (params, score) in evaluated {
260 let dist = self.parameter_distance(candidate, params)?;
261 let weight = (-0.5 * (dist / length_scale).powi(2)).exp();
262 total_weight += weight;
263 weighted_mean += weight * score;
264 }
265
266 let mean = if total_weight > 1e-10 {
267 weighted_mean / total_weight
268 } else {
269 evaluated.iter().map(|(_, score)| score).sum::<f64>() / evaluated.len() as f64
270 };
271
272 let min_dist = evaluated
274 .iter()
275 .map(|(params, _)| self.parameter_distance(candidate, params).unwrap_or(1.0))
276 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
277 .unwrap_or(1.0);
278
279 let std = (0.1 + 0.9 * (1.0 - (-min_dist.powi(2)).exp())).min(1.0);
280
281 Ok((mean, std))
282 }
283
284 fn parameter_distance(&self, a: &ParameterSet, b: &ParameterSet) -> Result<f64> {
286 let c_dist = ((a.c.ln() - b.c.ln()) / 3.0).powi(2); let tol_dist = ((a.tol.ln() - b.tol.ln()) / 3.0).powi(2);
289 let max_iter_dist = ((a.max_iter as f64 - b.max_iter as f64) / 2500.0).powi(2);
290
291 let kernel_dist = if std::mem::discriminant(&a.kernel) == std::mem::discriminant(&b.kernel)
293 {
294 0.0
295 } else {
296 1.0
297 };
298
299 Ok((c_dist + tol_dist + max_iter_dist + kernel_dist).sqrt())
300 }
301
302 fn normal_cdf(&self, x: f64) -> f64 {
304 0.5 * (1.0 + self.erf(x / std::f64::consts::SQRT_2))
305 }
306
307 fn normal_pdf(&self, x: f64) -> f64 {
309 (1.0 / (2.0 * std::f64::consts::PI).sqrt()) * (-0.5 * x.powi(2)).exp()
310 }
311
312 fn erf(&self, x: f64) -> f64 {
314 let a1 = 0.254829592;
316 let a2 = -0.284496736;
317 let a3 = 1.421413741;
318 let a4 = -1.453152027;
319 let a5 = 1.061405429;
320 let p = 0.3275911;
321
322 let sign = if x < 0.0 { -1.0 } else { 1.0 };
323 let x = x.abs();
324
325 let t = 1.0 / (1.0 + p * x);
326 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
327
328 sign * y
329 }
330
331 fn sample_value(&mut self, spec: &ParameterSpec) -> Result<f64> {
333 match spec {
334 ParameterSpec::Fixed(value) => Ok(*value),
335 ParameterSpec::Uniform { min, max } => {
336 use scirs2_core::random::essentials::Uniform;
337 let dist = Uniform::new(*min, *max).map_err(|e| {
338 SklearsError::InvalidInput(format!(
339 "Failed to create uniform distribution: {}",
340 e
341 ))
342 })?;
343 Ok(self.rng.sample(dist))
344 }
345 ParameterSpec::LogUniform { min, max } => {
346 use scirs2_core::random::essentials::Uniform;
347 let log_min = min.ln();
348 let log_max = max.ln();
349 let dist = Uniform::new(log_min, log_max).map_err(|e| {
350 SklearsError::InvalidInput(format!(
351 "Failed to create uniform distribution: {}",
352 e
353 ))
354 })?;
355 let log_val = self.rng.sample(dist);
356 Ok(log_val.exp())
357 }
358 ParameterSpec::Choice(choices) => {
359 if choices.is_empty() {
360 return Err(SklearsError::InvalidInput("Empty choice list".to_string()));
361 }
362 use scirs2_core::random::essentials::Uniform;
363 let dist = Uniform::new(0, choices.len()).map_err(|e| {
364 SklearsError::InvalidInput(format!(
365 "Failed to create uniform distribution: {}",
366 e
367 ))
368 })?;
369 let idx = self.rng.sample(dist);
370 Ok(choices[idx])
371 }
372 ParameterSpec::KernelChoice(_) => Err(SklearsError::InvalidInput(
373 "Use sample_kernel for kernel specs".to_string(),
374 )),
375 }
376 }
377
378 fn sample_kernel(&mut self, spec: &ParameterSpec) -> Result<KernelType> {
380 match spec {
381 ParameterSpec::KernelChoice(kernels) => {
382 if kernels.is_empty() {
383 return Err(SklearsError::InvalidInput(
384 "Empty kernel choice list".to_string(),
385 ));
386 }
387 use scirs2_core::random::essentials::Uniform;
388 let dist = Uniform::new(0, kernels.len()).map_err(|e| {
389 SklearsError::InvalidInput(format!(
390 "Failed to create uniform distribution: {}",
391 e
392 ))
393 })?;
394 let idx = self.rng.sample(dist);
395 Ok(kernels[idx].clone())
396 }
397 _ => Err(SklearsError::InvalidInput(
398 "Invalid kernel specification".to_string(),
399 )),
400 }
401 }
402
403 fn evaluate_params(
405 &self,
406 params: &ParameterSet,
407 x: &Array2<f64>,
408 y: &Array1<f64>,
409 ) -> Result<f64> {
410 let scores = self.cross_validate(params, x, y)?;
411 Ok(scores.iter().sum::<f64>() / scores.len() as f64)
412 }
413
414 fn cross_validate(
416 &self,
417 params: &ParameterSet,
418 x: &Array2<f64>,
419 y: &Array1<f64>,
420 ) -> Result<Vec<f64>> {
421 let n_samples = x.nrows();
422 let fold_size = n_samples / self.config.cv_folds;
423 let mut scores = Vec::new();
424
425 for fold in 0..self.config.cv_folds {
426 let start_idx = fold * fold_size;
427 let end_idx = if fold == self.config.cv_folds - 1 {
428 n_samples
429 } else {
430 (fold + 1) * fold_size
431 };
432
433 let mut x_train_data = Vec::new();
435 let mut y_train_vals = Vec::new();
436 let mut x_test_data = Vec::new();
437 let mut y_test_vals = Vec::new();
438
439 for i in 0..n_samples {
440 if i >= start_idx && i < end_idx {
441 for j in 0..x.ncols() {
443 x_test_data.push(x[[i, j]]);
444 }
445 y_test_vals.push(y[i]);
446 } else {
447 for j in 0..x.ncols() {
449 x_train_data.push(x[[i, j]]);
450 }
451 y_train_vals.push(y[i]);
452 }
453 }
454
455 let n_train = y_train_vals.len();
456 let n_test = y_test_vals.len();
457 let n_features = x.ncols();
458
459 let x_train = Array2::from_shape_vec((n_train, n_features), x_train_data)?;
460 let y_train = Array1::from_vec(y_train_vals);
461 let x_test = Array2::from_shape_vec((n_test, n_features), x_test_data)?;
462 let y_test = Array1::from_vec(y_test_vals);
463
464 let svm = SVC::new()
466 .c(params.c)
467 .kernel(params.kernel.clone())
468 .tol(params.tol)
469 .max_iter(params.max_iter);
470
471 let fitted_svm = svm.fit(&x_train, &y_train)?;
472 let y_pred = fitted_svm.predict(&x_test)?;
473
474 let score = self.calculate_score(&y_test, &y_pred)?;
475 scores.push(score);
476 }
477
478 Ok(scores)
479 }
480
481 fn calculate_score(&self, y_true: &Array1<f64>, y_pred: &Array1<f64>) -> Result<f64> {
483 match self.config.scoring {
484 ScoringMetric::Accuracy => {
485 let correct = y_true
486 .iter()
487 .zip(y_pred.iter())
488 .map(|(&t, &p)| if (t - p).abs() < 0.5 { 1.0 } else { 0.0 })
489 .sum::<f64>();
490 Ok(correct / y_true.len() as f64)
491 }
492 ScoringMetric::MeanSquaredError => {
493 let mse = y_true
494 .iter()
495 .zip(y_pred.iter())
496 .map(|(&t, &p)| (t - p).powi(2))
497 .sum::<f64>()
498 / y_true.len() as f64;
499 Ok(-mse) }
501 ScoringMetric::MeanAbsoluteError => {
502 let mae = y_true
503 .iter()
504 .zip(y_pred.iter())
505 .map(|(&t, &p)| (t - p).abs())
506 .sum::<f64>()
507 / y_true.len() as f64;
508 Ok(-mae) }
510 _ => {
511 let correct = y_true
513 .iter()
514 .zip(y_pred.iter())
515 .map(|(&t, &p)| if (t - p).abs() < 0.5 { 1.0 } else { 0.0 })
516 .sum::<f64>();
517 Ok(correct / y_true.len() as f64)
518 }
519 }
520 }
521}