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