Skip to main content

optirs_learned/transformer/training/
meta_learning.rs

1// Meta-learning capabilities for transformer optimization
2//
3// This module implements meta-learning strategies that allow the transformer
4// optimizer to quickly adapt to new tasks and optimization landscapes.
5
6use scirs2_core::ndarray::{Array1, Array2};
7use scirs2_core::numeric::Float;
8use std::collections::{HashMap, VecDeque};
9use std::fmt::Debug;
10
11use crate::error::{OptimError, Result};
12
13/// Meta-learning strategies
14#[derive(Debug, Clone, Copy)]
15pub enum MetaLearningStrategy {
16    /// Model-Agnostic Meta-Learning (MAML)
17    MAML,
18    /// Reptile algorithm
19    Reptile,
20    /// Gradient-based meta-learning
21    GradientBased,
22    /// Memory-augmented meta-learning
23    MemoryAugmented,
24    /// Task-agnostic meta-learning
25    TaskAgnostic,
26    /// Few-shot meta-learning
27    FewShot,
28    /// Continual meta-learning
29    Continual,
30}
31
32/// Meta-learner for transformer optimizer
33#[derive(Debug, Clone)]
34pub struct TransformerMetaLearner<
35    T: Float
36        + Debug
37        + Default
38        + Clone
39        + std::iter::Sum
40        + scirs2_core::ndarray::ScalarOperand
41        + Send
42        + Sync
43        + 'static,
44> {
45    /// Meta-learning strategy
46    strategy: MetaLearningStrategy,
47
48    /// Task embeddings
49    task_embeddings: HashMap<String, Array1<T>>,
50
51    /// Meta-training history
52    meta_history: VecDeque<MetaTrainingEvent<T>>,
53
54    /// Few-shot learning capabilities
55    few_shot_learner: FewShotLearner<T>,
56
57    /// Continual learning state
58    continual_learning: ContinualLearningState<T>,
59
60    /// Meta-learning parameters
61    meta_params: MetaLearningParams<T>,
62
63    /// Meta-initialization shared across tasks (learned)
64    meta_parameters: Option<Array1<T>>,
65
66    /// Task embedding recorded for the most recently adapted task, used by the
67    /// continual-learning penalty
68    previous_task_parameters: Option<Array1<T>>,
69}
70
71/// Meta-training event
72#[derive(Debug, Clone)]
73pub struct MetaTrainingEvent<T: Float + Debug + Send + Sync + 'static> {
74    /// Event type
75    event_type: MetaEventType,
76
77    /// Task information
78    task_info: TaskInfo<T>,
79
80    /// Performance metrics
81    performance: MetaPerformanceMetrics<T>,
82
83    /// Adaptation steps
84    adaptation_steps: usize,
85
86    /// Timestamp
87    timestamp: usize,
88}
89
90impl<T: Float + Debug + Send + Sync + 'static> MetaTrainingEvent<T> {
91    /// Which meta-learning phase produced this event.
92    pub fn event_type(&self) -> MetaEventType {
93        self.event_type
94    }
95
96    /// The task the event was recorded for.
97    pub fn task_info(&self) -> &TaskInfo<T> {
98        &self.task_info
99    }
100
101    /// Measured performance of the adaptation.
102    pub fn performance(&self) -> &MetaPerformanceMetrics<T> {
103        &self.performance
104    }
105
106    /// Number of inner-loop steps the adaptation ran.
107    pub fn adaptation_steps(&self) -> usize {
108        self.adaptation_steps
109    }
110
111    /// Position of this event in the meta-training history.
112    pub fn timestamp(&self) -> usize {
113        self.timestamp
114    }
115}
116
117/// Meta-event types
118#[derive(Debug, Clone, Copy)]
119pub enum MetaEventType {
120    /// Task adaptation
121    TaskAdaptation,
122    /// Domain transfer
123    DomainTransfer,
124    /// Few-shot learning
125    FewShotLearning,
126    /// Continual learning
127    ContinualLearning,
128    /// Meta-validation
129    MetaValidation,
130}
131
132/// Task information
133#[derive(Debug, Clone)]
134pub struct TaskInfo<T: Float + Debug + Send + Sync + 'static> {
135    /// Task identifier
136    pub task_id: String,
137
138    /// Task characteristics
139    pub characteristics: TaskCharacteristics<T>,
140
141    /// Domain information
142    pub domain: DomainInfo,
143
144    /// Difficulty level
145    pub difficulty: T,
146
147    /// Expected performance
148    pub expected_performance: Option<T>,
149}
150
151impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> TaskInfo<T> {
152    /// Build a task descriptor with default characteristics.
153    pub fn new(task_id: impl Into<String>, dimensionality: usize, difficulty: T) -> Self {
154        Self {
155            task_id: task_id.into(),
156            characteristics: TaskCharacteristics {
157                dimensionality,
158                landscape_complexity: T::zero(),
159                noise_level: T::zero(),
160                conditioning: T::one(),
161                sparsity: T::zero(),
162                temporal_dependencies: T::zero(),
163                feature_correlations: Array2::eye(dimensionality.max(1)),
164            },
165            domain: DomainInfo {
166                name: "general".to_string(),
167                domain_type: DomainType::General,
168                related_domains: Vec::new(),
169                features: HashMap::new(),
170            },
171            difficulty,
172            expected_performance: None,
173        }
174    }
175}
176
177/// Task characteristics
178#[derive(Debug, Clone)]
179pub struct TaskCharacteristics<T: Float + Debug + Send + Sync + 'static> {
180    /// Problem dimensionality
181    pub dimensionality: usize,
182
183    /// Landscape complexity
184    pub landscape_complexity: T,
185
186    /// Noise level
187    pub noise_level: T,
188
189    /// Conditioning number
190    pub conditioning: T,
191
192    /// Sparsity level
193    pub sparsity: T,
194
195    /// Temporal dependencies
196    pub temporal_dependencies: T,
197
198    /// Feature correlations
199    pub feature_correlations: Array2<T>,
200}
201
202/// Domain information
203#[derive(Debug, Clone)]
204pub struct DomainInfo {
205    /// Domain name
206    pub name: String,
207
208    /// Domain type
209    pub domain_type: DomainType,
210
211    /// Related domains
212    pub related_domains: Vec<String>,
213
214    /// Domain-specific features
215    pub features: HashMap<String, f64>,
216}
217
218/// Domain types
219#[derive(Debug, Clone, Copy)]
220pub enum DomainType {
221    /// Computer vision
222    Vision,
223    /// Natural language processing
224    NLP,
225    /// Reinforcement learning
226    RL,
227    /// Time series
228    TimeSeries,
229    /// Graph neural networks
230    Graph,
231    /// Scientific computing
232    Scientific,
233    /// General optimization
234    General,
235}
236
237/// Meta-performance metrics
238#[derive(Debug, Clone)]
239pub struct MetaPerformanceMetrics<T: Float + Debug + Send + Sync + 'static> {
240    /// Final performance
241    final_performance: T,
242
243    /// Convergence speed
244    convergence_speed: T,
245
246    /// Sample efficiency
247    sample_efficiency: T,
248
249    /// Generalization score
250    generalization: T,
251
252    /// Stability measure
253    stability: T,
254
255    /// Resource usage
256    resource_usage: T,
257}
258
259impl<T: Float + Debug + Send + Sync + 'static> MetaPerformanceMetrics<T> {
260    /// Query loss after adaptation (lower is better).
261    pub fn final_performance(&self) -> T {
262        self.final_performance
263    }
264
265    /// Relative loss reduction achieved by the inner loop.
266    pub fn convergence_speed(&self) -> T {
267        self.convergence_speed
268    }
269
270    /// Performance achieved per support sample.
271    pub fn sample_efficiency(&self) -> T {
272        self.sample_efficiency
273    }
274
275    /// `1 / (1 + query_loss)`: bounded generalization score.
276    pub fn generalization(&self) -> T {
277        self.generalization
278    }
279
280    /// `1 / (1 + |query_loss - support_loss|)`: bounded support/query agreement.
281    pub fn stability(&self) -> T {
282        self.stability
283    }
284
285    /// Inner-loop steps spent, as a resource proxy.
286    pub fn resource_usage(&self) -> T {
287        self.resource_usage
288    }
289}
290
291/// Few-shot learner component
292#[derive(Debug, Clone)]
293pub struct FewShotLearner<T: Float + Debug + Send + Sync + 'static> {
294    /// Support set memory
295    support_memory: HashMap<String, Vec<Array1<T>>>,
296
297    /// Prototype vectors
298    prototypes: HashMap<String, Array1<T>>,
299}
300
301/// Continual learning state
302#[derive(Debug, Clone)]
303pub struct ContinualLearningState<T: Float + Debug + Send + Sync + 'static> {
304    /// Elastic weight consolidation parameters
305    ewc_params: HashMap<String, Array1<T>>,
306
307    /// Fisher information matrix
308    fisher_information: HashMap<String, Array2<T>>,
309
310    /// Previous task importance scores
311    task_importance: HashMap<String, T>,
312}
313
314/// Meta-learning parameters
315#[derive(Debug, Clone)]
316pub struct MetaLearningParams<T: Float + Debug + Send + Sync + 'static> {
317    /// Learning rate for meta-updates
318    pub meta_learning_rate: T,
319
320    /// Learning rate used inside the task adaptation loop
321    pub inner_learning_rate: T,
322
323    /// Number of inner gradient steps
324    pub inner_steps: usize,
325
326    /// Meta-batch size
327    pub meta_batch_size: usize,
328
329    /// Task diversity weight
330    pub diversity_weight: T,
331
332    /// Transfer learning coefficient
333    pub transfer_coefficient: T,
334
335    /// Memory retention factor
336    pub memory_retention: T,
337}
338
339impl<
340        T: Float
341            + Debug
342            + Send
343            + Sync
344            + 'static
345            + Default
346            + Clone
347            + std::iter::Sum
348            + scirs2_core::ndarray::ScalarOperand,
349    > TransformerMetaLearner<T>
350{
351    /// Create new meta-learner
352    pub fn new(strategy: MetaLearningStrategy) -> Result<Self> {
353        Ok(Self {
354            strategy,
355            task_embeddings: HashMap::new(),
356            meta_history: VecDeque::new(),
357            few_shot_learner: FewShotLearner::new()?,
358            continual_learning: ContinualLearningState::new()?,
359            meta_params: MetaLearningParams::default(),
360            meta_parameters: None,
361            previous_task_parameters: None,
362        })
363    }
364
365    /// Adapt to a new task and return the post-adaptation query loss.
366    ///
367    /// Every variant operates on the same concrete objective - fitting a single
368    /// parameter vector `theta` to the task's samples under the mean squared
369    /// distance `L(theta) = mean_i ||theta - x_i||^2`, whose gradient is
370    /// `2 (theta - mean(x))`. That keeps the meta-learning algorithms honest
371    /// (they really run inner gradient loops and really update the shared
372    /// meta-initialization) while staying independent of any particular model.
373    pub fn adapt_to_task(
374        &mut self,
375        task_info: &TaskInfo<T>,
376        support_data: &[Array1<T>],
377        query_data: &[Array1<T>],
378    ) -> Result<T> {
379        let dim = Self::validate_samples(support_data, query_data)?;
380        let theta0 = match self.meta_parameters.as_ref() {
381            Some(theta) if theta.len() == dim => theta.clone(),
382            _ => {
383                let theta = Array1::zeros(dim);
384                self.meta_parameters = Some(theta.clone());
385                theta
386            }
387        };
388
389        let adapted = match self.strategy {
390            MetaLearningStrategy::MAML => self.maml_adaptation(&theta0, support_data),
391            MetaLearningStrategy::Reptile => self.reptile_adaptation(&theta0, support_data),
392            MetaLearningStrategy::GradientBased => {
393                self.gradient_based_adaptation(&theta0, support_data, query_data)
394            }
395            MetaLearningStrategy::MemoryAugmented => {
396                self.memory_augmented_adaptation(task_info, &theta0, support_data)
397            }
398            MetaLearningStrategy::TaskAgnostic => self.task_agnostic_adaptation(&theta0, dim),
399            MetaLearningStrategy::FewShot => {
400                self.few_shot_adaptation(task_info, &theta0, support_data)
401            }
402            MetaLearningStrategy::Continual => {
403                self.continual_adaptation(task_info, &theta0, support_data)?
404            }
405        };
406
407        let query_loss = Self::sample_loss(&adapted, query_data);
408        let support_loss = Self::sample_loss(&adapted, support_data);
409
410        // Meta-update of the shared initialization.
411        self.apply_meta_update(&theta0, &adapted);
412
413        self.task_embeddings
414            .insert(task_info.task_id.clone(), adapted.clone());
415        self.previous_task_parameters = Some(adapted);
416
417        let event = MetaTrainingEvent {
418            event_type: Self::event_type(self.strategy),
419            task_info: task_info.clone(),
420            performance: MetaPerformanceMetrics {
421                final_performance: query_loss,
422                convergence_speed: Self::convergence_speed(
423                    Self::sample_loss(&theta0, query_data),
424                    query_loss,
425                ),
426                sample_efficiency: Self::sample_efficiency(support_data.len(), query_loss),
427                generalization: T::one() / (T::one() + query_loss),
428                stability: T::one() / (T::one() + (query_loss - support_loss).abs()),
429                resource_usage: scirs2_core::numeric::NumCast::from(
430                    self.meta_params.inner_steps as f64,
431                )
432                .unwrap_or_else(|| T::zero()),
433            },
434            adaptation_steps: self.meta_params.inner_steps,
435            timestamp: self.meta_history.len(),
436        };
437        self.meta_history.push_back(event);
438        if self.meta_history.len() > 1000 {
439            self.meta_history.pop_front();
440        }
441
442        Ok(query_loss)
443    }
444
445    /// The meta-event category each strategy records.
446    fn event_type(strategy: MetaLearningStrategy) -> MetaEventType {
447        match strategy {
448            MetaLearningStrategy::FewShot => MetaEventType::FewShotLearning,
449            MetaLearningStrategy::Continual => MetaEventType::ContinualLearning,
450            MetaLearningStrategy::TaskAgnostic => MetaEventType::DomainTransfer,
451            _ => MetaEventType::TaskAdaptation,
452        }
453    }
454
455    /// Validate that every sample shares one dimensionality and return it.
456    fn validate_samples(support: &[Array1<T>], query: &[Array1<T>]) -> Result<usize> {
457        let dim = support
458            .first()
459            .or_else(|| query.first())
460            .map(|v| v.len())
461            .ok_or_else(|| {
462                OptimError::InvalidConfig(
463                    "Adaptation needs at least one support or query sample".to_string(),
464                )
465            })?;
466        if dim == 0 {
467            return Err(OptimError::InvalidConfig(
468                "Adaptation samples must be non-empty".to_string(),
469            ));
470        }
471        for sample in support.iter().chain(query.iter()) {
472            if sample.len() != dim {
473                return Err(OptimError::InvalidConfig(format!(
474                    "Inconsistent sample dimensions: expected {dim}, found {}",
475                    sample.len()
476                )));
477            }
478        }
479        Ok(dim)
480    }
481
482    /// Mean of a sample set, or `None` when the set is empty.
483    fn mean_vector(data: &[Array1<T>], dim: usize) -> Option<Array1<T>> {
484        if data.is_empty() {
485            return None;
486        }
487        let mut sum = Array1::zeros(dim);
488        for sample in data {
489            sum = sum + sample;
490        }
491        let count: T =
492            scirs2_core::numeric::NumCast::from(data.len() as f64).unwrap_or_else(|| T::one());
493        Some(sum / count)
494    }
495
496    /// `L(theta) = mean_i ||theta - x_i||^2`
497    fn sample_loss(theta: &Array1<T>, data: &[Array1<T>]) -> T {
498        if data.is_empty() {
499            return T::zero();
500        }
501        let mut total = T::zero();
502        for sample in data {
503            for (a, b) in theta.iter().zip(sample.iter()) {
504                let diff = *a - *b;
505                total = total + diff * diff;
506            }
507        }
508        let count: T =
509            scirs2_core::numeric::NumCast::from(data.len() as f64).unwrap_or_else(|| T::one());
510        total / count
511    }
512
513    /// `grad L(theta) = 2 (theta - mean(x))`
514    fn sample_gradient(theta: &Array1<T>, data: &[Array1<T>]) -> Array1<T> {
515        match Self::mean_vector(data, theta.len()) {
516            Some(mean) => {
517                let two: T = scirs2_core::numeric::NumCast::from(2.0).unwrap_or_else(|| T::one());
518                (theta - &mean) * two
519            }
520            None => Array1::zeros(theta.len()),
521        }
522    }
523
524    /// Run `steps` gradient-descent steps of the task objective.
525    fn inner_loop(&self, theta: &Array1<T>, data: &[Array1<T>], steps: usize) -> Array1<T> {
526        let lr = self.meta_params.inner_learning_rate;
527        let mut current = theta.clone();
528        for _ in 0..steps {
529            let gradient = Self::sample_gradient(&current, data);
530            current = current - gradient * lr;
531        }
532        current
533    }
534
535    /// MAML: adapt from the shared initialization with the full inner loop.
536    fn maml_adaptation(&self, theta0: &Array1<T>, support: &[Array1<T>]) -> Array1<T> {
537        self.inner_loop(theta0, support, self.meta_params.inner_steps)
538    }
539
540    /// Reptile: identical inner loop, but the meta-update (below) interpolates
541    /// toward the adapted parameters instead of following the query gradient.
542    fn reptile_adaptation(&self, theta0: &Array1<T>, support: &[Array1<T>]) -> Array1<T> {
543        self.inner_loop(theta0, support, self.meta_params.inner_steps)
544    }
545
546    /// Gradient-based: descend the combined support and query objective.
547    fn gradient_based_adaptation(
548        &self,
549        theta0: &Array1<T>,
550        support: &[Array1<T>],
551        query: &[Array1<T>],
552    ) -> Array1<T> {
553        let combined: Vec<Array1<T>> = support.iter().chain(query.iter()).cloned().collect();
554        self.inner_loop(theta0, &combined, self.meta_params.inner_steps)
555    }
556
557    /// Memory-augmented: blend the inner-loop solution with the closest task
558    /// embedding already stored in memory.
559    fn memory_augmented_adaptation(
560        &self,
561        task_info: &TaskInfo<T>,
562        theta0: &Array1<T>,
563        support: &[Array1<T>],
564    ) -> Array1<T> {
565        let adapted = self.inner_loop(theta0, support, self.meta_params.inner_steps);
566
567        let mut best: Option<(T, &Array1<T>)> = None;
568        for (task_id, embedding) in &self.task_embeddings {
569            if task_id == &task_info.task_id || embedding.len() != adapted.len() {
570                continue;
571            }
572            let distance = adapted
573                .iter()
574                .zip(embedding.iter())
575                .map(|(a, b)| (*a - *b) * (*a - *b))
576                .fold(T::zero(), |x, y| x + y);
577            if best.as_ref().is_none_or(|(d, _)| distance < *d) {
578                best = Some((distance, embedding));
579            }
580        }
581
582        match best {
583            Some((_, neighbour)) => {
584                let weight = self.meta_params.memory_retention;
585                adapted * weight + neighbour * (T::one() - weight)
586            }
587            None => adapted,
588        }
589    }
590
591    /// Task-agnostic: ignore the current task's samples and return the average
592    /// of every task embedding seen so far (falling back to the shared
593    /// initialization when nothing has been stored yet).
594    fn task_agnostic_adaptation(&self, theta0: &Array1<T>, dim: usize) -> Array1<T> {
595        let embeddings: Vec<Array1<T>> = self
596            .task_embeddings
597            .values()
598            .filter(|v| v.len() == dim)
599            .cloned()
600            .collect();
601        Self::mean_vector(&embeddings, dim).unwrap_or_else(|| theta0.clone())
602    }
603
604    /// Few-shot: use the support prototype directly, with no gradient steps.
605    fn few_shot_adaptation(
606        &mut self,
607        task_info: &TaskInfo<T>,
608        theta0: &Array1<T>,
609        support: &[Array1<T>],
610    ) -> Array1<T> {
611        let prototype = Self::mean_vector(support, theta0.len()).unwrap_or_else(|| theta0.clone());
612        self.few_shot_learner
613            .record(&task_info.task_id, support, prototype.clone());
614        prototype
615    }
616
617    /// Continual: inner loop with an elastic penalty pulling the solution back
618    /// toward the previous task's parameters, damping catastrophic forgetting.
619    fn continual_adaptation(
620        &mut self,
621        task_info: &TaskInfo<T>,
622        theta0: &Array1<T>,
623        support: &[Array1<T>],
624    ) -> Result<Array1<T>> {
625        self.continual_learning
626            .update_for_task(task_info, support)?;
627
628        let adapted = self.inner_loop(theta0, support, self.meta_params.inner_steps);
629        let anchored = match self.previous_task_parameters.as_ref() {
630            Some(previous) if previous.len() == adapted.len() => {
631                let retention = self.meta_params.memory_retention;
632                adapted * retention + previous * (T::one() - retention)
633            }
634            _ => adapted,
635        };
636
637        Ok(anchored)
638    }
639
640    /// Meta-update of the shared initialization.
641    fn apply_meta_update(&mut self, theta0: &Array1<T>, adapted: &Array1<T>) {
642        let meta_lr = self.meta_params.meta_learning_rate;
643        let updated = match self.strategy {
644            // Reptile moves the initialization toward the adapted parameters.
645            MetaLearningStrategy::Reptile => {
646                theta0 + &((adapted - theta0) * self.meta_params.transfer_coefficient)
647            }
648            // Everything else takes a (first-order) step along the adaptation
649            // direction scaled by the meta learning rate.
650            _ => theta0 + &((adapted - theta0) * meta_lr),
651        };
652        self.meta_parameters = Some(updated);
653    }
654
655    /// Relative improvement achieved by adaptation, clamped to [0, 1].
656    fn convergence_speed(before: T, after: T) -> T {
657        if before <= T::zero() {
658            return T::zero();
659        }
660        ((before - after) / before).max(T::zero()).min(T::one())
661    }
662
663    /// Query performance per support sample.
664    fn sample_efficiency(support_count: usize, query_loss: T) -> T {
665        let count: T =
666            scirs2_core::numeric::NumCast::from(support_count as f64).unwrap_or_else(|| T::one());
667        if count <= T::zero() {
668            return T::zero();
669        }
670        T::one() / ((T::one() + query_loss) * count)
671    }
672
673    /// The current meta-initialization, if any task has been adapted.
674    pub fn meta_parameters(&self) -> Option<&Array1<T>> {
675        self.meta_parameters.as_ref()
676    }
677
678    /// Embedding learned for a task.
679    pub fn task_embedding(&self, task_id: &str) -> Option<&Array1<T>> {
680        self.task_embeddings.get(task_id)
681    }
682
683    /// The active meta-learning strategy.
684    pub fn strategy(&self) -> MetaLearningStrategy {
685        self.strategy
686    }
687
688    /// Change the active meta-learning strategy.
689    pub fn set_strategy(&mut self, strategy: MetaLearningStrategy) {
690        self.strategy = strategy;
691    }
692
693    /// Get meta-learning statistics
694    pub fn get_meta_statistics(&self) -> HashMap<String, T> {
695        let mut stats = HashMap::new();
696
697        stats.insert(
698            "meta_events_count".to_string(),
699            scirs2_core::numeric::NumCast::from(self.meta_history.len() as f64)
700                .unwrap_or_else(|| T::zero()),
701        );
702        stats.insert(
703            "task_embeddings_count".to_string(),
704            scirs2_core::numeric::NumCast::from(self.task_embeddings.len() as f64)
705                .unwrap_or_else(|| T::zero()),
706        );
707
708        // Compute average performance
709        if !self.meta_history.is_empty() {
710            let avg_performance = self
711                .meta_history
712                .iter()
713                .map(|event| event.performance.final_performance)
714                .fold(T::zero(), |a, b| a + b)
715                / scirs2_core::numeric::NumCast::from(self.meta_history.len() as f64)
716                    .unwrap_or_else(|| T::one());
717            stats.insert("average_performance".to_string(), avg_performance);
718        }
719
720        stats
721    }
722
723    /// Update meta-parameters
724    pub fn update_meta_parameters(&mut self, params: MetaLearningParams<T>) {
725        self.meta_params = params;
726    }
727
728    /// Prototype learned for a task by the few-shot component.
729    pub fn few_shot_prototype(&self, task_id: &str) -> Option<&Array1<T>> {
730        self.few_shot_learner.prototype(task_id)
731    }
732
733    /// Recorded meta-training events, oldest first (capped at 1000).
734    ///
735    /// [`Self::adapt_to_task`] fills every field of each event with a measured
736    /// quantity, but nothing could read them: the events went into a private
737    /// deque whose only observable property was its length (via
738    /// [`Self::get_meta_statistics`]'s `meta_events_count`).
739    pub fn meta_history(&self) -> impl ExactSizeIterator<Item = &MetaTrainingEvent<T>> {
740        self.meta_history.iter()
741    }
742
743    /// The most recently recorded meta-training event, if any.
744    pub fn last_meta_event(&self) -> Option<&MetaTrainingEvent<T>> {
745        self.meta_history.back()
746    }
747
748    /// Reset meta-learner state
749    pub fn reset(&mut self) {
750        self.meta_parameters = None;
751        self.previous_task_parameters = None;
752        self.task_embeddings.clear();
753        self.meta_history.clear();
754        self.few_shot_learner.reset();
755        self.continual_learning.reset();
756    }
757}
758
759impl<
760        T: Float
761            + Debug
762            + Send
763            + Sync
764            + 'static
765            + Default
766            + Clone
767            + std::iter::Sum
768            + scirs2_core::ndarray::ScalarOperand,
769    > FewShotLearner<T>
770{
771    fn new() -> Result<Self> {
772        Ok(Self {
773            support_memory: HashMap::new(),
774            prototypes: HashMap::new(),
775        })
776    }
777
778    /// Store the support set and its prototype for a task.
779    fn record(&mut self, task_id: &str, support: &[Array1<T>], prototype: Array1<T>) {
780        self.support_memory
781            .insert(task_id.to_string(), support.to_vec());
782        self.prototypes.insert(task_id.to_string(), prototype);
783    }
784
785    /// Prototype learned for a task, if one has been recorded.
786    fn prototype(&self, task_id: &str) -> Option<&Array1<T>> {
787        self.prototypes.get(task_id)
788    }
789
790    fn reset(&mut self) {
791        self.support_memory.clear();
792        self.prototypes.clear();
793    }
794}
795
796impl<
797        T: Float
798            + Debug
799            + Send
800            + Sync
801            + 'static
802            + Default
803            + Clone
804            + std::iter::Sum
805            + scirs2_core::ndarray::ScalarOperand,
806    > ContinualLearningState<T>
807{
808    fn new() -> Result<Self> {
809        Ok(Self {
810            ewc_params: HashMap::new(),
811            fisher_information: HashMap::new(),
812            task_importance: HashMap::new(),
813        })
814    }
815
816    fn update_for_task(
817        &mut self,
818        task_info: &TaskInfo<T>,
819        _support_data: &[Array1<T>],
820    ) -> Result<()> {
821        self.task_importance
822            .insert(task_info.task_id.clone(), task_info.difficulty);
823        Ok(())
824    }
825
826    fn reset(&mut self) {
827        self.ewc_params.clear();
828        self.fisher_information.clear();
829        self.task_importance.clear();
830    }
831}
832
833impl<
834        T: Float
835            + Debug
836            + Send
837            + Sync
838            + 'static
839            + Default
840            + Clone
841            + std::iter::Sum
842            + scirs2_core::ndarray::ScalarOperand,
843    > Default for MetaLearningParams<T>
844{
845    fn default() -> Self {
846        Self {
847            meta_learning_rate: scirs2_core::numeric::NumCast::from(0.05)
848                .unwrap_or_else(|| T::zero()),
849            inner_learning_rate: scirs2_core::numeric::NumCast::from(0.1)
850                .unwrap_or_else(|| T::zero()),
851            inner_steps: 5,
852            meta_batch_size: 32,
853            diversity_weight: scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero()),
854            transfer_coefficient: scirs2_core::numeric::NumCast::from(0.5)
855                .unwrap_or_else(|| T::zero()),
856            memory_retention: scirs2_core::numeric::NumCast::from(0.95)
857                .unwrap_or_else(|| T::zero()),
858        }
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    fn samples(values: &[[f64; 2]]) -> Vec<Array1<f64>> {
867        values
868            .iter()
869            .map(|v| Array1::from_vec(v.to_vec()))
870            .collect()
871    }
872
873    fn all_strategies() -> [MetaLearningStrategy; 7] {
874        [
875            MetaLearningStrategy::MAML,
876            MetaLearningStrategy::Reptile,
877            MetaLearningStrategy::GradientBased,
878            MetaLearningStrategy::MemoryAugmented,
879            MetaLearningStrategy::TaskAgnostic,
880            MetaLearningStrategy::FewShot,
881            MetaLearningStrategy::Continual,
882        ]
883    }
884
885    #[test]
886    fn every_strategy_adapts_end_to_end() {
887        let support = samples(&[[1.0, 2.0], [1.2, 1.8], [0.8, 2.2]]);
888        let query = samples(&[[1.1, 2.1], [0.9, 1.9]]);
889
890        for strategy in all_strategies() {
891            let mut learner =
892                TransformerMetaLearner::<f64>::new(strategy).expect("meta-learner creation");
893            let task = TaskInfo::new("task_a", 2, 0.5);
894
895            let loss = learner
896                .adapt_to_task(&task, &support, &query)
897                .unwrap_or_else(|e| panic!("{strategy:?} failed: {e}"));
898
899            assert!(loss.is_finite(), "{strategy:?} produced {loss}");
900            assert!(loss >= 0.0, "{strategy:?} produced a negative loss");
901            assert!(
902                learner.meta_parameters().is_some(),
903                "{strategy:?} never wrote meta-parameters"
904            );
905            assert!(
906                learner.task_embedding("task_a").is_some(),
907                "{strategy:?} never recorded a task embedding"
908            );
909            let stats = learner.get_meta_statistics();
910            assert!(stats.contains_key("meta_events_count"));
911        }
912    }
913
914    /// Every field of a `MetaTrainingEvent` is filled with a measured quantity,
915    /// but they were all private with no accessor: the recorded history was
916    /// write-only and only its *length* was observable.
917    #[test]
918    fn the_recorded_meta_history_is_readable_and_measured() {
919        let support = samples(&[[1.0, 2.0], [1.2, 1.8], [0.8, 2.2]]);
920        let query = samples(&[[1.1, 2.1], [0.9, 1.9]]);
921        let mut learner =
922            TransformerMetaLearner::<f64>::new(MetaLearningStrategy::MAML).expect("learner");
923
924        assert_eq!(learner.meta_history().len(), 0);
925        assert!(learner.last_meta_event().is_none());
926
927        for i in 0..3 {
928            let task = TaskInfo::new(format!("task_{i}"), 2, 0.5);
929            learner
930                .adapt_to_task(&task, &support, &query)
931                .expect("adaptation");
932        }
933
934        assert_eq!(learner.meta_history().len(), 3);
935        for (i, event) in learner.meta_history().enumerate() {
936            assert_eq!(event.timestamp(), i, "events must be in recording order");
937            assert_eq!(event.task_info().task_id, format!("task_{i}"));
938            assert!(event.adaptation_steps() > 0);
939            assert!(matches!(event.event_type(), MetaEventType::TaskAdaptation));
940
941            let perf = event.performance();
942            assert!(perf.final_performance().is_finite());
943            assert!(perf.convergence_speed().is_finite());
944            assert!(perf.sample_efficiency().is_finite());
945            // Both scores are `1/(1+x)` for a non-negative `x`, so they live in
946            // `(0, 1]` — a constant 0 would mean nothing was measured.
947            assert!(perf.generalization() > 0.0 && perf.generalization() <= 1.0);
948            assert!(perf.stability() > 0.0 && perf.stability() <= 1.0);
949            assert!(perf.resource_usage() > 0.0);
950        }
951
952        let last = learner.last_meta_event().expect("a last event");
953        assert_eq!(last.timestamp(), 2);
954
955        learner.reset();
956        assert_eq!(learner.meta_history().len(), 0);
957    }
958
959    #[test]
960    fn maml_inner_loop_reduces_the_query_loss() {
961        let support = samples(&[[3.0, -3.0], [3.2, -2.8], [2.8, -3.2]]);
962        let query = samples(&[[3.1, -3.1], [2.9, -2.9]]);
963
964        let mut learner = TransformerMetaLearner::<f64>::new(MetaLearningStrategy::MAML)
965            .expect("meta-learner creation");
966        let task = TaskInfo::new("task_a", 2, 0.5);
967
968        let zero = Array1::<f64>::zeros(2);
969        let baseline = TransformerMetaLearner::<f64>::sample_loss(&zero, &query);
970        let adapted_loss = learner
971            .adapt_to_task(&task, &support, &query)
972            .expect("adaptation");
973
974        assert!(
975            adapted_loss < baseline,
976            "adaptation did not help: {adapted_loss} vs {baseline}"
977        );
978    }
979
980    #[test]
981    fn repeated_meta_updates_move_the_initialization() {
982        let support = samples(&[[5.0, 5.0], [5.0, 5.0]]);
983        let query = samples(&[[5.0, 5.0]]);
984        let mut learner = TransformerMetaLearner::<f64>::new(MetaLearningStrategy::Reptile)
985            .expect("meta-learner creation");
986
987        let mut losses = Vec::new();
988        for i in 0..20 {
989            let task = TaskInfo::new(format!("task_{i}"), 2, 0.5);
990            losses.push(
991                learner
992                    .adapt_to_task(&task, &support, &query)
993                    .expect("adaptation"),
994            );
995        }
996
997        assert!(
998            losses[19] < losses[0],
999            "meta-training did not improve: {:?} -> {:?}",
1000            losses[0],
1001            losses[19]
1002        );
1003        let meta = learner.meta_parameters().expect("meta-parameters");
1004        assert!(
1005            meta.iter().any(|&v| v.abs() > 1e-6),
1006            "meta-init stayed zero"
1007        );
1008    }
1009
1010    #[test]
1011    fn few_shot_uses_the_support_prototype() {
1012        let support = samples(&[[2.0, 4.0], [4.0, 8.0]]);
1013        let query = samples(&[[3.0, 6.0]]);
1014        let mut learner = TransformerMetaLearner::<f64>::new(MetaLearningStrategy::FewShot)
1015            .expect("meta-learner creation");
1016        let task = TaskInfo::new("proto", 2, 0.5);
1017
1018        learner
1019            .adapt_to_task(&task, &support, &query)
1020            .expect("adaptation");
1021
1022        let prototype = learner.few_shot_prototype("proto").expect("prototype");
1023        assert!((prototype[0] - 3.0).abs() < 1e-12);
1024        assert!((prototype[1] - 6.0).abs() < 1e-12);
1025    }
1026
1027    #[test]
1028    fn strategies_reach_different_solutions() {
1029        let support = samples(&[[1.0, 0.0], [3.0, 0.0]]);
1030        let query = samples(&[[10.0, 0.0]]);
1031
1032        let run = |strategy| {
1033            let mut learner =
1034                TransformerMetaLearner::<f64>::new(strategy).expect("meta-learner creation");
1035            let task = TaskInfo::new("task", 2, 0.5);
1036            learner
1037                .adapt_to_task(&task, &support, &query)
1038                .expect("adaptation")
1039        };
1040
1041        let maml = run(MetaLearningStrategy::MAML);
1042        let few_shot = run(MetaLearningStrategy::FewShot);
1043        let gradient_based = run(MetaLearningStrategy::GradientBased);
1044        let task_agnostic = run(MetaLearningStrategy::TaskAgnostic);
1045
1046        let mut values = vec![maml, few_shot, gradient_based, task_agnostic];
1047        values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1048        values.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
1049        assert!(
1050            values.len() >= 3,
1051            "strategies collapsed to identical behaviour: {values:?}"
1052        );
1053    }
1054
1055    #[test]
1056    fn inconsistent_sample_dimensions_are_rejected() {
1057        let mut learner = TransformerMetaLearner::<f64>::new(MetaLearningStrategy::MAML)
1058            .expect("meta-learner creation");
1059        let task = TaskInfo::new("task", 2, 0.5);
1060        let support = vec![Array1::from_vec(vec![1.0, 2.0])];
1061        let query = vec![Array1::from_vec(vec![1.0, 2.0, 3.0])];
1062        assert!(learner.adapt_to_task(&task, &support, &query).is_err());
1063    }
1064
1065    #[test]
1066    fn empty_sample_sets_are_rejected() {
1067        let mut learner = TransformerMetaLearner::<f64>::new(MetaLearningStrategy::MAML)
1068            .expect("meta-learner creation");
1069        let task = TaskInfo::new("task", 2, 0.5);
1070        assert!(learner.adapt_to_task(&task, &[], &[]).is_err());
1071    }
1072
1073    #[test]
1074    fn reset_clears_learned_state() {
1075        let support = samples(&[[1.0, 1.0]]);
1076        let query = samples(&[[1.0, 1.0]]);
1077        let mut learner = TransformerMetaLearner::<f64>::new(MetaLearningStrategy::MAML)
1078            .expect("meta-learner creation");
1079        let task = TaskInfo::new("task", 2, 0.5);
1080        learner
1081            .adapt_to_task(&task, &support, &query)
1082            .expect("adaptation");
1083        learner.reset();
1084        assert!(learner.meta_parameters().is_none());
1085        assert!(learner.task_embedding("task").is_none());
1086    }
1087}