1#![allow(non_snake_case)] use 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#[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 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 pub fn transfer_strength(mut self, strength: Float) -> Self {
73 self.transfer_strength = strength;
74 self
75 }
76
77 pub fn learning_rate(mut self, lr: Float) -> Self {
79 self.learning_rate = lr;
80 self
81 }
82
83 pub fn max_iter(mut self, max_iter: usize) -> Self {
85 self.max_iter = max_iter;
86 self
87 }
88
89 pub fn random_state(mut self, seed: Option<u64>) -> Self {
91 self.random_state = seed;
92 self
93 }
94
95 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 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 for _ in 0..self.max_iter {
155 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 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 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 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 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 pub fn transfer_matrix(&self) -> &Array2<Float> {
225 &self.state.transfer_matrix
226 }
227
228 pub fn source_weights(&self) -> &Array2<Float> {
230 &self.state.source_weights
231 }
232
233 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#[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 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 pub fn adaptation_strength(mut self, strength: Float) -> Self {
319 self.adaptation_strength = strength;
320 self
321 }
322
323 pub fn learning_rate(mut self, lr: Float) -> Self {
325 self.learning_rate = lr;
326 self
327 }
328
329 pub fn max_iter(mut self, max_iter: usize) -> Self {
331 self.max_iter = max_iter;
332 self
333 }
334
335 pub fn random_state(mut self, seed: Option<u64>) -> Self {
337 self.random_state = seed;
338 self
339 }
340
341 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 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 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 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 for _ in 0..self.max_iter {
415 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 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 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 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 }
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 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 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 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#[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 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 pub fn transfer_strength(mut self, strength: Float) -> Self {
576 self.transfer_strength = strength;
577 self
578 }
579
580 pub fn learning_rate(mut self, lr: Float) -> Self {
582 self.learning_rate = lr;
583 self
584 }
585
586 pub fn max_iter(mut self, max_iter: usize) -> Self {
588 self.max_iter = max_iter;
589 self
590 }
591
592 pub fn random_state(mut self, seed: Option<u64>) -> Self {
594 self.random_state = seed;
595 self
596 }
597
598 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 let task_order = task_order.unwrap_or_else(|| (0..n_tasks).collect());
619
620 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 for &task_idx in &task_order {
633 let task_y = y.column(task_idx);
634
635 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 for _ in 0..self.max_iter {
644 let shared_features = X.dot(&shared_weights);
646
647 let task_pred = shared_features.dot(&task_weight);
649 let task_error = &task_pred.column(0) - &task_y;
650
651 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 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 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 pub fn shared_weights(&self) -> &Array2<Float> {
708 &self.state.shared_weights
709 }
710
711 pub fn task_weights(&self) -> &Vec<Array2<Float>> {
713 &self.state.task_weights
714 }
715
716 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#[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 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 pub fn importance_weight(mut self, weight: Float) -> Self {
806 self.importance_weight = weight;
807 self
808 }
809
810 pub fn learning_rate(mut self, lr: Float) -> Self {
812 self.learning_rate = lr;
813 self
814 }
815
816 pub fn max_iter(mut self, max_iter: usize) -> Self {
818 self.max_iter = max_iter;
819 self
820 }
821
822 pub fn random_state(mut self, seed: Option<u64>) -> Self {
824 self.random_state = seed;
825 self
826 }
827
828 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 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 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 let old_weights = weights.clone();
870
871 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 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 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 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 pub fn task_weights(&self) -> &[Array2<Float>] {
926 &self.state.task_weights
927 }
928
929 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#[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 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 pub fn temperature(mut self, temp: Float) -> Self {
1015 self.temperature = temp;
1016 self
1017 }
1018
1019 pub fn alpha(mut self, alpha: Float) -> Self {
1021 self.alpha = alpha;
1022 self
1023 }
1024
1025 pub fn learning_rate(mut self, lr: Float) -> Self {
1027 self.learning_rate = lr;
1028 self
1029 }
1030
1031 pub fn max_iter(mut self, max_iter: usize) -> Self {
1033 self.max_iter = max_iter;
1034 self
1035 }
1036
1037 pub fn random_state(mut self, seed: Option<u64>) -> Self {
1039 self.random_state = seed;
1040 self
1041 }
1042
1043 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 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 for _ in 0..self.max_iter {
1085 let student_predictions = X.dot(&student_weights);
1086
1087 let soft_targets = teacher_predictions / self.temperature;
1089 let student_soft = &student_predictions / self.temperature;
1090
1091 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 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 pub fn student_weights(&self) -> &Array2<Float> {
1131 &self.state.student_weights
1132 }
1133
1134 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::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]]; let target_y = array![[1.0, 0.0]];
1206
1207 let transfer = CrossTaskTransferLearning::new();
1208
1209 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); 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 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]; 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]]; 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 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]]; 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 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]]; 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 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}