sklears_svm/hyperparameter_optimization/
evolutionary_optimization.rs1use std::time::Instant;
4
5#[cfg(feature = "parallel")]
6use rayon::prelude::*;
7use scirs2_core::ndarray::{Array1, Array2};
8use scirs2_core::random::Random;
9
10use crate::kernels::KernelType;
11use crate::svc::SVC;
12use sklears_core::error::{Result, SklearsError};
13use sklears_core::traits::{Fit, Predict};
14
15use super::{
16 OptimizationConfig, OptimizationResult, ParameterSet, ParameterSpec, ScoringMetric, SearchSpace,
17};
18
19#[derive(Debug, Clone)]
21pub enum SelectionMethod {
22 Tournament { size: usize },
23 RouletteWheel,
24 RankBased,
25}
26
27#[derive(Debug, Clone)]
29pub struct Individual {
30 pub params: ParameterSet,
31 pub fitness: f64,
32}
33
34impl Individual {
35 pub fn new(params: ParameterSet) -> Self {
36 Self {
37 params,
38 fitness: -f64::INFINITY,
39 }
40 }
41}
42
43pub struct EvolutionaryOptimizationCV {
45 config: OptimizationConfig,
46 search_space: SearchSpace,
47 rng: Random<scirs2_core::random::rngs::StdRng>,
48 population_size: usize,
49 selection_method: SelectionMethod,
50 mutation_rate: f64,
51 crossover_rate: f64,
52 elite_ratio: f64,
53}
54
55impl EvolutionaryOptimizationCV {
56 pub fn new(config: OptimizationConfig, search_space: SearchSpace) -> Self {
58 let rng = if let Some(seed) = config.random_state {
59 Random::seed(seed)
60 } else {
61 Random::seed(42) };
63
64 Self {
65 config,
66 search_space,
67 rng,
68 population_size: 50,
69 selection_method: SelectionMethod::Tournament { size: 3 },
70 mutation_rate: 0.1,
71 crossover_rate: 0.8,
72 elite_ratio: 0.1,
73 }
74 }
75
76 pub fn population_size(mut self, size: usize) -> Self {
78 self.population_size = size;
79 self
80 }
81
82 pub fn selection_method(mut self, method: SelectionMethod) -> Self {
84 self.selection_method = method;
85 self
86 }
87
88 pub fn mutation_rate(mut self, rate: f64) -> Self {
90 self.mutation_rate = rate;
91 self
92 }
93
94 pub fn crossover_rate(mut self, rate: f64) -> Self {
96 self.crossover_rate = rate;
97 self
98 }
99
100 pub fn elite_ratio(mut self, ratio: f64) -> Self {
102 self.elite_ratio = ratio;
103 self
104 }
105
106 pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
108 let start_time = Instant::now();
109
110 if self.config.verbose {
111 println!(
112 "Evolutionary optimization with {} generations, population size {}",
113 self.config.n_iterations, self.population_size
114 );
115 }
116
117 let mut population = self.initialize_population()?;
119
120 self.evaluate_population(&mut population, x, y)?;
122
123 let mut best_individual = population[0].clone();
124 let mut cv_results = Vec::new();
125 let mut score_history = Vec::new();
126 let mut generations_without_improvement = 0;
127
128 for generation in 0..self.config.n_iterations {
130 population.sort_by(|a, b| {
132 b.fitness
133 .partial_cmp(&a.fitness)
134 .unwrap_or(std::cmp::Ordering::Equal)
135 });
136
137 if population[0].fitness > best_individual.fitness {
139 best_individual = population[0].clone();
140 generations_without_improvement = 0;
141 } else {
142 generations_without_improvement += 1;
143 }
144
145 for ind in &population {
147 cv_results.push((ind.params.clone(), ind.fitness));
148 }
149 score_history.push(best_individual.fitness);
150
151 if self.config.verbose && (generation + 1) % 10 == 0 {
152 println!(
153 "Generation {}/{}: Best score {:.6}",
154 generation + 1,
155 self.config.n_iterations,
156 best_individual.fitness
157 );
158 }
159
160 if let Some(patience) = self.config.early_stopping_patience {
162 if generations_without_improvement >= patience {
163 if self.config.verbose {
164 println!("Early stopping at generation {}", generation + 1);
165 }
166 break;
167 }
168 }
169
170 let mut next_generation = Vec::new();
172
173 let n_elite = (self.population_size as f64 * self.elite_ratio) as usize;
175 for individual in population.iter().take(n_elite.min(population.len())) {
176 next_generation.push(individual.clone());
177 }
178
179 while next_generation.len() < self.population_size {
181 let parent1 = self.select_individual(&population)?;
183 let parent2 = self.select_individual(&population)?;
184
185 use scirs2_core::random::essentials::Uniform;
187 let dist = Uniform::new(0.0, 1.0).map_err(|e| {
188 SklearsError::InvalidInput(format!(
189 "Failed to create uniform distribution: {}",
190 e
191 ))
192 })?;
193 let offspring = if self.rng.sample(dist) < self.crossover_rate {
194 self.crossover(&parent1.params, &parent2.params)?
195 } else {
196 parent1.params.clone()
197 };
198
199 let dist = Uniform::new(0.0, 1.0).map_err(|e| {
201 SklearsError::InvalidInput(format!(
202 "Failed to create uniform distribution: {}",
203 e
204 ))
205 })?;
206 let mutated = if self.rng.sample(dist) < self.mutation_rate {
207 self.mutate(&offspring)?
208 } else {
209 offspring
210 };
211
212 next_generation.push(Individual::new(mutated));
213 }
214
215 self.evaluate_population(&mut next_generation, x, y)?;
217 population = next_generation;
218 }
219
220 population.sort_by(|a, b| {
222 b.fitness
223 .partial_cmp(&a.fitness)
224 .unwrap_or(std::cmp::Ordering::Equal)
225 });
226 let best_individual = population[0].clone();
227
228 if self.config.verbose {
229 println!("Best score: {:.6}", best_individual.fitness);
230 println!("Best params: {:?}", best_individual.params);
231 }
232
233 Ok(OptimizationResult {
234 best_params: best_individual.params,
235 best_score: best_individual.fitness,
236 cv_results,
237 n_iterations: score_history.len(),
238 optimization_time: start_time.elapsed().as_secs_f64(),
239 score_history,
240 })
241 }
242
243 fn initialize_population(&mut self) -> Result<Vec<Individual>> {
245 let mut population = Vec::with_capacity(self.population_size);
246
247 let c_spec = self.search_space.c.clone();
249 let kernel_spec = self.search_space.kernel.clone();
250 let tol_spec = self.search_space.tol.clone();
251 let max_iter_spec = self.search_space.max_iter.clone();
252
253 for _ in 0..self.population_size {
254 let c = self.sample_value(&c_spec)?;
255
256 let kernel = if let Some(ref spec) = kernel_spec {
257 self.sample_kernel(spec)?
258 } else {
259 KernelType::Rbf { gamma: 1.0 }
260 };
261
262 let tol = if let Some(ref spec) = tol_spec {
263 self.sample_value(spec)?
264 } else {
265 1e-3
266 };
267
268 let max_iter = if let Some(ref spec) = max_iter_spec {
269 self.sample_value(spec)? as usize
270 } else {
271 1000
272 };
273
274 population.push(Individual::new(ParameterSet {
275 c,
276 kernel,
277 tol,
278 max_iter,
279 }));
280 }
281
282 Ok(population)
283 }
284
285 fn evaluate_population(
287 &self,
288 population: &mut [Individual],
289 x: &Array2<f64>,
290 y: &Array1<f64>,
291 ) -> Result<()> {
292 #[cfg(feature = "parallel")]
293 if self.config.n_jobs.is_some() {
294 let fitnesses: Vec<f64> = population
296 .par_iter()
297 .map(|ind| {
298 self.evaluate_params(&ind.params, x, y)
299 .unwrap_or(-f64::INFINITY)
300 })
301 .collect();
302
303 for (ind, fitness) in population.iter_mut().zip(fitnesses.iter()) {
304 ind.fitness = *fitness;
305 }
306 } else {
307 for ind in population.iter_mut() {
309 ind.fitness = self.evaluate_params(&ind.params, x, y)?;
310 }
311 }
312
313 #[cfg(not(feature = "parallel"))]
314 {
315 for ind in population.iter_mut() {
317 ind.fitness = self.evaluate_params(&ind.params, x, y)?;
318 }
319 }
320
321 Ok(())
322 }
323
324 fn select_individual(&mut self, population: &[Individual]) -> Result<Individual> {
326 match &self.selection_method {
327 SelectionMethod::Tournament { size } => self.tournament_selection(population, *size),
328 SelectionMethod::RouletteWheel => self.roulette_wheel_selection(population),
329 SelectionMethod::RankBased => self.rank_based_selection(population),
330 }
331 }
332
333 fn tournament_selection(
335 &mut self,
336 population: &[Individual],
337 tournament_size: usize,
338 ) -> Result<Individual> {
339 use scirs2_core::random::essentials::Uniform;
340 let dist = Uniform::new(0, population.len()).map_err(|e| {
341 SklearsError::InvalidInput(format!("Failed to create uniform distribution: {}", e))
342 })?;
343
344 let mut best_idx = self.rng.sample(dist);
345 let mut best_fitness = population[best_idx].fitness;
346
347 for _ in 1..tournament_size {
348 let idx = self.rng.sample(dist);
349 if population[idx].fitness > best_fitness {
350 best_idx = idx;
351 best_fitness = population[idx].fitness;
352 }
353 }
354
355 Ok(population[best_idx].clone())
356 }
357
358 fn roulette_wheel_selection(&mut self, population: &[Individual]) -> Result<Individual> {
360 let min_fitness = population
362 .iter()
363 .map(|ind| ind.fitness)
364 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
365 .unwrap_or(0.0);
366 let offset = if min_fitness < 0.0 {
367 -min_fitness + 1.0
368 } else {
369 0.0
370 };
371
372 let total_fitness: f64 = population.iter().map(|ind| ind.fitness + offset).sum();
373
374 if total_fitness <= 0.0 {
375 use scirs2_core::random::essentials::Uniform;
377 let dist = Uniform::new(0, population.len()).map_err(|e| {
378 SklearsError::InvalidInput(format!("Failed to create uniform distribution: {}", e))
379 })?;
380 let idx = self.rng.sample(dist);
381 return Ok(population[idx].clone());
382 }
383
384 use scirs2_core::random::essentials::Uniform;
385 let dist = Uniform::new(0.0, total_fitness).map_err(|e| {
386 SklearsError::InvalidInput(format!("Failed to create uniform distribution: {}", e))
387 })?;
388 let mut spin = self.rng.sample(dist);
389
390 for ind in population {
391 spin -= ind.fitness + offset;
392 if spin <= 0.0 {
393 return Ok(ind.clone());
394 }
395 }
396
397 Ok(population[population.len() - 1].clone())
399 }
400
401 fn rank_based_selection(&mut self, population: &[Individual]) -> Result<Individual> {
403 let total_rank = population.len() * (population.len() + 1) / 2;
405
406 use scirs2_core::random::essentials::Uniform;
407 let dist = Uniform::new(0.0, total_rank as f64).map_err(|e| {
408 SklearsError::InvalidInput(format!("Failed to create uniform distribution: {}", e))
409 })?;
410 let mut spin = self.rng.sample(dist);
411
412 for (i, ind) in population.iter().enumerate() {
413 let rank = population.len() - i; spin -= rank as f64;
415 if spin <= 0.0 {
416 return Ok(ind.clone());
417 }
418 }
419
420 Ok(population[population.len() - 1].clone())
422 }
423
424 fn crossover(
426 &mut self,
427 parent1: &ParameterSet,
428 parent2: &ParameterSet,
429 ) -> Result<ParameterSet> {
430 use scirs2_core::random::essentials::Uniform;
431 let dist = Uniform::new(0.0, 1.0).map_err(|e| {
432 SklearsError::InvalidInput(format!("Failed to create uniform distribution: {}", e))
433 })?;
434
435 let c = if self.rng.sample(dist) < 0.5 {
437 parent1.c
438 } else {
439 parent2.c
440 };
441
442 let kernel = if self.rng.sample(dist) < 0.5 {
443 parent1.kernel.clone()
444 } else {
445 parent2.kernel.clone()
446 };
447
448 let tol = if self.rng.sample(dist) < 0.5 {
449 parent1.tol
450 } else {
451 parent2.tol
452 };
453
454 let max_iter = if self.rng.sample(dist) < 0.5 {
455 parent1.max_iter
456 } else {
457 parent2.max_iter
458 };
459
460 Ok(ParameterSet {
461 c,
462 kernel,
463 tol,
464 max_iter,
465 })
466 }
467
468 fn mutate(&mut self, params: &ParameterSet) -> Result<ParameterSet> {
470 use scirs2_core::random::essentials::Uniform;
471 let dist = Uniform::new(0.0, 1.0).map_err(|e| {
472 SklearsError::InvalidInput(format!("Failed to create uniform distribution: {}", e))
473 })?;
474
475 let c_spec = self.search_space.c.clone();
477 let kernel_spec = self.search_space.kernel.clone();
478 let tol_spec = self.search_space.tol.clone();
479 let max_iter_spec = self.search_space.max_iter.clone();
480
481 let c = if self.rng.sample(dist) < 0.2 {
483 self.sample_value(&c_spec)?
484 } else {
485 params.c
486 };
487
488 let kernel = if self.rng.sample(dist) < 0.2 {
489 if let Some(ref spec) = kernel_spec {
490 self.sample_kernel(spec)?
491 } else {
492 params.kernel.clone()
493 }
494 } else {
495 params.kernel.clone()
496 };
497
498 let tol = if self.rng.sample(dist) < 0.2 {
499 if let Some(ref spec) = tol_spec {
500 self.sample_value(spec)?
501 } else {
502 params.tol
503 }
504 } else {
505 params.tol
506 };
507
508 let max_iter = if self.rng.sample(dist) < 0.2 {
509 if let Some(ref spec) = max_iter_spec {
510 self.sample_value(spec)? as usize
511 } else {
512 params.max_iter
513 }
514 } else {
515 params.max_iter
516 };
517
518 Ok(ParameterSet {
519 c,
520 kernel,
521 tol,
522 max_iter,
523 })
524 }
525
526 fn sample_value(&mut self, spec: &ParameterSpec) -> Result<f64> {
528 match spec {
529 ParameterSpec::Fixed(value) => Ok(*value),
530 ParameterSpec::Uniform { min, max } => {
531 use scirs2_core::random::essentials::Uniform;
532 let dist = Uniform::new(*min, *max).map_err(|e| {
533 SklearsError::InvalidInput(format!(
534 "Failed to create uniform distribution: {}",
535 e
536 ))
537 })?;
538 Ok(self.rng.sample(dist))
539 }
540 ParameterSpec::LogUniform { min, max } => {
541 use scirs2_core::random::essentials::Uniform;
542 let log_min = min.ln();
543 let log_max = max.ln();
544 let dist = Uniform::new(log_min, log_max).map_err(|e| {
545 SklearsError::InvalidInput(format!(
546 "Failed to create uniform distribution: {}",
547 e
548 ))
549 })?;
550 let log_val = self.rng.sample(dist);
551 Ok(log_val.exp())
552 }
553 ParameterSpec::Choice(choices) => {
554 if choices.is_empty() {
555 return Err(SklearsError::InvalidInput("Empty choice list".to_string()));
556 }
557 use scirs2_core::random::essentials::Uniform;
558 let dist = Uniform::new(0, choices.len()).map_err(|e| {
559 SklearsError::InvalidInput(format!(
560 "Failed to create uniform distribution: {}",
561 e
562 ))
563 })?;
564 let idx = self.rng.sample(dist);
565 Ok(choices[idx])
566 }
567 ParameterSpec::KernelChoice(_) => Err(SklearsError::InvalidInput(
568 "Use sample_kernel for kernel specs".to_string(),
569 )),
570 }
571 }
572
573 fn sample_kernel(&mut self, spec: &ParameterSpec) -> Result<KernelType> {
575 match spec {
576 ParameterSpec::KernelChoice(kernels) => {
577 if kernels.is_empty() {
578 return Err(SklearsError::InvalidInput(
579 "Empty kernel choice list".to_string(),
580 ));
581 }
582 use scirs2_core::random::essentials::Uniform;
583 let dist = Uniform::new(0, kernels.len()).map_err(|e| {
584 SklearsError::InvalidInput(format!(
585 "Failed to create uniform distribution: {}",
586 e
587 ))
588 })?;
589 let idx = self.rng.sample(dist);
590 Ok(kernels[idx].clone())
591 }
592 _ => Err(SklearsError::InvalidInput(
593 "Invalid kernel specification".to_string(),
594 )),
595 }
596 }
597
598 fn evaluate_params(
600 &self,
601 params: &ParameterSet,
602 x: &Array2<f64>,
603 y: &Array1<f64>,
604 ) -> Result<f64> {
605 let scores = self.cross_validate(params, x, y)?;
606 Ok(scores.iter().sum::<f64>() / scores.len() as f64)
607 }
608
609 fn cross_validate(
611 &self,
612 params: &ParameterSet,
613 x: &Array2<f64>,
614 y: &Array1<f64>,
615 ) -> Result<Vec<f64>> {
616 let n_samples = x.nrows();
617 let fold_size = n_samples / self.config.cv_folds;
618 let mut scores = Vec::new();
619
620 for fold in 0..self.config.cv_folds {
621 let start_idx = fold * fold_size;
622 let end_idx = if fold == self.config.cv_folds - 1 {
623 n_samples
624 } else {
625 (fold + 1) * fold_size
626 };
627
628 let mut x_train_data = Vec::new();
630 let mut y_train_vals = Vec::new();
631 let mut x_test_data = Vec::new();
632 let mut y_test_vals = Vec::new();
633
634 for i in 0..n_samples {
635 if i >= start_idx && i < end_idx {
636 for j in 0..x.ncols() {
638 x_test_data.push(x[[i, j]]);
639 }
640 y_test_vals.push(y[i]);
641 } else {
642 for j in 0..x.ncols() {
644 x_train_data.push(x[[i, j]]);
645 }
646 y_train_vals.push(y[i]);
647 }
648 }
649
650 let n_train = y_train_vals.len();
651 let n_test = y_test_vals.len();
652 let n_features = x.ncols();
653
654 let x_train = Array2::from_shape_vec((n_train, n_features), x_train_data)?;
655 let y_train = Array1::from_vec(y_train_vals);
656 let x_test = Array2::from_shape_vec((n_test, n_features), x_test_data)?;
657 let y_test = Array1::from_vec(y_test_vals);
658
659 let svm = SVC::new()
661 .c(params.c)
662 .kernel(params.kernel.clone())
663 .tol(params.tol)
664 .max_iter(params.max_iter);
665
666 let fitted_svm = svm.fit(&x_train, &y_train)?;
667 let y_pred = fitted_svm.predict(&x_test)?;
668
669 let score = self.calculate_score(&y_test, &y_pred)?;
670 scores.push(score);
671 }
672
673 Ok(scores)
674 }
675
676 fn calculate_score(&self, y_true: &Array1<f64>, y_pred: &Array1<f64>) -> Result<f64> {
678 match self.config.scoring {
679 ScoringMetric::Accuracy => {
680 let correct = y_true
681 .iter()
682 .zip(y_pred.iter())
683 .map(|(&t, &p)| if (t - p).abs() < 0.5 { 1.0 } else { 0.0 })
684 .sum::<f64>();
685 Ok(correct / y_true.len() as f64)
686 }
687 ScoringMetric::MeanSquaredError => {
688 let mse = y_true
689 .iter()
690 .zip(y_pred.iter())
691 .map(|(&t, &p)| (t - p).powi(2))
692 .sum::<f64>()
693 / y_true.len() as f64;
694 Ok(-mse) }
696 ScoringMetric::MeanAbsoluteError => {
697 let mae = y_true
698 .iter()
699 .zip(y_pred.iter())
700 .map(|(&t, &p)| (t - p).abs())
701 .sum::<f64>()
702 / y_true.len() as f64;
703 Ok(-mae) }
705 _ => {
706 let correct = y_true
708 .iter()
709 .zip(y_pred.iter())
710 .map(|(&t, &p)| if (t - p).abs() < 0.5 { 1.0 } else { 0.0 })
711 .sum::<f64>();
712 Ok(correct / y_true.len() as f64)
713 }
714 }
715 }
716}