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;
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 {
130 super::ParameterDistribution::Uniform { low, high } => {
131 Array1::from_shape_fn(task.problem.dimension, |_| {
132 low + scirs2_core::random::rng().random_range(0.0..1.0) * (high - low)
133 })
134 }
135 super::ParameterDistribution::Normal { mean, std } => {
136 Array1::from_shape_fn(task.problem.dimension, |_| {
137 mean + std * (scirs2_core::random::rng().random_range(0.0..1.0) - 0.5) * 2.0
138 })
139 }
140 super::ParameterDistribution::Custom { samples } => {
141 if !samples.is_empty() {
142 samples[0].clone()
143 } else {
144 Array1::zeros(task.problem.dimension)
145 }
146 }
147 };
148
149 let training_objective = |x: &ArrayView1<f64>| x.iter().map(|&xi| xi * xi).sum::<f64>();
151
152 let initial_value = training_objective(&initial_params.view());
153 let mut current_params = initial_params;
154 let mut current_value = initial_value;
155
156 for _ in 0..self.config.inner_steps {
158 let direction = self.compute_meta_direction(¤t_params, &training_objective)?;
159 let step_size = self.compute_meta_step_size(&optimizer.parameters)?;
160
161 for i in 0..current_params.len().min(direction.len()) {
162 current_params[i] -= step_size * direction[i];
163 }
164
165 current_value = training_objective(¤t_params.view());
166 }
167
168 let improvement = initial_value - current_value;
169 Ok(improvement.max(0.0))
170 }
171
172 fn compute_meta_direction<F>(
174 &self,
175 params: &Array1<f64>,
176 objective: &F,
177 ) -> OptimizeResult<Array1<f64>>
178 where
179 F: Fn(&ArrayView1<f64>) -> f64,
180 {
181 let h = 1e-6;
182 let f0 = objective(¶ms.view());
183 let mut direction = Array1::zeros(params.len());
184
185 for i in 0..params.len() {
187 let mut params_plus = params.clone();
188 params_plus[i] += h;
189 let f_plus = objective(¶ms_plus.view());
190 direction[i] = (f_plus - f0) / h;
191 }
192
193 self.apply_meta_transformation(&mut direction)?;
195
196 Ok(direction)
197 }
198
199 fn apply_meta_transformation(&self, gradient: &mut Array1<f64>) -> OptimizeResult<()> {
201 for i in 0..gradient.len() {
203 let meta_idx = i % self.meta_state.meta_params.len();
204 let meta_factor = self.meta_state.meta_params[meta_idx];
205 gradient[i] *= 1.0 + meta_factor * 0.1;
206 }
207
208 Ok(())
209 }
210
211 fn compute_meta_step_size(&self, task_params: &Array1<f64>) -> OptimizeResult<f64> {
213 let mut step_size = self.config.inner_learning_rate;
215
216 if !task_params.is_empty() {
218 let param_norm = (task_params.iter().map(|&x| x * x).sum::<f64>()).sqrt();
219 step_size *= (1.0 + param_norm * 0.1).recip();
220 }
221
222 if !self.meta_state.meta_params.is_empty() {
224 let meta_factor = self.meta_state.meta_params[0];
225 step_size *= (1.0 + meta_factor * 0.2).max(0.1).min(2.0);
226 }
227
228 Ok(step_size)
229 }
230
231 fn update_meta_parameters(
233 &mut self,
234 problem: &OptimizationProblem,
235 performance: f64,
236 ) -> OptimizeResult<()> {
237 let learning_rate = self.config.meta_learning_rate;
238
239 let performance_gradient = if performance > 0.0 { 1.0 } else { -1.0 };
241
242 for i in 0..self.meta_state.meta_params.len() {
244 let update = learning_rate
246 * performance_gradient
247 * (scirs2_core::random::rng().random_range(0.0..1.0) - 0.5)
248 * 0.1;
249 self.meta_state.meta_params[i] += update;
250
251 self.meta_state.meta_params[i] = self.meta_state.meta_params[i].max(-1.0).min(1.0);
253 }
254
255 self.meta_state.performance_history.push(performance);
257
258 self.meta_state.adaptation_stats.avg_convergence_rate =
260 self.meta_state.performance_history.iter().sum::<f64>()
261 / self.meta_state.performance_history.len() as f64;
262
263 Ok(())
264 }
265
266 pub fn adapt_to_new_problem(
268 &mut self,
269 problem: &OptimizationProblem,
270 ) -> OptimizeResult<TaskSpecificOptimizer> {
271 let similar_task = self.find_most_similar_task(problem)?;
273
274 let mut new_optimizer = if let Some(similar_optimizer) = similar_task {
276 let mut adapted = similar_optimizer.clone();
278 adapted.task_id = problem.name.clone();
279
280 self.adapt_optimizer_parameters(&mut adapted, problem)?;
282 adapted
283 } else {
284 self.create_task_optimizer(problem)?
286 };
287
288 self.fine_tune_for_problem(&mut new_optimizer, problem)?;
290
291 Ok(new_optimizer)
292 }
293
294 fn find_most_similar_task(
296 &self,
297 problem: &OptimizationProblem,
298 ) -> OptimizeResult<Option<&TaskSpecificOptimizer>> {
299 let mut best_similarity = 0.0;
300 let mut best_optimizer = None;
301
302 for (task_name, optimizer) in &self.task_optimizers {
303 let similarity = self.compute_task_similarity(problem, task_name)?;
304 if similarity > best_similarity {
305 best_similarity = similarity;
306 best_optimizer = Some(optimizer);
307 }
308 }
309
310 if best_similarity > 0.5 {
311 Ok(best_optimizer)
312 } else {
313 Ok(None)
314 }
315 }
316
317 fn compute_task_similarity(
319 &self,
320 problem: &OptimizationProblem,
321 task_name: &str,
322 ) -> OptimizeResult<f64> {
323 let similarity = if task_name.contains(&problem.problem_class) {
325 0.8
326 } else {
327 0.2
328 };
329
330 let dim_factor = 1.0 / (1.0 + (problem.dimension as f64 - 100.0).abs() / 100.0);
332
333 Ok(similarity * dim_factor)
334 }
335
336 fn adapt_optimizer_parameters(
338 &self,
339 optimizer: &mut TaskSpecificOptimizer,
340 problem: &OptimizationProblem,
341 ) -> OptimizeResult<()> {
342 let adaptation_factor = match problem.problem_class.as_str() {
344 "quadratic" => 1.0,
345 "neural_network" => 1.2,
346 "sparse" => 0.8,
347 _ => 1.0,
348 };
349
350 for param in &mut optimizer.parameters {
352 *param *= adaptation_factor;
353 }
354
355 Ok(())
356 }
357
358 fn fine_tune_for_problem(
360 &mut self,
361 optimizer: &mut TaskSpecificOptimizer,
362 problem: &OptimizationProblem,
363 ) -> OptimizeResult<()> {
364 let meta_influence = 0.1;
366
367 for (i, param) in optimizer.parameters.iter_mut().enumerate() {
368 let meta_idx = i % self.meta_state.meta_params.len();
369 let meta_adjustment = self.meta_state.meta_params[meta_idx] * meta_influence;
370 *param += meta_adjustment;
371 }
372
373 Ok(())
374 }
375
376 pub fn get_meta_stats(&self) -> &MetaLearningStats {
378 &self.meta_stats
379 }
380
381 fn update_meta_stats(&mut self) {
383 if !self.meta_state.performance_history.is_empty() {
385 let recent_improvements: Vec<f64> = self
386 .meta_state
387 .performance_history
388 .windows(2)
389 .map(|w| w[1] - w[0])
390 .collect();
391
392 if !recent_improvements.is_empty() {
393 self.meta_stats.avg_adaptation_speed = recent_improvements
394 .iter()
395 .map(|&x| if x > 0.0 { 1.0 } else { 0.0 })
396 .sum::<f64>()
397 / recent_improvements.len() as f64;
398 }
399 }
400
401 self.meta_stats.transfer_efficiency = if self.meta_stats.tasks_learned > 1 {
403 self.meta_stats.avg_adaptation_speed / self.meta_stats.tasks_learned as f64
404 } else {
405 0.0
406 };
407
408 self.meta_stats.meta_gradient_norm = (self
410 .meta_state
411 .meta_params
412 .iter()
413 .map(|&x| x * x)
414 .sum::<f64>())
415 .sqrt();
416 }
417}
418
419impl Default for MetaLearningStats {
420 fn default() -> Self {
421 Self {
422 tasks_learned: 0,
423 avg_adaptation_speed: 0.0,
424 transfer_efficiency: 0.0,
425 meta_gradient_norm: 0.0,
426 }
427 }
428}
429
430impl LearnedOptimizer for MetaLearningOptimizer {
431 fn meta_train(&mut self, training_tasks: &[TrainingTask]) -> OptimizeResult<()> {
432 self.learn_meta_strategy(training_tasks)?;
433 self.update_meta_stats();
434 Ok(())
435 }
436
437 fn adapt_to_problem(
438 &mut self,
439 problem: &OptimizationProblem,
440 initial_params: &ArrayView1<f64>,
441 ) -> OptimizeResult<()> {
442 let adapted_optimizer = self.adapt_to_new_problem(problem)?;
443 self.task_optimizers
444 .insert(problem.name.clone(), adapted_optimizer);
445 Ok(())
446 }
447
448 fn optimize<F>(
449 &mut self,
450 objective: F,
451 initial_params: &ArrayView1<f64>,
452 ) -> OptimizeResult<OptimizeResults<f64>>
453 where
454 F: Fn(&ArrayView1<f64>) -> f64,
455 {
456 let mut current_params = initial_params.to_owned();
457 let mut best_value = objective(initial_params);
458 let mut iterations = 0;
459
460 for iter in 0..1000 {
462 iterations = iter;
463
464 let direction = self.compute_meta_direction(¤t_params, &objective)?;
466
467 let step_size = if !self.meta_state.meta_params.is_empty() {
469 let base_step = self.config.inner_learning_rate;
470 let meta_factor = self.meta_state.meta_params[0];
471 base_step * (1.0 + meta_factor * 0.1)
472 } else {
473 self.config.inner_learning_rate
474 };
475
476 for i in 0..current_params.len().min(direction.len()) {
478 current_params[i] -= step_size * direction[i];
479 }
480
481 let current_value = objective(¤t_params.view());
482
483 if current_value < best_value {
484 best_value = current_value;
485 }
486
487 if direction
489 .iter()
490 .map(|&x| x.abs())
491 .max_by(|a, b| a.partial_cmp(b).unwrap())
492 .unwrap_or(0.0)
493 < 1e-8
494 {
495 break;
496 }
497 }
498
499 Ok(OptimizeResults::<f64> {
500 x: current_params,
501 fun: best_value,
502 success: true,
503 nit: iterations,
504 message: "Meta-learning optimization completed".to_string(),
505 jac: None,
506 hess: None,
507 constr: None,
508 nfev: iterations * 10, njev: 0,
510 nhev: 0,
511 maxcv: 0,
512 status: 0,
513 })
514 }
515
516 fn get_state(&self) -> &MetaOptimizerState {
517 &self.meta_state
518 }
519
520 fn reset(&mut self) {
521 self.task_optimizers.clear();
522 self.meta_stats = MetaLearningStats::default();
523 self.meta_state.episode = 0;
524 self.meta_state.performance_history.clear();
525 }
526}
527
528#[allow(dead_code)]
530pub fn meta_learning_optimize<F>(
531 objective: F,
532 initial_params: &ArrayView1<f64>,
533 config: Option<LearnedOptimizationConfig>,
534) -> OptimizeResult<OptimizeResults<f64>>
535where
536 F: Fn(&ArrayView1<f64>) -> f64,
537{
538 let config = config.unwrap_or_default();
539 let mut optimizer = MetaLearningOptimizer::new(config);
540 optimizer.optimize(objective, initial_params)
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_meta_learning_optimizer_creation() {
549 let config = LearnedOptimizationConfig::default();
550 let optimizer = MetaLearningOptimizer::new(config);
551
552 assert_eq!(optimizer.meta_stats.tasks_learned, 0);
553 assert!(optimizer.task_optimizers.is_empty());
554 }
555
556 #[test]
557 fn test_task_optimizer_creation() {
558 let config = LearnedOptimizationConfig::default();
559 let optimizer = MetaLearningOptimizer::new(config);
560
561 let problem = OptimizationProblem {
562 name: "test".to_string(),
563 dimension: 10,
564 problem_class: "quadratic".to_string(),
565 metadata: HashMap::new(),
566 max_evaluations: 1000,
567 target_accuracy: 1e-6,
568 };
569
570 let task_optimizer = optimizer.create_task_optimizer(&problem).unwrap();
571 assert_eq!(task_optimizer.task_id, "test");
572 assert!(!task_optimizer.parameters.is_empty());
573 }
574
575 #[test]
576 fn test_meta_direction_computation() {
577 let config = LearnedOptimizationConfig::default();
578 let optimizer = MetaLearningOptimizer::new(config);
579
580 let params = Array1::from(vec![1.0, 2.0]);
581 let objective = |x: &ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
582
583 let direction = optimizer
584 .compute_meta_direction(¶ms, &objective)
585 .unwrap();
586
587 assert_eq!(direction.len(), 2);
588 assert!(direction.iter().all(|&x| x.is_finite()));
589 }
590
591 #[test]
592 fn test_meta_step_size_computation() {
593 let config = LearnedOptimizationConfig::default();
594 let mut optimizer = MetaLearningOptimizer::new(config);
595
596 optimizer.meta_state.meta_params[0] = 0.5;
598
599 let task_params = Array1::from(vec![0.1, 0.2, 0.3]);
600 let step_size = optimizer.compute_meta_step_size(&task_params).unwrap();
601
602 assert!(step_size > 0.0);
603 assert!(step_size < 1.0);
604 }
605
606 #[test]
607 fn test_task_similarity() {
608 let config = LearnedOptimizationConfig::default();
609 let optimizer = MetaLearningOptimizer::new(config);
610
611 let problem = OptimizationProblem {
612 name: "test".to_string(),
613 dimension: 100,
614 problem_class: "quadratic".to_string(),
615 metadata: HashMap::new(),
616 max_evaluations: 1000,
617 target_accuracy: 1e-6,
618 };
619
620 let similarity1 = optimizer
621 .compute_task_similarity(&problem, "quadratic_task")
622 .unwrap();
623 let similarity2 = optimizer
624 .compute_task_similarity(&problem, "neural_network_task")
625 .unwrap();
626
627 assert!(similarity1 > similarity2);
628 }
629
630 #[test]
631 fn test_meta_learning_optimization() {
632 let objective = |x: &ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
633 let initial = Array1::from(vec![2.0, 2.0]);
634
635 let config = LearnedOptimizationConfig {
636 inner_steps: 10,
637 inner_learning_rate: 0.1,
638 ..Default::default()
639 };
640
641 let result = meta_learning_optimize(objective, &initial.view(), Some(config)).unwrap();
642
643 assert!(result.fun >= 0.0);
644 assert_eq!(result.x.len(), 2);
645 assert!(result.success);
646 }
647
648 #[test]
649 fn test_meta_parameter_update() {
650 let config = LearnedOptimizationConfig::default();
651 let mut optimizer = MetaLearningOptimizer::new(config);
652
653 let problem = OptimizationProblem {
654 name: "test".to_string(),
655 dimension: 5,
656 problem_class: "quadratic".to_string(),
657 metadata: HashMap::new(),
658 max_evaluations: 100,
659 target_accuracy: 1e-6,
660 };
661
662 let initial_params = optimizer.meta_state.meta_params.clone();
663 optimizer.update_meta_parameters(&problem, 1.5).unwrap();
664
665 assert!(optimizer.meta_state.meta_params != initial_params);
667 assert_eq!(optimizer.meta_state.performance_history.len(), 1);
668 }
669}
670
671#[allow(dead_code)]
672pub fn placeholder() {
673 }