scirs2_optimize/learned_optimizers/
meta_learning_optimizer.rs1use super::{
7 LearnedOptimizationConfig, LearnedOptimizer, MetaOptimizerState, OptimizationProblem,
8 TrainingTask,
9};
10use crate::error::OptimizeResult;
11use crate::result::OptimizeResults;
12use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
13use scirs2_core::random::{Rng, RngExt};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
18pub struct MetaLearningOptimizer {
19 config: LearnedOptimizationConfig,
21 meta_state: MetaOptimizerState,
23 task_optimizers: HashMap<String, TaskSpecificOptimizer>,
25 meta_stats: MetaLearningStats,
27}
28
29#[derive(Debug, Clone)]
31pub struct TaskSpecificOptimizer {
32 parameters: Array1<f64>,
34 performance_history: Vec<f64>,
36 task_id: String,
38}
39
40#[derive(Debug, Clone)]
42pub struct MetaLearningStats {
43 tasks_learned: usize,
45 avg_adaptation_speed: f64,
47 transfer_efficiency: f64,
49 meta_gradient_norm: f64,
51}
52
53impl MetaLearningOptimizer {
54 pub fn new(config: LearnedOptimizationConfig) -> Self {
56 let hidden_size = config.hidden_size;
57 Self {
58 config,
59 meta_state: MetaOptimizerState {
60 meta_params: Array1::zeros(hidden_size),
61 network_weights: Array2::zeros((hidden_size, hidden_size)),
62 performance_history: Vec::new(),
63 adaptation_stats: super::AdaptationStatistics::default(),
64 episode: 0,
65 },
66 task_optimizers: HashMap::new(),
67 meta_stats: MetaLearningStats::default(),
68 }
69 }
70
71 pub fn learn_meta_strategy(&mut self, training_tasks: &[TrainingTask]) -> OptimizeResult<()> {
73 for task in training_tasks {
74 let task_optimizer = self.create_task_optimizer(&task.problem)?;
76
77 let performance = self.train_on_task(&task_optimizer, task)?;
79
80 self.update_meta_parameters(&task.problem, performance)?;
82
83 self.task_optimizers
85 .insert(task.problem.name.clone(), task_optimizer);
86 }
87
88 self.meta_stats.tasks_learned = training_tasks.len();
89 Ok(())
90 }
91
92 fn create_task_optimizer(
94 &self,
95 problem: &OptimizationProblem,
96 ) -> OptimizeResult<TaskSpecificOptimizer> {
97 let param_size = self.estimate_parameter_size(problem);
98
99 Ok(TaskSpecificOptimizer {
100 parameters: Array1::from_shape_fn(param_size, |_| {
101 scirs2_core::random::rng().random_range(0.0..0.1)
102 }),
103 performance_history: Vec::new(),
104 task_id: problem.name.clone(),
105 })
106 }
107
108 fn estimate_parameter_size(&self, problem: &OptimizationProblem) -> usize {
110 let base_size = 64;
112 let dimension_factor = (problem.dimension as f64).sqrt() as usize;
113
114 match problem.problem_class.as_str() {
115 "quadratic" => base_size,
116 "neural_network" => base_size * 2 + dimension_factor,
117 "sparse" => base_size + dimension_factor / 2,
118 _ => base_size + dimension_factor,
119 }
120 }
121
122 fn train_on_task(
124 &mut self,
125 optimizer: &TaskSpecificOptimizer,
126 task: &TrainingTask,
127 ) -> OptimizeResult<f64> {
128 let initial_params = match &task.initial_distribution {
137 super::ParameterDistribution::Uniform { low, high } => {
138 Array1::from_shape_fn(task.problem.dimension, |_| {
139 low + scirs2_core::random::rng().random_range(0.0..1.0) * (high - low)
140 })
141 }
142 super::ParameterDistribution::Normal { mean, std } => {
143 Array1::from_shape_fn(task.problem.dimension, |_| {
144 mean + std * (scirs2_core::random::rng().random_range(0.0..1.0) - 0.5) * 2.0
145 })
146 }
147 super::ParameterDistribution::Custom { samples } => {
148 if !samples.is_empty() {
149 samples[0].clone()
150 } else {
151 Array1::zeros(task.problem.dimension)
152 }
153 }
154 };
155
156 let center = match &task.true_optimum {
158 Some(opt) if opt.len() == task.problem.dimension => opt.clone(),
159 _ => Array1::zeros(task.problem.dimension),
160 };
161 let problem_class = task.problem.problem_class.clone();
162
163 let training_objective = move |x: &ArrayView1<f64>| -> f64 {
168 let mut value = 0.0;
169 for i in 0..x.len().min(center.len()) {
170 let d = x[i] - center[i];
171 value += d * d;
172 match problem_class.as_str() {
173 "neural_network" => value += 0.1 * d.sin().powi(2),
174 "sparse" => value += 0.1 * (d * d + 1e-12).sqrt(),
175 _ => {}
176 }
177 }
178 value
179 };
180
181 let initial_value = training_objective(&initial_params.view());
182 let mut current_params = initial_params;
183 let mut current_value = initial_value;
184
185 for _ in 0..self.config.inner_steps {
187 let direction = self.compute_meta_direction(¤t_params, &training_objective)?;
188 let step_size = self.compute_meta_step_size(&optimizer.parameters)?;
189
190 for i in 0..current_params.len().min(direction.len()) {
191 current_params[i] -= step_size * direction[i];
192 }
193
194 current_value = training_objective(¤t_params.view());
195 }
196
197 let improvement = initial_value - current_value;
198 Ok(improvement.max(0.0))
199 }
200
201 fn compute_meta_direction<F>(
203 &self,
204 params: &Array1<f64>,
205 objective: &F,
206 ) -> OptimizeResult<Array1<f64>>
207 where
208 F: Fn(&ArrayView1<f64>) -> f64,
209 {
210 let h = 1e-6;
211 let f0 = objective(¶ms.view());
212 let mut direction = Array1::zeros(params.len());
213
214 for i in 0..params.len() {
216 let mut params_plus = params.clone();
217 params_plus[i] += h;
218 let f_plus = objective(¶ms_plus.view());
219 direction[i] = (f_plus - f0) / h;
220 }
221
222 self.apply_meta_transformation(&mut direction)?;
224
225 Ok(direction)
226 }
227
228 fn apply_meta_transformation(&self, gradient: &mut Array1<f64>) -> OptimizeResult<()> {
230 for i in 0..gradient.len() {
232 let meta_idx = i % self.meta_state.meta_params.len();
233 let meta_factor = self.meta_state.meta_params[meta_idx];
234 gradient[i] *= 1.0 + meta_factor * 0.1;
235 }
236
237 Ok(())
238 }
239
240 fn compute_meta_step_size(&self, task_params: &Array1<f64>) -> OptimizeResult<f64> {
242 let mut step_size = self.config.inner_learning_rate;
244
245 if !task_params.is_empty() {
247 let param_norm = (task_params.iter().map(|&x| x * x).sum::<f64>()).sqrt();
248 step_size *= (1.0 + param_norm * 0.1).recip();
249 }
250
251 if !self.meta_state.meta_params.is_empty() {
253 let meta_factor = self.meta_state.meta_params[0];
254 step_size *= (1.0 + meta_factor * 0.2).max(0.1).min(2.0);
255 }
256
257 Ok(step_size)
258 }
259
260 fn update_meta_parameters(
262 &mut self,
263 problem: &OptimizationProblem,
264 performance: f64,
265 ) -> OptimizeResult<()> {
266 let learning_rate = self.config.meta_learning_rate;
267
268 let performance_gradient = if performance > 0.0 { 1.0 } else { -1.0 };
270
271 for i in 0..self.meta_state.meta_params.len() {
273 let update = learning_rate
275 * performance_gradient
276 * (scirs2_core::random::rng().random_range(0.0..1.0) - 0.5)
277 * 0.1;
278 self.meta_state.meta_params[i] += update;
279
280 self.meta_state.meta_params[i] = self.meta_state.meta_params[i].max(-1.0).min(1.0);
282 }
283
284 self.meta_state.performance_history.push(performance);
286
287 self.meta_state.adaptation_stats.avg_convergence_rate =
289 self.meta_state.performance_history.iter().sum::<f64>()
290 / self.meta_state.performance_history.len() as f64;
291
292 Ok(())
293 }
294
295 pub fn adapt_to_new_problem(
297 &mut self,
298 problem: &OptimizationProblem,
299 ) -> OptimizeResult<TaskSpecificOptimizer> {
300 let similar_task = self.find_most_similar_task(problem)?;
302
303 let mut new_optimizer = if let Some(similar_optimizer) = similar_task {
305 let mut adapted = similar_optimizer.clone();
307 adapted.task_id = problem.name.clone();
308
309 self.adapt_optimizer_parameters(&mut adapted, problem)?;
311 adapted
312 } else {
313 self.create_task_optimizer(problem)?
315 };
316
317 self.fine_tune_for_problem(&mut new_optimizer, problem)?;
319
320 Ok(new_optimizer)
321 }
322
323 fn find_most_similar_task(
325 &self,
326 problem: &OptimizationProblem,
327 ) -> OptimizeResult<Option<&TaskSpecificOptimizer>> {
328 let mut best_similarity = 0.0;
329 let mut best_optimizer = None;
330
331 for (task_name, optimizer) in &self.task_optimizers {
332 let similarity = self.compute_task_similarity(problem, task_name)?;
333 if similarity > best_similarity {
334 best_similarity = similarity;
335 best_optimizer = Some(optimizer);
336 }
337 }
338
339 if best_similarity > 0.5 {
340 Ok(best_optimizer)
341 } else {
342 Ok(None)
343 }
344 }
345
346 fn compute_task_similarity(
348 &self,
349 problem: &OptimizationProblem,
350 task_name: &str,
351 ) -> OptimizeResult<f64> {
352 let similarity = if task_name.contains(&problem.problem_class) {
354 0.8
355 } else {
356 0.2
357 };
358
359 let dim_factor = 1.0 / (1.0 + (problem.dimension as f64 - 100.0).abs() / 100.0);
361
362 Ok(similarity * dim_factor)
363 }
364
365 fn adapt_optimizer_parameters(
367 &self,
368 optimizer: &mut TaskSpecificOptimizer,
369 problem: &OptimizationProblem,
370 ) -> OptimizeResult<()> {
371 let adaptation_factor = match problem.problem_class.as_str() {
373 "quadratic" => 1.0,
374 "neural_network" => 1.2,
375 "sparse" => 0.8,
376 _ => 1.0,
377 };
378
379 for param in &mut optimizer.parameters {
381 *param *= adaptation_factor;
382 }
383
384 Ok(())
385 }
386
387 fn fine_tune_for_problem(
389 &mut self,
390 optimizer: &mut TaskSpecificOptimizer,
391 problem: &OptimizationProblem,
392 ) -> OptimizeResult<()> {
393 let meta_influence = 0.1;
395
396 for (i, param) in optimizer.parameters.iter_mut().enumerate() {
397 let meta_idx = i % self.meta_state.meta_params.len();
398 let meta_adjustment = self.meta_state.meta_params[meta_idx] * meta_influence;
399 *param += meta_adjustment;
400 }
401
402 Ok(())
403 }
404
405 pub fn get_meta_stats(&self) -> &MetaLearningStats {
407 &self.meta_stats
408 }
409
410 fn update_meta_stats(&mut self) {
412 if !self.meta_state.performance_history.is_empty() {
414 let recent_improvements: Vec<f64> = self
415 .meta_state
416 .performance_history
417 .windows(2)
418 .map(|w| w[1] - w[0])
419 .collect();
420
421 if !recent_improvements.is_empty() {
422 self.meta_stats.avg_adaptation_speed = recent_improvements
423 .iter()
424 .map(|&x| if x > 0.0 { 1.0 } else { 0.0 })
425 .sum::<f64>()
426 / recent_improvements.len() as f64;
427 }
428 }
429
430 self.meta_stats.transfer_efficiency = if self.meta_stats.tasks_learned > 1 {
432 self.meta_stats.avg_adaptation_speed / self.meta_stats.tasks_learned as f64
433 } else {
434 0.0
435 };
436
437 self.meta_stats.meta_gradient_norm = (self
439 .meta_state
440 .meta_params
441 .iter()
442 .map(|&x| x * x)
443 .sum::<f64>())
444 .sqrt();
445 }
446}
447
448impl Default for MetaLearningStats {
449 fn default() -> Self {
450 Self {
451 tasks_learned: 0,
452 avg_adaptation_speed: 0.0,
453 transfer_efficiency: 0.0,
454 meta_gradient_norm: 0.0,
455 }
456 }
457}
458
459impl LearnedOptimizer for MetaLearningOptimizer {
460 fn meta_train(&mut self, training_tasks: &[TrainingTask]) -> OptimizeResult<()> {
461 self.learn_meta_strategy(training_tasks)?;
462 self.update_meta_stats();
463 Ok(())
464 }
465
466 fn adapt_to_problem(
467 &mut self,
468 problem: &OptimizationProblem,
469 initial_params: &ArrayView1<f64>,
470 ) -> OptimizeResult<()> {
471 let adapted_optimizer = self.adapt_to_new_problem(problem)?;
472 self.task_optimizers
473 .insert(problem.name.clone(), adapted_optimizer);
474 Ok(())
475 }
476
477 fn optimize<F>(
478 &mut self,
479 objective: F,
480 initial_params: &ArrayView1<f64>,
481 ) -> OptimizeResult<OptimizeResults<f64>>
482 where
483 F: Fn(&ArrayView1<f64>) -> f64,
484 {
485 let mut current_params = initial_params.to_owned();
486 let mut best_value = objective(initial_params);
487 let mut iterations = 0;
488
489 for iter in 0..1000 {
491 iterations = iter;
492
493 let direction = self.compute_meta_direction(¤t_params, &objective)?;
495
496 let step_size = if !self.meta_state.meta_params.is_empty() {
498 let base_step = self.config.inner_learning_rate;
499 let meta_factor = self.meta_state.meta_params[0];
500 base_step * (1.0 + meta_factor * 0.1)
501 } else {
502 self.config.inner_learning_rate
503 };
504
505 for i in 0..current_params.len().min(direction.len()) {
507 current_params[i] -= step_size * direction[i];
508 }
509
510 let current_value = objective(¤t_params.view());
511
512 if current_value < best_value {
513 best_value = current_value;
514 }
515
516 if direction
518 .iter()
519 .map(|&x| x.abs())
520 .max_by(|a, b| a.partial_cmp(b).expect("Operation failed"))
521 .unwrap_or(0.0)
522 < 1e-8
523 {
524 break;
525 }
526 }
527
528 Ok(OptimizeResults::<f64> {
529 x: current_params,
530 fun: best_value,
531 success: true,
532 nit: iterations,
533 message: "Meta-learning optimization completed".to_string(),
534 jac: None,
535 hess: None,
536 constr: None,
537 nfev: iterations * 10, njev: 0,
539 nhev: 0,
540 maxcv: 0,
541 status: 0,
542 })
543 }
544
545 fn get_state(&self) -> &MetaOptimizerState {
546 &self.meta_state
547 }
548
549 fn reset(&mut self) {
550 self.task_optimizers.clear();
551 self.meta_stats = MetaLearningStats::default();
552 self.meta_state.episode = 0;
553 self.meta_state.performance_history.clear();
554 }
555}
556
557#[allow(dead_code)]
559pub fn meta_learning_optimize<F>(
560 objective: F,
561 initial_params: &ArrayView1<f64>,
562 config: Option<LearnedOptimizationConfig>,
563) -> OptimizeResult<OptimizeResults<f64>>
564where
565 F: Fn(&ArrayView1<f64>) -> f64,
566{
567 let config = config.unwrap_or_default();
568 let mut optimizer = MetaLearningOptimizer::new(config);
569 optimizer.optimize(objective, initial_params)
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn test_meta_learning_optimizer_creation() {
578 let config = LearnedOptimizationConfig::default();
579 let optimizer = MetaLearningOptimizer::new(config);
580
581 assert_eq!(optimizer.meta_stats.tasks_learned, 0);
582 assert!(optimizer.task_optimizers.is_empty());
583 }
584
585 #[test]
586 fn test_task_optimizer_creation() {
587 let config = LearnedOptimizationConfig::default();
588 let optimizer = MetaLearningOptimizer::new(config);
589
590 let problem = OptimizationProblem {
591 name: "test".to_string(),
592 dimension: 10,
593 problem_class: "quadratic".to_string(),
594 metadata: HashMap::new(),
595 max_evaluations: 1000,
596 target_accuracy: 1e-6,
597 };
598
599 let task_optimizer = optimizer
600 .create_task_optimizer(&problem)
601 .expect("Operation failed");
602 assert_eq!(task_optimizer.task_id, "test");
603 assert!(!task_optimizer.parameters.is_empty());
604 }
605
606 #[test]
607 fn test_meta_direction_computation() {
608 let config = LearnedOptimizationConfig::default();
609 let optimizer = MetaLearningOptimizer::new(config);
610
611 let params = Array1::from(vec![1.0, 2.0]);
612 let objective = |x: &ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
613
614 let direction = optimizer
615 .compute_meta_direction(¶ms, &objective)
616 .expect("Operation failed");
617
618 assert_eq!(direction.len(), 2);
619 assert!(direction.iter().all(|&x| x.is_finite()));
620 }
621
622 #[test]
623 fn test_meta_step_size_computation() {
624 let config = LearnedOptimizationConfig::default();
625 let mut optimizer = MetaLearningOptimizer::new(config);
626
627 optimizer.meta_state.meta_params[0] = 0.5;
629
630 let task_params = Array1::from(vec![0.1, 0.2, 0.3]);
631 let step_size = optimizer
632 .compute_meta_step_size(&task_params)
633 .expect("Operation failed");
634
635 assert!(step_size > 0.0);
636 assert!(step_size < 1.0);
637 }
638
639 #[test]
640 fn test_task_similarity() {
641 let config = LearnedOptimizationConfig::default();
642 let optimizer = MetaLearningOptimizer::new(config);
643
644 let problem = OptimizationProblem {
645 name: "test".to_string(),
646 dimension: 100,
647 problem_class: "quadratic".to_string(),
648 metadata: HashMap::new(),
649 max_evaluations: 1000,
650 target_accuracy: 1e-6,
651 };
652
653 let similarity1 = optimizer
654 .compute_task_similarity(&problem, "quadratic_task")
655 .expect("Operation failed");
656 let similarity2 = optimizer
657 .compute_task_similarity(&problem, "neural_network_task")
658 .expect("Operation failed");
659
660 assert!(similarity1 > similarity2);
661 }
662
663 #[test]
664 fn test_meta_learning_optimization() {
665 let objective = |x: &ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
666 let initial = Array1::from(vec![2.0, 2.0]);
667
668 let config = LearnedOptimizationConfig {
669 inner_steps: 10,
670 inner_learning_rate: 0.1,
671 ..Default::default()
672 };
673
674 let result = meta_learning_optimize(objective, &initial.view(), Some(config))
675 .expect("Operation failed");
676
677 assert!(result.fun >= 0.0);
678 assert_eq!(result.x.len(), 2);
679 assert!(result.success);
680 }
681
682 #[test]
683 fn test_meta_parameter_update() {
684 let config = LearnedOptimizationConfig::default();
685 let mut optimizer = MetaLearningOptimizer::new(config);
686
687 let problem = OptimizationProblem {
688 name: "test".to_string(),
689 dimension: 5,
690 problem_class: "quadratic".to_string(),
691 metadata: HashMap::new(),
692 max_evaluations: 100,
693 target_accuracy: 1e-6,
694 };
695
696 let initial_params = optimizer.meta_state.meta_params.clone();
697 optimizer
698 .update_meta_parameters(&problem, 1.5)
699 .expect("Operation failed");
700
701 assert!(optimizer.meta_state.meta_params != initial_params);
703 assert_eq!(optimizer.meta_state.performance_history.len(), 1);
704 }
705}