Skip to main content

sklears_multioutput/
transfer_learning.rs

1//! Transfer Learning for Multi-Task Learning
2//!
3//! This module provides transfer learning algorithms for multi-task scenarios,
4//! including domain adaptation, progressive transfer, and continual learning methods.
5#![allow(non_snake_case)] // Standard ML notation: X for feature matrices, K for kernels
6
7// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
8use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2, Axis};
9use scirs2_core::random::thread_rng;
10use scirs2_core::random::RandNormal;
11use sklears_core::{
12    error::{Result as SklResult, SklearsError},
13    traits::{Estimator, Untrained},
14    types::Float,
15};
16
17/// Cross-Task Transfer Learning
18///
19/// Implements cross-task transfer learning for multi-task scenarios where
20/// knowledge from source tasks is transferred to target tasks.
21///
22/// # Examples
23///
24/// ```
25/// use sklears_multioutput::transfer_learning::CrossTaskTransferLearning;
26/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
27/// use scirs2_core::ndarray::array;
28///
29/// let source_data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
30/// let source_labels = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
31/// let target_data = array![[1.1, 2.1], [2.1, 3.1]];
32/// let target_labels = array![[1.0, 0.0], [0.0, 1.0]];
33///
34/// let transfer = CrossTaskTransferLearning::new()
35///     .transfer_strength(0.5)
36///     .learning_rate(0.01);
37/// ```
38#[derive(Debug, Clone)]
39pub struct CrossTaskTransferLearning<S = Untrained> {
40    state: S,
41    transfer_strength: Float,
42    learning_rate: Float,
43    max_iter: usize,
44    random_state: Option<u64>,
45}
46
47#[derive(Debug, Clone)]
48pub struct CrossTaskTransferLearningTrained {
49    source_weights: Array2<Float>,
50    target_weights: Array2<Float>,
51    transfer_matrix: Array2<Float>,
52    n_features: usize,
53    #[allow(dead_code)]
54    n_source_tasks: usize,
55    #[allow(dead_code)]
56    n_target_tasks: usize,
57}
58
59impl CrossTaskTransferLearning<Untrained> {
60    /// Create a new CrossTaskTransferLearning instance
61    pub fn new() -> Self {
62        Self {
63            state: Untrained,
64            transfer_strength: 0.5,
65            learning_rate: 0.01,
66            max_iter: 1000,
67            random_state: None,
68        }
69    }
70
71    /// Set the transfer strength (higher values = more transfer)
72    pub fn transfer_strength(mut self, strength: Float) -> Self {
73        self.transfer_strength = strength;
74        self
75    }
76
77    /// Set the learning rate
78    pub fn learning_rate(mut self, lr: Float) -> Self {
79        self.learning_rate = lr;
80        self
81    }
82
83    /// Set the maximum number of iterations
84    pub fn max_iter(mut self, max_iter: usize) -> Self {
85        self.max_iter = max_iter;
86        self
87    }
88
89    /// Set the random state for reproducibility
90    pub fn random_state(mut self, seed: Option<u64>) -> Self {
91        self.random_state = seed;
92        self
93    }
94
95    /// Fit the transfer learning model
96    pub fn fit(
97        &self,
98        source_X: &ArrayView2<Float>,
99        source_y: &ArrayView2<Float>,
100        target_X: &ArrayView2<Float>,
101        target_y: &ArrayView2<Float>,
102    ) -> SklResult<CrossTaskTransferLearning<CrossTaskTransferLearningTrained>> {
103        let n_source_samples = source_X.nrows();
104        let n_target_samples = target_X.nrows();
105        let n_features = source_X.ncols();
106        let n_source_tasks = source_y.ncols();
107        let n_target_tasks = target_y.ncols();
108
109        if source_X.ncols() != target_X.ncols() {
110            return Err(SklearsError::InvalidInput(
111                "Source and target data must have the same number of features".to_string(),
112            ));
113        }
114
115        if n_source_samples != source_y.nrows() {
116            return Err(SklearsError::InvalidInput(
117                "Number of source samples must match source labels".to_string(),
118            ));
119        }
120
121        if n_target_samples != target_y.nrows() {
122            return Err(SklearsError::InvalidInput(
123                "Number of target samples must match target labels".to_string(),
124            ));
125        }
126
127        let mut rng = thread_rng();
128
129        // Initialize weights
130        let normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
131
132        let mut source_weights = Array2::<Float>::zeros((n_features, n_source_tasks));
133        for i in 0..n_features {
134            for j in 0..n_source_tasks {
135                source_weights[[i, j]] = rng.sample(normal_dist);
136            }
137        }
138
139        let mut target_weights = Array2::<Float>::zeros((n_features, n_target_tasks));
140        for i in 0..n_features {
141            for j in 0..n_target_tasks {
142                target_weights[[i, j]] = rng.sample(normal_dist);
143            }
144        }
145
146        let mut transfer_matrix = Array2::<Float>::zeros((n_source_tasks, n_target_tasks));
147        for i in 0..n_source_tasks {
148            for j in 0..n_target_tasks {
149                transfer_matrix[[i, j]] = rng.sample(normal_dist);
150            }
151        }
152
153        // Training loop
154        for _ in 0..self.max_iter {
155            // Update source weights
156            let source_pred = source_X.dot(&source_weights);
157            let source_error = &source_pred - source_y;
158            let source_grad = source_X.t().dot(&source_error) / n_source_samples as Float;
159            source_weights -= &(source_grad * self.learning_rate);
160
161            // Update target weights with transfer
162            let target_pred = target_X.dot(&target_weights);
163            let transferred_pred = target_X.dot(&source_weights).dot(&transfer_matrix);
164            let target_error = &target_pred - target_y;
165            let transfer_error = &transferred_pred - target_y;
166
167            let target_grad = target_X.t().dot(&target_error) / n_target_samples as Float;
168            let transfer_grad = target_X.t().dot(&transfer_error) / n_target_samples as Float;
169
170            target_weights -= &(target_grad * self.learning_rate);
171            target_weights -= &(transfer_grad * self.learning_rate * self.transfer_strength);
172
173            // Update transfer matrix
174            let transfer_matrix_grad =
175                target_X.dot(&source_weights).t().dot(&transfer_error) / n_target_samples as Float;
176            transfer_matrix -=
177                &(transfer_matrix_grad * self.learning_rate * self.transfer_strength);
178        }
179
180        Ok(CrossTaskTransferLearning {
181            state: CrossTaskTransferLearningTrained {
182                source_weights,
183                target_weights,
184                transfer_matrix,
185                n_features,
186                n_source_tasks,
187                n_target_tasks,
188            },
189            transfer_strength: self.transfer_strength,
190            learning_rate: self.learning_rate,
191            max_iter: self.max_iter,
192            random_state: self.random_state,
193        })
194    }
195}
196
197impl CrossTaskTransferLearning<CrossTaskTransferLearningTrained> {
198    /// Predict using the trained transfer learning model
199    pub fn predict(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
200        if X.ncols() != self.state.n_features {
201            return Err(SklearsError::InvalidInput(
202                "Number of features must match training data".to_string(),
203            ));
204        }
205
206        let target_pred = X.dot(&self.state.target_weights);
207        Ok(target_pred)
208    }
209
210    /// Predict using source task knowledge
211    pub fn predict_from_source(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
212        if X.ncols() != self.state.n_features {
213            return Err(SklearsError::InvalidInput(
214                "Number of features must match training data".to_string(),
215            ));
216        }
217
218        let source_pred = X.dot(&self.state.source_weights);
219        let transferred_pred = source_pred.dot(&self.state.transfer_matrix);
220        Ok(transferred_pred)
221    }
222
223    /// Get the transfer matrix
224    pub fn transfer_matrix(&self) -> &Array2<Float> {
225        &self.state.transfer_matrix
226    }
227
228    /// Get the source weights
229    pub fn source_weights(&self) -> &Array2<Float> {
230        &self.state.source_weights
231    }
232
233    /// Get the target weights
234    pub fn target_weights(&self) -> &Array2<Float> {
235        &self.state.target_weights
236    }
237}
238
239impl Default for CrossTaskTransferLearning<Untrained> {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245impl Estimator for CrossTaskTransferLearning<Untrained> {
246    type Config = ();
247    type Error = SklearsError;
248    type Float = Float;
249
250    fn config(&self) -> &Self::Config {
251        &()
252    }
253}
254
255impl Estimator for CrossTaskTransferLearning<CrossTaskTransferLearningTrained> {
256    type Config = ();
257    type Error = SklearsError;
258    type Float = Float;
259
260    fn config(&self) -> &Self::Config {
261        &()
262    }
263}
264
265/// Domain Adaptation for Multi-Task Learning
266///
267/// Implements domain adaptation techniques to transfer knowledge
268/// between different domains in multi-task settings.
269///
270/// # Examples
271///
272/// ```
273/// use sklears_multioutput::transfer_learning::DomainAdaptation;
274/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
275/// use scirs2_core::ndarray::array;
276///
277/// let source_data = array![[1.0, 2.0], [2.0, 3.0]];
278/// let source_labels = array![[1.0], [0.0]];
279/// let target_data = array![[1.1, 2.1], [2.1, 3.1]];
280/// let target_labels = array![[1.0], [0.0]];
281///
282/// let adaptation = DomainAdaptation::new()
283///     .adaptation_strength(0.3)
284///     .learning_rate(0.01);
285/// ```
286#[derive(Debug, Clone)]
287pub struct DomainAdaptation<S = Untrained> {
288    state: S,
289    adaptation_strength: Float,
290    learning_rate: Float,
291    max_iter: usize,
292    random_state: Option<u64>,
293}
294
295#[derive(Debug, Clone)]
296pub struct DomainAdaptationTrained {
297    feature_extractor: Array2<Float>,
298    classifier: Array2<Float>,
299    domain_discriminator: Array2<Float>,
300    n_features: usize,
301    #[allow(dead_code)]
302    n_tasks: usize,
303}
304
305impl DomainAdaptation<Untrained> {
306    /// Create a new DomainAdaptation instance
307    pub fn new() -> Self {
308        Self {
309            state: Untrained,
310            adaptation_strength: 0.3,
311            learning_rate: 0.01,
312            max_iter: 1000,
313            random_state: None,
314        }
315    }
316
317    /// Set the adaptation strength
318    pub fn adaptation_strength(mut self, strength: Float) -> Self {
319        self.adaptation_strength = strength;
320        self
321    }
322
323    /// Set the learning rate
324    pub fn learning_rate(mut self, lr: Float) -> Self {
325        self.learning_rate = lr;
326        self
327    }
328
329    /// Set the maximum number of iterations
330    pub fn max_iter(mut self, max_iter: usize) -> Self {
331        self.max_iter = max_iter;
332        self
333    }
334
335    /// Set the random state for reproducibility
336    pub fn random_state(mut self, seed: Option<u64>) -> Self {
337        self.random_state = seed;
338        self
339    }
340
341    /// Fit the domain adaptation model
342    pub fn fit(
343        &self,
344        source_X: &ArrayView2<Float>,
345        source_y: &ArrayView2<Float>,
346        target_X: &ArrayView2<Float>,
347        target_y: &ArrayView2<Float>,
348    ) -> SklResult<DomainAdaptation<DomainAdaptationTrained>> {
349        let n_source_samples = source_X.nrows();
350        let n_target_samples = target_X.nrows();
351        let n_features = source_X.ncols();
352        let n_tasks = source_y.ncols();
353
354        if source_X.ncols() != target_X.ncols() {
355            return Err(SklearsError::InvalidInput(
356                "Source and target data must have the same number of features".to_string(),
357            ));
358        }
359
360        if n_source_samples != source_y.nrows() {
361            return Err(SklearsError::InvalidInput(
362                "Number of source samples must match source labels".to_string(),
363            ));
364        }
365
366        if n_target_samples != target_y.nrows() {
367            return Err(SklearsError::InvalidInput(
368                "Number of target samples must match target labels".to_string(),
369            ));
370        }
371
372        let mut rng = thread_rng();
373
374        // Initialize networks
375        let hidden_dim = (n_features + n_tasks) / 2;
376        let mut feature_extractor = Array2::<Float>::zeros((n_features, hidden_dim));
377        let normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
378        for i in 0..n_features {
379            for j in 0..hidden_dim {
380                feature_extractor[[i, j]] = rng.sample(normal_dist);
381            }
382        }
383        let mut classifier = Array2::<Float>::zeros((hidden_dim, n_tasks));
384        let classifier_normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
385        for i in 0..hidden_dim {
386            for j in 0..n_tasks {
387                classifier[[i, j]] = rng.sample(classifier_normal_dist);
388            }
389        }
390        let mut domain_discriminator = Array2::<Float>::zeros((hidden_dim, 1));
391        let discriminator_normal_dist =
392            RandNormal::new(0.0, 0.1).expect("operation should succeed");
393        for i in 0..hidden_dim {
394            domain_discriminator[[i, 0]] = rng.sample(discriminator_normal_dist);
395        }
396
397        // Create domain labels (0 for source, 1 for target)
398        let mut domain_labels = Array2::<Float>::zeros((n_source_samples + n_target_samples, 1));
399        for i in n_source_samples..(n_source_samples + n_target_samples) {
400            domain_labels[(i, 0)] = 1.0;
401        }
402
403        // Combine data
404        let mut combined_X =
405            Array2::<Float>::zeros((n_source_samples + n_target_samples, n_features));
406        combined_X
407            .slice_mut(s![..n_source_samples, ..])
408            .assign(source_X);
409        combined_X
410            .slice_mut(s![n_source_samples.., ..])
411            .assign(target_X);
412
413        // Training loop
414        for _ in 0..self.max_iter {
415            // Extract features
416            let features = combined_X.dot(&feature_extractor);
417            let source_features = features.slice(s![..n_source_samples, ..]);
418            let _target_features = features.slice(s![n_source_samples.., ..]);
419
420            // Train classifier on source domain
421            let source_pred = source_features.dot(&classifier);
422            let classification_error = &source_pred - source_y;
423            let classifier_grad =
424                source_features.t().dot(&classification_error) / n_source_samples as Float;
425            classifier -= &(&classifier_grad * self.learning_rate);
426
427            // Train domain discriminator (distinguish source from target)
428            let domain_pred = features.dot(&domain_discriminator);
429            let domain_error = &domain_pred - &domain_labels;
430            let discriminator_grad =
431                features.t().dot(&domain_error) / (n_source_samples + n_target_samples) as Float;
432            domain_discriminator -= &(&discriminator_grad * self.learning_rate);
433
434            // Update feature extractor (adversarial training)
435            let feat_class_grad =
436                combined_X.t().dot(&features.dot(&classifier_grad.t())) / n_source_samples as Float;
437            let feat_domain_grad = combined_X.t().dot(&features.dot(&discriminator_grad))
438                / (n_source_samples + n_target_samples) as Float;
439
440            feature_extractor -= &(feat_class_grad * self.learning_rate);
441            feature_extractor +=
442                &(feat_domain_grad * self.learning_rate * self.adaptation_strength);
443            // Adversarial
444        }
445
446        Ok(DomainAdaptation {
447            state: DomainAdaptationTrained {
448                feature_extractor,
449                classifier,
450                domain_discriminator,
451                n_features,
452                n_tasks,
453            },
454            adaptation_strength: self.adaptation_strength,
455            learning_rate: self.learning_rate,
456            max_iter: self.max_iter,
457            random_state: self.random_state,
458        })
459    }
460}
461
462impl DomainAdaptation<DomainAdaptationTrained> {
463    /// Predict using the trained domain adaptation model
464    pub fn predict(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
465        if X.ncols() != self.state.n_features {
466            return Err(SklearsError::InvalidInput(
467                "Number of features must match training data".to_string(),
468            ));
469        }
470
471        let features = X.dot(&self.state.feature_extractor);
472        let predictions = features.dot(&self.state.classifier);
473        Ok(predictions)
474    }
475
476    /// Extract domain-invariant features
477    pub fn extract_features(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
478        if X.ncols() != self.state.n_features {
479            return Err(SklearsError::InvalidInput(
480                "Number of features must match training data".to_string(),
481            ));
482        }
483
484        let features = X.dot(&self.state.feature_extractor);
485        Ok(features)
486    }
487
488    /// Predict domain labels (0 for source-like, 1 for target-like)
489    pub fn predict_domain(&self, X: &ArrayView2<Float>) -> SklResult<Array1<Float>> {
490        if X.ncols() != self.state.n_features {
491            return Err(SklearsError::InvalidInput(
492                "Number of features must match training data".to_string(),
493            ));
494        }
495
496        let features = X.dot(&self.state.feature_extractor);
497        let domain_pred = features.dot(&self.state.domain_discriminator);
498        Ok(domain_pred.column(0).to_owned())
499    }
500}
501
502impl Default for DomainAdaptation<Untrained> {
503    fn default() -> Self {
504        Self::new()
505    }
506}
507
508impl Estimator for DomainAdaptation<Untrained> {
509    type Config = ();
510    type Error = SklearsError;
511    type Float = Float;
512
513    fn config(&self) -> &Self::Config {
514        &()
515    }
516}
517
518impl Estimator for DomainAdaptation<DomainAdaptationTrained> {
519    type Config = ();
520    type Error = SklearsError;
521    type Float = Float;
522
523    fn config(&self) -> &Self::Config {
524        &()
525    }
526}
527
528/// Progressive Transfer Learning
529///
530/// Implements progressive transfer learning where tasks are learned
531/// sequentially, with knowledge from earlier tasks helping later ones.
532///
533/// # Examples
534///
535/// ```
536/// use sklears_multioutput::transfer_learning::ProgressiveTransferLearning;
537/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
538///
539/// let transfer = ProgressiveTransferLearning::new()
540///     .transfer_strength(0.4)
541///     .learning_rate(0.01)
542///     .max_iter(500);
543/// ```
544#[derive(Debug, Clone)]
545pub struct ProgressiveTransferLearning<S = Untrained> {
546    state: S,
547    transfer_strength: Float,
548    learning_rate: Float,
549    max_iter: usize,
550    random_state: Option<u64>,
551}
552
553#[derive(Debug, Clone)]
554pub struct ProgressiveTransferLearningTrained {
555    task_weights: Vec<Array2<Float>>,
556    shared_weights: Array2<Float>,
557    task_order: Vec<usize>,
558    n_features: usize,
559    n_tasks: usize,
560}
561
562impl ProgressiveTransferLearning<Untrained> {
563    /// Create a new ProgressiveTransferLearning instance
564    pub fn new() -> Self {
565        Self {
566            state: Untrained,
567            transfer_strength: 0.4,
568            learning_rate: 0.01,
569            max_iter: 500,
570            random_state: None,
571        }
572    }
573
574    /// Set the transfer strength
575    pub fn transfer_strength(mut self, strength: Float) -> Self {
576        self.transfer_strength = strength;
577        self
578    }
579
580    /// Set the learning rate
581    pub fn learning_rate(mut self, lr: Float) -> Self {
582        self.learning_rate = lr;
583        self
584    }
585
586    /// Set the maximum number of iterations
587    pub fn max_iter(mut self, max_iter: usize) -> Self {
588        self.max_iter = max_iter;
589        self
590    }
591
592    /// Set the random state for reproducibility
593    pub fn random_state(mut self, seed: Option<u64>) -> Self {
594        self.random_state = seed;
595        self
596    }
597
598    /// Fit the progressive transfer learning model
599    pub fn fit(
600        &self,
601        X: &ArrayView2<Float>,
602        y: &ArrayView2<Float>,
603        task_order: Option<Vec<usize>>,
604    ) -> SklResult<ProgressiveTransferLearning<ProgressiveTransferLearningTrained>> {
605        let n_samples = X.nrows();
606        let n_features = X.ncols();
607        let n_tasks = y.ncols();
608
609        if n_samples != y.nrows() {
610            return Err(SklearsError::InvalidInput(
611                "Number of samples must match number of labels".to_string(),
612            ));
613        }
614
615        let mut rng = thread_rng();
616
617        // Determine task order
618        let task_order = task_order.unwrap_or_else(|| (0..n_tasks).collect());
619
620        // Initialize shared weights
621        let mut shared_weights = Array2::<Float>::zeros((n_features, n_features));
622        let shared_normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
623        for i in 0..n_features {
624            for j in 0..n_features {
625                shared_weights[[i, j]] = rng.sample(shared_normal_dist);
626            }
627        }
628
629        let mut task_weights = Vec::with_capacity(n_tasks);
630
631        // Train tasks progressively
632        for &task_idx in &task_order {
633            let task_y = y.column(task_idx);
634
635            // Initialize task-specific weights
636            let mut task_weight = Array2::<Float>::zeros((n_features, 1));
637            let task_normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
638            for i in 0..n_features {
639                task_weight[[i, 0]] = rng.sample(task_normal_dist);
640            }
641
642            // Train this task
643            for _ in 0..self.max_iter {
644                // Compute shared features
645                let shared_features = X.dot(&shared_weights);
646
647                // Compute task prediction
648                let task_pred = shared_features.dot(&task_weight);
649                let task_error = &task_pred.column(0) - &task_y;
650
651                // Update task weights
652                let task_error_2d = task_error.insert_axis(Axis(1));
653                let task_grad = shared_features.t().dot(&task_error_2d) / n_samples as Float;
654                task_weight -= &(&task_grad * self.learning_rate);
655
656                // Update shared weights (transfer from previous tasks)
657                if !task_weights.is_empty() {
658                    let shared_grad =
659                        X.t().dot(&task_error_2d.dot(&task_weight.t())) / n_samples as Float;
660                    shared_weights -= &(shared_grad * self.learning_rate * self.transfer_strength);
661                }
662            }
663
664            task_weights.push(task_weight);
665        }
666
667        Ok(ProgressiveTransferLearning {
668            state: ProgressiveTransferLearningTrained {
669                task_weights,
670                shared_weights,
671                task_order,
672                n_features,
673                n_tasks,
674            },
675            transfer_strength: self.transfer_strength,
676            learning_rate: self.learning_rate,
677            max_iter: self.max_iter,
678            random_state: self.random_state,
679        })
680    }
681}
682
683impl ProgressiveTransferLearning<ProgressiveTransferLearningTrained> {
684    /// Predict using the trained progressive transfer learning model
685    pub fn predict(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
686        if X.ncols() != self.state.n_features {
687            return Err(SklearsError::InvalidInput(
688                "Number of features must match training data".to_string(),
689            ));
690        }
691
692        let n_samples = X.nrows();
693        let shared_features = X.dot(&self.state.shared_weights);
694        let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_tasks));
695
696        for (i, &task_idx) in self.state.task_order.iter().enumerate() {
697            let task_pred = shared_features.dot(&self.state.task_weights[i]);
698            predictions
699                .column_mut(task_idx)
700                .assign(&task_pred.column(0));
701        }
702
703        Ok(predictions)
704    }
705
706    /// Get the shared weights
707    pub fn shared_weights(&self) -> &Array2<Float> {
708        &self.state.shared_weights
709    }
710
711    /// Get the task-specific weights
712    pub fn task_weights(&self) -> &Vec<Array2<Float>> {
713        &self.state.task_weights
714    }
715
716    /// Get the task order
717    pub fn task_order(&self) -> &Vec<usize> {
718        &self.state.task_order
719    }
720}
721
722impl Default for ProgressiveTransferLearning<Untrained> {
723    fn default() -> Self {
724        Self::new()
725    }
726}
727
728impl Estimator for ProgressiveTransferLearning<Untrained> {
729    type Config = ();
730    type Error = SklearsError;
731    type Float = Float;
732
733    fn config(&self) -> &Self::Config {
734        &()
735    }
736}
737
738impl Estimator for ProgressiveTransferLearning<ProgressiveTransferLearningTrained> {
739    type Config = ();
740    type Error = SklearsError;
741    type Float = Float;
742
743    fn config(&self) -> &Self::Config {
744        &()
745    }
746}
747
748/// Continual Learning for Multi-Task Learning
749///
750/// Implements continual learning where new tasks are learned sequentially
751/// without forgetting previously learned tasks using elastic weight consolidation.
752///
753/// # Examples
754///
755/// ```
756/// use sklears_multioutput::transfer_learning::ContinualLearning;
757/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
758/// use scirs2_core::ndarray::array;
759///
760/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
761/// let y = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
762///
763/// let continual = ContinualLearning::new()
764///     .importance_weight(1000.0)
765///     .learning_rate(0.01);
766/// ```
767#[derive(Debug, Clone)]
768pub struct ContinualLearning<S = Untrained> {
769    state: S,
770    importance_weight: Float,
771    learning_rate: Float,
772    max_iter: usize,
773    random_state: Option<u64>,
774}
775
776#[derive(Debug, Clone)]
777pub struct ContinualLearningTrained {
778    task_weights: Vec<Array2<Float>>,
779    fisher_information: Array2<Float>,
780    optimal_weights: Array2<Float>,
781    n_features: usize,
782    #[allow(dead_code)]
783    n_tasks: usize,
784}
785
786impl Default for ContinualLearning<Untrained> {
787    fn default() -> Self {
788        Self::new()
789    }
790}
791
792impl ContinualLearning<Untrained> {
793    /// Create a new ContinualLearning instance
794    pub fn new() -> Self {
795        Self {
796            state: Untrained,
797            importance_weight: 1000.0,
798            learning_rate: 0.01,
799            max_iter: 1000,
800            random_state: None,
801        }
802    }
803
804    /// Set the importance weight for preventing forgetting
805    pub fn importance_weight(mut self, weight: Float) -> Self {
806        self.importance_weight = weight;
807        self
808    }
809
810    /// Set the learning rate
811    pub fn learning_rate(mut self, lr: Float) -> Self {
812        self.learning_rate = lr;
813        self
814    }
815
816    /// Set the maximum number of iterations
817    pub fn max_iter(mut self, max_iter: usize) -> Self {
818        self.max_iter = max_iter;
819        self
820    }
821
822    /// Set the random state for reproducibility
823    pub fn random_state(mut self, seed: Option<u64>) -> Self {
824        self.random_state = seed;
825        self
826    }
827
828    /// Fit the continual learning model
829    pub fn fit(
830        &self,
831        tasks_X: &[ArrayView2<Float>],
832        tasks_y: &[ArrayView2<Float>],
833    ) -> SklResult<ContinualLearning<ContinualLearningTrained>> {
834        if tasks_X.len() != tasks_y.len() {
835            return Err(SklearsError::InvalidInput(
836                "Number of X and y task arrays must match".to_string(),
837            ));
838        }
839
840        if tasks_X.is_empty() {
841            return Err(SklearsError::InvalidInput("No tasks provided".to_string()));
842        }
843
844        let n_features = tasks_X[0].ncols();
845        let n_tasks = tasks_y[0].ncols();
846
847        // Initialize with random weights
848        let mut rng = thread_rng();
849
850        let mut weights = Array2::<Float>::zeros((n_features, n_tasks));
851        let weights_normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
852        for i in 0..n_features {
853            for j in 0..n_tasks {
854                weights[[i, j]] = rng.sample(weights_normal_dist);
855            }
856        }
857        let mut fisher_information = Array2::<Float>::zeros((n_features, n_tasks));
858        let mut task_weights = Vec::new();
859
860        // Learn tasks sequentially
861        for (task_idx, (X, y)) in tasks_X.iter().zip(tasks_y.iter()).enumerate() {
862            if X.nrows() != y.nrows() {
863                return Err(SklearsError::InvalidInput(
864                    "Number of samples in X and y must match".to_string(),
865                ));
866            }
867
868            // Store weights before learning new task
869            let old_weights = weights.clone();
870
871            // Learn current task
872            for _ in 0..self.max_iter {
873                let predictions = X.dot(&weights);
874                let errors = &predictions - y;
875                let gradient = X.t().dot(&errors) / X.nrows() as Float;
876
877                // Add elastic weight consolidation penalty for previous tasks
878                if task_idx > 0 {
879                    let penalty =
880                        &fisher_information * (&weights - &old_weights) * self.importance_weight;
881                    weights = &weights - self.learning_rate * (&gradient + penalty);
882                } else {
883                    weights = &weights - self.learning_rate * &gradient;
884                }
885            }
886
887            // Update Fisher information matrix
888            let predictions = X.dot(&weights);
889            let errors = &predictions - y;
890            let grad_squared = X.t().dot(&errors.mapv(|x| x * x)) / X.nrows() as Float;
891            fisher_information = &fisher_information + grad_squared;
892
893            task_weights.push(weights.clone());
894        }
895
896        Ok(ContinualLearning {
897            state: ContinualLearningTrained {
898                task_weights,
899                fisher_information,
900                optimal_weights: weights,
901                n_features,
902                n_tasks,
903            },
904            importance_weight: self.importance_weight,
905            learning_rate: self.learning_rate,
906            max_iter: self.max_iter,
907            random_state: self.random_state,
908        })
909    }
910}
911
912impl ContinualLearning<ContinualLearningTrained> {
913    /// Predict using the continual learning model
914    pub fn predict(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
915        if X.ncols() != self.state.n_features {
916            return Err(SklearsError::InvalidInput(
917                "Number of features must match training data".to_string(),
918            ));
919        }
920
921        Ok(X.dot(&self.state.optimal_weights))
922    }
923
924    /// Get the task weights
925    pub fn task_weights(&self) -> &[Array2<Float>] {
926        &self.state.task_weights
927    }
928
929    /// Get the Fisher information matrix
930    pub fn fisher_information(&self) -> &Array2<Float> {
931        &self.state.fisher_information
932    }
933}
934
935impl Estimator for ContinualLearning<Untrained> {
936    type Config = ();
937    type Error = SklearsError;
938    type Float = Float;
939
940    fn config(&self) -> &Self::Config {
941        &()
942    }
943}
944
945impl Estimator for ContinualLearning<ContinualLearningTrained> {
946    type Config = ();
947    type Error = SklearsError;
948    type Float = Float;
949
950    fn config(&self) -> &Self::Config {
951        &()
952    }
953}
954
955/// Knowledge Distillation for Multi-Task Learning
956///
957/// Implements knowledge distillation where a smaller student network learns
958/// from a larger teacher network for improved efficiency and performance.
959///
960/// # Examples
961///
962/// ```
963/// use sklears_multioutput::transfer_learning::KnowledgeDistillation;
964/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
965/// use scirs2_core::ndarray::array;
966///
967/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
968/// let y = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
969///
970/// let distillation = KnowledgeDistillation::new()
971///     .temperature(3.0)
972///     .alpha(0.7)
973///     .learning_rate(0.01);
974/// ```
975#[derive(Debug, Clone)]
976pub struct KnowledgeDistillation<S = Untrained> {
977    state: S,
978    temperature: Float,
979    alpha: Float,
980    learning_rate: Float,
981    max_iter: usize,
982    random_state: Option<u64>,
983}
984
985#[derive(Debug, Clone)]
986pub struct KnowledgeDistillationTrained {
987    student_weights: Array2<Float>,
988    teacher_weights: Array2<Float>,
989    n_features: usize,
990    #[allow(dead_code)]
991    n_tasks: usize,
992}
993
994impl Default for KnowledgeDistillation<Untrained> {
995    fn default() -> Self {
996        Self::new()
997    }
998}
999
1000impl KnowledgeDistillation<Untrained> {
1001    /// Create a new KnowledgeDistillation instance
1002    pub fn new() -> Self {
1003        Self {
1004            state: Untrained,
1005            temperature: 3.0,
1006            alpha: 0.7,
1007            learning_rate: 0.01,
1008            max_iter: 1000,
1009            random_state: None,
1010        }
1011    }
1012
1013    /// Set the temperature for softening teacher predictions
1014    pub fn temperature(mut self, temp: Float) -> Self {
1015        self.temperature = temp;
1016        self
1017    }
1018
1019    /// Set the alpha parameter for balancing hard and soft targets
1020    pub fn alpha(mut self, alpha: Float) -> Self {
1021        self.alpha = alpha;
1022        self
1023    }
1024
1025    /// Set the learning rate
1026    pub fn learning_rate(mut self, lr: Float) -> Self {
1027        self.learning_rate = lr;
1028        self
1029    }
1030
1031    /// Set the maximum number of iterations
1032    pub fn max_iter(mut self, max_iter: usize) -> Self {
1033        self.max_iter = max_iter;
1034        self
1035    }
1036
1037    /// Set the random state for reproducibility
1038    pub fn random_state(mut self, seed: Option<u64>) -> Self {
1039        self.random_state = seed;
1040        self
1041    }
1042
1043    /// Fit the knowledge distillation model
1044    pub fn fit(
1045        &self,
1046        X: &ArrayView2<Float>,
1047        y: &ArrayView2<Float>,
1048        teacher_predictions: &ArrayView2<Float>,
1049    ) -> SklResult<KnowledgeDistillation<KnowledgeDistillationTrained>> {
1050        if X.nrows() != y.nrows() {
1051            return Err(SklearsError::InvalidInput(
1052                "Number of samples in X and y must match".to_string(),
1053            ));
1054        }
1055
1056        if X.nrows() != teacher_predictions.nrows() {
1057            return Err(SklearsError::InvalidInput(
1058                "Number of samples in X and teacher predictions must match".to_string(),
1059            ));
1060        }
1061
1062        let n_features = X.ncols();
1063        let n_tasks = y.ncols();
1064
1065        // Initialize with random weights
1066        let mut rng = thread_rng();
1067
1068        let mut student_weights = Array2::<Float>::zeros((n_features, n_tasks));
1069        let student_normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
1070        for i in 0..n_features {
1071            for j in 0..n_tasks {
1072                student_weights[[i, j]] = rng.sample(student_normal_dist);
1073            }
1074        }
1075        let mut teacher_weights = Array2::<Float>::zeros((n_features, n_tasks));
1076        let teacher_normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
1077        for i in 0..n_features {
1078            for j in 0..n_tasks {
1079                teacher_weights[[i, j]] = rng.sample(teacher_normal_dist);
1080            }
1081        }
1082
1083        // Train student network
1084        for _ in 0..self.max_iter {
1085            let student_predictions = X.dot(&student_weights);
1086
1087            // Soft targets from teacher (temperature-scaled)
1088            let soft_targets = teacher_predictions / self.temperature;
1089            let student_soft = &student_predictions / self.temperature;
1090
1091            // Combined loss: weighted sum of hard and soft targets
1092            let hard_loss = &student_predictions - y;
1093            let soft_loss = &student_soft - &soft_targets;
1094
1095            let combined_loss = (1.0 - self.alpha) * hard_loss + self.alpha * soft_loss;
1096            let gradient = X.t().dot(&combined_loss) / X.nrows() as Float;
1097
1098            student_weights = &student_weights - self.learning_rate * &gradient;
1099        }
1100
1101        Ok(KnowledgeDistillation {
1102            state: KnowledgeDistillationTrained {
1103                student_weights,
1104                teacher_weights,
1105                n_features,
1106                n_tasks,
1107            },
1108            temperature: self.temperature,
1109            alpha: self.alpha,
1110            learning_rate: self.learning_rate,
1111            max_iter: self.max_iter,
1112            random_state: self.random_state,
1113        })
1114    }
1115}
1116
1117impl KnowledgeDistillation<KnowledgeDistillationTrained> {
1118    /// Predict using the student network
1119    pub fn predict(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
1120        if X.ncols() != self.state.n_features {
1121            return Err(SklearsError::InvalidInput(
1122                "Number of features must match training data".to_string(),
1123            ));
1124        }
1125
1126        Ok(X.dot(&self.state.student_weights))
1127    }
1128
1129    /// Get the student weights
1130    pub fn student_weights(&self) -> &Array2<Float> {
1131        &self.state.student_weights
1132    }
1133
1134    /// Get the teacher weights
1135    pub fn teacher_weights(&self) -> &Array2<Float> {
1136        &self.state.teacher_weights
1137    }
1138}
1139
1140impl Estimator for KnowledgeDistillation<Untrained> {
1141    type Config = ();
1142    type Error = SklearsError;
1143    type Float = Float;
1144
1145    fn config(&self) -> &Self::Config {
1146        &()
1147    }
1148}
1149
1150impl Estimator for KnowledgeDistillation<KnowledgeDistillationTrained> {
1151    type Config = ();
1152    type Error = SklearsError;
1153    type Float = Float;
1154
1155    fn config(&self) -> &Self::Config {
1156        &()
1157    }
1158}
1159
1160#[allow(non_snake_case)]
1161#[cfg(test)]
1162mod tests {
1163    use super::*;
1164    // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
1165    use scirs2_core::ndarray::array;
1166
1167    #[test]
1168    fn test_cross_task_transfer_learning_basic() {
1169        let source_X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [1.0, 3.0]];
1170        let source_y = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.0, 0.0]];
1171        let target_X = array![[1.1, 2.1], [2.1, 3.1]];
1172        let target_y = array![[1.0, 0.0], [0.0, 1.0]];
1173
1174        let transfer = CrossTaskTransferLearning::new()
1175            .transfer_strength(0.5)
1176            .learning_rate(0.01)
1177            .max_iter(100)
1178            .random_state(Some(42));
1179
1180        let trained = transfer
1181            .fit(
1182                &source_X.view(),
1183                &source_y.view(),
1184                &target_X.view(),
1185                &target_y.view(),
1186            )
1187            .expect("operation should succeed");
1188
1189        let predictions = trained
1190            .predict(&target_X.view())
1191            .expect("prediction should succeed");
1192        assert_eq!(predictions.dim(), (2, 2));
1193
1194        let source_predictions = trained
1195            .predict_from_source(&target_X.view())
1196            .expect("operation should succeed");
1197        assert_eq!(source_predictions.dim(), (2, 2));
1198    }
1199
1200    #[test]
1201    fn test_cross_task_transfer_learning_validation() {
1202        let source_X = array![[1.0, 2.0], [2.0, 3.0]];
1203        let source_y = array![[1.0, 0.0], [0.0, 1.0]];
1204        let target_X = array![[1.1, 2.1, 3.1]]; // Different number of features
1205        let target_y = array![[1.0, 0.0]];
1206
1207        let transfer = CrossTaskTransferLearning::new();
1208
1209        // Should fail due to feature mismatch
1210        assert!(transfer
1211            .fit(
1212                &source_X.view(),
1213                &source_y.view(),
1214                &target_X.view(),
1215                &target_y.view()
1216            )
1217            .is_err());
1218    }
1219
1220    #[test]
1221    fn test_domain_adaptation_basic() {
1222        let source_X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [1.0, 3.0]];
1223        let source_y = array![[1.0], [0.0], [1.0], [0.0]];
1224        let target_X = array![[1.1, 2.1], [2.1, 3.1]];
1225        let target_y = array![[1.0], [0.0]];
1226
1227        let adaptation = DomainAdaptation::new()
1228            .adaptation_strength(0.3)
1229            .learning_rate(0.01)
1230            .max_iter(100)
1231            .random_state(Some(42));
1232
1233        let trained = adaptation
1234            .fit(
1235                &source_X.view(),
1236                &source_y.view(),
1237                &target_X.view(),
1238                &target_y.view(),
1239            )
1240            .expect("operation should succeed");
1241
1242        let predictions = trained
1243            .predict(&target_X.view())
1244            .expect("prediction should succeed");
1245        assert_eq!(predictions.dim(), (2, 1));
1246
1247        let features = trained
1248            .extract_features(&target_X.view())
1249            .expect("operation should succeed");
1250        assert_eq!(features.ncols(), 1); // Hidden dimension
1251
1252        let domain_pred = trained
1253            .predict_domain(&target_X.view())
1254            .expect("operation should succeed");
1255        assert_eq!(domain_pred.len(), 2);
1256    }
1257
1258    #[test]
1259    #[allow(non_snake_case)]
1260    fn test_progressive_transfer_learning_basic() {
1261        let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [1.0, 3.0]];
1262        let y = array![
1263            [1.0, 0.0, 1.0],
1264            [0.0, 1.0, 0.0],
1265            [1.0, 1.0, 1.0],
1266            [0.0, 0.0, 0.0]
1267        ];
1268
1269        let transfer = ProgressiveTransferLearning::new()
1270            .transfer_strength(0.4)
1271            .learning_rate(0.01)
1272            .max_iter(100)
1273            .random_state(Some(42));
1274
1275        let trained = transfer
1276            .fit(&X.view(), &y.view(), None)
1277            .expect("model fitting should succeed");
1278
1279        let predictions = trained
1280            .predict(&X.view())
1281            .expect("prediction should succeed");
1282        assert_eq!(predictions.dim(), (4, 3));
1283
1284        // Check that we have weights for all tasks
1285        assert_eq!(trained.task_weights().len(), 3);
1286        assert_eq!(trained.task_order().len(), 3);
1287    }
1288
1289    #[test]
1290    #[allow(non_snake_case)]
1291    fn test_progressive_transfer_learning_custom_order() {
1292        let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
1293        let y = array![[1.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 1.0, 1.0]];
1294
1295        let transfer = ProgressiveTransferLearning::new().random_state(Some(42));
1296
1297        let custom_order = vec![2, 0, 1]; // Start with task 2, then 0, then 1
1298        let trained = transfer
1299            .fit(&X.view(), &y.view(), Some(custom_order.clone()))
1300            .expect("operation should succeed");
1301
1302        assert_eq!(trained.task_order(), &custom_order);
1303    }
1304
1305    #[test]
1306    #[allow(non_snake_case)]
1307    fn test_transfer_learning_error_handling() {
1308        let X = array![[1.0, 2.0], [2.0, 3.0]];
1309        let y = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; // Mismatched samples
1310
1311        let transfer = ProgressiveTransferLearning::new();
1312        assert!(transfer.fit(&X.view(), &y.view(), None).is_err());
1313    }
1314
1315    #[test]
1316    fn test_continual_learning_basic() {
1317        let X1 = array![[1.0, 2.0], [2.0, 3.0]];
1318        let y1 = array![[1.0, 0.0], [0.0, 1.0]];
1319        let X2 = array![[3.0, 1.0], [1.0, 3.0]];
1320        let y2 = array![[1.0, 1.0], [0.0, 0.0]];
1321
1322        let tasks_X = vec![X1.view(), X2.view()];
1323        let tasks_y = vec![y1.view(), y2.view()];
1324
1325        let continual = ContinualLearning::new()
1326            .importance_weight(1000.0)
1327            .learning_rate(0.01)
1328            .max_iter(100)
1329            .random_state(Some(42));
1330
1331        let trained = continual
1332            .fit(&tasks_X, &tasks_y)
1333            .expect("model fitting should succeed");
1334
1335        let predictions = trained
1336            .predict(&X1.view())
1337            .expect("prediction should succeed");
1338        assert_eq!(predictions.dim(), (2, 2));
1339
1340        // Check that we have weights for both tasks
1341        assert_eq!(trained.task_weights().len(), 2);
1342        assert_eq!(trained.fisher_information().dim(), (2, 2));
1343    }
1344
1345    #[test]
1346    fn test_continual_learning_error_handling() {
1347        let X1 = array![[1.0, 2.0], [2.0, 3.0]];
1348        let y1 = array![[1.0, 0.0], [0.0, 1.0]];
1349        let X2 = array![[3.0, 1.0]]; // Wrong number of samples
1350        let y2 = array![[1.0, 1.0], [0.0, 0.0]];
1351
1352        let tasks_X = vec![X1.view(), X2.view()];
1353        let tasks_y = vec![y1.view(), y2.view()];
1354
1355        let continual = ContinualLearning::new();
1356        assert!(continual.fit(&tasks_X, &tasks_y).is_err());
1357    }
1358
1359    #[test]
1360    #[allow(non_snake_case)]
1361    fn test_knowledge_distillation_basic() {
1362        let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
1363        let y = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1364        let teacher_predictions = array![[0.9, 0.1], [0.1, 0.9], [0.8, 0.8]];
1365
1366        let distillation = KnowledgeDistillation::new()
1367            .temperature(3.0)
1368            .alpha(0.7)
1369            .learning_rate(0.01)
1370            .max_iter(100)
1371            .random_state(Some(42));
1372
1373        let trained = distillation
1374            .fit(&X.view(), &y.view(), &teacher_predictions.view())
1375            .expect("operation should succeed");
1376
1377        let predictions = trained
1378            .predict(&X.view())
1379            .expect("prediction should succeed");
1380        assert_eq!(predictions.dim(), (3, 2));
1381
1382        // Check that we have student and teacher weights
1383        assert_eq!(trained.student_weights().dim(), (2, 2));
1384        assert_eq!(trained.teacher_weights().dim(), (2, 2));
1385    }
1386
1387    #[test]
1388    #[allow(non_snake_case)]
1389    fn test_knowledge_distillation_error_handling() {
1390        let X = array![[1.0, 2.0], [2.0, 3.0]];
1391        let y = array![[1.0, 0.0], [0.0, 1.0]];
1392        let teacher_predictions = array![[0.9, 0.1], [0.1, 0.9], [0.8, 0.8]]; // Wrong number of samples
1393
1394        let distillation = KnowledgeDistillation::new();
1395        assert!(distillation
1396            .fit(&X.view(), &y.view(), &teacher_predictions.view())
1397            .is_err());
1398    }
1399
1400    #[test]
1401    fn test_knowledge_distillation_configuration() {
1402        let distillation = KnowledgeDistillation::new()
1403            .temperature(5.0)
1404            .alpha(0.5)
1405            .learning_rate(0.001)
1406            .max_iter(2000)
1407            .random_state(Some(123));
1408
1409        // Test configuration parameters
1410        assert_eq!(distillation.temperature, 5.0);
1411        assert_eq!(distillation.alpha, 0.5);
1412        assert_eq!(distillation.learning_rate, 0.001);
1413        assert_eq!(distillation.max_iter, 2000);
1414        assert_eq!(distillation.random_state, Some(123));
1415    }
1416}