1use 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#[derive(Debug, Clone, Copy)]
15pub enum MetaLearningStrategy {
16 MAML,
18 Reptile,
20 GradientBased,
22 MemoryAugmented,
24 TaskAgnostic,
26 FewShot,
28 Continual,
30}
31
32#[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 strategy: MetaLearningStrategy,
47
48 task_embeddings: HashMap<String, Array1<T>>,
50
51 meta_history: VecDeque<MetaTrainingEvent<T>>,
53
54 few_shot_learner: FewShotLearner<T>,
56
57 continual_learning: ContinualLearningState<T>,
59
60 meta_params: MetaLearningParams<T>,
62
63 meta_parameters: Option<Array1<T>>,
65
66 previous_task_parameters: Option<Array1<T>>,
69}
70
71#[derive(Debug, Clone)]
73pub struct MetaTrainingEvent<T: Float + Debug + Send + Sync + 'static> {
74 event_type: MetaEventType,
76
77 task_info: TaskInfo<T>,
79
80 performance: MetaPerformanceMetrics<T>,
82
83 adaptation_steps: usize,
85
86 timestamp: usize,
88}
89
90impl<T: Float + Debug + Send + Sync + 'static> MetaTrainingEvent<T> {
91 pub fn event_type(&self) -> MetaEventType {
93 self.event_type
94 }
95
96 pub fn task_info(&self) -> &TaskInfo<T> {
98 &self.task_info
99 }
100
101 pub fn performance(&self) -> &MetaPerformanceMetrics<T> {
103 &self.performance
104 }
105
106 pub fn adaptation_steps(&self) -> usize {
108 self.adaptation_steps
109 }
110
111 pub fn timestamp(&self) -> usize {
113 self.timestamp
114 }
115}
116
117#[derive(Debug, Clone, Copy)]
119pub enum MetaEventType {
120 TaskAdaptation,
122 DomainTransfer,
124 FewShotLearning,
126 ContinualLearning,
128 MetaValidation,
130}
131
132#[derive(Debug, Clone)]
134pub struct TaskInfo<T: Float + Debug + Send + Sync + 'static> {
135 pub task_id: String,
137
138 pub characteristics: TaskCharacteristics<T>,
140
141 pub domain: DomainInfo,
143
144 pub difficulty: T,
146
147 pub expected_performance: Option<T>,
149}
150
151impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> TaskInfo<T> {
152 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#[derive(Debug, Clone)]
179pub struct TaskCharacteristics<T: Float + Debug + Send + Sync + 'static> {
180 pub dimensionality: usize,
182
183 pub landscape_complexity: T,
185
186 pub noise_level: T,
188
189 pub conditioning: T,
191
192 pub sparsity: T,
194
195 pub temporal_dependencies: T,
197
198 pub feature_correlations: Array2<T>,
200}
201
202#[derive(Debug, Clone)]
204pub struct DomainInfo {
205 pub name: String,
207
208 pub domain_type: DomainType,
210
211 pub related_domains: Vec<String>,
213
214 pub features: HashMap<String, f64>,
216}
217
218#[derive(Debug, Clone, Copy)]
220pub enum DomainType {
221 Vision,
223 NLP,
225 RL,
227 TimeSeries,
229 Graph,
231 Scientific,
233 General,
235}
236
237#[derive(Debug, Clone)]
239pub struct MetaPerformanceMetrics<T: Float + Debug + Send + Sync + 'static> {
240 final_performance: T,
242
243 convergence_speed: T,
245
246 sample_efficiency: T,
248
249 generalization: T,
251
252 stability: T,
254
255 resource_usage: T,
257}
258
259impl<T: Float + Debug + Send + Sync + 'static> MetaPerformanceMetrics<T> {
260 pub fn final_performance(&self) -> T {
262 self.final_performance
263 }
264
265 pub fn convergence_speed(&self) -> T {
267 self.convergence_speed
268 }
269
270 pub fn sample_efficiency(&self) -> T {
272 self.sample_efficiency
273 }
274
275 pub fn generalization(&self) -> T {
277 self.generalization
278 }
279
280 pub fn stability(&self) -> T {
282 self.stability
283 }
284
285 pub fn resource_usage(&self) -> T {
287 self.resource_usage
288 }
289}
290
291#[derive(Debug, Clone)]
293pub struct FewShotLearner<T: Float + Debug + Send + Sync + 'static> {
294 support_memory: HashMap<String, Vec<Array1<T>>>,
296
297 prototypes: HashMap<String, Array1<T>>,
299}
300
301#[derive(Debug, Clone)]
303pub struct ContinualLearningState<T: Float + Debug + Send + Sync + 'static> {
304 ewc_params: HashMap<String, Array1<T>>,
306
307 fisher_information: HashMap<String, Array2<T>>,
309
310 task_importance: HashMap<String, T>,
312}
313
314#[derive(Debug, Clone)]
316pub struct MetaLearningParams<T: Float + Debug + Send + Sync + 'static> {
317 pub meta_learning_rate: T,
319
320 pub inner_learning_rate: T,
322
323 pub inner_steps: usize,
325
326 pub meta_batch_size: usize,
328
329 pub diversity_weight: T,
331
332 pub transfer_coefficient: T,
334
335 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 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 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 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 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 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 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 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 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 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(¤t, data);
530 current = current - gradient * lr;
531 }
532 current
533 }
534
535 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 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 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 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 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 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 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 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 MetaLearningStrategy::Reptile => {
646 theta0 + &((adapted - theta0) * self.meta_params.transfer_coefficient)
647 }
648 _ => theta0 + &((adapted - theta0) * meta_lr),
651 };
652 self.meta_parameters = Some(updated);
653 }
654
655 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 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 pub fn meta_parameters(&self) -> Option<&Array1<T>> {
675 self.meta_parameters.as_ref()
676 }
677
678 pub fn task_embedding(&self, task_id: &str) -> Option<&Array1<T>> {
680 self.task_embeddings.get(task_id)
681 }
682
683 pub fn strategy(&self) -> MetaLearningStrategy {
685 self.strategy
686 }
687
688 pub fn set_strategy(&mut self, strategy: MetaLearningStrategy) {
690 self.strategy = strategy;
691 }
692
693 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 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 pub fn update_meta_parameters(&mut self, params: MetaLearningParams<T>) {
725 self.meta_params = params;
726 }
727
728 pub fn few_shot_prototype(&self, task_id: &str) -> Option<&Array1<T>> {
730 self.few_shot_learner.prototype(task_id)
731 }
732
733 pub fn meta_history(&self) -> impl ExactSizeIterator<Item = &MetaTrainingEvent<T>> {
740 self.meta_history.iter()
741 }
742
743 pub fn last_meta_event(&self) -> Option<&MetaTrainingEvent<T>> {
745 self.meta_history.back()
746 }
747
748 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 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 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 #[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 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}