1use crate::error::{OptimError, Result};
10use scirs2_core::ndarray::Array1;
11use scirs2_core::numeric::Float;
12use std::collections::HashMap;
13use std::fmt::Debug;
14
15use super::functions::MetaLearner;
16use super::types::*;
17
18pub struct TaskDistributionManager<T: Float + Debug + Send + Sync + 'static> {
24 config: MetaLearningConfig,
25 seed: u64,
27 draws: usize,
30 _phantom: std::marker::PhantomData<T>,
31}
32impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> TaskDistributionManager<T> {
33 pub fn new(config: &MetaLearningConfig) -> Result<Self> {
34 Ok(Self {
35 config: config.clone(),
36 seed: 0x5EED_1234_ABCD_0001,
37 draws: 0,
38 _phantom: std::marker::PhantomData,
39 })
40 }
41
42 pub fn with_seed(mut self, seed: u64) -> Self {
44 self.seed = seed;
45 self
46 }
47
48 pub fn strategy(&self) -> TaskSamplingStrategy {
50 self.config.task_sampling_strategy
51 }
52
53 pub fn sample_task_batch(
58 &mut self,
59 tasks: &[MetaTask<T>],
60 batch_size: usize,
61 ) -> Result<Vec<MetaTask<T>>> {
62 if tasks.is_empty() {
63 return Err(OptimError::InsufficientData(
64 "cannot sample a task batch from an empty task pool".to_string(),
65 ));
66 }
67 if batch_size == 0 {
68 return Err(OptimError::InvalidConfig(
69 "task batch size must be greater than zero".to_string(),
70 ));
71 }
72
73 let draw = self.draws;
74 self.draws += 1;
75 let mut rng = scirs2_core::random::Random::seed(self.seed.wrapping_add(draw as u64));
76
77 let indices: Vec<usize> = match self.config.task_sampling_strategy {
78 TaskSamplingStrategy::Uniform => (0..batch_size)
79 .map(|_| rng.random_range(0..tasks.len()))
80 .collect(),
81 TaskSamplingStrategy::Curriculum => {
82 let order = Self::order_by_difficulty(tasks, false);
84 (0..batch_size)
85 .map(|i| order[(draw * batch_size + i) % order.len()])
86 .collect()
87 }
88 TaskSamplingStrategy::DifficultyBased => {
89 Self::sample_weighted_by_difficulty(tasks, batch_size, &mut rng)
90 }
91 TaskSamplingStrategy::DiversityBased => Self::greedy_diverse(tasks, batch_size),
92 TaskSamplingStrategy::ActiveLearning | TaskSamplingStrategy::Adversarial => {
93 let order = Self::order_by_difficulty(tasks, true);
96 (0..batch_size).map(|i| order[i % order.len()]).collect()
97 }
98 };
99
100 let mut batch = Vec::with_capacity(indices.len());
101 for idx in indices {
102 let task = tasks.get(idx).ok_or_else(|| {
103 OptimError::ComputationError(format!("sampled task index {idx} out of range"))
104 })?;
105 batch.push(task.clone());
106 }
107 Ok(batch)
108 }
109
110 fn order_by_difficulty(tasks: &[MetaTask<T>], hardest_first: bool) -> Vec<usize> {
113 let mut order: Vec<usize> = (0..tasks.len()).collect();
114 order.sort_by(|&a, &b| {
115 let ord = tasks[a]
116 .difficulty
117 .partial_cmp(&tasks[b].difficulty)
118 .unwrap_or(std::cmp::Ordering::Equal);
119 if hardest_first {
120 ord.reverse()
121 } else {
122 ord
123 }
124 });
125 order
126 }
127
128 fn sample_weighted_by_difficulty<R: scirs2_core::random::Rng>(
129 tasks: &[MetaTask<T>],
130 batch_size: usize,
131 rng: &mut scirs2_core::random::Random<R>,
132 ) -> Vec<usize> {
133 let mut weights: Vec<f64> = tasks
135 .iter()
136 .map(|t| t.difficulty.to_f64().unwrap_or(0.0))
137 .collect();
138 let min = weights.iter().copied().fold(f64::INFINITY, f64::min);
139 let shift = if min.is_finite() && min < 0.0 {
140 -min
141 } else {
142 0.0
143 };
144 for w in weights.iter_mut() {
145 if !w.is_finite() {
146 *w = 0.0;
147 }
148 *w = *w + shift + 1e-9;
149 }
150 let total: f64 = weights.iter().sum();
151 (0..batch_size)
152 .map(|_| {
153 let mut target = rng.random_range(0.0..total);
154 for (i, w) in weights.iter().enumerate() {
155 target -= *w;
156 if target <= 0.0 {
157 return i;
158 }
159 }
160 weights.len() - 1
161 })
162 .collect()
163 }
164
165 fn greedy_diverse(tasks: &[MetaTask<T>], batch_size: usize) -> Vec<usize> {
167 let signatures: Vec<Vec<f64>> = tasks.iter().map(Self::task_signature).collect();
168 let mut chosen: Vec<usize> = Vec::with_capacity(batch_size);
169 chosen.push(0);
170 while chosen.len() < batch_size {
171 let mut best = 0usize;
172 let mut best_dist = -1.0f64;
173 for (i, sig) in signatures.iter().enumerate() {
174 let d = chosen
175 .iter()
176 .map(|&c| Self::signature_distance(sig, &signatures[c]))
177 .fold(f64::INFINITY, f64::min);
178 if d > best_dist {
179 best_dist = d;
180 best = i;
181 }
182 }
183 chosen.push(best);
184 if chosen.len() >= batch_size {
185 break;
186 }
187 }
188 chosen.truncate(batch_size);
189 chosen
190 }
191
192 fn task_signature(task: &MetaTask<T>) -> Vec<f64> {
193 let mut acc: Vec<f64> = Vec::new();
194 for feat in &task.support_set.features {
195 if acc.len() < feat.len() {
196 acc.resize(feat.len(), 0.0);
197 }
198 for (i, v) in feat.iter().enumerate() {
199 acc[i] += v.to_f64().unwrap_or(0.0);
200 }
201 }
202 let n = task.support_set.features.len().max(1) as f64;
203 for a in acc.iter_mut() {
204 *a /= n;
205 }
206 acc
207 }
208
209 fn signature_distance(a: &[f64], b: &[f64]) -> f64 {
210 let n = a.len().max(b.len());
211 let mut acc = 0.0;
212 for i in 0..n {
213 let d = a.get(i).copied().unwrap_or(0.0) - b.get(i).copied().unwrap_or(0.0);
214 acc += d * d;
215 }
216 acc.sqrt()
217 }
218}
219pub struct ContinualLearningSystem<T: Float + Debug + Send + Sync + 'static> {
222 settings: ContinualLearningSettings,
223 steps_per_task: usize,
225 last_forgetting: T,
227}
228impl<
229 T: Float
230 + Debug
231 + Send
232 + Sync
233 + 'static
234 + Default
235 + Clone
236 + std::iter::Sum
237 + scirs2_core::ndarray::ScalarOperand,
238 > ContinualLearningSystem<T>
239{
240 pub fn new(settings: &ContinualLearningSettings) -> Result<Self> {
241 Ok(Self {
242 settings: settings.clone(),
243 steps_per_task: 20,
244 last_forgetting: T::zero(),
245 })
246 }
247
248 pub fn with_steps_per_task(mut self, steps: usize) -> Result<Self> {
250 if steps == 0 {
251 return Err(OptimError::InvalidConfig(
252 "steps_per_task must be greater than zero".to_string(),
253 ));
254 }
255 self.steps_per_task = steps;
256 Ok(self)
257 }
258 pub fn learn_sequence(
266 &mut self,
267 sequence: &[MetaTask<T>],
268 meta_parameters: &mut HashMap<String, Array1<T>>,
269 ) -> Result<ContinualLearningResult<T>> {
270 if sequence.is_empty() {
271 return Err(OptimError::InsufficientData(
272 "continual learning requires at least one task".to_string(),
273 ));
274 }
275
276 let lambda: T = scirs2_core::numeric::NumCast::from(
277 1.0 - self.settings.plasticity_stability_balance.clamp(0.0, 1.0),
278 )
279 .unwrap_or_else(T::zero);
280 let uses_ewc = self
281 .settings
282 .anti_forgetting_strategies
283 .iter()
284 .any(|s| matches!(s, AntiForgettingStrategy::ElasticWeightConsolidation));
285
286 let mut ewc = crate::continual_learning::ElasticWeightConsolidation::<T>::new(lambda)
287 .with_num_samples(self.settings.memory_replay.replay_frequency.max(2));
288
289 let lr: T = scirs2_core::numeric::NumCast::from(0.05).unwrap_or_else(T::zero);
290 let mut sequence_results = Vec::with_capacity(sequence.len());
291 let mut first_losses: Vec<T> = Vec::with_capacity(sequence.len());
292 let mut total_steps = 0usize;
293
294 for (task_idx, task) in sequence.iter().enumerate() {
295 let before = crate::meta_learning::linear_model::mse_loss(
296 &task.support_set.features,
297 &task.support_set.targets,
298 meta_parameters,
299 )?;
300
301 for _ in 0..self.steps_per_task {
302 let mut grads = crate::meta_learning::linear_model::mse_gradients(
303 &task.support_set.features,
304 &task.support_set.targets,
305 meta_parameters,
306 )?;
307 if uses_ewc && task_idx > 0 {
308 let penalty = ewc.ewc_gradient(meta_parameters)?;
309 for (name, g) in grads.iter_mut() {
310 if let Some(p) = penalty.get(name) {
311 let n = g.len().min(p.len());
312 for i in 0..n {
313 g[i] = g[i] + p[i];
314 }
315 }
316 }
317 }
318 crate::meta_learning::linear_model::descend(meta_parameters, &grads, lr)?;
319 total_steps += 1;
320 }
321
322 let after = crate::meta_learning::linear_model::mse_loss(
323 &task.support_set.features,
324 &task.support_set.targets,
325 meta_parameters,
326 )?;
327 first_losses.push(after);
328
329 if uses_ewc {
330 let features = task.support_set.features.clone();
332 let targets = task.support_set.targets.clone();
333 ewc.compute_fisher_diagonal(meta_parameters, |params, sample_idx| {
334 let i = sample_idx % features.len().max(1);
335 let feats = features.get(i).cloned().ok_or_else(|| {
336 OptimError::InsufficientData("empty support set".to_string())
337 })?;
338 let target = targets.get(i).copied().ok_or_else(|| {
339 OptimError::InsufficientData("empty target set".to_string())
340 })?;
341 crate::meta_learning::linear_model::mse_gradients(&[feats], &[target], params)
342 })?;
343 ewc.consolidate(meta_parameters)?;
344 }
345
346 let mut metrics = HashMap::new();
347 metrics.insert("loss_before".to_string(), before);
348 metrics.insert("loss_after".to_string(), after);
349 sequence_results.push(TaskResult {
350 task_id: task.id.clone(),
351 loss: after,
352 metrics,
353 });
354 }
355
356 let mut regressions: Vec<T> = Vec::with_capacity(sequence.len());
359 for (idx, task) in sequence.iter().enumerate() {
360 let now = crate::meta_learning::linear_model::mse_loss(
361 &task.support_set.features,
362 &task.support_set.targets,
363 meta_parameters,
364 )?;
365 if let Some(right_after) = first_losses.get(idx) {
366 regressions.push((now - *right_after).max(T::zero()));
367 }
368 }
369 let forgetting_measure =
370 crate::meta_learning::metrics::mean_of(®ressions).unwrap_or_else(T::zero);
371 self.last_forgetting = forgetting_measure;
372
373 let improvement = sequence_results
374 .iter()
375 .filter_map(|r| r.metrics.get("loss_before").map(|b| *b - r.loss))
376 .fold(T::zero(), |a, b| a + b);
377 let steps_t = T::from(total_steps.max(1)).unwrap_or_else(T::one);
378 let adaptation_efficiency = improvement / steps_t;
379
380 Ok(ContinualLearningResult {
381 sequence_results,
382 forgetting_measure,
383 adaptation_efficiency,
384 })
385 }
386
387 pub fn forgetting_measure(&self) -> T {
391 self.last_forgetting
392 }
393}
394pub struct FewShotLearner<T: Float + Debug + Send + Sync + 'static> {
399 settings: FewShotSettings,
400 adaptation_steps: usize,
402 learning_rate: T,
404 episode_accuracies: Vec<T>,
406}
407impl<
408 T: Float
409 + Debug
410 + Send
411 + Sync
412 + 'static
413 + Default
414 + Clone
415 + std::iter::Sum
416 + scirs2_core::ndarray::ScalarOperand,
417 > FewShotLearner<T>
418{
419 pub fn new(settings: &FewShotSettings) -> Result<Self> {
420 Ok(Self {
421 settings: settings.clone(),
422 adaptation_steps: settings.num_shots.max(1) * 4,
423 learning_rate: scirs2_core::numeric::NumCast::from(0.05).unwrap_or_else(T::zero),
424 episode_accuracies: Vec::new(),
425 })
426 }
427
428 pub fn algorithm(&self) -> FewShotAlgorithm {
430 self.settings.algorithm
431 }
432
433 pub fn learn(
435 &mut self,
436 support_set: &TaskDataset<T>,
437 query_set: &TaskDataset<T>,
438 meta_parameters: &HashMap<String, Array1<T>>,
439 ) -> Result<FewShotResult<T>> {
440 if support_set.features.is_empty() {
441 return Err(OptimError::InsufficientData(
442 "few-shot learning requires a non-empty support set".to_string(),
443 ));
444 }
445 if query_set.features.is_empty() {
446 return Err(OptimError::InsufficientData(
447 "few-shot learning requires a non-empty query set".to_string(),
448 ));
449 }
450
451 let mut adapted = meta_parameters.clone();
452 for _ in 0..self.adaptation_steps {
453 let grads = crate::meta_learning::linear_model::mse_gradients(
454 &support_set.features,
455 &support_set.targets,
456 &adapted,
457 )?;
458 crate::meta_learning::linear_model::descend(&mut adapted, &grads, self.learning_rate)?;
459 }
460
461 let predictions =
462 crate::meta_learning::linear_model::predict_all(&query_set.features, &adapted)?;
463 let accuracy =
466 crate::meta_learning::linear_model::label_accuracy(&predictions, &query_set.targets)
467 .or_else(|| {
468 crate::meta_learning::linear_model::r_squared(&predictions, &query_set.targets)
469 .map(|r| r.max(T::zero()).min(T::one()))
470 })
471 .unwrap_or_else(T::zero);
472
473 let uncertainty_estimates: Vec<T> = predictions
476 .iter()
477 .zip(query_set.targets.iter())
478 .map(|(p, y)| (*p - *y).abs())
479 .collect();
480 let mean_uncertainty =
481 crate::meta_learning::metrics::mean_of(&uncertainty_estimates).unwrap_or_else(T::zero);
482 let confidence = T::one() / (T::one() + mean_uncertainty);
483
484 self.episode_accuracies.push(accuracy);
485
486 Ok(FewShotResult {
487 accuracy,
488 confidence,
489 adaptation_steps: self.adaptation_steps,
490 uncertainty_estimates,
491 })
492 }
493
494 pub fn average_performance(&self) -> T {
498 crate::meta_learning::metrics::mean_of(&self.episode_accuracies).unwrap_or_else(T::zero)
499 }
500}
501pub struct MetaOptimizationTracker<T: Float + Debug + Send + Sync + 'static> {
506 step_count: usize,
508 tasks_seen: usize,
510 training_losses: Vec<f64>,
512 validation_losses: Vec<f64>,
514 best_parameters: Option<MetaParameters<T>>,
516}
517impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> MetaOptimizationTracker<T> {
518 pub fn new() -> Self {
519 Self {
520 step_count: 0,
521 tasks_seen: 0,
522 training_losses: Vec::new(),
523 validation_losses: Vec::new(),
524 best_parameters: None,
525 }
526 }
527
528 pub fn record_epoch(
531 &mut self,
532 _epoch: usize,
533 training_result: &TrainingResult,
534 validation_result: &ValidationResult,
535 tasks_in_epoch: usize,
536 ) -> Result<()> {
537 self.step_count += 1;
538 self.tasks_seen += tasks_in_epoch;
539 self.training_losses.push(training_result.training_loss);
540 self.validation_losses
541 .push(validation_result.validation_loss);
542 Ok(())
543 }
544
545 pub fn update_best_parameters(&mut self, metaparameters: &MetaParameters<T>) -> Result<()> {
546 self.best_parameters = Some(metaparameters.clone());
547 Ok(())
548 }
549
550 pub fn best_parameters(&self) -> Option<&MetaParameters<T>> {
552 self.best_parameters.as_ref()
553 }
554
555 pub fn epochs_recorded(&self) -> usize {
557 self.step_count
558 }
559
560 pub fn total_tasks_seen(&self) -> usize {
562 self.tasks_seen
563 }
564
565 pub fn adaptation_efficiency(&self) -> T {
570 if self.training_losses.len() < 2 {
571 return T::zero();
572 }
573 let first = self.training_losses.first().copied().unwrap_or(0.0);
574 let last = self.training_losses.last().copied().unwrap_or(0.0);
575 let epochs = (self.training_losses.len() - 1) as f64;
576 T::from((first - last) / epochs).unwrap_or_else(T::zero)
577 }
578
579 pub fn validation_losses(&self) -> &[f64] {
581 &self.validation_losses
582 }
583}
584pub struct MetaLearningFramework<T: Float + Debug + Send + Sync + 'static> {
586 config: MetaLearningConfig,
588 meta_learner: Box<dyn MetaLearner<T> + Send + Sync>,
590 task_manager: TaskDistributionManager<T>,
592 meta_validator: MetaValidator<T>,
594 adaptation_engine: AdaptationEngine<T>,
596 transfer_manager: TransferLearningManager<T>,
598 continual_learner: ContinualLearningSystem<T>,
600 multitask_coordinator: MultiTaskCoordinator<T>,
602 meta_tracker: MetaOptimizationTracker<T>,
604 few_shot_learner: FewShotLearner<T>,
606}
607impl<
608 T: Float
609 + Default
610 + Clone
611 + Send
612 + Sync
613 + std::iter::Sum
614 + for<'a> std::iter::Sum<&'a T>
615 + scirs2_core::ndarray::ScalarOperand
616 + std::fmt::Debug,
617 > MetaLearningFramework<T>
618{
619 pub fn new(config: MetaLearningConfig) -> Result<Self> {
621 let meta_learner = Self::create_meta_learner(&config)?;
622 let task_manager = TaskDistributionManager::new(&config)?;
623 let meta_validator = MetaValidator::new(&config)?;
624 let adaptation_engine = AdaptationEngine::new(&config)?;
625 let transfer_manager = TransferLearningManager::new(&config.transfer_settings)?;
626 let continual_learner = ContinualLearningSystem::new(&config.continual_settings)?;
627 let multitask_coordinator = MultiTaskCoordinator::new(&config.multitask_settings)?;
628 let meta_tracker = MetaOptimizationTracker::new();
629 let few_shot_learner = FewShotLearner::new(&config.few_shot_settings)?;
630 Ok(Self {
631 config,
632 meta_learner,
633 task_manager,
634 meta_validator,
635 adaptation_engine,
636 transfer_manager,
637 continual_learner,
638 multitask_coordinator,
639 meta_tracker,
640 few_shot_learner,
641 })
642 }
643 fn create_meta_learner(
649 config: &MetaLearningConfig,
650 ) -> Result<Box<dyn MetaLearner<T> + Send + Sync>> {
651 let inner_lr: T = scirs2_core::numeric::NumCast::from(config.inner_learning_rate)
652 .ok_or_else(|| {
653 OptimError::InvalidConfig("inner learning rate is not representable".to_string())
654 })?;
655 let outer_lr: T = scirs2_core::numeric::NumCast::from(config.meta_learning_rate)
656 .ok_or_else(|| {
657 OptimError::InvalidConfig("meta learning rate is not representable".to_string())
658 })?;
659
660 match config.algorithm {
661 MetaLearningAlgorithm::MAML | MetaLearningAlgorithm::FOMAML => {
662 let maml_config = MAMLConfig {
663 second_order: matches!(config.algorithm, MetaLearningAlgorithm::MAML)
665 && config.second_order,
666 inner_lr,
667 outer_lr,
668 inner_steps: config.inner_steps,
669 allow_unused: true,
670 gradient_clip: Some(config.gradient_clip),
671 };
672 Ok(Box::new(MAMLLearner::<T, scirs2_core::ndarray::Ix1>::new(
673 maml_config,
674 )?))
675 }
676 MetaLearningAlgorithm::Reptile => Ok(Box::new(
677 crate::meta_learning::reptile_learner::ReptileLearner::new(outer_lr, inner_lr)
678 .with_inner_steps(config.inner_steps),
679 )),
680 MetaLearningAlgorithm::MetaSGD => Ok(Box::new(
681 crate::meta_learning::meta_sgd_learner::MetaSGDLearner::new(inner_lr)
682 .with_alpha_lr(outer_lr)
683 .with_outer_lr(outer_lr)
684 .with_inner_steps(config.inner_steps),
685 )),
686 other => Err(OptimError::InvalidConfig(format!(
687 "meta-learning algorithm {other:?} is not implemented; \
688 available algorithms are MAML, FOMAML, Reptile and MetaSGD"
689 ))),
690 }
691 }
692 pub fn algorithm(&self) -> MetaLearningAlgorithm {
694 self.meta_learner.get_algorithm()
695 }
696
697 pub fn config(&self) -> &MetaLearningConfig {
699 &self.config
700 }
701
702 pub async fn meta_train(
708 &mut self,
709 tasks: Vec<MetaTask<T>>,
710 num_epochs: usize,
711 ) -> Result<MetaTrainingResults<T>> {
712 if tasks.is_empty() {
713 return Err(OptimError::InsufficientData(
714 "meta-training requires a non-empty task pool".to_string(),
715 ));
716 }
717 let meta_params_raw = self.initialize_meta_parameters(&tasks)?;
718 let mut meta_parameters = MetaParameters {
719 parameters: meta_params_raw,
720 metadata: HashMap::new(),
721 };
722 let mut training_history = Vec::new();
723 let mut best_performance = T::neg_infinity();
724 let batch_size = self.config.task_batch_size;
725 for epoch in 0..num_epochs {
726 let task_batch = self.task_manager.sample_task_batch(&tasks, batch_size)?;
727 let batch_len = task_batch.len();
728 let training_result = self
730 .meta_learner
731 .meta_train_step(&task_batch, &mut meta_parameters.parameters)?;
732 let validation_result =
733 self.meta_validator
734 .validate(&meta_parameters, &tasks, &mut *self.meta_learner)?;
735 let mut training_metrics = HashMap::new();
736 training_metrics.insert(
737 "gradient_alignment".to_string(),
738 training_result
739 .metrics
740 .gradient_alignment
741 .to_f64()
742 .unwrap_or(0.0),
743 );
744 training_metrics.insert(
745 "adaptation_speed".to_string(),
746 training_result
747 .metrics
748 .avg_adaptation_speed
749 .to_f64()
750 .unwrap_or(0.0),
751 );
752 let training_result_simple = TrainingResult {
753 training_loss: training_result.meta_loss.to_f64().unwrap_or(f64::NAN),
754 metrics: training_metrics,
755 steps: epoch,
756 };
757 self.meta_tracker.record_epoch(
758 epoch,
759 &training_result_simple,
760 &validation_result,
761 batch_len,
762 )?;
763 let current_performance =
764 T::from(-validation_result.validation_loss).unwrap_or_default();
765 if current_performance > best_performance {
766 best_performance = current_performance;
767 self.meta_tracker.update_best_parameters(&meta_parameters)?;
768 }
769 let mut task_specific_metrics = HashMap::new();
770 for (name, value) in &validation_result.metrics {
771 if let Some(v) = T::from(*value) {
772 task_specific_metrics.insert(name.clone(), v);
773 }
774 }
775 let generalization_gap = T::from(validation_result.validation_loss)
777 .unwrap_or_else(T::zero)
778 - training_result.meta_loss;
779 let meta_validation_result = MetaValidationResult {
780 performance: current_performance,
781 adaptation_speed: training_result.metrics.avg_adaptation_speed,
782 generalization_gap,
783 task_specific_metrics,
784 };
785 training_history.push(MetaTrainingEpoch {
786 epoch,
787 training_result,
788 validation_result: meta_validation_result,
789 meta_parameters: meta_parameters.parameters.clone(),
790 });
791 if self.should_early_stop(&training_history) {
792 break;
793 }
794 }
795 let total_epochs = training_history.len();
796 Ok(MetaTrainingResults {
797 final_parameters: meta_parameters.parameters,
798 training_history,
799 best_performance,
800 total_epochs,
801 })
802 }
803 pub fn adapt_to_task(
805 &mut self,
806 task: &MetaTask<T>,
807 meta_parameters: &HashMap<String, Array1<T>>,
808 ) -> Result<TaskAdaptationResult<T>> {
809 self.adaptation_engine.adapt(
810 task,
811 meta_parameters,
812 &mut *self.meta_learner,
813 self.config.inner_steps,
814 )
815 }
816 pub fn few_shot_learning(
818 &mut self,
819 support_set: &TaskDataset<T>,
820 query_set: &TaskDataset<T>,
821 meta_parameters: &HashMap<String, Array1<T>>,
822 ) -> Result<FewShotResult<T>> {
823 self.few_shot_learner
824 .learn(support_set, query_set, meta_parameters)
825 }
826 pub fn transfer_to_domain(
828 &mut self,
829 source_tasks: &[MetaTask<T>],
830 target_tasks: &[MetaTask<T>],
831 meta_parameters: &HashMap<String, Array1<T>>,
832 ) -> Result<TransferLearningResult<T>> {
833 self.transfer_manager
834 .transfer(source_tasks, target_tasks, meta_parameters)
835 }
836 pub fn continual_learning(
838 &mut self,
839 task_sequence: &[MetaTask<T>],
840 meta_parameters: &mut HashMap<String, Array1<T>>,
841 ) -> Result<ContinualLearningResult<T>> {
842 self.continual_learner
843 .learn_sequence(task_sequence, meta_parameters)
844 }
845 pub fn multi_task_learning(
847 &mut self,
848 tasks: &[MetaTask<T>],
849 meta_parameters: &mut HashMap<String, Array1<T>>,
850 ) -> Result<MultiTaskResult<T>> {
851 self.multitask_coordinator
852 .learn_simultaneously(tasks, meta_parameters)
853 }
854 pub fn initialize_meta_parameters(
861 &self,
862 tasks: &[MetaTask<T>],
863 ) -> Result<HashMap<String, Array1<T>>> {
864 let feature_dim = tasks
865 .iter()
866 .flat_map(|t| {
867 t.support_set
868 .features
869 .iter()
870 .chain(t.query_set.features.iter())
871 })
872 .map(|f| f.len())
873 .max()
874 .unwrap_or(0);
875 if feature_dim == 0 {
876 return Err(OptimError::InsufficientData(
877 "cannot size meta-parameters: no task carries any features".to_string(),
878 ));
879 }
880
881 let scale = 1.0 / (feature_dim as f64).sqrt();
882 let mut rng = scirs2_core::random::Random::seed(0x1234_5678_9ABC_DEF0);
883 let mut weights = Array1::zeros(feature_dim);
884 for i in 0..feature_dim {
885 let v: f64 = rng.random_range(-scale..scale);
886 weights[i] = scirs2_core::numeric::NumCast::from(v).unwrap_or_else(T::zero);
887 }
888
889 let mut parameters = HashMap::new();
890 parameters.insert(
891 crate::meta_learning::linear_model::WEIGHTS_KEY.to_string(),
892 weights,
893 );
894 parameters.insert(
895 crate::meta_learning::linear_model::BIAS_KEY.to_string(),
896 Array1::zeros(1),
897 );
898 Ok(parameters)
899 }
900
901 fn should_early_stop(&self, history: &[MetaTrainingEpoch<T>]) -> bool {
907 const WINDOW: usize = 5;
908 if history.len() < 2 * WINDOW {
909 return false;
910 }
911 let recent: Vec<T> = history
912 .iter()
913 .rev()
914 .take(WINDOW)
915 .map(|epoch| epoch.validation_result.performance)
916 .collect();
917 let max_recent = recent.iter().fold(T::neg_infinity(), |a, &b| a.max(b));
918 let min_recent = recent.iter().fold(T::infinity(), |a, &b| a.min(b));
919 if !max_recent.is_finite() || !min_recent.is_finite() {
920 return false;
921 }
922 let spread = max_recent - min_recent;
923 let scale = max_recent.abs().max(T::one());
924 let threshold: T = scirs2_core::numeric::NumCast::from(1e-6).unwrap_or_else(T::zero);
925 spread / scale < threshold
926 }
927 pub fn get_meta_learning_statistics(&self) -> MetaLearningStatistics<T> {
929 MetaLearningStatistics {
930 algorithm: self.config.algorithm,
931 total_tasks_seen: self.meta_tracker.total_tasks_seen(),
932 adaptation_efficiency: self.meta_tracker.adaptation_efficiency(),
933 transfer_success_rate: self.transfer_manager.success_rate(),
934 forgetting_measure: self.continual_learner.forgetting_measure(),
935 multitask_interference: self.multitask_coordinator.interference_measure(),
936 few_shot_performance: self.few_shot_learner.average_performance(),
937 }
938 }
939}
940pub struct TransferLearningManager<T: Float + Debug + Send + Sync + 'static> {
943 settings: TransferLearningSettings,
944 attempts: usize,
946 successes: usize,
948 _phantom: std::marker::PhantomData<T>,
949}
950impl<
951 T: Float
952 + Debug
953 + Send
954 + Sync
955 + 'static
956 + Default
957 + Clone
958 + std::iter::Sum
959 + scirs2_core::ndarray::ScalarOperand,
960 > TransferLearningManager<T>
961{
962 pub fn new(settings: &TransferLearningSettings) -> Result<Self> {
963 Ok(Self {
964 settings: settings.clone(),
965 attempts: 0,
966 successes: 0,
967 _phantom: std::marker::PhantomData,
968 })
969 }
970
971 pub fn domain_adaptation_enabled(&self) -> bool {
973 self.settings.domain_adaptation
974 }
975
976 pub fn transfer(
984 &mut self,
985 source_tasks: &[MetaTask<T>],
986 target_tasks: &[MetaTask<T>],
987 meta_parameters: &HashMap<String, Array1<T>>,
988 ) -> Result<TransferLearningResult<T>> {
989 if source_tasks.is_empty() || target_tasks.is_empty() {
990 return Err(OptimError::InsufficientData(
991 "transfer requires at least one source and one target task".to_string(),
992 ));
993 }
994 let weights = meta_parameters
995 .get(crate::meta_learning::linear_model::WEIGHTS_KEY)
996 .ok_or_else(|| {
997 OptimError::InvalidConfig(
998 "transfer requires a 'weights' entry in the meta-parameters".to_string(),
999 )
1000 })?;
1001
1002 let source_repr = Self::group_signature(source_tasks, weights.len())?;
1003 let target_repr = Self::group_signature(target_tasks, weights.len())?;
1004
1005 let mut engine = crate::cross_domain_transfer::CrossDomainTransfer::<T>::new(weights.len());
1006 engine.register_domain(crate::cross_domain_transfer::DomainKnowledge {
1007 domain_name: "source".to_string(),
1008 shared_representation: source_repr,
1009 domain_specific_params: weights.clone(),
1010 performance_history: Vec::new(),
1011 })?;
1012 engine.register_domain(crate::cross_domain_transfer::DomainKnowledge {
1013 domain_name: "target".to_string(),
1014 shared_representation: target_repr,
1015 domain_specific_params: weights.clone(),
1016 performance_history: Vec::new(),
1017 })?;
1018
1019 let similarity = engine.compute_domain_similarity("source", "target")?;
1020 let result = engine.transfer("source", "target")?;
1021
1022 let mut transferred = meta_parameters.clone();
1023 transferred.insert(
1024 crate::meta_learning::linear_model::WEIGHTS_KEY.to_string(),
1025 result.transferred_params.clone(),
1026 );
1027
1028 let source_before = Self::group_loss(source_tasks, meta_parameters)?;
1029 let source_after = Self::group_loss(source_tasks, &transferred)?;
1030 let target_before = Self::group_loss(target_tasks, meta_parameters)?;
1031 let target_after = Self::group_loss(target_tasks, &transferred)?;
1032
1033 self.attempts += 1;
1034 if target_after < target_before {
1035 self.successes += 1;
1036 }
1037
1038 let relative = |before: T, after: T| -> T {
1039 if before > T::zero() {
1040 ((before - after) / before).max(T::zero()).min(T::one())
1041 } else {
1042 T::zero()
1043 }
1044 };
1045
1046 Ok(TransferLearningResult {
1047 transfer_efficiency: relative(target_before, target_after),
1048 domain_adaptation_score: (similarity + T::one()) / (T::one() + T::one()),
1050 source_task_retention: if source_after > T::zero() {
1052 (source_before / source_after).min(T::one())
1053 } else {
1054 T::one()
1055 },
1056 target_task_performance: T::one() / (T::one() + target_after),
1057 })
1058 }
1059
1060 pub fn success_rate(&self) -> T {
1064 if self.attempts == 0 {
1065 return T::zero();
1066 }
1067 let a = T::from(self.attempts).unwrap_or_else(T::one);
1068 T::from(self.successes).unwrap_or_else(T::zero) / a
1069 }
1070
1071 fn group_signature(tasks: &[MetaTask<T>], dim: usize) -> Result<Array1<T>> {
1073 let mut acc: Array1<T> = Array1::zeros(dim);
1074 let mut count = 0usize;
1075 for task in tasks {
1076 for feat in &task.support_set.features {
1077 let n = dim.min(feat.len());
1078 for i in 0..n {
1079 acc[i] = acc[i] + feat[i];
1080 }
1081 count += 1;
1082 }
1083 }
1084 if count == 0 {
1085 return Err(OptimError::InsufficientData(
1086 "task group has no support samples".to_string(),
1087 ));
1088 }
1089 let c = T::from(count).unwrap_or_else(T::one);
1090 Ok(acc.mapv(|v| v / c))
1091 }
1092
1093 fn group_loss(tasks: &[MetaTask<T>], parameters: &HashMap<String, Array1<T>>) -> Result<T> {
1095 let mut total = T::zero();
1096 let mut count = 0usize;
1097 for task in tasks {
1098 if task.query_set.features.is_empty() {
1099 continue;
1100 }
1101 total = total
1102 + crate::meta_learning::linear_model::mse_loss(
1103 &task.query_set.features,
1104 &task.query_set.targets,
1105 parameters,
1106 )?;
1107 count += 1;
1108 }
1109 if count == 0 {
1110 return Err(OptimError::InsufficientData(
1111 "task group has no query samples".to_string(),
1112 ));
1113 }
1114 let c = T::from(count).unwrap_or_else(T::one);
1115 Ok(total / c)
1116 }
1117}
1118pub struct MultiTaskCoordinator<T: Float + Debug + Send + Sync + 'static> {
1125 settings: MultiTaskSettings,
1126 steps: usize,
1128 last_interference: T,
1130}
1131impl<
1132 T: Float
1133 + Debug
1134 + Send
1135 + Sync
1136 + 'static
1137 + Default
1138 + Clone
1139 + std::iter::Sum
1140 + scirs2_core::ndarray::ScalarOperand,
1141 > MultiTaskCoordinator<T>
1142{
1143 pub fn new(settings: &MultiTaskSettings) -> Result<Self> {
1144 Ok(Self {
1145 settings: settings.clone(),
1146 steps: 20,
1147 last_interference: T::zero(),
1148 })
1149 }
1150
1151 pub fn with_steps(mut self, steps: usize) -> Result<Self> {
1153 if steps == 0 {
1154 return Err(OptimError::InvalidConfig(
1155 "multi-task steps must be greater than zero".to_string(),
1156 ));
1157 }
1158 self.steps = steps;
1159 Ok(self)
1160 }
1161
1162 pub fn learn_simultaneously(
1163 &mut self,
1164 tasks: &[MetaTask<T>],
1165 meta_parameters: &mut HashMap<String, Array1<T>>,
1166 ) -> Result<MultiTaskResult<T>> {
1167 if tasks.is_empty() {
1168 return Err(OptimError::InsufficientData(
1169 "multi-task learning requires at least one task".to_string(),
1170 ));
1171 }
1172
1173 let lr: T = scirs2_core::numeric::NumCast::from(0.05).unwrap_or_else(T::zero);
1174 let start = std::time::Instant::now();
1175 let mut last_gradients: Vec<HashMap<String, Array1<T>>> = Vec::new();
1176 let mut initial_losses: Vec<T> = Vec::with_capacity(tasks.len());
1177 for task in tasks {
1178 initial_losses.push(crate::meta_learning::linear_model::mse_loss(
1179 &task.support_set.features,
1180 &task.support_set.targets,
1181 meta_parameters,
1182 )?);
1183 }
1184
1185 for _ in 0..self.steps {
1186 let mut per_task: Vec<HashMap<String, Array1<T>>> = Vec::with_capacity(tasks.len());
1187 let mut losses: Vec<T> = Vec::with_capacity(tasks.len());
1188 for task in tasks {
1189 losses.push(crate::meta_learning::linear_model::mse_loss(
1190 &task.support_set.features,
1191 &task.support_set.targets,
1192 meta_parameters,
1193 )?);
1194 per_task.push(crate::meta_learning::linear_model::mse_gradients(
1195 &task.support_set.features,
1196 &task.support_set.targets,
1197 meta_parameters,
1198 )?);
1199 }
1200
1201 let weights = self.task_weights(&losses, &per_task)?;
1202 let mut combined: HashMap<String, Array1<T>> = meta_parameters
1203 .iter()
1204 .map(|(name, p)| (name.clone(), Array1::zeros(p.len())))
1205 .collect();
1206 for (grad, weight) in per_task.iter().zip(weights.iter()) {
1207 for (name, g) in grad {
1208 if let Some(acc) = combined.get_mut(name) {
1209 let n = acc.len().min(g.len());
1210 for i in 0..n {
1211 acc[i] = acc[i] + *weight * g[i];
1212 }
1213 }
1214 }
1215 }
1216 crate::meta_learning::linear_model::descend(meta_parameters, &combined, lr)?;
1217 last_gradients = per_task;
1218 }
1219
1220 let mut conflicts: Vec<T> = Vec::new();
1224 for i in 0..last_gradients.len() {
1225 for j in (i + 1)..last_gradients.len() {
1226 if let Some(c) = crate::meta_learning::linear_model::map_cosine(
1227 &last_gradients[i],
1228 &last_gradients[j],
1229 ) {
1230 conflicts.push((T::one() - c) / (T::one() + T::one()));
1231 }
1232 }
1233 }
1234 self.last_interference =
1235 crate::meta_learning::metrics::mean_of(&conflicts).unwrap_or_else(T::zero);
1236
1237 let mut task_results = Vec::with_capacity(tasks.len());
1238 let mut converged = true;
1239 for (idx, task) in tasks.iter().enumerate() {
1240 let loss = crate::meta_learning::linear_model::mse_loss(
1241 &task.support_set.features,
1242 &task.support_set.targets,
1243 meta_parameters,
1244 )?;
1245 if let Some(before) = initial_losses.get(idx) {
1246 if loss >= *before {
1247 converged = false;
1248 }
1249 }
1250 let mut metrics = HashMap::new();
1251 if let Some(before) = initial_losses.get(idx) {
1252 metrics.insert("loss_before".to_string(), *before);
1253 }
1254 metrics.insert("loss_after".to_string(), loss);
1255 task_results.push(TaskResult {
1256 task_id: task.id.clone(),
1257 loss,
1258 metrics,
1259 });
1260 }
1261
1262 let elapsed = T::from(start.elapsed().as_secs_f64()).unwrap_or_else(T::zero);
1263 Ok(MultiTaskResult {
1264 task_results,
1265 coordination_overhead: elapsed,
1266 convergence_status: if converged {
1267 "converged".to_string()
1268 } else {
1269 "not_converged".to_string()
1270 },
1271 })
1272 }
1273
1274 fn task_weights(
1276 &self,
1277 losses: &[T],
1278 gradients: &[HashMap<String, Array1<T>>],
1279 ) -> Result<Vec<T>> {
1280 let n = T::from(losses.len().max(1)).unwrap_or_else(T::one);
1281 let raw: Vec<T> = match self.settings.task_weighting {
1282 TaskWeightingStrategy::Uniform => vec![T::one(); losses.len()],
1283 TaskWeightingStrategy::PerformanceBased | TaskWeightingStrategy::Adaptive => {
1284 losses.iter().map(|l| l.max(T::zero())).collect()
1286 }
1287 TaskWeightingStrategy::UncertaintyBased => {
1288 losses
1290 .iter()
1291 .map(|l| T::one() / (T::one() + l.max(T::zero())))
1292 .collect()
1293 }
1294 TaskWeightingStrategy::GradientMagnitude | TaskWeightingStrategy::Learned => gradients
1295 .iter()
1296 .map(crate::meta_learning::linear_model::map_norm)
1297 .collect(),
1298 };
1299
1300 let total = raw.iter().copied().fold(T::zero(), |a, b| a + b);
1301 let normalised: Vec<T> = if total > T::zero() {
1302 raw.iter().map(|w| *w / total).collect()
1303 } else {
1304 vec![T::one() / n; losses.len()]
1305 };
1306
1307 Ok(match self.settings.gradient_balancing {
1308 GradientBalancingMethod::GradNorm => {
1311 let norms: Vec<T> = gradients
1312 .iter()
1313 .map(crate::meta_learning::linear_model::map_norm)
1314 .collect();
1315 let mean_norm =
1316 crate::meta_learning::metrics::mean_of(&norms).unwrap_or_else(T::one);
1317 normalised
1318 .iter()
1319 .zip(norms.iter())
1320 .map(|(w, nrm)| {
1321 if *nrm > T::zero() {
1322 *w * mean_norm / *nrm
1323 } else {
1324 *w
1325 }
1326 })
1327 .collect()
1328 }
1329 _ => normalised,
1330 })
1331 }
1332
1333 pub fn interference_measure(&self) -> T {
1337 self.last_interference
1338 }
1339}
1340pub struct MetaValidator<T: Float + Debug + Send + Sync + 'static> {
1345 config: MetaLearningConfig,
1346 holdout_fraction: f64,
1348 _phantom: std::marker::PhantomData<T>,
1349}
1350impl<
1351 T: Float
1352 + Debug
1353 + Send
1354 + Sync
1355 + 'static
1356 + Default
1357 + Clone
1358 + std::iter::Sum
1359 + scirs2_core::ndarray::ScalarOperand,
1360 > MetaValidator<T>
1361{
1362 pub fn new(config: &MetaLearningConfig) -> Result<Self> {
1363 Ok(Self {
1364 config: config.clone(),
1365 holdout_fraction: 0.25,
1366 _phantom: std::marker::PhantomData,
1367 })
1368 }
1369
1370 pub fn with_holdout_fraction(mut self, fraction: f64) -> Result<Self> {
1372 if !(fraction > 0.0 && fraction < 1.0) {
1373 return Err(OptimError::InvalidConfig(
1374 "holdout fraction must lie strictly between 0 and 1".to_string(),
1375 ));
1376 }
1377 self.holdout_fraction = fraction;
1378 Ok(self)
1379 }
1380
1381 fn holdout_range(&self, len: usize) -> std::ops::Range<usize> {
1383 if len == 0 {
1384 return 0..0;
1385 }
1386 let count = ((len as f64) * self.holdout_fraction).ceil() as usize;
1387 let count = count.clamp(1, len);
1388 (len - count)..len
1389 }
1390
1391 pub fn validate(
1397 &self,
1398 meta_parameters: &MetaParameters<T>,
1399 tasks: &[MetaTask<T>],
1400 meta_learner: &mut dyn MetaLearner<T>,
1401 ) -> Result<ValidationResult> {
1402 let range = self.holdout_range(tasks.len());
1403 let holdout = tasks.get(range).unwrap_or(&[]);
1404 if holdout.is_empty() {
1405 return Err(OptimError::InsufficientData(
1406 "no held-out tasks available for meta-validation".to_string(),
1407 ));
1408 }
1409
1410 let mut total = 0.0f64;
1411 let mut pre_total = 0.0f64;
1412 let mut counted = 0usize;
1413 for task in holdout {
1414 if task.query_set.features.is_empty() || task.support_set.features.is_empty() {
1415 continue;
1416 }
1417 let pre = crate::meta_learning::linear_model::mse_loss(
1418 &task.query_set.features,
1419 &task.query_set.targets,
1420 &meta_parameters.parameters,
1421 )?;
1422 let adaptation = meta_learner.adapt_to_task(
1423 task,
1424 &meta_parameters.parameters,
1425 self.config.inner_steps,
1426 )?;
1427 let query = meta_learner.evaluate_query_set(task, &adaptation.adapted_parameters)?;
1428 total += query.query_loss.to_f64().unwrap_or(f64::NAN);
1429 pre_total += pre.to_f64().unwrap_or(f64::NAN);
1430 counted += 1;
1431 }
1432 if counted == 0 {
1433 return Err(OptimError::InsufficientData(
1434 "every held-out task was empty".to_string(),
1435 ));
1436 }
1437
1438 let validation_loss = total / counted as f64;
1439 let pre_loss = pre_total / counted as f64;
1440 let mut metrics = std::collections::HashMap::new();
1441 metrics.insert("held_out_tasks".to_string(), counted as f64);
1442 metrics.insert("pre_adaptation_loss".to_string(), pre_loss);
1443 metrics.insert("post_adaptation_loss".to_string(), validation_loss);
1444
1445 Ok(ValidationResult {
1446 is_valid: validation_loss.is_finite(),
1447 validation_loss,
1448 metrics,
1449 })
1450 }
1451}
1452pub struct AdaptationEngine<T: Float + Debug + Send + Sync + 'static> {
1454 config: MetaLearningConfig,
1455 _phantom: std::marker::PhantomData<T>,
1456}
1457impl<T: Float + Debug + Send + Sync + 'static + Default + Clone> AdaptationEngine<T> {
1458 pub fn new(config: &MetaLearningConfig) -> Result<Self> {
1459 Ok(Self {
1460 config: config.clone(),
1461 _phantom: std::marker::PhantomData,
1462 })
1463 }
1464 pub fn adapt(
1468 &mut self,
1469 task: &MetaTask<T>,
1470 meta_parameters: &HashMap<String, Array1<T>>,
1471 meta_learner: &mut dyn MetaLearner<T>,
1472 inner_steps: usize,
1473 ) -> Result<TaskAdaptationResult<T>> {
1474 let steps = if inner_steps == 0 {
1475 self.config.inner_steps
1476 } else {
1477 inner_steps
1478 };
1479 if steps == 0 {
1480 return Err(OptimError::InvalidConfig(
1481 "adaptation requires at least one inner step".to_string(),
1482 ));
1483 }
1484 meta_learner.adapt_to_task(task, meta_parameters, steps)
1485 }
1486
1487 pub fn strategies(&self) -> &[AdaptationStrategy] {
1489 &self.config.adaptation_strategies
1490 }
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::*;
1496 use crate::meta_learning::linear_model;
1497
1498 fn basis(x: f64) -> Array1<f64> {
1499 let u = x / 3.0;
1500 Array1::from_vec(vec![1.0, u, u * u, u * u * u])
1501 }
1502
1503 fn linear_task(id: usize, slope: f64, intercept: f64) -> MetaTask<f64> {
1504 let mut support_features = Vec::new();
1505 let mut support_targets = Vec::new();
1506 let mut query_features = Vec::new();
1507 let mut query_targets = Vec::new();
1508 for k in 0..10 {
1509 let x = -3.0 + 6.0 * (k as f64) / 9.0;
1510 let y = slope * x + intercept;
1511 if k % 2 == 0 {
1512 support_features.push(basis(x));
1513 support_targets.push(y);
1514 } else {
1515 query_features.push(basis(x));
1516 query_targets.push(y);
1517 }
1518 }
1519 MetaTask {
1520 id: format!("task_{id}"),
1521 support_set: TaskDataset {
1522 features: support_features,
1523 targets: support_targets,
1524 weights: Vec::new(),
1525 metadata: DatasetMetadata::default(),
1526 },
1527 query_set: TaskDataset {
1528 features: query_features,
1529 targets: query_targets,
1530 weights: Vec::new(),
1531 metadata: DatasetMetadata::default(),
1532 },
1533 metadata: TaskMetadata::default(),
1534 difficulty: slope.abs(),
1535 domain: format!("domain_{}", id % 2),
1536 task_type: TaskType::Regression,
1537 }
1538 }
1539
1540 fn task_pool(n: usize) -> Vec<MetaTask<f64>> {
1541 (0..n)
1542 .map(|i| linear_task(i, 0.2 + 0.3 * i as f64, -1.0 + 0.4 * i as f64))
1543 .collect()
1544 }
1545
1546 fn params(dim: usize) -> HashMap<String, Array1<f64>> {
1547 let mut p = HashMap::new();
1548 p.insert(linear_model::WEIGHTS_KEY.to_string(), Array1::zeros(dim));
1549 p.insert(linear_model::BIAS_KEY.to_string(), Array1::zeros(1));
1550 p
1551 }
1552
1553 #[test]
1556 fn test_sample_task_batch_uses_real_tasks() {
1557 let pool = task_pool(5);
1558 let config = MetaLearningConfig::default();
1559 let mut manager = TaskDistributionManager::<f64>::new(&config).expect("manager");
1560
1561 let batch = manager.sample_task_batch(&pool, 20).expect("batch");
1563 assert_eq!(batch.len(), 20);
1564 for task in &batch {
1565 assert!(pool.iter().any(|t| t.id == task.id), "unknown task sampled");
1566 assert!(!task.support_set.features.is_empty(), "empty support set");
1567 }
1568
1569 assert!(manager.sample_task_batch(&[], 4).is_err());
1570 assert!(manager.sample_task_batch(&pool, 0).is_err());
1571 }
1572
1573 #[test]
1575 fn test_sampling_strategies_differ() {
1576 let pool = task_pool(6);
1577
1578 let easiest = MetaLearningConfig {
1579 task_sampling_strategy: TaskSamplingStrategy::Curriculum,
1580 ..MetaLearningConfig::default()
1581 };
1582 let mut curriculum =
1583 TaskDistributionManager::<f64>::new(&easiest).expect("curriculum manager");
1584 let first = curriculum.sample_task_batch(&pool, 2).expect("batch");
1585
1586 let hardest = MetaLearningConfig {
1587 task_sampling_strategy: TaskSamplingStrategy::Adversarial,
1588 ..MetaLearningConfig::default()
1589 };
1590 let mut adversarial =
1591 TaskDistributionManager::<f64>::new(&hardest).expect("adversarial manager");
1592 let hard = adversarial.sample_task_batch(&pool, 2).expect("batch");
1593
1594 assert_ne!(first[0].id, hard[0].id);
1596 assert!(first[0].difficulty < hard[0].difficulty);
1597 }
1598
1599 #[test]
1602 fn test_meta_learner_dispatch() {
1603 let cases = [
1604 (MetaLearningAlgorithm::MAML, MetaLearningAlgorithm::FOMAML),
1605 (MetaLearningAlgorithm::FOMAML, MetaLearningAlgorithm::FOMAML),
1606 (
1607 MetaLearningAlgorithm::Reptile,
1608 MetaLearningAlgorithm::Reptile,
1609 ),
1610 (
1611 MetaLearningAlgorithm::MetaSGD,
1612 MetaLearningAlgorithm::MetaSGD,
1613 ),
1614 ];
1615 for (requested, expected) in cases {
1616 let config = MetaLearningConfig {
1617 algorithm: requested,
1618 ..MetaLearningConfig::default()
1619 };
1620 let framework = MetaLearningFramework::<f64>::new(config).expect("framework");
1621 assert_eq!(
1622 format!("{:?}", framework.algorithm()),
1623 format!("{expected:?}"),
1624 "dispatch for {requested:?}"
1625 );
1626 }
1627
1628 let config = MetaLearningConfig {
1630 second_order: true,
1631 ..MetaLearningConfig::default()
1632 };
1633 let framework = MetaLearningFramework::<f64>::new(config).expect("framework");
1634 assert!(matches!(framework.algorithm(), MetaLearningAlgorithm::MAML));
1635
1636 for unimplemented in [
1637 MetaLearningAlgorithm::ProtoNet,
1638 MetaLearningAlgorithm::MatchingNet,
1639 MetaLearningAlgorithm::IMaml,
1640 MetaLearningAlgorithm::L2L,
1641 ] {
1642 let config = MetaLearningConfig {
1643 algorithm: unimplemented,
1644 ..MetaLearningConfig::default()
1645 };
1646 assert!(
1647 MetaLearningFramework::<f64>::new(config).is_err(),
1648 "{unimplemented:?} must be refused, not silently replaced by MAML"
1649 );
1650 }
1651 }
1652
1653 #[test]
1655 fn test_adaptation_engine_delegates() {
1656 let config = MetaLearningConfig::default();
1657 let mut engine = AdaptationEngine::<f64>::new(&config).expect("engine");
1658 let mut learner = crate::meta_learning::reptile_learner::ReptileLearner::new(0.1f64, 0.3);
1659 let task = linear_task(0, 0.7, -0.2);
1660 let start = params(4);
1661
1662 let result = engine.adapt(&task, &start, &mut learner, 6).expect("adapt");
1663 assert_eq!(result.adaptation_trajectory.len(), 6);
1664 let moved = result.adapted_parameters[linear_model::WEIGHTS_KEY]
1665 .iter()
1666 .any(|w| w.abs() > 0.0);
1667 assert!(moved, "adaptation must move the parameters");
1668 let baseline = linear_model::mse_loss(
1669 &task.support_set.features,
1670 &task.support_set.targets,
1671 &start,
1672 )
1673 .expect("loss");
1674 assert!(result.final_loss < baseline);
1675 }
1676
1677 #[test]
1679 fn test_validator_measures_real_holdout_loss() {
1680 let pool = task_pool(8);
1681 let config = MetaLearningConfig::default();
1682 let validator = MetaValidator::<f64>::new(&config).expect("validator");
1683 let mut learner = crate::meta_learning::reptile_learner::ReptileLearner::new(0.1f64, 0.3);
1684
1685 let good = MetaParameters {
1686 parameters: params(4),
1687 metadata: HashMap::new(),
1688 };
1689 let mut bad_params = params(4);
1690 if let Some(w) = bad_params.get_mut(linear_model::WEIGHTS_KEY) {
1691 w.fill(50.0);
1692 }
1693 let bad = MetaParameters {
1694 parameters: bad_params,
1695 metadata: HashMap::new(),
1696 };
1697
1698 let good_result = validator
1699 .validate(&good, &pool, &mut learner)
1700 .expect("validate good");
1701 let bad_result = validator
1702 .validate(&bad, &pool, &mut learner)
1703 .expect("validate bad");
1704
1705 assert!(
1706 bad_result.validation_loss > good_result.validation_loss,
1707 "the validator must respond to the parameters: good={}, bad={}",
1708 good_result.validation_loss,
1709 bad_result.validation_loss
1710 );
1711 assert!(good_result.metrics.contains_key("held_out_tasks"));
1712 assert!(validator.validate(&good, &[], &mut learner).is_err());
1713 }
1714
1715 #[test]
1717 fn test_continual_learning_uses_ewc() {
1718 let config = MetaLearningConfig::default();
1719 let mut system =
1720 ContinualLearningSystem::<f64>::new(&config.continual_settings).expect("system");
1721 assert_eq!(system.forgetting_measure(), 0.0);
1722
1723 let sequence = task_pool(3);
1724 let mut parameters = params(4);
1725 let result = system
1726 .learn_sequence(&sequence, &mut parameters)
1727 .expect("sequence");
1728
1729 assert_eq!(result.sequence_results.len(), 3);
1730 for task_result in &result.sequence_results {
1731 let before = task_result.metrics["loss_before"];
1732 assert!(
1733 task_result.loss < before,
1734 "task {} did not improve: {} -> {}",
1735 task_result.task_id,
1736 before,
1737 task_result.loss
1738 );
1739 }
1740 assert!(result.forgetting_measure >= 0.0);
1741 assert!(system.forgetting_measure().is_finite());
1742 assert!(
1743 ContinualLearningSystem::<f64>::new(&config.continual_settings)
1744 .expect("system")
1745 .learn_sequence(&[], &mut parameters)
1746 .is_err()
1747 );
1748 }
1749
1750 #[test]
1752 fn test_transfer_manager_delegates() {
1753 let config = MetaLearningConfig::default();
1754 let mut manager =
1755 TransferLearningManager::<f64>::new(&config.transfer_settings).expect("manager");
1756 assert_eq!(manager.success_rate(), 0.0);
1757
1758 let source = vec![linear_task(0, 1.0, 0.0), linear_task(1, 1.1, 0.1)];
1759 let target = vec![linear_task(2, -1.0, 2.0)];
1760 let mut parameters = params(4);
1761 if let Some(w) = parameters.get_mut(linear_model::WEIGHTS_KEY) {
1762 w.fill(0.3);
1763 }
1764
1765 let result = manager
1766 .transfer(&source, &target, ¶meters)
1767 .expect("transfer");
1768 assert!(result.domain_adaptation_score.is_finite());
1769 assert!((0.0..=1.0).contains(&result.domain_adaptation_score));
1770 assert!(manager.transfer(&[], &target, ¶meters).is_err());
1771 }
1772
1773 #[test]
1775 fn test_tracker_counts_are_real() {
1776 let mut tracker = MetaOptimizationTracker::<f64>::new();
1777 assert_eq!(tracker.total_tasks_seen(), 0);
1778 assert_eq!(tracker.adaptation_efficiency(), 0.0);
1779
1780 for (epoch, loss) in [(0usize, 1.0f64), (1, 0.5), (2, 0.25)] {
1781 let training = TrainingResult {
1782 training_loss: loss,
1783 metrics: HashMap::new(),
1784 steps: epoch,
1785 };
1786 let validation = ValidationResult {
1787 is_valid: true,
1788 validation_loss: loss,
1789 metrics: HashMap::new(),
1790 };
1791 tracker
1792 .record_epoch(epoch, &training, &validation, 4)
1793 .expect("record");
1794 }
1795
1796 assert_eq!(tracker.epochs_recorded(), 3);
1797 assert_eq!(tracker.total_tasks_seen(), 12);
1799 approx::assert_abs_diff_eq!(tracker.adaptation_efficiency(), 0.375, epsilon = 1e-12);
1800 assert_eq!(tracker.validation_losses().len(), 3);
1801 }
1802
1803 #[test]
1805 fn test_few_shot_learner_is_real() {
1806 let config = MetaLearningConfig::default();
1807 let mut learner = FewShotLearner::<f64>::new(&config.few_shot_settings).expect("learner");
1808 assert_eq!(learner.average_performance(), 0.0);
1809
1810 let task = linear_task(0, 0.5, 0.0);
1811 let result = learner
1812 .learn(&task.support_set, &task.query_set, ¶ms(4))
1813 .expect("few-shot");
1814 assert_eq!(
1815 result.uncertainty_estimates.len(),
1816 task.query_set.features.len()
1817 );
1818 assert!(result.confidence > 0.0 && result.confidence <= 1.0);
1819 assert!(learner.average_performance().is_finite());
1820
1821 let empty = TaskDataset::<f64>::default();
1822 assert!(learner.learn(&empty, &task.query_set, ¶ms(4)).is_err());
1823 }
1824
1825 #[test]
1828 fn test_multi_task_coordinator_is_real() {
1829 let config = MetaLearningConfig::default();
1830 let mut coordinator =
1831 MultiTaskCoordinator::<f64>::new(&config.multitask_settings).expect("coordinator");
1832 assert_eq!(coordinator.interference_measure(), 0.0);
1833
1834 let tasks = vec![linear_task(0, 1.0, 0.0), linear_task(1, -1.0, 0.0)];
1835 let mut parameters = params(4);
1836 let result = coordinator
1837 .learn_simultaneously(&tasks, &mut parameters)
1838 .expect("multi-task");
1839 assert_eq!(result.task_results.len(), 2);
1840 let interference = coordinator.interference_measure();
1841 assert!(
1842 (0.0..=1.0).contains(&interference),
1843 "interference out of range: {interference}"
1844 );
1845 assert!(interference > 0.5, "opposed tasks should conflict");
1847 assert!(coordinator
1848 .learn_simultaneously(&[], &mut parameters)
1849 .is_err());
1850 }
1851
1852 #[test]
1855 fn test_initialize_meta_parameters_is_sized_and_nonzero() {
1856 let pool = task_pool(3);
1857 let framework =
1858 MetaLearningFramework::<f64>::new(MetaLearningConfig::default()).expect("framework");
1859 let initial = framework
1860 .initialize_meta_parameters(&pool)
1861 .expect("initialise");
1862 assert_eq!(initial[linear_model::WEIGHTS_KEY].len(), 4);
1863 assert!(initial[linear_model::WEIGHTS_KEY]
1864 .iter()
1865 .any(|w| w.abs() > 0.0));
1866 assert!(framework.initialize_meta_parameters(&[]).is_err());
1867 }
1868}