1use crate::error::{OptimError, Result};
7use scirs2_core::ndarray::{Array1, Array2, ScalarOperand};
8use scirs2_core::numeric::Float;
9use scirs2_core::random::thread_rng;
10use std::collections::{HashMap, VecDeque};
11use std::fmt::Debug;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum OptimizerType {
16 SGD,
18 SGDMomentum,
20 Adam,
22 AdamW,
24 RMSprop,
26 AdaGrad,
28 RAdam,
30 Lookahead,
32 LAMB,
34 LARS,
36 LBFGS,
38 SAM,
40}
41
42#[derive(Debug, Clone)]
44pub struct ProblemCharacteristics {
45 pub dataset_size: usize,
47 pub input_dim: usize,
49 pub output_dim: usize,
51 pub problem_type: ProblemType,
53 pub gradient_sparsity: f64,
55 pub gradient_noise: f64,
57 pub memory_budget: usize,
59 pub time_budget: f64,
61 pub batch_size: usize,
63 pub lr_sensitivity: f64,
65 pub regularization_strength: f64,
67 pub architecture_type: Option<String>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
73pub enum ProblemType {
74 Classification,
76 Regression,
78 Unsupervised,
80 ReinforcementLearning,
82 TimeSeries,
84 ComputerVision,
86 NaturalLanguage,
88 Recommendation,
90}
91
92#[derive(Debug, Clone)]
94pub struct PerformanceMetrics {
95 pub final_loss: f64,
97 pub convergence_steps: usize,
99 pub training_time: f64,
101 pub memory_usage: usize,
103 pub validation_performance: f64,
105 pub stability: f64,
107 pub generalization_gap: f64,
109}
110
111#[derive(Debug, Clone)]
113pub enum SelectionStrategy {
114 RuleBased,
116 LearningBased,
118 Ensemble {
120 num_candidates: usize,
122 evaluation_steps: usize,
124 },
125 Bandit {
127 epsilon: f64,
129 confidence: f64,
131 },
132 MetaLearning {
134 feature_dim: usize,
136 k_nearest: usize,
138 },
139}
140
141#[derive(Debug)]
143pub struct AdaptiveOptimizerSelector<A: Float> {
144 strategy: SelectionStrategy,
146 performance_history: HashMap<OptimizerType, Vec<PerformanceMetrics>>,
148 problem_optimizer_map: Vec<(ProblemCharacteristics, OptimizerType, PerformanceMetrics)>,
150 current_problem: Option<ProblemCharacteristics>,
152 arm_counts: HashMap<OptimizerType, usize>,
154 arm_rewards: HashMap<OptimizerType, f64>,
155 selection_network: Option<SelectionNetwork<A>>,
157 available_optimizers: Vec<OptimizerType>,
159 current_performance: VecDeque<f64>,
161 last_confidence: f64,
163}
164
165#[derive(Debug)]
172pub struct SelectionNetwork<A: Float> {
173 input_weights: Array2<A>,
175 output_weights: Array2<A>,
177 input_bias: Array1<A>,
179 output_bias: Array1<A>,
181 hidden_size: usize,
183}
184
185impl<A: Float + ScalarOperand + Debug + scirs2_core::numeric::FromPrimitive + Send + Sync>
186 SelectionNetwork<A>
187{
188 pub fn new(input_size: usize, hidden_size: usize, num_optimizers: usize) -> Self {
190 let mut rng = thread_rng();
191
192 let input_weights = Array2::from_shape_fn((hidden_size, input_size), |_| {
195 A::from(rng.random::<f64>() * 0.1 - 0.05).unwrap_or_else(A::zero)
196 });
197
198 let output_weights = Array2::from_shape_fn((num_optimizers, hidden_size), |_| {
199 A::from(rng.random::<f64>() * 0.1 - 0.05).unwrap_or_else(A::zero)
200 });
201
202 let input_bias = Array1::zeros(hidden_size);
203 let output_bias = Array1::zeros(num_optimizers);
204
205 Self {
206 input_weights,
207 output_weights,
208 input_bias,
209 output_bias,
210 hidden_size,
211 }
212 }
213
214 pub fn from_parameters(
225 input_weights: Array2<A>,
226 input_bias: Array1<A>,
227 output_weights: Array2<A>,
228 output_bias: Array1<A>,
229 ) -> Result<Self> {
230 let hidden_size = input_weights.nrows();
231 if input_bias.len() != hidden_size {
232 return Err(OptimError::InvalidConfig(format!(
233 "Input bias length {} does not match the hidden size {hidden_size}",
234 input_bias.len()
235 )));
236 }
237 if output_weights.ncols() != hidden_size {
238 return Err(OptimError::InvalidConfig(format!(
239 "Output weights have {} columns but the hidden size is {hidden_size}",
240 output_weights.ncols()
241 )));
242 }
243 if output_bias.len() != output_weights.nrows() {
244 return Err(OptimError::InvalidConfig(format!(
245 "Output bias length {} does not match the {} output units",
246 output_bias.len(),
247 output_weights.nrows()
248 )));
249 }
250
251 Ok(Self {
252 input_weights,
253 output_weights,
254 input_bias,
255 output_bias,
256 hidden_size,
257 })
258 }
259
260 pub fn hidden_size(&self) -> usize {
262 self.hidden_size
263 }
264
265 pub fn input_size(&self) -> usize {
267 self.input_weights.ncols()
268 }
269
270 pub fn num_outputs(&self) -> usize {
272 self.output_weights.nrows()
273 }
274
275 pub fn input_weights(&self) -> &Array2<A> {
277 &self.input_weights
278 }
279
280 pub fn output_weights(&self) -> &Array2<A> {
282 &self.output_weights
283 }
284
285 pub fn input_bias(&self) -> &Array1<A> {
287 &self.input_bias
288 }
289
290 pub fn output_bias(&self) -> &Array1<A> {
292 &self.output_bias
293 }
294
295 fn forward_with_activations(
301 &self,
302 features: &Array1<A>,
303 ) -> Result<(Array1<A>, Array1<A>, Array1<A>)> {
304 if features.len() != self.input_weights.ncols() {
305 return Err(OptimError::InvalidConfig(format!(
306 "Feature vector has length {} but the network expects {}",
307 features.len(),
308 self.input_weights.ncols()
309 )));
310 }
311
312 let pre_activation = self.input_weights.dot(features) + &self.input_bias;
314 let hidden_activated = pre_activation.mapv(|x| {
315 if x > A::zero() {
317 x
318 } else {
319 A::zero()
320 }
321 });
322
323 let output = self.output_weights.dot(&hidden_activated) + &self.output_bias;
325
326 let max_val = output.iter().fold(A::neg_infinity(), |a, &b| A::max(a, b));
328 let exp_output = output.mapv(|x| A::exp(x - max_val));
329 let sum_exp = exp_output.sum();
330 let probabilities = if sum_exp > A::zero() {
331 exp_output.mapv(|x| x / sum_exp)
332 } else {
333 let n = A::from(output.len().max(1)).unwrap_or_else(A::one);
335 Array1::from_elem(output.len(), A::one() / n)
336 };
337
338 Ok((pre_activation, hidden_activated, probabilities))
339 }
340
341 pub fn forward(&self, features: &Array1<A>) -> Result<Array1<A>> {
343 let (_, _, probabilities) = self.forward_with_activations(features)?;
344 Ok(probabilities)
345 }
346
347 pub fn average_loss(&self, features: &[Array1<A>], optimizer_labels: &[usize]) -> Result<A> {
352 Self::validate_dataset(features, optimizer_labels, self.output_weights.nrows())?;
353 if features.is_empty() {
354 return Ok(A::zero());
355 }
356
357 let floor = A::epsilon();
359 let mut total = A::zero();
360 for (feature, &label) in features.iter().zip(optimizer_labels.iter()) {
361 let probabilities = self.forward(feature)?;
362 let target = probabilities[label];
363 let clamped = if target > floor { target } else { floor };
364 total = total - A::ln(clamped);
365 }
366
367 let count = A::from(features.len()).unwrap_or_else(A::one);
368 Ok(total / count)
369 }
370
371 fn validate_dataset(
373 features: &[Array1<A>],
374 optimizer_labels: &[usize],
375 num_outputs: usize,
376 ) -> Result<()> {
377 if features.len() != optimizer_labels.len() {
378 return Err(OptimError::InvalidConfig(format!(
379 "Feature/label count mismatch: {} features vs {} labels",
380 features.len(),
381 optimizer_labels.len()
382 )));
383 }
384 if let Some(&bad) = optimizer_labels.iter().find(|&&l| l >= num_outputs) {
385 return Err(OptimError::InvalidConfig(format!(
386 "Optimizer label {bad} is out of range for {num_outputs} output units"
387 )));
388 }
389 Ok(())
390 }
391
392 pub fn train(
412 &mut self,
413 features: &[Array1<A>],
414 optimizer_labels: &[usize],
415 learning_rate: A,
416 epochs: usize,
417 ) -> Result<()> {
418 let num_outputs = self.output_weights.nrows();
419 Self::validate_dataset(features, optimizer_labels, num_outputs)?;
420
421 let hidden_units = self.output_weights.ncols();
422 let input_units = self.input_weights.ncols();
423
424 for _ in 0..epochs {
425 for (feature, &label) in features.iter().zip(optimizer_labels.iter()) {
426 let (pre_activation, hidden_activated, probabilities) =
428 self.forward_with_activations(feature)?;
429
430 let mut output_delta = probabilities;
432 output_delta[label] = output_delta[label] - A::one();
433
434 let mut hidden_delta: Array1<A> = Array1::zeros(hidden_units);
437 for j in 0..hidden_units {
438 if pre_activation[j] > A::zero() {
439 let mut acc = A::zero();
440 for i in 0..num_outputs {
441 acc = acc + self.output_weights[[i, j]] * output_delta[i];
442 }
443 hidden_delta[j] = acc;
444 }
445 }
448
449 for i in 0..num_outputs {
451 let delta = output_delta[i];
452 for j in 0..hidden_units {
453 self.output_weights[[i, j]] = self.output_weights[[i, j]]
454 - learning_rate * delta * hidden_activated[j];
455 }
456 self.output_bias[i] = self.output_bias[i] - learning_rate * delta;
457 }
458
459 for j in 0..hidden_units {
461 let delta = hidden_delta[j];
462 if delta == A::zero() {
463 continue;
464 }
465 for k in 0..input_units {
466 self.input_weights[[j, k]] =
467 self.input_weights[[j, k]] - learning_rate * delta * feature[k];
468 }
469 self.input_bias[j] = self.input_bias[j] - learning_rate * delta;
470 }
471 }
472 }
473 Ok(())
474 }
475}
476
477impl<A: Float + ScalarOperand + Debug + scirs2_core::numeric::FromPrimitive + Send + Sync>
478 AdaptiveOptimizerSelector<A>
479{
480 pub fn new(strategy: SelectionStrategy) -> Self {
482 let available_optimizers = vec![
483 OptimizerType::SGD,
484 OptimizerType::SGDMomentum,
485 OptimizerType::Adam,
486 OptimizerType::AdamW,
487 OptimizerType::RMSprop,
488 OptimizerType::AdaGrad,
489 OptimizerType::RAdam,
490 OptimizerType::LAMB,
491 ];
492
493 let mut arm_counts = HashMap::new();
494 let mut arm_rewards = HashMap::new();
495 for &optimizer in &available_optimizers {
496 arm_counts.insert(optimizer, 0);
497 arm_rewards.insert(optimizer, 0.0);
498 }
499
500 Self {
501 strategy,
502 performance_history: HashMap::new(),
503 problem_optimizer_map: Vec::new(),
504 current_problem: None,
505 arm_counts,
506 arm_rewards,
507 selection_network: None,
508 available_optimizers,
509 current_performance: VecDeque::new(),
510 last_confidence: 0.0,
511 }
512 }
513
514 pub fn set_problem(&mut self, problem: ProblemCharacteristics) {
516 self.current_problem = Some(problem);
517 }
518
519 pub fn select_optimizer(&mut self) -> Result<OptimizerType> {
521 let problem = self.current_problem.clone().ok_or_else(|| {
522 OptimError::InvalidConfig("No problem characteristics set".to_string())
523 })?;
524
525 match &self.strategy {
526 SelectionStrategy::RuleBased => self.rule_based_selection(&problem),
527 SelectionStrategy::LearningBased => self.learning_based_selection(&problem),
528 SelectionStrategy::Ensemble {
529 num_candidates,
530 evaluation_steps,
531 } => self.ensemble_selection(&problem, *num_candidates, *evaluation_steps),
532 SelectionStrategy::Bandit {
533 epsilon,
534 confidence,
535 } => self.bandit_selection(&problem, *epsilon, *confidence),
536 SelectionStrategy::MetaLearning {
537 feature_dim,
538 k_nearest: _,
539 } => self.meta_learning_selection(&problem, *feature_dim),
540 }
541 }
542
543 fn rule_based_selection(&self, problem: &ProblemCharacteristics) -> Result<OptimizerType> {
545 if problem.dataset_size > 100000 {
547 match problem.problem_type {
548 ProblemType::ComputerVision => return Ok(OptimizerType::AdamW),
549 ProblemType::NaturalLanguage => return Ok(OptimizerType::AdamW),
550 _ => return Ok(OptimizerType::Adam),
551 }
552 }
553
554 if problem.dataset_size < 1000 {
556 return Ok(OptimizerType::LBFGS);
557 }
558
559 if problem.gradient_sparsity > 0.5 {
561 return Ok(OptimizerType::AdaGrad);
562 }
563
564 if problem.batch_size > 256 {
566 return Ok(OptimizerType::LAMB);
567 }
568
569 if problem.memory_budget < 1_000_000 {
571 return Ok(OptimizerType::SGD);
572 }
573
574 if problem.gradient_noise > 0.3 {
576 return Ok(OptimizerType::RMSprop);
577 }
578
579 Ok(OptimizerType::Adam)
581 }
582
583 fn learning_based_selection(
585 &mut self,
586 problem: &ProblemCharacteristics,
587 ) -> Result<OptimizerType> {
588 if self.problem_optimizer_map.is_empty() {
589 return self.rule_based_selection(problem);
591 }
592
593 let mut best_similarity = -1.0;
595 let mut best_optimizer = OptimizerType::Adam;
596
597 for (hist_problem, optimizer, metrics) in &self.problem_optimizer_map {
598 let similarity = self.compute_problem_similarity(problem, hist_problem);
599
600 let weighted_similarity = similarity * metrics.validation_performance;
602
603 if weighted_similarity > best_similarity {
604 best_similarity = weighted_similarity;
605 best_optimizer = *optimizer;
606 }
607 }
608
609 self.last_confidence = best_similarity;
610 Ok(best_optimizer)
611 }
612
613 fn ensemble_selection(
615 &self,
616 _problem: &ProblemCharacteristics,
617 num_candidates: usize,
618 _evaluation_steps: usize,
619 ) -> Result<OptimizerType> {
620 let mut candidates = self.available_optimizers.clone();
622 candidates.truncate(num_candidates.min(candidates.len()));
623
624 Ok(candidates[0])
627 }
628
629 fn bandit_selection(
631 &self,
632 _problem: &ProblemCharacteristics,
633 epsilon: f64,
634 confidence: f64,
635 ) -> Result<OptimizerType> {
636 let mut rng = thread_rng();
637
638 if rng.random::<f64>() < epsilon {
640 let idx = rng.gen_range(0..self.available_optimizers.len());
642 return Ok(self.available_optimizers[idx]);
643 }
644
645 let mut best_ucb = f64::NEG_INFINITY;
647 let mut best_optimizer = OptimizerType::Adam;
648 let total_counts: usize = self.arm_counts.values().sum();
649
650 for &optimizer in &self.available_optimizers {
651 let count = self.arm_counts[&optimizer] as f64;
652 let reward = if count > 0.0 {
653 self.arm_rewards[&optimizer] / count
654 } else {
655 0.0
656 };
657
658 let ucb = if count > 0.0 {
659 reward + confidence * ((total_counts as f64).ln() / count).sqrt()
660 } else {
661 f64::INFINITY };
663
664 if ucb > best_ucb {
665 best_ucb = ucb;
666 best_optimizer = optimizer;
667 }
668 }
669
670 Ok(best_optimizer)
671 }
672
673 fn meta_learning_selection(
675 &mut self,
676 problem: &ProblemCharacteristics,
677 k_nearest: usize,
678 ) -> Result<OptimizerType> {
679 let features = self.extract_problem_features(problem);
681
682 if let Some(network) = &self.selection_network {
684 let probabilities = network.forward(&features)?;
685
686 let mut best_prob = A::neg_infinity();
688 let mut best_idx = 0;
689
690 for (i, &prob) in probabilities.iter().enumerate() {
691 if prob > best_prob {
692 best_prob = prob;
693 best_idx = i;
694 }
695 }
696
697 if best_idx < self.available_optimizers.len() {
698 return Ok(self.available_optimizers[best_idx]);
699 }
700 }
701
702 if self.problem_optimizer_map.len() >= k_nearest {
704 let mut similarities = Vec::new();
705
706 for (hist_problem, optimizer, metrics) in &self.problem_optimizer_map {
707 let similarity = self.compute_problem_similarity(problem, hist_problem);
708 similarities.push((similarity, *optimizer, metrics.validation_performance));
709 }
710
711 similarities.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
714
715 let mut votes: HashMap<OptimizerType, f64> = HashMap::new();
717 for (similarity, optimizer, performance) in similarities.iter().take(k_nearest) {
718 let weight = similarity * performance;
719 *votes.entry(*optimizer).or_insert(0.0) += weight;
720 }
721
722 let best_optimizer = votes
724 .iter()
725 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
726 .map(|(optimizer_, _)| *optimizer_)
727 .unwrap_or(OptimizerType::Adam);
728
729 return Ok(best_optimizer);
730 }
731
732 self.rule_based_selection(problem)
734 }
735
736 pub fn update_performance(
738 &mut self,
739 optimizer: OptimizerType,
740 metrics: PerformanceMetrics,
741 ) -> Result<()> {
742 self.performance_history
744 .entry(optimizer)
745 .or_default()
746 .push(metrics.clone());
747
748 *self.arm_counts.entry(optimizer).or_insert(0) += 1;
750 *self.arm_rewards.entry(optimizer).or_insert(0.0) += metrics.validation_performance;
751
752 if let Some(problem) = &self.current_problem {
754 self.problem_optimizer_map
755 .push((problem.clone(), optimizer, metrics.clone()));
756 }
757
758 self.current_performance
760 .push_back(metrics.validation_performance);
761 if self.current_performance.len() > 100 {
762 self.current_performance.pop_front();
763 }
764
765 Ok(())
766 }
767
768 pub fn train_selection_network(&mut self, learning_rate: A, epochs: usize) -> Result<()> {
770 if self.problem_optimizer_map.is_empty() {
771 return Ok(()); }
773
774 let mut features = Vec::new();
776 let mut labels = Vec::new();
777
778 for (problem, optimizer_, _metrics) in &self.problem_optimizer_map {
779 let Some(label) = self
782 .available_optimizers
783 .iter()
784 .position(|&opt| opt == *optimizer_)
785 else {
786 continue;
787 };
788
789 features.push(self.extract_problem_features(problem));
790 labels.push(label);
791 }
792
793 if features.is_empty() {
794 return Ok(()); }
796
797 if self.selection_network.is_none() {
799 let feature_dim = features[0].len();
800 let num_optimizers = self.available_optimizers.len();
801 self.selection_network = Some(SelectionNetwork::new(feature_dim, 32, num_optimizers));
802 }
803
804 if let Some(network) = &mut self.selection_network {
806 network.train(&features, &labels, learning_rate, epochs)?;
807 }
808
809 Ok(())
810 }
811
812 fn compute_problem_similarity(
814 &self,
815 problem1: &ProblemCharacteristics,
816 problem2: &ProblemCharacteristics,
817 ) -> f64 {
818 let mut similarity = 0.0;
819 let mut weight_sum = 0.0;
820
821 let size_sim = 1.0
823 - ((problem1.dataset_size as f64).ln() - (problem2.dataset_size as f64).ln()).abs()
824 / 10.0;
825 similarity += size_sim.max(0.0) * 0.2;
826 weight_sum += 0.2;
827
828 if problem1.problem_type == problem2.problem_type {
830 similarity += 0.3;
831 }
832 weight_sum += 0.3;
833
834 let batch_sim = 1.0
836 - ((problem1.batch_size as f64 - problem2.batch_size as f64).abs() / 256.0).min(1.0);
837 similarity += batch_sim * 0.1;
838 weight_sum += 0.1;
839
840 let sparsity_sim = 1.0 - (problem1.gradient_sparsity - problem2.gradient_sparsity).abs();
842 let noise_sim = 1.0 - (problem1.gradient_noise - problem2.gradient_noise).abs();
843 similarity += (sparsity_sim + noise_sim) * 0.2;
844 weight_sum += 0.4;
845
846 similarity / weight_sum
847 }
848
849 fn extract_problem_features(&self, problem: &ProblemCharacteristics) -> Array1<A> {
851 Array1::from_vec(vec![
852 A::from((problem.dataset_size as f64).ln()).unwrap_or_else(A::zero),
853 A::from((problem.input_dim as f64).ln()).unwrap_or_else(A::zero),
854 A::from((problem.output_dim as f64).ln()).unwrap_or_else(A::zero),
855 A::from(problem.problem_type as u8 as f64).unwrap_or_else(A::zero),
856 A::from(problem.gradient_sparsity).unwrap_or_else(A::zero),
857 A::from(problem.gradient_noise).unwrap_or_else(A::zero),
858 A::from((problem.memory_budget as f64).ln()).unwrap_or_else(A::zero),
859 A::from(problem.time_budget.ln()).unwrap_or_else(A::zero),
860 A::from((problem.batch_size as f64).ln()).unwrap_or_else(A::zero),
861 A::from(problem.lr_sensitivity).unwrap_or_else(A::zero),
862 A::from(problem.regularization_strength).unwrap_or_else(A::zero),
863 ])
864 }
865
866 pub fn get_optimizer_statistics(
868 &self,
869 optimizer: OptimizerType,
870 ) -> Option<OptimizerStatistics> {
871 if let Some(history) = self.performance_history.get(&optimizer) {
872 if history.is_empty() {
873 return None;
874 }
875
876 let performances: Vec<f64> = history.iter().map(|m| m.validation_performance).collect();
877 let mean = performances.iter().sum::<f64>() / performances.len() as f64;
878 let variance = performances.iter().map(|p| (p - mean).powi(2)).sum::<f64>()
879 / performances.len() as f64;
880 let std_dev = variance.sqrt();
881
882 Some(OptimizerStatistics {
883 optimizer,
884 num_trials: history.len(),
885 mean_performance: mean,
886 std_performance: std_dev,
887 best_performance: performances
888 .iter()
889 .copied()
890 .fold(f64::NEG_INFINITY, f64::max),
891 worst_performance: performances.iter().copied().fold(f64::INFINITY, f64::min),
892 success_rate: performances.iter().filter(|&&p| p > 0.7).count() as f64
893 / performances.len() as f64,
894 })
895 } else {
896 None
897 }
898 }
899
900 pub fn get_all_statistics(&self) -> Vec<OptimizerStatistics> {
902 self.available_optimizers
903 .iter()
904 .filter_map(|&opt| self.get_optimizer_statistics(opt))
905 .collect()
906 }
907
908 pub fn get_selection_confidence(&self) -> f64 {
910 self.last_confidence
911 }
912
913 pub fn reset(&mut self) {
915 self.performance_history.clear();
916 self.problem_optimizer_map.clear();
917 self.current_problem = None;
918 for count in self.arm_counts.values_mut() {
919 *count = 0;
920 }
921 for reward in self.arm_rewards.values_mut() {
922 *reward = 0.0;
923 }
924 self.current_performance.clear();
925 self.last_confidence = 0.0;
926 }
927}
928
929#[derive(Debug, Clone)]
931pub struct OptimizerStatistics {
932 pub optimizer: OptimizerType,
934 pub num_trials: usize,
936 pub mean_performance: f64,
938 pub std_performance: f64,
940 pub best_performance: f64,
942 pub worst_performance: f64,
944 pub success_rate: f64,
946}
947
948#[cfg(test)]
949mod tests {
950 use super::*;
951 use approx::assert_relative_eq;
952
953 #[test]
954 fn test_problem_characteristics() {
955 let problem = ProblemCharacteristics {
956 dataset_size: 10000,
957 input_dim: 784,
958 output_dim: 10,
959 problem_type: ProblemType::Classification,
960 gradient_sparsity: 0.1,
961 gradient_noise: 0.05,
962 memory_budget: 1_000_000,
963 time_budget: 3600.0,
964 batch_size: 64,
965 lr_sensitivity: 0.5,
966 regularization_strength: 0.01,
967 architecture_type: Some("CNN".to_string()),
968 };
969
970 assert_eq!(problem.dataset_size, 10000);
971 assert_eq!(problem.problem_type, ProblemType::Classification);
972 }
973
974 #[test]
975 fn test_rule_based_selection() {
976 let mut selector = AdaptiveOptimizerSelector::<f64>::new(SelectionStrategy::RuleBased);
977
978 let large_problem = ProblemCharacteristics {
980 dataset_size: 100001,
981 input_dim: 224,
982 output_dim: 1000,
983 problem_type: ProblemType::ComputerVision,
984 gradient_sparsity: 0.1,
985 gradient_noise: 0.05,
986 memory_budget: 10_000_000,
987 time_budget: 7200.0,
988 batch_size: 32,
989 lr_sensitivity: 0.5,
990 regularization_strength: 0.01,
991 architecture_type: Some("ResNet".to_string()),
992 };
993
994 selector.set_problem(large_problem);
995 let optimizer = selector
996 .select_optimizer()
997 .expect("selector.select_optimizer succeeds in test_rule_based_selection");
998 assert_eq!(optimizer, OptimizerType::AdamW);
999 }
1000
1001 #[test]
1002 fn test_selection_network() {
1003 let network = SelectionNetwork::<f64>::new(5, 10, 3);
1004 let features = Array1::from_vec(vec![1.0, 0.5, 2.0, 0.8, 1.5]);
1005
1006 let probabilities = network
1007 .forward(&features)
1008 .expect("network.forward succeeds in test_selection_network");
1009 assert_eq!(probabilities.len(), 3);
1010
1011 let sum: f64 = probabilities.iter().sum();
1013 assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
1014
1015 for &prob in probabilities.iter() {
1017 assert!(prob >= 0.0);
1018 }
1019 }
1020
1021 #[test]
1022 fn test_bandit_selection() {
1023 let mut selector = AdaptiveOptimizerSelector::<f64>::new(SelectionStrategy::Bandit {
1024 epsilon: 0.1,
1025 confidence: 2.0,
1026 });
1027
1028 let problem = ProblemCharacteristics {
1029 dataset_size: 1000,
1030 input_dim: 10,
1031 output_dim: 2,
1032 problem_type: ProblemType::Classification,
1033 gradient_sparsity: 0.0,
1034 gradient_noise: 0.1,
1035 memory_budget: 1_000_000,
1036 time_budget: 600.0,
1037 batch_size: 32,
1038 lr_sensitivity: 0.5,
1039 regularization_strength: 0.01,
1040 architecture_type: None,
1041 };
1042
1043 selector.set_problem(problem);
1044
1045 let optimizer = selector
1047 .select_optimizer()
1048 .expect("selector.select_optimizer succeeds in test_bandit_selection");
1049 assert!(selector.available_optimizers.contains(&optimizer));
1050 }
1051
1052 #[test]
1053 fn test_performance_update() {
1054 let mut selector = AdaptiveOptimizerSelector::<f64>::new(SelectionStrategy::RuleBased);
1055
1056 let metrics = PerformanceMetrics {
1057 final_loss: 0.1,
1058 convergence_steps: 100,
1059 training_time: 60.0,
1060 memory_usage: 500_000,
1061 validation_performance: 0.95,
1062 stability: 0.02,
1063 generalization_gap: 0.05,
1064 };
1065
1066 selector
1067 .update_performance(OptimizerType::Adam, metrics)
1068 .expect("update_performance succeeds in test_performance_update");
1069
1070 let stats = selector
1071 .get_optimizer_statistics(OptimizerType::Adam)
1072 .expect("get_optimizer_statistics succeeds in test_performance_update");
1073 assert_eq!(stats.num_trials, 1);
1074 assert_relative_eq!(stats.mean_performance, 0.95, epsilon = 1e-6);
1075 }
1076
1077 #[test]
1078 fn test_problem_similarity() {
1079 let selector = AdaptiveOptimizerSelector::<f64>::new(SelectionStrategy::RuleBased);
1080
1081 let problem1 = ProblemCharacteristics {
1082 dataset_size: 1000,
1083 input_dim: 10,
1084 output_dim: 2,
1085 problem_type: ProblemType::Classification,
1086 gradient_sparsity: 0.1,
1087 gradient_noise: 0.05,
1088 memory_budget: 1_000_000,
1089 time_budget: 600.0,
1090 batch_size: 32,
1091 lr_sensitivity: 0.5,
1092 regularization_strength: 0.01,
1093 architecture_type: None,
1094 };
1095
1096 let problem2 = problem1.clone();
1097 let similarity = selector.compute_problem_similarity(&problem1, &problem2);
1098 assert_relative_eq!(similarity, 1.0, epsilon = 1e-6);
1099
1100 let mut problem3 = problem1.clone();
1101 problem3.problem_type = ProblemType::Regression;
1102 let similarity = selector.compute_problem_similarity(&problem1, &problem3);
1103 assert!(similarity < 1.0);
1104 }
1105}