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;
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        // Simplified training simulation
129        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        // Simple quadratic objective for training
150        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        // Apply meta-learned optimization strategy
157        for _ in 0..self.config.inner_steps {
158            let direction = self.compute_meta_direction(&current_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(&current_params.view());
166        }
167
168        let improvement = initial_value - current_value;
169        Ok(improvement.max(0.0))
170    }
171
172    /// Compute meta-learned direction
173    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(&params.view());
183        let mut direction = Array1::zeros(params.len());
184
185        // Compute finite difference gradient
186        for i in 0..params.len() {
187            let mut params_plus = params.clone();
188            params_plus[i] += h;
189            let f_plus = objective(&params_plus.view());
190            direction[i] = (f_plus - f0) / h;
191        }
192
193        // Apply meta-learned transformation
194        self.apply_meta_transformation(&mut direction)?;
195
196        Ok(direction)
197    }
198
199    /// Apply meta-learned transformation to gradient
200    fn apply_meta_transformation(&self, gradient: &mut Array1<f64>) -> OptimizeResult<()> {
201        // Simple meta-transformation using meta-parameters
202        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    /// Compute meta-learned step size
212    fn compute_meta_step_size(&self, task_params: &Array1<f64>) -> OptimizeResult<f64> {
213        // Compute step size based on task parameters and meta-parameters
214        let mut step_size = self.config.inner_learning_rate;
215
216        // Use task parameters to modulate step size
217        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        // Apply meta-parameter modulation
223        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    /// Update meta-parameters based on task performance
232    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        // Simple meta-gradient based on performance
240        let performance_gradient = if performance > 0.0 { 1.0 } else { -1.0 };
241
242        // Update meta-parameters
243        for i in 0..self.meta_state.meta_params.len() {
244            // Simple update rule (in practice would use proper meta-gradients)
245            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            // Clip to reasonable range
252            self.meta_state.meta_params[i] = self.meta_state.meta_params[i].max(-1.0).min(1.0);
253        }
254
255        // Record performance
256        self.meta_state.performance_history.push(performance);
257
258        // Update adaptation statistics
259        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    /// Adapt to new problem using meta-knowledge
267    pub fn adapt_to_new_problem(
268        &mut self,
269        problem: &OptimizationProblem,
270    ) -> OptimizeResult<TaskSpecificOptimizer> {
271        // Find most similar task
272        let similar_task = self.find_most_similar_task(problem)?;
273
274        // Create new optimizer based on similar task
275        let mut new_optimizer = if let Some(similar_optimizer) = similar_task {
276            // Clone and adapt existing optimizer
277            let mut adapted = similar_optimizer.clone();
278            adapted.task_id = problem.name.clone();
279
280            // Apply adaptation based on problem differences
281            self.adapt_optimizer_parameters(&mut adapted, problem)?;
282            adapted
283        } else {
284            // Create from scratch using meta-parameters
285            self.create_task_optimizer(problem)?
286        };
287
288        // Fine-tune for the new problem
289        self.fine_tune_for_problem(&mut new_optimizer, problem)?;
290
291        Ok(new_optimizer)
292    }
293
294    /// Find most similar task
295    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    /// Compute similarity between problems
318    fn compute_task_similarity(
319        &self,
320        problem: &OptimizationProblem,
321        task_name: &str,
322    ) -> OptimizeResult<f64> {
323        // Simple similarity based on problem class and dimension
324        let similarity = if task_name.contains(&problem.problem_class) {
325            0.8
326        } else {
327            0.2
328        };
329
330        // Add dimension similarity
331        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    /// Adapt optimizer parameters for new problem
337    fn adapt_optimizer_parameters(
338        &self,
339        optimizer: &mut TaskSpecificOptimizer,
340        problem: &OptimizationProblem,
341    ) -> OptimizeResult<()> {
342        // Simple adaptation based on problem characteristics
343        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        // Scale parameters
351        for param in &mut optimizer.parameters {
352            *param *= adaptation_factor;
353        }
354
355        Ok(())
356    }
357
358    /// Fine-tune optimizer for specific problem
359    fn fine_tune_for_problem(
360        &mut self,
361        optimizer: &mut TaskSpecificOptimizer,
362        problem: &OptimizationProblem,
363    ) -> OptimizeResult<()> {
364        // Apply meta-learning based fine-tuning
365        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    /// Get meta-learning statistics
377    pub fn get_meta_stats(&self) -> &MetaLearningStats {
378        &self.meta_stats
379    }
380
381    /// Update meta-learning statistics
382    fn update_meta_stats(&mut self) {
383        // Compute adaptation speed
384        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        // Compute transfer efficiency
402        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        // Compute meta-gradient norm
409        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        // Use meta-learned optimization strategy
461        for iter in 0..1000 {
462            iterations = iter;
463
464            // Compute direction using meta-knowledge
465            let direction = self.compute_meta_direction(&current_params, &objective)?;
466
467            // Compute step size
468            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            // Update parameters
477            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(&current_params.view());
482
483            if current_value < best_value {
484                best_value = current_value;
485            }
486
487            // Check convergence
488            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, // Approximate function evaluations
509            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/// Convenience function for meta-learning optimization
529#[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(&params, &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        // Set some meta-parameters
597        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        // Parameters should have changed
666        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    // Placeholder function to prevent unused module warnings
674}