Skip to main content

scirs2_optimize/learned_optimizers/
meta_learning_optimizer.rs

1//! Meta-Learning Optimizer
2//!
3//! Implementation of a comprehensive meta-learning system for optimization that can
4//! learn to optimize across different problem classes and adapt quickly to new tasks.
5
6use 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/// Meta-Learning Optimizer with cross-problem adaptation
17#[derive(Debug, Clone)]
18pub struct MetaLearningOptimizer {
19    /// Configuration
20    config: LearnedOptimizationConfig,
21    /// Meta-optimizer state
22    meta_state: MetaOptimizerState,
23    /// Task-specific optimizers
24    task_optimizers: HashMap<String, TaskSpecificOptimizer>,
25    /// Meta-learning statistics
26    meta_stats: MetaLearningStats,
27}
28
29/// Task-specific optimizer
30#[derive(Debug, Clone)]
31pub struct TaskSpecificOptimizer {
32    /// Optimizer parameters
33    parameters: Array1<f64>,
34    /// Performance history
35    performance_history: Vec<f64>,
36    /// Task identifier
37    task_id: String,
38}
39
40/// Meta-learning statistics
41#[derive(Debug, Clone)]
42pub struct MetaLearningStats {
43    /// Number of tasks learned
44    tasks_learned: usize,
45    /// Average adaptation speed
46    avg_adaptation_speed: f64,
47    /// Transfer learning efficiency
48    transfer_efficiency: f64,
49    /// Meta-gradient norm
50    meta_gradient_norm: f64,
51}
52
53impl MetaLearningOptimizer {
54    /// Create new meta-learning optimizer
55    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    /// Learn meta-optimization strategy
72    pub fn learn_meta_strategy(&mut self, training_tasks: &[TrainingTask]) -> OptimizeResult<()> {
73        for task in training_tasks {
74            // Create task-specific optimizer
75            let task_optimizer = self.create_task_optimizer(&task.problem)?;
76
77            // Train on task
78            let performance = self.train_on_task(&task_optimizer, task)?;
79
80            // Update meta-parameters based on performance
81            self.update_meta_parameters(&task.problem, performance)?;
82
83            // Store task optimizer
84            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    /// Create task-specific optimizer
93    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    /// Estimate parameter size for problem
109    fn estimate_parameter_size(&self, problem: &OptimizationProblem) -> usize {
110        // Simple heuristic based on problem characteristics
111        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    /// Train on specific task
123    fn train_on_task(
124        &mut self,
125        optimizer: &TaskSpecificOptimizer,
126        task: &TrainingTask,
127    ) -> OptimizeResult<f64> {
128        // Meta-training rolls the current meta-strategy out on a smooth
129        // surrogate of the task. A `TrainingTask` supplies only a problem
130        // descriptor (dimension, class and, when available, the ground-truth
131        // optimum) rather than a callable objective, so the surrogate is built
132        // from that descriptor: a bowl centred on the known optimum (or the
133        // origin when it is unknown) whose curvature reflects the problem class.
134        // The returned value is the real objective improvement the strategy
135        // achieves on this surrogate, which drives the meta-parameter updates.
136        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        // Centre of the surrogate: the task's ground-truth optimum when known.
157        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        // Smooth surrogate objective with its global minimum at `center`. The
164        // problem class shapes the landscape: a plain quadratic bowl by default,
165        // a mildly non-convex bowl for neural-network-like tasks, and a bowl
166        // with a smooth L1 term rewarding sparse displacement for sparse tasks.
167        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        // Apply meta-learned optimization strategy
186        for _ in 0..self.config.inner_steps {
187            let direction = self.compute_meta_direction(&current_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(&current_params.view());
195        }
196
197        let improvement = initial_value - current_value;
198        Ok(improvement.max(0.0))
199    }
200
201    /// Compute meta-learned direction
202    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(&params.view());
212        let mut direction = Array1::zeros(params.len());
213
214        // Compute finite difference gradient
215        for i in 0..params.len() {
216            let mut params_plus = params.clone();
217            params_plus[i] += h;
218            let f_plus = objective(&params_plus.view());
219            direction[i] = (f_plus - f0) / h;
220        }
221
222        // Apply meta-learned transformation
223        self.apply_meta_transformation(&mut direction)?;
224
225        Ok(direction)
226    }
227
228    /// Apply meta-learned transformation to gradient
229    fn apply_meta_transformation(&self, gradient: &mut Array1<f64>) -> OptimizeResult<()> {
230        // Simple meta-transformation using meta-parameters
231        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    /// Compute meta-learned step size
241    fn compute_meta_step_size(&self, task_params: &Array1<f64>) -> OptimizeResult<f64> {
242        // Compute step size based on task parameters and meta-parameters
243        let mut step_size = self.config.inner_learning_rate;
244
245        // Use task parameters to modulate step size
246        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        // Apply meta-parameter modulation
252        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    /// Update meta-parameters based on task performance
261    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        // Simple meta-gradient based on performance
269        let performance_gradient = if performance > 0.0 { 1.0 } else { -1.0 };
270
271        // Update meta-parameters
272        for i in 0..self.meta_state.meta_params.len() {
273            // Simple update rule (in practice would use proper meta-gradients)
274            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            // Clip to reasonable range
281            self.meta_state.meta_params[i] = self.meta_state.meta_params[i].max(-1.0).min(1.0);
282        }
283
284        // Record performance
285        self.meta_state.performance_history.push(performance);
286
287        // Update adaptation statistics
288        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    /// Adapt to new problem using meta-knowledge
296    pub fn adapt_to_new_problem(
297        &mut self,
298        problem: &OptimizationProblem,
299    ) -> OptimizeResult<TaskSpecificOptimizer> {
300        // Find most similar task
301        let similar_task = self.find_most_similar_task(problem)?;
302
303        // Create new optimizer based on similar task
304        let mut new_optimizer = if let Some(similar_optimizer) = similar_task {
305            // Clone and adapt existing optimizer
306            let mut adapted = similar_optimizer.clone();
307            adapted.task_id = problem.name.clone();
308
309            // Apply adaptation based on problem differences
310            self.adapt_optimizer_parameters(&mut adapted, problem)?;
311            adapted
312        } else {
313            // Create from scratch using meta-parameters
314            self.create_task_optimizer(problem)?
315        };
316
317        // Fine-tune for the new problem
318        self.fine_tune_for_problem(&mut new_optimizer, problem)?;
319
320        Ok(new_optimizer)
321    }
322
323    /// Find most similar task
324    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    /// Compute similarity between problems
347    fn compute_task_similarity(
348        &self,
349        problem: &OptimizationProblem,
350        task_name: &str,
351    ) -> OptimizeResult<f64> {
352        // Simple similarity based on problem class and dimension
353        let similarity = if task_name.contains(&problem.problem_class) {
354            0.8
355        } else {
356            0.2
357        };
358
359        // Add dimension similarity
360        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    /// Adapt optimizer parameters for new problem
366    fn adapt_optimizer_parameters(
367        &self,
368        optimizer: &mut TaskSpecificOptimizer,
369        problem: &OptimizationProblem,
370    ) -> OptimizeResult<()> {
371        // Simple adaptation based on problem characteristics
372        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        // Scale parameters
380        for param in &mut optimizer.parameters {
381            *param *= adaptation_factor;
382        }
383
384        Ok(())
385    }
386
387    /// Fine-tune optimizer for specific problem
388    fn fine_tune_for_problem(
389        &mut self,
390        optimizer: &mut TaskSpecificOptimizer,
391        problem: &OptimizationProblem,
392    ) -> OptimizeResult<()> {
393        // Apply meta-learning based fine-tuning
394        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    /// Get meta-learning statistics
406    pub fn get_meta_stats(&self) -> &MetaLearningStats {
407        &self.meta_stats
408    }
409
410    /// Update meta-learning statistics
411    fn update_meta_stats(&mut self) {
412        // Compute adaptation speed
413        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        // Compute transfer efficiency
431        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        // Compute meta-gradient norm
438        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        // Use meta-learned optimization strategy
490        for iter in 0..1000 {
491            iterations = iter;
492
493            // Compute direction using meta-knowledge
494            let direction = self.compute_meta_direction(&current_params, &objective)?;
495
496            // Compute step size
497            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            // Update parameters
506            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(&current_params.view());
511
512            if current_value < best_value {
513                best_value = current_value;
514            }
515
516            // Check convergence
517            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, // Approximate function evaluations
538            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/// Convenience function for meta-learning optimization
558#[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(&params, &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        // Set some meta-parameters
628        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        // Parameters should have changed
702        assert!(optimizer.meta_state.meta_params != initial_params);
703        assert_eq!(optimizer.meta_state.performance_history.len(), 1);
704    }
705}