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 ndarray::{Array1, Array2, ArrayView1};
13use rand::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, |_| rand::rng().random_range(0.0..0.1)),
101            performance_history: Vec::new(),
102            task_id: problem.name.clone(),
103        })
104    }
105
106    /// Estimate parameter size for problem
107    fn estimate_parameter_size(&self, problem: &OptimizationProblem) -> usize {
108        // Simple heuristic based on problem characteristics
109        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    /// Train on specific task
121    fn train_on_task(
122        &mut self,
123        optimizer: &TaskSpecificOptimizer,
124        task: &TrainingTask,
125    ) -> OptimizeResult<f64> {
126        // Simplified training simulation
127        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        // Simple quadratic objective for training
148        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        // Apply meta-learned optimization strategy
155        for _ in 0..self.config.inner_steps {
156            let direction = self.compute_meta_direction(&current_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(&current_params.view());
164        }
165
166        let improvement = initial_value - current_value;
167        Ok(improvement.max(0.0))
168    }
169
170    /// Compute meta-learned direction
171    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(&params.view());
181        let mut direction = Array1::zeros(params.len());
182
183        // Compute finite difference gradient
184        for i in 0..params.len() {
185            let mut params_plus = params.clone();
186            params_plus[i] += h;
187            let f_plus = objective(&params_plus.view());
188            direction[i] = (f_plus - f0) / h;
189        }
190
191        // Apply meta-learned transformation
192        self.apply_meta_transformation(&mut direction)?;
193
194        Ok(direction)
195    }
196
197    /// Apply meta-learned transformation to gradient
198    fn apply_meta_transformation(&self, gradient: &mut Array1<f64>) -> OptimizeResult<()> {
199        // Simple meta-transformation using meta-parameters
200        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    /// Compute meta-learned step size
210    fn compute_meta_step_size(&self, task_params: &Array1<f64>) -> OptimizeResult<f64> {
211        // Compute step size based on task parameters and meta-parameters
212        let mut step_size = self.config.inner_learning_rate;
213
214        // Use task parameters to modulate step size
215        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        // Apply meta-parameter modulation
221        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    /// Update meta-parameters based on task performance
230    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        // Simple meta-gradient based on performance
238        let performance_gradient = if performance > 0.0 { 1.0 } else { -1.0 };
239
240        // Update meta-parameters
241        for i in 0..self.meta_state.meta_params.len() {
242            // Simple update rule (in practice would use proper meta-gradients)
243            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            // Clip to reasonable range
250            self.meta_state.meta_params[i] = self.meta_state.meta_params[i].max(-1.0).min(1.0);
251        }
252
253        // Record performance
254        self.meta_state.performance_history.push(performance);
255
256        // Update adaptation statistics
257        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    /// Adapt to new problem using meta-knowledge
265    pub fn adapt_to_new_problem(
266        &mut self,
267        problem: &OptimizationProblem,
268    ) -> OptimizeResult<TaskSpecificOptimizer> {
269        // Find most similar task
270        let similar_task = self.find_most_similar_task(problem)?;
271
272        // Create new optimizer based on similar task
273        let mut new_optimizer = if let Some(similar_optimizer) = similar_task {
274            // Clone and adapt existing optimizer
275            let mut adapted = similar_optimizer.clone();
276            adapted.task_id = problem.name.clone();
277
278            // Apply adaptation based on problem differences
279            self.adapt_optimizer_parameters(&mut adapted, problem)?;
280            adapted
281        } else {
282            // Create from scratch using meta-parameters
283            self.create_task_optimizer(problem)?
284        };
285
286        // Fine-tune for the new problem
287        self.fine_tune_for_problem(&mut new_optimizer, problem)?;
288
289        Ok(new_optimizer)
290    }
291
292    /// Find most similar task
293    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    /// Compute similarity between problems
316    fn compute_task_similarity(
317        &self,
318        problem: &OptimizationProblem,
319        task_name: &str,
320    ) -> OptimizeResult<f64> {
321        // Simple similarity based on problem class and dimension
322        let similarity = if task_name.contains(&problem.problem_class) {
323            0.8
324        } else {
325            0.2
326        };
327
328        // Add dimension similarity
329        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    /// Adapt optimizer parameters for new problem
335    fn adapt_optimizer_parameters(
336        &self,
337        optimizer: &mut TaskSpecificOptimizer,
338        problem: &OptimizationProblem,
339    ) -> OptimizeResult<()> {
340        // Simple adaptation based on problem characteristics
341        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        // Scale parameters
349        for param in &mut optimizer.parameters {
350            *param *= adaptation_factor;
351        }
352
353        Ok(())
354    }
355
356    /// Fine-tune optimizer for specific problem
357    fn fine_tune_for_problem(
358        &mut self,
359        optimizer: &mut TaskSpecificOptimizer,
360        problem: &OptimizationProblem,
361    ) -> OptimizeResult<()> {
362        // Apply meta-learning based fine-tuning
363        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    /// Get meta-learning statistics
375    pub fn get_meta_stats(&self) -> &MetaLearningStats {
376        &self.meta_stats
377    }
378
379    /// Update meta-learning statistics
380    fn update_meta_stats(&mut self) {
381        // Compute adaptation speed
382        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        // Compute transfer efficiency
400        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        // Compute meta-gradient norm
407        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        // Use meta-learned optimization strategy
459        for iter in 0..1000 {
460            iterations = iter;
461
462            // Compute direction using meta-knowledge
463            let direction = self.compute_meta_direction(&current_params, &objective)?;
464
465            // Compute step size
466            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            // Update parameters
475            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(&current_params.view());
480
481            if current_value < best_value {
482                best_value = current_value;
483            }
484
485            // Check convergence
486            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, // Approximate function evaluations
507            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/// Convenience function for meta-learning optimization
527#[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(&params, &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        // Set some meta-parameters
595        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        // Parameters should have changed
664        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    // Placeholder function to prevent unused module warnings
672}