1use scirs2_core::ndarray::{Array1, Array2};
20use scirs2_core::numeric::Float;
21use std::collections::{HashMap, VecDeque};
22use std::fmt::Debug;
23
24use super::bptt::{BpttConfig, DiagonalQuadraticTask};
25use super::trainer::MetaTrainer;
26use super::{
27 AdaptationEvent, AdaptiveLearningRateController, ConvergenceIndicators,
28 DomainSimilarityEstimator, FlowStability, GradientAnalyzer, GradientCorrelationTracker,
29 GradientFlowAnalyzer, GradientNoiseEstimator, GradientStatistics, HistoryBuffer,
30 InnerLoopState, LRAdaptationParams, LSTMNetwork, LossLandscapeAnalyzer, MetaLearner,
31 MetaLearningState, MetaTask, NoiseCharacteristics, NoiseType, OptimizationPhase,
32 OptimizationStateTracker, PerformanceTracker, PerformanceTrend, SimilarityFunction,
33 StabilityMetrics, TransferLearner, TransferMetrics, TransferResults,
34};
35use crate::error::{OptimError, Result};
36use crate::LearnedOptimizerConfig;
37
38const LR_TREND_WINDOW: usize = 8;
40
41const STATE_TREND_WINDOW: usize = 32;
43
44const MIN_UNROLL_STEPS: usize = 4;
48
49const MAX_UNROLL_STEPS: usize = 32;
52
53impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> MetaLearner<T> {
54 pub(super) fn new(config: &LearnedOptimizerConfig) -> Result<Self> {
55 let meta_lr: T =
56 scirs2_core::numeric::NumCast::from(config.meta_learning_rate).unwrap_or_else(T::zero);
57 Ok(Self {
58 meta_parameters: HashMap::new(),
59 meta_gradients: HashMap::new(),
60 task_history: VecDeque::new(),
61 meta_state: MetaLearningState {
62 meta_step: 0,
63 meta_lr,
64 adaptation_rate: scirs2_core::numeric::NumCast::from(0.1)
65 .unwrap_or_else(|| T::zero()),
66 meta_validation_performance: T::zero(),
67 adaptation_history: VecDeque::new(),
68 inner_loop_state: InnerLoopState {
69 inner_step: 0,
70 inner_parameters: Array1::zeros(1),
71 inner_optimizer_state: HashMap::new(),
72 inner_performance: T::zero(),
73 },
74 },
75 transfer_learner: TransferLearner {
76 source_knowledge: HashMap::new(),
77 adaptation_parameters: Array1::zeros(1),
78 transfer_metrics: TransferMetrics {
79 efficiency: T::zero(),
80 adaptation_speed: T::zero(),
81 knowledge_retention: T::zero(),
82 negative_transfer_score: T::zero(),
83 },
84 similarity_estimator: DomainSimilarityEstimator {
85 domain_embeddings: HashMap::new(),
86 similarity_params: Array1::zeros(1),
87 similarity_function: SimilarityFunction::Cosine,
88 },
89 },
90 })
91 }
92
93 pub(super) fn step(
112 &mut self,
113 tasks: &[MetaTask<T>],
114 network: &mut LSTMNetwork<T>,
115 ) -> Result<T> {
116 if tasks.is_empty() {
117 return Err(OptimError::InsufficientData(
118 "meta-learning step requires at least one task".to_string(),
119 ));
120 }
121
122 let mut surrogates: Vec<DiagonalQuadraticTask<T>> = Vec::with_capacity(tasks.len());
123 let mut weights: Vec<T> = Vec::with_capacity(tasks.len());
124 let mut rejected = 0usize;
125 for task in tasks {
126 match DiagonalQuadraticTask::from_trajectory(&task.training_trajectory) {
127 Ok(surrogate) => {
128 surrogates.push(surrogate);
129 weights.push(if task.weight > T::zero() {
130 task.weight
131 } else {
132 T::one()
133 });
134 }
135 Err(_) => rejected += 1,
136 }
137 }
138
139 if surrogates.is_empty() {
140 return Err(OptimError::InsufficientData(format!(
141 "none of the {} supplied tasks had a trajectory from which a \
142 differentiable surrogate could be identified ({rejected} rejected); \
143 each task needs at least two trajectory points with varying parameters",
144 tasks.len()
145 )));
146 }
147
148 let shortest_trajectory = tasks
156 .iter()
157 .map(|t| t.training_trajectory.len())
158 .min()
159 .unwrap_or(0);
160 let unroll = shortest_trajectory.clamp(MIN_UNROLL_STEPS, MAX_UNROLL_STEPS);
161 let bptt_config = BpttConfig {
162 unroll_steps: unroll,
163 meta_learning_rate: self.meta_state.meta_lr.to_f64().unwrap_or(1e-3),
164 gradient_clip: 1.0,
165 };
166 let mut trainer = MetaTrainer::new(bptt_config);
167 let meta_loss = trainer.meta_step(network, &surrogates, &weights)?;
168
169 let previous_loss = self.meta_state.meta_validation_performance;
173 self.meta_state.meta_step += 1;
174 self.meta_state.meta_validation_performance = meta_loss;
175 for task in tasks.iter().take(16) {
176 self.task_history.push_back(task.clone());
177 if self.task_history.len() > 64 {
178 self.task_history.pop_front();
179 }
180 }
181 self.meta_parameters.insert(
182 "flattened_controller".to_string(),
183 Array1::from_vec(super::bptt::flatten_parameters(network)),
184 );
185 self.meta_gradients.insert(
186 "last_meta_gradient".to_string(),
187 Array1::from_vec(trainer.last_gradient_vector()),
188 );
189
190 self.meta_state
193 .adaptation_history
194 .push_back(AdaptationEvent {
195 source_task: tasks
196 .first()
197 .map(|t| t.id.clone())
198 .unwrap_or_else(|| "unknown".to_string()),
199 target_task: tasks
200 .last()
201 .map(|t| t.id.clone())
202 .unwrap_or_else(|| "unknown".to_string()),
203 adaptation_steps: unroll,
204 transfer_efficiency: if previous_loss > T::zero() {
205 (previous_loss - meta_loss) / previous_loss
206 } else {
207 T::zero()
208 },
209 performance_improvement: previous_loss - meta_loss,
210 });
211 while self.meta_state.adaptation_history.len() > 256 {
212 self.meta_state.adaptation_history.pop_front();
213 }
214
215 Ok(meta_loss)
216 }
217
218 pub fn adaptation_history(&self) -> &VecDeque<AdaptationEvent<T>> {
220 &self.meta_state.adaptation_history
221 }
222
223 pub fn meta_step_count(&self) -> usize {
225 self.meta_state.meta_step
226 }
227
228 pub fn last_meta_loss(&self) -> T {
230 self.meta_state.meta_validation_performance
231 }
232}
233
234impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> TransferLearner<T> {
235 pub(super) fn transfer_to_domain(
251 &mut self,
252 target_tasks: &[MetaTask<T>],
253 network: &mut LSTMNetwork<T>,
254 ) -> Result<TransferResults<T>> {
255 if target_tasks.is_empty() {
256 return Err(OptimError::InsufficientData(
257 "transfer requires at least one target task".to_string(),
258 ));
259 }
260
261 let mut surrogates = Vec::new();
262 let mut weights = Vec::new();
263 for task in target_tasks {
264 if let Ok(s) = DiagonalQuadraticTask::from_trajectory(&task.training_trajectory) {
265 surrogates.push(s);
266 weights.push(if task.weight > T::zero() {
267 task.weight
268 } else {
269 T::one()
270 });
271 }
272 }
273 if surrogates.is_empty() {
274 return Err(OptimError::InsufficientData(
275 "no target task had a trajectory from which a surrogate could be identified"
276 .to_string(),
277 ));
278 }
279
280 let config = BpttConfig {
281 unroll_steps: 12,
282 meta_learning_rate: 1e-2,
283 gradient_clip: 1.0,
284 };
285 let mut trainer = MetaTrainer::new(config);
286
287 let initial_performance = trainer.evaluate(network, &surrogates, &weights)?;
288 let adaptation_steps = 8usize;
289 for _ in 0..adaptation_steps {
290 trainer.meta_step(network, &surrogates, &weights)?;
291 }
292 let final_performance = trainer.evaluate(network, &surrogates, &weights)?;
293
294 let efficiency = if initial_performance > T::zero() {
295 let ratio = (initial_performance - final_performance) / initial_performance;
296 let one = T::one();
297 if ratio > one {
298 one
299 } else if ratio < -one {
300 -one
301 } else {
302 ratio
303 }
304 } else {
305 T::zero()
306 };
307
308 self.transfer_metrics.efficiency = efficiency;
309 self.transfer_metrics.adaptation_speed = efficiency
310 / scirs2_core::numeric::NumCast::from(adaptation_steps.max(1)).unwrap_or_else(T::one);
311 self.transfer_metrics.knowledge_retention = if initial_performance > T::zero() {
312 let retained = final_performance / initial_performance;
313 if retained > T::one() {
314 T::zero()
315 } else {
316 T::one() - retained
317 }
318 } else {
319 T::zero()
320 };
321 self.transfer_metrics.negative_transfer_score = if efficiency < T::zero() {
322 -efficiency
323 } else {
324 T::zero()
325 };
326
327 Ok(TransferResults {
328 initial_performance,
329 final_performance,
330 adaptation_steps,
331 transfer_efficiency: efficiency,
332 })
333 }
334
335 pub fn metrics(&self) -> &TransferMetrics<T> {
337 &self.transfer_metrics
338 }
339}
340
341impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> AdaptiveLearningRateController<T> {
342 pub(super) fn new(config: &LearnedOptimizerConfig) -> Result<Self> {
343 let base_lr: T =
344 scirs2_core::numeric::NumCast::from(config.learning_rate).unwrap_or_else(T::zero);
345 if base_lr <= T::zero() {
346 return Err(OptimError::InvalidConfig(
347 "learning_rate must be positive".to_string(),
348 ));
349 }
350 let momentum: T = scirs2_core::numeric::NumCast::from(if config.use_momentum {
351 config.momentum_decay
352 } else {
353 0.0
354 })
355 .unwrap_or_else(T::zero);
356
357 Ok(Self {
358 base_lr,
359 current_lr: base_lr,
360 adaptation_params: LRAdaptationParams {
361 momentum,
362 gradient_sensitivity: scirs2_core::numeric::NumCast::from(0.1)
363 .unwrap_or_else(|| T::zero()),
364 loss_sensitivity: scirs2_core::numeric::NumCast::from(0.1)
365 .unwrap_or_else(|| T::zero()),
366 min_lr: base_lr / scirs2_core::numeric::NumCast::from(100.0).unwrap_or_else(T::one),
369 max_lr: base_lr * scirs2_core::numeric::NumCast::from(100.0).unwrap_or_else(T::one),
370 adaptation_rate: scirs2_core::numeric::NumCast::from(0.01)
371 .unwrap_or_else(|| T::zero()),
372 },
373 lr_history: VecDeque::new(),
374 performance_tracker: PerformanceTracker {
375 recent_losses: VecDeque::new(),
376 trend: PerformanceTrend::Unknown,
377 stagnation_counter: 0,
378 best_performance: T::infinity(),
379 improvement_rate: T::zero(),
380 },
381 })
382 }
383
384 pub(super) fn compute_lr(
405 &mut self,
406 gradients: &Array1<T>,
407 loss: Option<T>,
408 history: &HistoryBuffer<T>,
409 ) -> Result<T> {
410 if gradients.is_empty() {
411 return Err(OptimError::InsufficientData(
412 "cannot compute an adaptive learning rate from an empty gradient".to_string(),
413 ));
414 }
415
416 let grad_norm = gradients
417 .iter()
418 .map(|&g| g * g)
419 .fold(T::zero(), |a, b| a + b)
420 .sqrt();
421
422 let grad_factor =
424 T::one() / (T::one() + self.adaptation_params.gradient_sensitivity * grad_norm);
425
426 if let Some(l) = loss {
428 self.performance_tracker.recent_losses.push_back(l);
429 while self.performance_tracker.recent_losses.len() > LR_TREND_WINDOW {
430 self.performance_tracker.recent_losses.pop_front();
431 }
432 if l < self.performance_tracker.best_performance {
433 self.performance_tracker.best_performance = l;
434 self.performance_tracker.stagnation_counter = 0;
435 } else {
436 self.performance_tracker.stagnation_counter += 1;
437 }
438 }
439
440 let losses: Vec<T> = if self.performance_tracker.recent_losses.len() >= 2 {
443 self.performance_tracker
444 .recent_losses
445 .iter()
446 .copied()
447 .collect()
448 } else {
449 history
450 .losses
451 .iter()
452 .rev()
453 .take(LR_TREND_WINDOW)
454 .rev()
455 .copied()
456 .collect()
457 };
458
459 let (trend_factor, improvement_rate) = if losses.len() >= 2 {
460 let first = losses[0];
461 let last = losses[losses.len() - 1];
462 let rate = if first.abs() > T::zero() {
463 (first - last) / first.abs()
464 } else {
465 T::zero()
466 };
467 let factor = if last < first {
468 T::one() + self.adaptation_params.adaptation_rate
469 } else if last > first {
470 T::one() - self.adaptation_params.adaptation_rate
471 } else {
472 T::one()
473 };
474 (factor, rate)
475 } else {
476 (T::one(), T::zero())
477 };
478 self.performance_tracker.improvement_rate = improvement_rate;
479 self.performance_tracker.trend = if losses.len() < 2 {
480 PerformanceTrend::Unknown
481 } else if improvement_rate > T::zero() {
482 PerformanceTrend::Improving
483 } else if improvement_rate < T::zero() {
484 PerformanceTrend::Degrading
485 } else {
486 PerformanceTrend::Stagnating
487 };
488
489 let stagnation_factor = if self.performance_tracker.stagnation_counter >= LR_TREND_WINDOW {
491 T::one() + self.adaptation_params.loss_sensitivity
492 } else {
493 T::one()
494 };
495
496 let target = self.base_lr * grad_factor * trend_factor * stagnation_factor;
497
498 let m = self.adaptation_params.momentum;
500 let smoothed = m * self.current_lr + (T::one() - m) * target;
501 let clamped = if smoothed < self.adaptation_params.min_lr {
502 self.adaptation_params.min_lr
503 } else if smoothed > self.adaptation_params.max_lr {
504 self.adaptation_params.max_lr
505 } else {
506 smoothed
507 };
508
509 self.current_lr = clamped;
510 self.lr_history.push_back(clamped);
511 while self.lr_history.len() > 1000 {
512 self.lr_history.pop_front();
513 }
514 Ok(clamped)
515 }
516
517 pub fn current_lr(&self) -> T {
519 self.current_lr
520 }
521
522 pub fn trend(&self) -> PerformanceTrend {
524 self.performance_tracker.trend
525 }
526
527 pub fn stagnation_counter(&self) -> usize {
529 self.performance_tracker.stagnation_counter
530 }
531}
532
533impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> OptimizationStateTracker<T> {
534 pub(super) fn new() -> Self {
535 Self {
536 phase: OptimizationPhase::InitialDescent,
537 convergence_indicators: ConvergenceIndicators {
538 gradient_norm_trend: Vec::new(),
539 loss_change_trend: Vec::new(),
540 parameter_change_magnitude: T::zero(),
541 convergence_probability: T::zero(),
542 estimated_steps_to_convergence: None,
543 },
544 gradient_analyzer: GradientAnalyzer {
545 gradient_stats: GradientStatistics {
546 mean_norm: T::zero(),
547 norm_variance: T::zero(),
548 direction_consistency: T::zero(),
549 magnitude_distribution: Vec::new(),
550 component_stats: Array1::zeros(1),
551 },
552 correlation_tracker: GradientCorrelationTracker {
553 correlation_matrix: Array2::zeros((1, 1)),
554 temporal_correlations: VecDeque::new(),
555 cross_correlations: HashMap::new(),
556 },
557 noise_estimator: GradientNoiseEstimator {
558 noise_level: T::zero(),
559 signal_to_noise_ratio: T::zero(),
560 noise_characteristics: NoiseCharacteristics {
561 noise_type: NoiseType::White,
562 scale: T::zero(),
563 temporal_correlation: T::zero(),
564 spatial_correlation: T::zero(),
565 },
566 },
567 flow_analyzer: GradientFlowAnalyzer {
568 flow_field: Array2::zeros((1, 1)),
569 critical_points: Vec::new(),
570 stability: FlowStability::Unknown,
571 attractors: Vec::new(),
572 repellers: Vec::new(),
573 },
574 },
575 landscape_analyzer: LossLandscapeAnalyzer {
576 local_curvature: T::zero(),
577 hessian_eigenvalues: None,
578 roughness: T::zero(),
579 basin_size: T::zero(),
580 barrier_heights: Vec::new(),
581 },
582 stability_metrics: StabilityMetrics {
583 lyapunov_exponents: Array1::zeros(1),
584 stability_margin: T::zero(),
585 perturbation_sensitivity: T::zero(),
586 robustness_score: T::zero(),
587 },
588 previous_gradient: None,
589 previous_loss: None,
590 step_count: 0,
591 }
592 }
593
594 pub(super) fn update(&mut self, gradients: &Array1<T>, updates: &Array1<T>, loss: Option<T>) {
615 if gradients.is_empty() {
616 return;
617 }
618 self.step_count += 1;
619 let n_t: T = scirs2_core::numeric::NumCast::from(self.step_count).unwrap_or_else(T::one);
620
621 let grad_norm = gradients
622 .iter()
623 .map(|&g| g * g)
624 .fold(T::zero(), |a, b| a + b)
625 .sqrt();
626 let update_norm = updates
627 .iter()
628 .map(|&u| u * u)
629 .fold(T::zero(), |a, b| a + b)
630 .sqrt();
631
632 self.convergence_indicators
634 .gradient_norm_trend
635 .push(grad_norm);
636 if self.convergence_indicators.gradient_norm_trend.len() > STATE_TREND_WINDOW {
637 self.convergence_indicators.gradient_norm_trend.remove(0);
638 }
639 if let (Some(current), Some(previous)) = (loss, self.previous_loss) {
640 self.convergence_indicators
641 .loss_change_trend
642 .push(current - previous);
643 if self.convergence_indicators.loss_change_trend.len() > STATE_TREND_WINDOW {
644 self.convergence_indicators.loss_change_trend.remove(0);
645 }
646 }
647 self.convergence_indicators.parameter_change_magnitude = update_norm;
648 self.convergence_indicators.convergence_probability = T::one() / (T::one() + grad_norm);
649
650 let delta = grad_norm - self.gradient_analyzer.gradient_stats.mean_norm;
652 self.gradient_analyzer.gradient_stats.mean_norm =
653 self.gradient_analyzer.gradient_stats.mean_norm + delta / n_t;
654 let delta2 = grad_norm - self.gradient_analyzer.gradient_stats.mean_norm;
655 let prev_m2 = self.gradient_analyzer.gradient_stats.norm_variance * (n_t - T::one());
657 let m2 = prev_m2 + delta * delta2;
658 self.gradient_analyzer.gradient_stats.norm_variance = m2 / n_t;
659
660 self.gradient_analyzer
661 .gradient_stats
662 .magnitude_distribution
663 .push(grad_norm);
664 if self
665 .gradient_analyzer
666 .gradient_stats
667 .magnitude_distribution
668 .len()
669 > STATE_TREND_WINDOW
670 {
671 self.gradient_analyzer
672 .gradient_stats
673 .magnitude_distribution
674 .remove(0);
675 }
676 self.gradient_analyzer.gradient_stats.component_stats = gradients.mapv(|g| g.abs());
677
678 if let Some(previous) = &self.previous_gradient {
680 let width = previous.len().min(gradients.len());
681 if width > 0 {
682 let mut dot = T::zero();
683 let mut prev_sq = T::zero();
684 let mut diff_sq = T::zero();
685 for i in 0..width {
686 let p = previous[i];
687 let g = gradients[i];
688 dot = dot + p * g;
689 prev_sq = prev_sq + p * p;
690 diff_sq = diff_sq + (g - p) * (g - p);
691 }
692 let prev_norm = prev_sq.sqrt();
693 if prev_norm > T::zero() && grad_norm > T::zero() {
694 self.gradient_analyzer.gradient_stats.direction_consistency =
695 dot / (prev_norm * grad_norm);
696 }
697 let noise = diff_sq.sqrt();
698 self.gradient_analyzer.noise_estimator.noise_level = noise;
699 self.gradient_analyzer.noise_estimator.signal_to_noise_ratio = if noise > T::zero()
700 {
701 grad_norm / noise
702 } else {
703 T::infinity()
704 };
705 self.gradient_analyzer
706 .noise_estimator
707 .noise_characteristics
708 .scale = noise;
709 self.gradient_analyzer
710 .noise_estimator
711 .noise_characteristics
712 .temporal_correlation =
713 self.gradient_analyzer.gradient_stats.direction_consistency;
714
715 if update_norm > T::zero() {
717 self.landscape_analyzer.local_curvature = noise / update_norm;
718 }
719 }
720 }
721
722 let trend = &self.convergence_indicators.gradient_norm_trend;
724 if trend.len() >= 3 {
725 let mut second_diff = T::zero();
726 for w in trend.windows(3) {
727 second_diff = second_diff + (w[2] - w[1] - (w[1] - w[0])).abs();
728 }
729 let count: T =
730 scirs2_core::numeric::NumCast::from(trend.len() - 2).unwrap_or_else(T::one);
731 let mean_second = second_diff / count;
732 self.landscape_analyzer.roughness = mean_second / (T::one() + mean_second);
733 }
734 self.stability_metrics.perturbation_sensitivity =
735 self.gradient_analyzer.noise_estimator.noise_level;
736 self.stability_metrics.stability_margin =
737 T::one() / (T::one() + self.landscape_analyzer.local_curvature);
738 self.stability_metrics.robustness_score = (T::one()
739 + self.gradient_analyzer.gradient_stats.direction_consistency)
740 / scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(T::one);
741
742 self.phase = Self::classify_phase(
744 trend,
745 self.gradient_analyzer.gradient_stats.direction_consistency,
746 );
747
748 self.previous_gradient = Some(gradients.clone());
749 if let Some(l) = loss {
750 self.previous_loss = Some(l);
751 }
752 }
753
754 fn classify_phase(trend: &[T], consistency: T) -> OptimizationPhase {
763 if trend.len() < 4 {
764 return OptimizationPhase::InitialDescent;
765 }
766 let first = trend[0];
767 let last = trend[trend.len() - 1];
768 if first <= T::zero() {
769 return OptimizationPhase::Converged;
770 }
771 let ratio = (last / first).to_f64().unwrap_or(1.0);
772 let _ = consistency;
773 if ratio < 0.25 {
774 OptimizationPhase::Converged
775 } else if ratio > 1.5 {
776 OptimizationPhase::Diverging
777 } else if ratio > 0.9 {
778 OptimizationPhase::Plateau
779 } else if ratio < 0.6 {
780 OptimizationPhase::FineTuning
781 } else {
782 OptimizationPhase::SteadyProgress
783 }
784 }
785
786 pub fn observed_steps(&self) -> usize {
788 self.step_count
789 }
790}