optirs_core/streaming/adaptive_streaming/
meta_transfer.rs1use super::meta_learning::MetaExperience;
20use scirs2_core::numeric::Float;
21use std::collections::HashMap;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TransferStrategy {
26 ParameterTransfer,
28 FeatureTransfer,
30 InstanceTransfer,
32 RelationalTransfer,
34 MetaTransfer,
36}
37
38impl TransferStrategy {
39 pub fn is_supported(self) -> bool {
47 matches!(self, TransferStrategy::InstanceTransfer)
48 }
49}
50
51#[derive(Debug, Clone)]
54pub struct DomainAdaptation<A: Float + Send + Sync> {
55 source_characteristics: Vec<A>,
57 target_characteristics: Vec<A>,
59 adaptation_weights: Vec<A>,
63 domain_similarity: Option<A>,
66}
67
68impl<A: Float + Send + Sync> Default for DomainAdaptation<A> {
69 fn default() -> Self {
70 Self {
71 source_characteristics: Vec::new(),
72 target_characteristics: Vec::new(),
73 adaptation_weights: Vec::new(),
74 domain_similarity: None,
75 }
76 }
77}
78
79impl<A: Float + Send + Sync> DomainAdaptation<A> {
80 pub fn set_source_characteristics(&mut self, characteristics: Vec<A>) {
82 self.source_characteristics = characteristics;
83 self.recompute();
84 }
85
86 pub fn set_target_characteristics(&mut self, characteristics: Vec<A>) {
88 self.target_characteristics = characteristics;
89 self.recompute();
90 }
91
92 pub fn domain_similarity(&self) -> Option<A> {
95 self.domain_similarity
96 }
97
98 pub fn adaptation_weights(&self) -> &[A] {
101 &self.adaptation_weights
102 }
103
104 fn recompute(&mut self) {
105 let shared = self
106 .source_characteristics
107 .len()
108 .min(self.target_characteristics.len());
109 if shared == 0 {
110 self.domain_similarity = None;
111 self.adaptation_weights.clear();
112 return;
113 }
114
115 let mut dot = A::zero();
116 let mut source_norm = A::zero();
117 let mut target_norm = A::zero();
118 self.adaptation_weights.clear();
119 for index in 0..shared {
120 let source = self.source_characteristics[index];
121 let target = self.target_characteristics[index];
122 dot = dot + source * target;
123 source_norm = source_norm + source * source;
124 target_norm = target_norm + target * target;
125 self.adaptation_weights.push(if source == A::zero() {
126 A::one()
127 } else {
128 target / source
129 });
130 }
131
132 self.domain_similarity = if source_norm > A::zero() && target_norm > A::zero() {
133 Some(dot / (source_norm.sqrt() * target_norm.sqrt()))
134 } else {
135 None
136 };
137 }
138}
139
140#[derive(Debug, Clone)]
146pub struct TransferMetrics<A: Float + Send + Sync> {
147 pub success_rate: Option<A>,
150 pub improvement: Option<A>,
152 pub efficiency: Option<A>,
155 pub negative_transfer_count: usize,
157 pub evaluated_transfers: usize,
159}
160
161impl<A: Float + Send + Sync> Default for TransferMetrics<A> {
162 fn default() -> Self {
163 Self {
164 success_rate: None,
165 improvement: None,
166 efficiency: None,
167 negative_transfer_count: 0,
168 evaluated_transfers: 0,
169 }
170 }
171}
172
173pub const MIN_TRANSFER_SIMILARITY: f64 = 0.5;
177
178#[derive(Debug, Clone)]
181pub struct TransferLearning<A: Float + Send + Sync> {
182 source_experiences: HashMap<String, Vec<MetaExperience<A>>>,
184 transfer_strategies: Vec<TransferStrategy>,
186 domain_adaptation: DomainAdaptation<A>,
188 transfer_metrics: TransferMetrics<A>,
190 improvement_total: A,
193 successful_transfers: usize,
195}
196
197impl<A: Float + Send + Sync + Clone> Default for TransferLearning<A> {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203impl<A: Float + Send + Sync + Clone> TransferLearning<A> {
204 pub fn new() -> Self {
206 Self {
207 source_experiences: HashMap::new(),
208 transfer_strategies: vec![TransferStrategy::InstanceTransfer],
209 domain_adaptation: DomainAdaptation::default(),
210 transfer_metrics: TransferMetrics::default(),
211 improvement_total: A::zero(),
212 successful_transfers: 0,
213 }
214 }
215
216 pub fn set_strategies(&mut self, strategies: Vec<TransferStrategy>) -> Result<(), String> {
221 if let Some(unsupported) = strategies.iter().find(|s| !s.is_supported()) {
222 return Err(format!(
223 "transfer strategy {unsupported:?} is not implemented: the meta-learner \
224 stores experiences, not model parameters, feature extractors or \
225 relational structure; only InstanceTransfer is supported"
226 ));
227 }
228 if strategies.is_empty() {
229 return Err("at least one transfer strategy is required".to_string());
230 }
231 self.transfer_strategies = strategies;
232 Ok(())
233 }
234
235 pub fn strategies(&self) -> &[TransferStrategy] {
237 &self.transfer_strategies
238 }
239
240 pub fn register_source(
242 &mut self,
243 source_id: String,
244 experiences: Vec<MetaExperience<A>>,
245 characteristics: Vec<A>,
246 ) {
247 self.domain_adaptation
248 .set_source_characteristics(characteristics);
249 self.source_experiences.insert(source_id, experiences);
250 }
251
252 pub fn source_domain_count(&self) -> usize {
254 self.source_experiences.len()
255 }
256
257 pub fn metrics(&self) -> &TransferMetrics<A> {
259 &self.transfer_metrics
260 }
261
262 pub fn domain_similarity(&self) -> Option<A> {
264 self.domain_adaptation.domain_similarity()
265 }
266
267 pub fn select_transfer_batch(
275 &mut self,
276 target_characteristics: Vec<A>,
277 limit: usize,
278 ) -> Vec<MetaExperience<A>> {
279 self.domain_adaptation
280 .set_target_characteristics(target_characteristics);
281
282 let available: usize = self.source_experiences.values().map(Vec::len).sum();
283 if available == 0 || limit == 0 {
284 return Vec::new();
285 }
286
287 let Some(similarity) = self.domain_adaptation.domain_similarity() else {
288 return Vec::new();
289 };
290 let threshold = A::from(MIN_TRANSFER_SIMILARITY).unwrap_or_else(A::zero);
291 if similarity < threshold {
292 self.record_efficiency(0, available);
293 return Vec::new();
294 }
295
296 let mut candidates: Vec<MetaExperience<A>> = self
299 .source_experiences
300 .values()
301 .flat_map(|batch| batch.iter().cloned())
302 .collect();
303 candidates.sort_by(|a, b| crate::utils::total_order(&b.priority, &a.priority));
304 candidates.truncate(limit);
305
306 for experience in &mut candidates {
310 experience.priority = experience.priority * similarity;
311 }
312
313 self.record_efficiency(candidates.len(), available);
314 candidates
315 }
316
317 pub fn record_transfer_outcome(&mut self, reward_before: A, reward_after: A) {
320 let improvement = reward_after - reward_before;
321 self.transfer_metrics.evaluated_transfers += 1;
322 self.improvement_total = self.improvement_total + improvement;
323
324 if improvement > A::zero() {
325 self.successful_transfers += 1;
326 } else if improvement < A::zero() {
327 self.transfer_metrics.negative_transfer_count += 1;
328 }
329
330 if let Some(evaluated) = A::from(self.transfer_metrics.evaluated_transfers) {
331 if evaluated > A::zero() {
332 self.transfer_metrics.improvement = Some(self.improvement_total / evaluated);
333 self.transfer_metrics.success_rate =
334 A::from(self.successful_transfers).map(|successes| successes / evaluated);
335 }
336 }
337 }
338
339 fn record_efficiency(&mut self, retained: usize, available: usize) {
340 if available == 0 {
341 self.transfer_metrics.efficiency = None;
342 return;
343 }
344 let (Some(retained), Some(available)) = (A::from(retained), A::from(available)) else {
345 return;
346 };
347 self.transfer_metrics.efficiency = Some(retained / available);
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::streaming::adaptive_streaming::meta_learning::{
355 EpisodeContext, EpisodeOutcome, MetaAction, MetaExperience, MetaState,
356 };
357 use crate::streaming::adaptive_streaming::optimizer::AdaptationType;
358 use std::time::{Duration, Instant};
359
360 fn experience(priority: f64, reward: f64) -> MetaExperience<f64> {
361 MetaExperience {
362 id: 1,
363 state: MetaState {
364 performance_metrics: vec![reward],
365 resource_state: vec![1.0],
366 drift_indicators: vec![0.0],
367 adaptation_history: 0,
368 timestamp: Instant::now(),
369 },
370 action: MetaAction {
371 adaptation_magnitudes: vec![0.1],
372 adaptation_types: vec![AdaptationType::LearningRate],
373 learning_rate_change: 0.1,
374 buffer_size_change: 0.0,
375 timestamp: Instant::now(),
376 },
377 reward,
378 next_state: None,
379 timestamp: Instant::now(),
380 episode_context: EpisodeContext {
381 episode_id: 0,
382 start_time: Instant::now(),
383 duration: Duration::ZERO,
384 initial_performance: 0.0,
385 final_performance: reward,
386 adaptation_count: 1,
387 outcome: EpisodeOutcome::Neutral,
388 },
389 priority,
390 replay_count: 0,
391 }
392 }
393
394 #[test]
397 fn fresh_metrics_are_absent_not_fabricated() {
398 let transfer = TransferLearning::<f64>::new();
399 let metrics = transfer.metrics();
400 assert!(metrics.success_rate.is_none());
401 assert!(metrics.improvement.is_none());
402 assert!(metrics.efficiency.is_none());
403 assert_eq!(metrics.evaluated_transfers, 0);
404 assert!(
405 transfer.domain_similarity().is_none(),
406 "no domain has been described yet, so there is no similarity to report"
407 );
408 }
409
410 #[test]
413 fn similar_domains_transfer_with_similarity_scaled_priorities() {
414 let mut transfer = TransferLearning::<f64>::new();
415 transfer.register_source(
416 "source".to_string(),
417 vec![experience(1.0, 1.0), experience(0.2, 0.0)],
418 vec![1.0, 1.0],
419 );
420
421 let batch = transfer.select_transfer_batch(vec![1.0, 1.0], 2);
422 assert_eq!(batch.len(), 2, "identical domains must transfer everything");
423 let similarity = transfer.domain_similarity().expect("similarity");
424 assert!((similarity - 1.0).abs() < 1e-9, "similarity = {similarity}");
425 assert!((batch[0].priority - 1.0).abs() < 1e-9);
427 assert_eq!(transfer.metrics().efficiency, Some(1.0));
428 }
429
430 #[test]
433 fn dissimilar_domains_transfer_nothing() {
434 let mut transfer = TransferLearning::<f64>::new();
435 transfer.register_source(
436 "source".to_string(),
437 vec![experience(1.0, 1.0)],
438 vec![1.0, 0.0],
439 );
440
441 let batch = transfer.select_transfer_batch(vec![0.0, 1.0], 4);
443 assert!(batch.is_empty(), "orthogonal domains must not transfer");
444 assert_eq!(transfer.metrics().efficiency, Some(0.0));
445 }
446
447 #[test]
449 fn transfer_outcomes_are_measured() {
450 let mut transfer = TransferLearning::<f64>::new();
451 transfer.record_transfer_outcome(1.0, 2.0); transfer.record_transfer_outcome(1.0, 0.0); let metrics = transfer.metrics();
455 assert_eq!(metrics.evaluated_transfers, 2);
456 assert_eq!(metrics.negative_transfer_count, 1);
457 assert_eq!(metrics.success_rate, Some(0.5));
458 assert_eq!(metrics.improvement, Some(0.0));
459 }
460
461 #[test]
464 fn unsupported_strategies_are_refused() {
465 let mut transfer = TransferLearning::<f64>::new();
466 let err = transfer
467 .set_strategies(vec![TransferStrategy::ParameterTransfer])
468 .expect_err("ParameterTransfer must be refused");
469 assert!(err.contains("ParameterTransfer"), "{err}");
470 transfer
471 .set_strategies(vec![TransferStrategy::InstanceTransfer])
472 .expect("InstanceTransfer is supported");
473 assert_eq!(transfer.strategies(), [TransferStrategy::InstanceTransfer]);
474 }
475}