1use super::config::*;
8use super::meta_bandit::{arm_index_for, arm_table, state_features, BanditArm, FeatureScaler};
9use super::meta_transfer::TransferLearning;
10use super::optimizer::{Adaptation, AdaptationPriority, AdaptationType, StreamingDataPoint};
11use super::performance::PerformanceTracker;
12
13pub use super::meta_transfer::{
14 DomainAdaptation, TransferMetrics, TransferStrategy, MIN_TRANSFER_SIMILARITY,
15};
16
17use crate::utils::{scalar_or, try_scalar_str};
18use scirs2_core::numeric::Float;
19use scirs2_core::random::thread_rng;
20use std::collections::{HashMap, VecDeque};
21use std::time::{Duration, Instant};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum MetaModelType {
26 NeuralNetwork,
27 LinearRegression,
28 RandomForest,
29 GradientBoosting,
30 SupportVectorMachine,
31}
32
33pub struct MetaLearner<A: Float + Send + Sync> {
35 config: MetaLearningConfig,
37 experience_buffer: ExperienceBuffer<A>,
39 meta_model: MetaModel<A>,
41 strategy_selector: StrategySelector<A>,
43 transfer_learning: TransferLearning<A>,
45 statistics: MetaLearningStatistics<A>,
47 learning_rate_adapter: LearningRateAdapter<A>,
49 episode_start: Instant,
51 episode_initial_performance: Option<A>,
53 episode_adaptation_count: usize,
55 context_resource_state: Vec<A>,
58 context_drift_indicators: Vec<A>,
60}
61
62pub type ExperienceReplay<A> = ExperienceBuffer<A>;
64
65pub struct ExperienceBuffer<A: Float + Send + Sync> {
67 config: ExperienceReplayConfig,
69 experiences: VecDeque<MetaExperience<A>>,
71 priority_queue: VecDeque<(MetaExperience<A>, A)>,
73 importance_weights: HashMap<usize, A>,
75 capacity: usize,
78}
79
80#[derive(Debug, Clone)]
82pub struct MetaExperience<A: Float + Send + Sync> {
83 pub id: u64,
85 pub state: MetaState<A>,
87 pub action: MetaAction<A>,
89 pub reward: A,
91 pub next_state: Option<MetaState<A>>,
93 pub timestamp: Instant,
95 pub episode_context: EpisodeContext<A>,
97 pub priority: A,
99 pub replay_count: usize,
101}
102
103#[derive(Debug, Clone)]
105pub struct MetaState<A: Float + Send + Sync> {
106 pub performance_metrics: Vec<A>,
108 pub resource_state: Vec<A>,
110 pub drift_indicators: Vec<A>,
112 pub adaptation_history: usize,
114 pub timestamp: Instant,
116}
117
118#[derive(Debug, Clone)]
120pub struct MetaAction<A: Float + Send + Sync> {
121 pub adaptation_magnitudes: Vec<A>,
123 pub adaptation_types: Vec<AdaptationType>,
125 pub learning_rate_change: A,
127 pub buffer_size_change: A,
129 pub timestamp: Instant,
131}
132
133#[derive(Debug, Clone)]
135pub struct EpisodeContext<A: Float + Send + Sync> {
136 pub episode_id: u64,
138 pub start_time: Instant,
140 pub duration: Duration,
142 pub initial_performance: A,
144 pub final_performance: A,
146 pub adaptation_count: usize,
148 pub outcome: EpisodeOutcome,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum EpisodeOutcome {
155 Success,
157 PartialSuccess,
159 Neutral,
161 Failure,
163 CriticalFailure,
165}
166
167pub struct MetaModel<A: Float + Send + Sync> {
169 parameters: MetaModelParameters<A>,
171 training_history: VecDeque<TrainingEpisode<A>>,
173 performance_metrics: ModelPerformanceMetrics<A>,
175 feature_importance: Vec<A>,
177 arms: Vec<BanditArm<A>>,
180 training_steps: usize,
182 feature_scaler: FeatureScaler<A>,
184}
185
186#[derive(Debug, Clone)]
188pub struct MetaModelParameters<A: Float + Send + Sync> {
189 pub weights: Vec<Vec<A>>,
191 pub biases: Vec<A>,
193 pub learning_rate: A,
195 pub regularization: RegularizationParams<A>,
197 pub optimization: OptimizationParams<A>,
199}
200
201#[derive(Debug, Clone)]
203pub struct RegularizationParams<A: Float + Send + Sync> {
204 pub l1_lambda: A,
206 pub l2_lambda: A,
208 pub dropout_rate: A,
210 pub early_stopping_patience: usize,
212}
213
214#[derive(Debug, Clone)]
216pub struct OptimizationParams<A: Float + Send + Sync> {
217 pub momentum: A,
219 pub beta1: A,
221 pub beta2: A,
223 pub epsilon: A,
225 pub grad_clip_threshold: A,
227}
228
229#[derive(Debug, Clone)]
231pub struct TrainingEpisode<A: Float + Send + Sync> {
232 pub episode_id: u64,
234 pub training_loss: A,
236 pub validation_loss: A,
238 pub training_accuracy: A,
240 pub validation_accuracy: A,
242 pub duration: Duration,
244 pub timestamp: Instant,
246}
247
248#[derive(Debug, Clone)]
250pub struct ModelPerformanceMetrics<A: Float + Send + Sync> {
251 pub prediction_accuracy: A,
253 pub decision_quality: A,
255 pub adaptation_effectiveness: A,
257 pub transfer_success_rate: A,
259 pub generalization_performance: A,
261}
262
263pub struct StrategySelector<A: Float + Send + Sync> {
265 strategies: HashMap<String, AdaptationStrategy<A>>,
267 strategy_performance: HashMap<String, StrategyPerformance<A>>,
269 selection_policy: SelectionPolicy,
271 exploration_params: ExplorationParams<A>,
273}
274
275#[derive(Debug, Clone)]
277pub struct AdaptationStrategy<A: Float + Send + Sync> {
278 pub name: String,
280 pub parameters: HashMap<String, A>,
282 pub strategy_type: StrategyType,
284 pub conditions: Vec<StrategyCondition<A>>,
286 pub expected_outcomes: Vec<A>,
288}
289
290#[derive(Debug, Clone)]
292pub enum StrategyType {
293 Conservative,
295 Aggressive,
297 Balanced,
299 Reactive,
301 Proactive,
303 Custom(String),
305}
306
307#[derive(Debug, Clone)]
309pub struct StrategyCondition<A: Float + Send + Sync> {
310 pub condition_type: ConditionType,
312 pub threshold: A,
314 pub operator: ComparisonOperator,
316 pub weight: A,
318}
319
320#[derive(Debug, Clone)]
322pub enum ConditionType {
323 Performance,
325 ResourceUtilization,
327 DataQuality,
329 DriftDetection,
331 Temporal,
333 Custom(String),
335}
336
337#[derive(Debug, Clone)]
339pub enum ComparisonOperator {
340 GreaterThan,
342 LessThan,
344 EqualTo,
346 Between(f64, f64),
348 InSet(Vec<f64>),
350}
351
352#[derive(Debug, Clone)]
354pub struct StrategyPerformance<A: Float + Send + Sync> {
355 pub usage_count: usize,
357 pub success_rate: A,
359 pub avg_improvement: A,
361 pub best_improvement: A,
363 pub worst_outcome: A,
365 pub recent_trend: TrendDirection,
367 pub context_performance: HashMap<String, A>,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
373pub enum TrendDirection {
374 Improving,
376 Declining,
378 Stable,
380 Oscillating,
382}
383
384#[derive(Debug, Clone)]
386pub enum SelectionPolicy {
387 EpsilonGreedy { epsilon: f64 },
389 UCB { confidence_parameter: f64 },
391 ThompsonSampling,
393 Softmax { temperature: f64 },
395 ContextAware,
397 MultiArmedBandit,
399}
400
401#[derive(Debug, Clone)]
403pub struct ExplorationParams<A: Float + Send + Sync> {
404 pub exploration_rate: A,
406 pub exploration_decay: A,
408 pub min_exploration_rate: A,
410 pub curiosity_weight: A,
412 pub novelty_weight: A,
414}
415
416pub struct LearningRateAdapter<A: Float + Send + Sync> {
418 current_rate: A,
420 rate_history: VecDeque<A>,
422 min_rate: A,
424 max_rate: A,
425}
426
427#[derive(Debug, Clone)]
429pub enum LearningRateStrategy {
430 Fixed,
432 StepDecay { decay_factor: f64, step_size: usize },
434 ExponentialDecay { decay_rate: f64 },
436 PerformanceBased,
438 Cyclical {
440 min_lr: f64,
441 max_lr: f64,
442 cycle_length: usize,
443 },
444 Adaptive,
446}
447
448#[derive(Debug, Clone)]
450pub struct MetaLearningStatistics<A: Float + Send + Sync> {
451 pub total_experiences: usize,
453 pub training_episodes: usize,
455 pub avg_reward_per_episode: A,
457 pub best_episode_reward: A,
459 pub learning_progress: A,
461 pub strategy_selection_accuracy: A,
463 pub transfer_success_rate: A,
465 pub replay_effectiveness: A,
467}
468
469impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync + std::fmt::Debug> MetaLearner<A> {
470 pub fn new(config: &StreamingConfig) -> Result<Self, String> {
472 let meta_config = config.meta_learning_config.clone();
473
474 let experience_buffer = ExperienceBuffer::new(
475 &meta_config.replay_config,
476 meta_config.experience_buffer_size,
477 );
478 let meta_model = MetaModel::new(meta_config.model_complexity.clone())?;
479 let strategy_selector = StrategySelector::new();
480 let transfer_learning = TransferLearning::new();
481 let learning_rate_adapter = LearningRateAdapter::new(meta_config.meta_learning_rate);
482
483 let statistics = MetaLearningStatistics {
484 total_experiences: 0,
485 training_episodes: 0,
486 avg_reward_per_episode: A::zero(),
487 best_episode_reward: A::zero(),
488 learning_progress: A::zero(),
489 strategy_selection_accuracy: A::zero(),
490 transfer_success_rate: A::zero(),
491 replay_effectiveness: A::zero(),
492 };
493
494 Ok(Self {
495 config: meta_config,
496 experience_buffer,
497 meta_model,
498 strategy_selector,
499 transfer_learning,
500 statistics,
501 learning_rate_adapter,
502 episode_start: Instant::now(),
503 episode_initial_performance: None,
504 episode_adaptation_count: 0,
505 context_resource_state: Vec::new(),
506 context_drift_indicators: Vec::new(),
507 })
508 }
509
510 pub fn update_context_signals(&mut self, resource_state: Vec<A>, drift_indicators: Vec<A>) {
518 self.context_resource_state = resource_state;
519 self.context_drift_indicators = drift_indicators;
520 }
521
522 pub fn update_experience(
524 &mut self,
525 state: MetaState<A>,
526 action: MetaAction<A>,
527 reward: A,
528 ) -> Result<(), String> {
529 let priority = self.calculate_experience_priority(&state, &action, reward);
532 let experience = MetaExperience {
533 id: self.generate_experience_id(),
534 state,
535 action,
536 reward,
537 next_state: None, timestamp: Instant::now(),
539 episode_context: self.create_episode_context(reward)?,
540 priority,
541 replay_count: 0,
542 };
543
544 self.experience_buffer.add_experience(experience)?;
546
547 if self.episode_initial_performance.is_none() {
549 self.episode_initial_performance = Some(reward);
550 }
551 self.episode_adaptation_count = self.episode_adaptation_count.saturating_add(1);
552
553 self.statistics.total_experiences += 1;
555
556 let experiences = self.statistics.total_experiences;
562 let update_due = self.config.update_frequency > 0
563 && experiences.is_multiple_of(self.config.update_frequency);
564 let replay_due = self.config.replay_config.replay_frequency > 0
565 && experiences.is_multiple_of(self.config.replay_config.replay_frequency);
566 if update_due || replay_due {
567 self.trigger_learning()?;
568 }
569
570 Ok(())
571 }
572
573 fn generate_experience_id(&self) -> u64 {
575 self.statistics.total_experiences as u64 + 1
576 }
577
578 fn create_episode_context(&self, reward: A) -> Result<EpisodeContext<A>, String> {
587 let convert = |value: f64| -> Result<A, String> {
588 A::from(value).ok_or_else(|| format!("{value} is not representable"))
589 };
590 let outcome = if reward > convert(0.8)? {
591 EpisodeOutcome::Success
592 } else if reward > convert(0.5)? {
593 EpisodeOutcome::PartialSuccess
594 } else if reward > convert(0.2)? {
595 EpisodeOutcome::Neutral
596 } else if reward > convert(-0.2)? {
597 EpisodeOutcome::Failure
598 } else {
599 EpisodeOutcome::CriticalFailure
600 };
601
602 Ok(EpisodeContext {
603 episode_id: self.statistics.training_episodes as u64,
604 start_time: self.episode_start,
605 duration: self.episode_start.elapsed(),
607 initial_performance: self.episode_initial_performance.unwrap_or(reward),
610 final_performance: reward,
611 adaptation_count: self.episode_adaptation_count + 1,
614 outcome,
615 })
616 }
617
618 fn calculate_experience_priority(
620 &self,
621 state: &MetaState<A>,
622 action: &MetaAction<A>,
623 reward: A,
624 ) -> A {
625 let floor = scalar_or(1e-6, A::zero());
629 let priority = match self.config.replay_config.priority_method {
630 PriorityMethod::TDError => {
635 match self.meta_model.estimate_reward(state, action) {
636 Some(predicted) => (reward - predicted).abs(),
637 None => A::one(),
641 }
642 }
643 PriorityMethod::Surprise => {
646 let mean = self.statistics.avg_reward_per_episode;
647 let scale = mean.abs().max(A::one());
648 (reward - mean).abs() / scale
649 }
650 PriorityMethod::GradientMagnitude => action
652 .adaptation_magnitudes
653 .iter()
654 .fold(A::zero(), |acc, m| acc + m.abs()),
655 PriorityMethod::LossImprovement => {
657 (reward - self.statistics.best_episode_reward).max(A::zero())
658 }
659 PriorityMethod::Random => scalar_or(thread_rng().gen_range(0.0..1.0), A::one()),
661 };
662 priority.max(floor)
663 }
664
665 #[cfg(test)]
668 pub(crate) fn record_probe_experience_for_test(&mut self, reward: A) -> Result<(), String> {
669 let state = MetaState {
670 performance_metrics: vec![reward],
671 resource_state: vec![A::one()],
672 drift_indicators: vec![A::zero()],
673 adaptation_history: 0,
674 timestamp: Instant::now(),
675 };
676 let action = MetaAction {
677 adaptation_magnitudes: vec![reward],
678 adaptation_types: vec![AdaptationType::LearningRate],
679 learning_rate_change: reward,
680 buffer_size_change: A::zero(),
681 timestamp: Instant::now(),
682 };
683 self.update_experience(state, action, reward)
684 }
685
686 #[cfg(test)]
688 pub(crate) fn experience_count_for_test(&self) -> usize {
689 self.experience_buffer.experiences.len()
690 }
691
692 fn target_domain_characteristics(&self) -> Vec<A> {
697 let mut characteristics = self.context_resource_state.clone();
698 characteristics.extend(self.context_drift_indicators.iter().copied());
699 characteristics
700 }
701
702 pub fn register_transfer_source(
710 &mut self,
711 source_id: String,
712 experiences: Vec<MetaExperience<A>>,
713 source_characteristics: Vec<A>,
714 ) -> Result<(), String> {
715 if !self.config.enable_transfer_learning {
716 return Err(
717 "MetaLearningConfig::enable_transfer_learning is disabled, so no transfer \
718 source can be registered"
719 .to_string(),
720 );
721 }
722 self.transfer_learning
723 .register_source(source_id, experiences, source_characteristics);
724 Ok(())
725 }
726
727 pub fn transfer_metrics(&self) -> Option<&TransferMetrics<A>> {
730 self.config
731 .enable_transfer_learning
732 .then(|| self.transfer_learning.metrics())
733 }
734
735 fn trigger_learning(&mut self) -> Result<(), String> {
737 let mut training_batch = self
739 .experience_buffer
740 .sample_batch(self.config.replay_config.batch_size)?;
741
742 if self.config.enable_transfer_learning && self.transfer_learning.source_domain_count() > 0
745 {
746 let reward_before = self.meta_model.performance_metrics.prediction_accuracy;
747 let target_characteristics = self.target_domain_characteristics();
748 let transferred = self.transfer_learning.select_transfer_batch(
749 target_characteristics,
750 self.config.replay_config.batch_size,
751 );
752 if !transferred.is_empty() {
753 training_batch.extend(transferred);
754 self.meta_model.train_on_batch(&training_batch)?;
755 let reward_after = self.meta_model.performance_metrics.prediction_accuracy;
757 self.transfer_learning
758 .record_transfer_outcome(reward_before, reward_after);
759 self.statistics.transfer_success_rate = self
760 .transfer_learning
761 .metrics()
762 .success_rate
763 .unwrap_or_else(A::zero);
764 }
765 }
766
767 let weights: HashMap<u64, A> = training_batch
770 .iter()
771 .filter_map(|experience| {
772 self.experience_buffer
773 .importance_weight(experience.id)
774 .map(|weight| (experience.id, weight))
775 })
776 .collect();
777 self.meta_model
778 .train_on_weighted_batch(&training_batch, |id| {
779 weights.get(&id).copied().unwrap_or_else(A::one)
780 })?;
781
782 self.strategy_selector
784 .update_from_experiences(&training_batch)?;
785
786 self.statistics.training_episodes += 1;
789 if !training_batch.is_empty() {
790 let count = A::from(training_batch.len())
791 .ok_or_else(|| "batch size is not representable".to_string())?;
792 let total = training_batch
793 .iter()
794 .fold(A::zero(), |acc, experience| acc + experience.reward);
795 let mean = total / count;
796 self.statistics.avg_reward_per_episode = mean;
797 for experience in &training_batch {
798 if experience.reward > self.statistics.best_episode_reward {
799 self.statistics.best_episode_reward = experience.reward;
800 }
801 }
802 self.statistics.learning_progress =
805 self.meta_model.performance_metrics.prediction_accuracy;
806 self.statistics.strategy_selection_accuracy =
807 self.meta_model.performance_metrics.decision_quality;
808 self.statistics.replay_effectiveness = mean;
809 }
810
811 self.episode_start = Instant::now();
814 self.episode_initial_performance = None;
815 self.episode_adaptation_count = 0;
816
817 Ok(())
818 }
819
820 pub fn recommend_adaptations(
832 &mut self,
833 _current_data: &[StreamingDataPoint<A>],
834 performance_tracker: &PerformanceTracker<A>,
835 ) -> Result<Vec<Adaptation<A>>, String> {
836 let current_state = self.extract_meta_state(performance_tracker)?;
838
839 let predicted_action = self.meta_model.predict_action(¤t_state)?;
841
842 let strategy = self.strategy_selector.select_strategy(¤t_state)?;
844
845 let adaptations =
847 self.generate_adaptations_from_prediction(&predicted_action, &strategy)?;
848
849 Ok(adaptations)
850 }
851
852 fn extract_meta_state(
858 &self,
859 performance_tracker: &PerformanceTracker<A>,
860 ) -> Result<MetaState<A>, String> {
861 let recent_performance = performance_tracker.get_recent_performance(5);
863 let performance_metrics = if !recent_performance.is_empty() {
864 vec![
865 recent_performance[0].loss,
866 recent_performance[0].accuracy.unwrap_or(A::zero()),
867 recent_performance[0].convergence_rate.unwrap_or(A::zero()),
868 ]
869 } else {
870 vec![A::zero(), A::zero(), A::zero()]
871 };
872
873 let resource_state = self.context_resource_state.clone();
877 let drift_indicators = self.context_drift_indicators.clone();
878
879 Ok(MetaState {
880 performance_metrics,
881 resource_state,
882 drift_indicators,
883 adaptation_history: self.statistics.total_experiences,
884 timestamp: Instant::now(),
885 })
886 }
887
888 fn generate_adaptations_from_prediction(
890 &self,
891 predicted_action: &MetaAction<A>,
892 _strategy: &AdaptationStrategy<A>,
893 ) -> Result<Vec<Adaptation<A>>, String> {
894 let mut adaptations = Vec::new();
895
896 for (i, &magnitude) in predicted_action.adaptation_magnitudes.iter().enumerate() {
898 if magnitude.abs() > try_scalar_str::<A, _>(0.05)? {
899 let adaptation_type = if i < predicted_action.adaptation_types.len() {
901 predicted_action.adaptation_types[i].clone()
902 } else {
903 AdaptationType::LearningRate };
905
906 let adaptation = Adaptation {
907 adaptation_type,
908 magnitude,
909 target_component: "meta_learner".to_string(),
910 parameters: std::collections::HashMap::new(),
911 priority: if magnitude.abs() > try_scalar_str::<A, _>(0.3)? {
912 AdaptationPriority::High
913 } else {
914 AdaptationPriority::Normal
915 },
916 timestamp: Instant::now(),
917 };
918
919 adaptations.push(adaptation);
920 }
921 }
922
923 Ok(adaptations)
924 }
925
926 pub fn apply_adaptation(&mut self, adaptation: &Adaptation<A>) -> Result<(), String> {
928 match adaptation.adaptation_type {
929 AdaptationType::MetaLearning => {
930 let new_rate = self.learning_rate_adapter.current_rate + adaptation.magnitude;
932 self.learning_rate_adapter.update_rate(new_rate)?;
933 }
934 _ => {
935 }
937 }
938
939 Ok(())
940 }
941
942 pub fn get_effectiveness_score(&self) -> f32 {
944 self.statistics.learning_progress.to_f32().unwrap_or(0.0)
945 }
946
947 pub fn get_diagnostics(&self) -> MetaLearningDiagnostics {
949 MetaLearningDiagnostics {
950 total_experiences: self.statistics.total_experiences,
951 training_episodes: self.statistics.training_episodes,
952 current_learning_rate: self
953 .learning_rate_adapter
954 .current_rate
955 .to_f64()
956 .unwrap_or(0.0),
957 model_accuracy: self
958 .meta_model
959 .performance_metrics
960 .prediction_accuracy
961 .to_f64()
962 .unwrap_or(0.0),
963 strategy_count: self.strategy_selector.strategies.len(),
964 transfer_success_rate: self
965 .statistics
966 .transfer_success_rate
967 .to_f64()
968 .unwrap_or(0.0),
969 }
970 }
971}
972
973const IMPORTANCE_SAMPLING_BETA: f64 = 1.0;
978
979impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> ExperienceBuffer<A> {
980 fn new(config: &ExperienceReplayConfig, capacity: usize) -> Self {
985 let capacity = capacity.max(1);
986 Self {
987 config: config.clone(),
988 experiences: VecDeque::with_capacity(capacity.min(64 * 1024)),
989 priority_queue: VecDeque::new(),
990 importance_weights: HashMap::new(),
991 capacity,
992 }
993 }
994
995 fn add_experience(&mut self, experience: MetaExperience<A>) -> Result<(), String> {
996 while self.experiences.len() >= self.capacity {
998 if let Some(evicted) = self.experiences.pop_front() {
999 self.importance_weights.remove(&(evicted.id as usize));
1000 } else {
1001 break;
1002 }
1003 }
1004 self.experiences.push_back(experience.clone());
1005
1006 if self.config.enable_prioritized_replay {
1008 let priority = experience.priority;
1009 self.priority_queue.push_back((experience, priority));
1010
1011 self.priority_queue
1013 .make_contiguous()
1014 .sort_by(|a, b| crate::utils::total_order(&b.1, &a.1));
1015
1016 while self.priority_queue.len() > self.capacity {
1019 self.priority_queue.pop_back();
1020 }
1021 }
1022
1023 Ok(())
1024 }
1025
1026 fn record_importance_weights(&mut self, batch: &[MetaExperience<A>], total_priority: A) {
1036 self.importance_weights.clear();
1037 if !self.config.importance_sampling || batch.is_empty() || total_priority <= A::zero() {
1038 return;
1039 }
1040 let n = match A::from(self.experiences.len().max(1)) {
1041 Some(n) => n,
1042 None => return,
1043 };
1044 let beta = match A::from(IMPORTANCE_SAMPLING_BETA) {
1045 Some(beta) => beta,
1046 None => return,
1047 };
1048
1049 let mut raw: Vec<(usize, A)> = Vec::with_capacity(batch.len());
1050 let mut max_weight = A::zero();
1051 for experience in batch {
1052 let probability = experience.priority / total_priority;
1053 if probability <= A::zero() {
1054 continue;
1055 }
1056 let weight = (A::one() / (n * probability)).powf(beta);
1057 if weight > max_weight {
1058 max_weight = weight;
1059 }
1060 raw.push((experience.id as usize, weight));
1061 }
1062 if max_weight <= A::zero() {
1063 return;
1064 }
1065 for (id, weight) in raw {
1066 self.importance_weights.insert(id, weight / max_weight);
1067 }
1068 }
1069
1070 fn importance_weight(&self, experience_id: u64) -> Option<A> {
1073 self.importance_weights
1074 .get(&(experience_id as usize))
1075 .copied()
1076 }
1077
1078 fn sample_batch(&mut self, batch_size: usize) -> Result<Vec<MetaExperience<A>>, String> {
1079 if self.experiences.is_empty() {
1080 return Ok(Vec::new());
1081 }
1082
1083 let mut batch = Vec::with_capacity(batch_size);
1084 let total_priority: A = self.experiences.iter().map(|e| e.priority).sum();
1085
1086 if self.config.enable_prioritized_replay && !self.priority_queue.is_empty() {
1087 for _ in 0..batch_size.min(self.priority_queue.len()) {
1089 if let Some((experience, _)) = self.priority_queue.pop_front() {
1090 batch.push(experience);
1091 }
1092 }
1093 } else {
1094 for _ in 0..batch_size.min(self.experiences.len()) {
1096 let idx = thread_rng().gen_range(0..self.experiences.len());
1097 if let Some(experience) = self.experiences.get(idx) {
1098 batch.push(experience.clone());
1099 }
1100 }
1101 }
1102
1103 self.record_importance_weights(&batch, total_priority);
1104 Ok(batch)
1105 }
1106}
1107
1108impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> MetaModel<A> {
1109 fn new(complexity: MetaModelComplexity) -> Result<Self, String> {
1110 let parameters = match complexity {
1111 MetaModelComplexity::Low => MetaModelParameters {
1112 weights: vec![vec![try_scalar_str::<A, _>(0.1)?; 10]; 2],
1113 biases: vec![A::zero(); 10],
1114 learning_rate: A::from(0.5).unwrap_or_else(A::one),
1117 regularization: RegularizationParams {
1118 l1_lambda: try_scalar_str::<A, _>(0.001)?,
1119 l2_lambda: try_scalar_str::<A, _>(0.001)?,
1120 dropout_rate: try_scalar_str::<A, _>(0.1)?,
1121 early_stopping_patience: 10,
1122 },
1123 optimization: OptimizationParams {
1124 momentum: try_scalar_str::<A, _>(0.9)?,
1125 beta1: try_scalar_str::<A, _>(0.9)?,
1126 beta2: try_scalar_str::<A, _>(0.999)?,
1127 epsilon: try_scalar_str::<A, _>(1e-8)?,
1128 grad_clip_threshold: try_scalar_str::<A, _>(1.0)?,
1129 },
1130 },
1131 _ => MetaModelParameters {
1132 weights: vec![vec![try_scalar_str::<A, _>(0.1)?; 50]; 3],
1133 biases: vec![A::zero(); 50],
1134 learning_rate: A::from(0.3).unwrap_or_else(A::one),
1136 regularization: RegularizationParams {
1137 l1_lambda: try_scalar_str::<A, _>(0.0001)?,
1138 l2_lambda: try_scalar_str::<A, _>(0.0001)?,
1139 dropout_rate: try_scalar_str::<A, _>(0.2)?,
1140 early_stopping_patience: 20,
1141 },
1142 optimization: OptimizationParams {
1143 momentum: try_scalar_str::<A, _>(0.9)?,
1144 beta1: try_scalar_str::<A, _>(0.9)?,
1145 beta2: try_scalar_str::<A, _>(0.999)?,
1146 epsilon: try_scalar_str::<A, _>(1e-8)?,
1147 grad_clip_threshold: try_scalar_str::<A, _>(1.0)?,
1148 },
1149 },
1150 };
1151
1152 let arms: Vec<BanditArm<A>> = arm_table()
1153 .into_iter()
1154 .map(|(prefix, adaptation_type, magnitude)| {
1155 BanditArm::new(format!("{prefix}:{magnitude}"), adaptation_type, magnitude)
1156 })
1157 .collect();
1158
1159 Ok(Self {
1160 parameters,
1161 training_history: VecDeque::with_capacity(1000),
1162 performance_metrics: ModelPerformanceMetrics {
1163 prediction_accuracy: A::zero(),
1167 decision_quality: A::zero(),
1168 adaptation_effectiveness: A::zero(),
1169 transfer_success_rate: A::zero(),
1170 generalization_performance: A::zero(),
1171 },
1172 feature_importance: Vec::new(),
1173 arms,
1174 training_steps: 0,
1175 feature_scaler: FeatureScaler::default(),
1176 })
1177 }
1178
1179 fn train_on_batch(&mut self, batch: &[MetaExperience<A>]) -> Result<(), String> {
1191 self.train_on_weighted_batch(batch, |_| A::one())
1192 }
1193
1194 fn train_on_weighted_batch(
1205 &mut self,
1206 batch: &[MetaExperience<A>],
1207 weight_of: impl Fn(u64) -> A,
1208 ) -> Result<(), String> {
1209 if batch.is_empty() {
1210 return Ok(());
1211 }
1212
1213 let training_started = Instant::now();
1214
1215 for experience in batch {
1218 self.feature_scaler
1219 .observe(&state_features(&experience.state));
1220 }
1221
1222 let mut squared_error_total = A::zero();
1223 let mut scale_total = A::zero();
1224 let mut trained = 0usize;
1225
1226 for experience in batch {
1227 let features = self
1228 .feature_scaler
1229 .standardize(&state_features(&experience.state));
1230 let arm = match arm_index_for(&experience.action) {
1231 Some(index) => index,
1232 None => continue,
1235 };
1236
1237 let predicted = self.arms[arm].predict(&features);
1238 let error = predicted - experience.reward;
1239 squared_error_total = squared_error_total + error * error;
1240 scale_total = scale_total + experience.reward.abs();
1241
1242 let learning_rate = self.parameters.learning_rate * weight_of(experience.id);
1245 let l2 = self.parameters.regularization.l2_lambda;
1246 self.arms[arm].sgd_step(&features, error, learning_rate, l2);
1247 self.arms[arm].observe(experience.reward);
1248 trained += 1;
1249 }
1250
1251 if trained == 0 {
1252 return Ok(());
1253 }
1254
1255 let count =
1256 A::from(trained).ok_or_else(|| format!("batch size {trained} is not representable"))?;
1257 let rmse = (squared_error_total / count).sqrt();
1258 let mean_scale = (scale_total / count)
1259 .max(A::from(1e-8).ok_or_else(|| "1e-8 is not representable".to_string())?);
1260
1261 let accuracy = (A::one() - (rmse / mean_scale).min(A::one())).max(A::zero());
1265 self.performance_metrics.prediction_accuracy = accuracy;
1266
1267 let average_reward = self.average_observed_reward();
1271 let better = batch
1272 .iter()
1273 .filter(|experience| experience.reward > average_reward)
1274 .count();
1275 if let Some(fraction) = A::from(better as f64 / batch.len() as f64) {
1276 self.performance_metrics.decision_quality = fraction;
1277 }
1278
1279 self.training_steps += 1;
1281 if self.training_history.len() >= 1000 {
1282 self.training_history.pop_front();
1283 }
1284 self.training_history.push_back(TrainingEpisode {
1285 episode_id: self.training_steps as u64,
1286 training_loss: rmse,
1287 validation_loss: rmse,
1291 training_accuracy: accuracy,
1292 validation_accuracy: accuracy,
1293 duration: training_started.elapsed(),
1294 timestamp: Instant::now(),
1295 });
1296
1297 if accuracy > A::from(0.9).unwrap_or_else(A::one) {
1302 let decay = A::from(0.99).unwrap_or_else(A::one);
1303 let floor = A::from(NLMS_STEP_FLOOR).unwrap_or_else(A::zero);
1304 self.parameters.learning_rate = (self.parameters.learning_rate * decay).max(floor);
1305 }
1306
1307 self.feature_importance = self.mean_absolute_weights();
1310
1311 Ok(())
1312 }
1313
1314 fn predict_action(&self, state: &MetaState<A>) -> Result<MetaAction<A>, String> {
1325 let features = self.feature_scaler.standardize(&state_features(state));
1326 if self.arms.is_empty() {
1327 return Err("meta-model has no action arms".to_string());
1328 }
1329
1330 let mut best_index = 0usize;
1331 let mut best_value = self.arms[0].predict(&features);
1332 for (index, arm) in self.arms.iter().enumerate().skip(1) {
1333 let value = arm.predict(&features);
1334 if value > best_value {
1335 best_value = value;
1336 best_index = index;
1337 }
1338 }
1339
1340 let arm = &self.arms[best_index];
1341 let magnitude = arm
1342 .magnitude()
1343 .ok_or_else(|| "arm magnitude is not representable".to_string())?;
1344
1345 let (learning_rate_change, buffer_size_change) = match arm.adaptation_type {
1346 AdaptationType::LearningRate => (magnitude, A::zero()),
1347 AdaptationType::BufferSize => (A::zero(), magnitude),
1348 _ => (A::zero(), A::zero()),
1349 };
1350
1351 Ok(MetaAction {
1352 adaptation_magnitudes: vec![magnitude],
1353 adaptation_types: vec![arm.adaptation_type.clone()],
1354 learning_rate_change,
1355 buffer_size_change,
1356 timestamp: Instant::now(),
1357 })
1358 }
1359
1360 fn estimate_reward(&self, state: &MetaState<A>, action: &MetaAction<A>) -> Option<A> {
1368 let index = arm_index_for(action)?;
1369 let arm = self.arms.get(index)?;
1370 if arm.pulls == 0 {
1371 return None;
1372 }
1373 let features = self.feature_scaler.standardize(&state_features(state));
1374 Some(arm.predict(&features))
1375 }
1376
1377 fn average_observed_reward(&self) -> A {
1379 let total_pulls: usize = self.arms.iter().map(|arm| arm.pulls).sum();
1380 if total_pulls == 0 {
1381 return A::zero();
1382 }
1383 let Some(count) = A::from(total_pulls) else {
1384 return A::zero();
1385 };
1386 let total: A = self
1387 .arms
1388 .iter()
1389 .fold(A::zero(), |acc, arm| acc + arm.reward_total);
1390 total / count
1391 }
1392
1393 fn mean_absolute_weights(&self) -> Vec<A> {
1395 let width = self
1396 .arms
1397 .iter()
1398 .map(|arm| arm.weights.len())
1399 .max()
1400 .unwrap_or(0);
1401 let Some(arm_count) = A::from(self.arms.len().max(1)) else {
1402 return Vec::new();
1403 };
1404 (0..width)
1405 .map(|index| {
1406 let total = self.arms.iter().fold(A::zero(), |acc, arm| {
1407 acc + arm
1408 .weights
1409 .get(index)
1410 .map(|w| w.abs())
1411 .unwrap_or_else(A::zero)
1412 });
1413 total / arm_count
1414 })
1415 .collect()
1416 }
1417
1418 pub fn arm_pull_counts(&self) -> Vec<(String, usize)> {
1420 self.arms
1421 .iter()
1422 .map(|arm| (arm.label.clone(), arm.pulls))
1423 .collect()
1424 }
1425}
1426
1427impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> StrategySelector<A> {
1431 fn new() -> Self {
1432 let mut strategies = HashMap::new();
1433
1434 strategies.insert(
1436 "conservative".to_string(),
1437 AdaptationStrategy {
1438 name: "conservative".to_string(),
1439 parameters: HashMap::new(),
1440 strategy_type: StrategyType::Conservative,
1441 conditions: Vec::new(),
1442 expected_outcomes: vec![scalar_or(0.05, A::zero())],
1443 },
1444 );
1445
1446 strategies.insert(
1447 "aggressive".to_string(),
1448 AdaptationStrategy {
1449 name: "aggressive".to_string(),
1450 parameters: HashMap::new(),
1451 strategy_type: StrategyType::Aggressive,
1452 expected_outcomes: vec![A::from(0.2).unwrap_or_else(A::zero)],
1455 conditions: Vec::new(),
1456 },
1457 );
1458
1459 Self {
1460 strategies,
1461 strategy_performance: HashMap::new(),
1462 selection_policy: SelectionPolicy::EpsilonGreedy { epsilon: 0.1 },
1463 exploration_params: ExplorationParams {
1464 exploration_rate: scalar_or(0.1, A::zero()),
1465 exploration_decay: scalar_or(0.99, A::zero()),
1466 min_exploration_rate: scalar_or(0.01, A::zero()),
1467 curiosity_weight: scalar_or(0.1, A::zero()),
1468 novelty_weight: scalar_or(0.1, A::zero()),
1469 },
1470 }
1471 }
1472
1473 fn select_strategy(&self, state: &MetaState<A>) -> Result<AdaptationStrategy<A>, String> {
1485 if self.strategies.is_empty() {
1486 return Err("No strategies available".to_string());
1487 }
1488
1489 let mut names: Vec<&String> = self.strategies.keys().collect();
1491 names.sort();
1492
1493 let scored: Vec<(&String, A, usize)> = names
1497 .iter()
1498 .map(|name| {
1499 let (value, usage) = match self.strategy_performance.get(*name) {
1500 Some(performance) if performance.usage_count > 0 => {
1501 (performance.avg_improvement, performance.usage_count)
1502 }
1503 _ => {
1504 let prior = self
1505 .strategies
1506 .get(*name)
1507 .and_then(|strategy| strategy.expected_outcomes.first().copied())
1508 .unwrap_or_else(A::zero);
1509 (prior, 0)
1510 }
1511 };
1512 (*name, value, usage)
1513 })
1514 .collect();
1515
1516 let chosen = match &self.selection_policy {
1517 SelectionPolicy::EpsilonGreedy { epsilon } => {
1518 let effective_epsilon = self
1519 .exploration_params
1520 .exploration_rate
1521 .to_f64()
1522 .unwrap_or(*epsilon)
1523 .max(
1524 self.exploration_params
1525 .min_exploration_rate
1526 .to_f64()
1527 .unwrap_or(0.0),
1528 )
1529 .clamp(0.0, 1.0);
1530 if thread_rng().gen_range(0.0..1.0) < effective_epsilon {
1531 scored
1534 .iter()
1535 .min_by(|a, b| a.2.cmp(&b.2).then_with(|| a.0.cmp(b.0)))
1536 .map(|entry| entry.0)
1537 } else {
1538 best_by_value(&scored)
1539 }
1540 }
1541 SelectionPolicy::UCB {
1542 confidence_parameter,
1543 } => {
1544 let total_usage: usize = scored.iter().map(|entry| entry.2).sum();
1545 let total = (total_usage.max(1) as f64).ln();
1546 let c = *confidence_parameter;
1547 scored
1548 .iter()
1549 .max_by(|a, b| {
1550 let score_a = ucb_score(a.1, a.2, total, c);
1551 let score_b = ucb_score(b.1, b.2, total, c);
1552 score_a
1553 .partial_cmp(&score_b)
1554 .unwrap_or(std::cmp::Ordering::Equal)
1555 .then_with(|| b.0.cmp(a.0))
1556 })
1557 .map(|entry| entry.0)
1558 }
1559 SelectionPolicy::Softmax { temperature } => {
1560 let temperature = temperature.abs().max(1e-6);
1561 let values: Vec<f64> = scored
1562 .iter()
1563 .map(|entry| entry.1.to_f64().unwrap_or(0.0) / temperature)
1564 .collect();
1565 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1566 let exponentials: Vec<f64> =
1567 values.iter().map(|value| (value - max).exp()).collect();
1568 let total: f64 = exponentials.iter().sum();
1569 if total <= 0.0 {
1570 best_by_value(&scored)
1571 } else {
1572 let mut draw = thread_rng().gen_range(0.0..total);
1573 let mut selected = scored.last().map(|entry| entry.0);
1574 for (entry, weight) in scored.iter().zip(exponentials.iter()) {
1575 draw -= *weight;
1576 if draw <= 0.0 {
1577 selected = Some(entry.0);
1578 break;
1579 }
1580 }
1581 selected
1582 }
1583 }
1584 SelectionPolicy::ThompsonSampling | SelectionPolicy::MultiArmedBandit => {
1585 scored
1589 .iter()
1590 .map(|entry| {
1591 let scale = 1.0 / ((entry.2 as f64) + 1.0).sqrt();
1592 let noise = thread_rng().gen_range(-1.0..1.0) * scale;
1593 (entry.0, entry.1.to_f64().unwrap_or(0.0) + noise)
1594 })
1595 .max_by(|a, b| {
1596 a.1.partial_cmp(&b.1)
1597 .unwrap_or(std::cmp::Ordering::Equal)
1598 .then_with(|| b.0.cmp(a.0))
1599 })
1600 .map(|entry| entry.0)
1601 }
1602 SelectionPolicy::ContextAware => {
1603 let context_key = context_key_for(state);
1606 scored
1607 .iter()
1608 .max_by(|a, b| {
1609 let value_a = self
1610 .strategy_performance
1611 .get(a.0)
1612 .and_then(|p| p.context_performance.get(&context_key).copied())
1613 .unwrap_or(a.1);
1614 let value_b = self
1615 .strategy_performance
1616 .get(b.0)
1617 .and_then(|p| p.context_performance.get(&context_key).copied())
1618 .unwrap_or(b.1);
1619 value_a
1620 .partial_cmp(&value_b)
1621 .unwrap_or(std::cmp::Ordering::Equal)
1622 .then_with(|| b.0.cmp(a.0))
1623 })
1624 .map(|entry| entry.0)
1625 }
1626 };
1627
1628 let name = chosen.ok_or_else(|| "strategy selection produced no candidate".to_string())?;
1629 self.strategies
1630 .get(name)
1631 .cloned()
1632 .ok_or_else(|| format!("selected strategy '{name}' is not registered"))
1633 }
1634
1635 fn update_from_experiences(&mut self, experiences: &[MetaExperience<A>]) -> Result<(), String> {
1644 if experiences.is_empty() {
1645 return Ok(());
1646 }
1647
1648 let success_threshold =
1649 A::from(0.5).ok_or_else(|| "0.5 is not representable".to_string())?;
1650
1651 for experience in experiences {
1652 let strategy_name = strategy_name_for(&experience.action);
1653 if !self.strategies.contains_key(&strategy_name) {
1654 continue;
1655 }
1656 let context_key = context_key_for(&experience.state);
1657 let reward = experience.reward;
1658
1659 let entry = self
1660 .strategy_performance
1661 .entry(strategy_name)
1662 .or_insert_with(|| StrategyPerformance {
1663 usage_count: 0,
1664 success_rate: A::zero(),
1665 avg_improvement: A::zero(),
1666 best_improvement: reward,
1667 worst_outcome: reward,
1668 recent_trend: TrendDirection::Stable,
1669 context_performance: HashMap::new(),
1670 });
1671
1672 let previous_average = entry.avg_improvement;
1673 entry.usage_count = entry.usage_count.saturating_add(1);
1674 let count = A::from(entry.usage_count)
1675 .ok_or_else(|| "usage count is not representable".to_string())?;
1676
1677 entry.avg_improvement = previous_average + (reward - previous_average) / count;
1679
1680 let success = if reward > success_threshold {
1682 A::one()
1683 } else {
1684 A::zero()
1685 };
1686 entry.success_rate = entry.success_rate + (success - entry.success_rate) / count;
1687
1688 if reward > entry.best_improvement {
1689 entry.best_improvement = reward;
1690 }
1691 if reward < entry.worst_outcome {
1692 entry.worst_outcome = reward;
1693 }
1694
1695 entry.recent_trend = if entry.avg_improvement > previous_average {
1696 TrendDirection::Improving
1697 } else if entry.avg_improvement < previous_average {
1698 TrendDirection::Declining
1699 } else {
1700 TrendDirection::Stable
1701 };
1702
1703 let context_entry = entry
1705 .context_performance
1706 .entry(context_key)
1707 .or_insert_with(|| reward);
1708 let smoothing = A::from(0.2).unwrap_or_else(A::one);
1709 *context_entry = smoothing * reward + (A::one() - smoothing) * *context_entry;
1710 }
1711
1712 let decayed =
1715 self.exploration_params.exploration_rate * self.exploration_params.exploration_decay;
1716 self.exploration_params.exploration_rate =
1717 decayed.max(self.exploration_params.min_exploration_rate);
1718
1719 Ok(())
1720 }
1721
1722 pub fn strategy_performance_for(&self, name: &str) -> Option<&StrategyPerformance<A>> {
1724 self.strategy_performance.get(name)
1725 }
1726
1727 pub fn exploration_rate(&self) -> A {
1729 self.exploration_params.exploration_rate
1730 }
1731}
1732
1733fn best_by_value<'a, A: Float + Send + Sync>(
1735 scored: &'a [(&'a String, A, usize)],
1736) -> Option<&'a String> {
1737 scored
1738 .iter()
1739 .max_by(|a, b| {
1740 a.1.partial_cmp(&b.1)
1741 .unwrap_or(std::cmp::Ordering::Equal)
1742 .then_with(|| b.0.cmp(a.0))
1743 })
1744 .map(|entry| entry.0)
1745}
1746
1747fn ucb_score<A: Float + Send + Sync>(
1750 value: A,
1751 usage: usize,
1752 log_total_usage: f64,
1753 confidence: f64,
1754) -> f64 {
1755 let mean = value.to_f64().unwrap_or(0.0);
1756 if usage == 0 {
1757 return f64::INFINITY;
1759 }
1760 mean + confidence * (log_total_usage / usage as f64).sqrt()
1761}
1762
1763fn context_key_for<A: Float + Send + Sync>(state: &MetaState<A>) -> String {
1768 let loss = state
1769 .performance_metrics
1770 .first()
1771 .and_then(|value| value.to_f64())
1772 .unwrap_or(0.0);
1773 let drift = state
1774 .drift_indicators
1775 .first()
1776 .and_then(|value| value.to_f64())
1777 .unwrap_or(0.0);
1778 let loss_band = if loss <= 0.1 {
1779 "loss:low"
1780 } else if loss <= 1.0 {
1781 "loss:mid"
1782 } else {
1783 "loss:high"
1784 };
1785 let drift_band = if drift < 0.5 {
1786 "drift:none"
1787 } else if drift < 1.5 {
1788 "drift:warning"
1789 } else {
1790 "drift:active"
1791 };
1792 format!("{loss_band}|{drift_band}")
1793}
1794
1795fn strategy_name_for<A: Float + Send + Sync>(action: &MetaAction<A>) -> String {
1801 let magnitude = action
1802 .adaptation_magnitudes
1803 .first()
1804 .and_then(|value| value.to_f64())
1805 .map(f64::abs)
1806 .unwrap_or(0.0);
1807 if magnitude >= AGGRESSIVE_MAGNITUDE_THRESHOLD {
1808 "aggressive".to_string()
1809 } else {
1810 "conservative".to_string()
1811 }
1812}
1813
1814const AGGRESSIVE_MAGNITUDE_THRESHOLD: f64 = 0.15;
1817
1818const NLMS_STEP_FLOOR: f64 = 0.01;
1821
1822impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> LearningRateAdapter<A> {
1823 fn new(initial_rate: f64) -> Self {
1824 Self {
1825 current_rate: scalar_or(initial_rate, A::zero()),
1826 rate_history: VecDeque::with_capacity(100),
1827 min_rate: scalar_or(1e-6, A::zero()),
1828 max_rate: scalar_or(0.1, A::zero()),
1829 }
1830 }
1831
1832 fn update_rate(&mut self, new_rate: A) -> Result<(), String> {
1833 self.current_rate = new_rate.max(self.min_rate).min(self.max_rate);
1834
1835 if self.rate_history.len() >= 100 {
1836 self.rate_history.pop_front();
1837 }
1838 self.rate_history.push_back(self.current_rate);
1839
1840 Ok(())
1841 }
1842}
1843
1844#[derive(Debug, Clone)]
1846pub struct MetaLearningDiagnostics {
1847 pub total_experiences: usize,
1848 pub training_episodes: usize,
1849 pub current_learning_rate: f64,
1850 pub model_accuracy: f64,
1851 pub strategy_count: usize,
1852 pub transfer_success_rate: f64,
1853}
1854
1855#[cfg(test)]
1856#[path = "meta_learning_regression_tests.rs"]
1857mod regression_tests;