1use scirs2_core::ndarray::{Array1, Array2, Axis};
7use scirs2_core::random::rng as make_rng;
8use scirs2_core::random::rngs::StdRng;
9use scirs2_core::random::{SeedableRng, SliceRandom};
10use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
11use sklears_core::{
12 error::{Result, SklearsError},
13 traits::{Fit, Trained, Transform, Untrained},
14 types::Float,
15};
16use std::marker::PhantomData;
17
18#[derive(Debug, Clone, Copy)]
20pub enum KernelFunction {
21 Linear,
23 Rbf { gamma: Float },
25 Polynomial {
27 degree: i32,
28 gamma: Float,
29 coef0: Float,
30 },
31 Sigmoid { gamma: Float, coef0: Float },
33 Laplacian { gamma: Float },
35 ChiSquared { gamma: Float },
37}
38
39impl Default for KernelFunction {
40 fn default() -> Self {
41 KernelFunction::Rbf { gamma: 1.0 }
42 }
43}
44
45impl KernelFunction {
46 pub fn compute(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
48 match self {
49 KernelFunction::Linear => x.dot(y),
50 KernelFunction::Rbf { gamma } => {
51 let diff = x - y;
52 let dist_sq = diff.dot(&diff);
53 (-gamma * dist_sq).exp()
54 }
55 KernelFunction::Polynomial {
56 degree,
57 gamma,
58 coef0,
59 } => {
60 let dot_product = x.dot(y);
61 (gamma * dot_product + coef0).powi(*degree)
62 }
63 KernelFunction::Sigmoid { gamma, coef0 } => {
64 let dot_product = x.dot(y);
65 (gamma * dot_product + coef0).tanh()
66 }
67 KernelFunction::Laplacian { gamma } => {
68 let l1_dist = (x - y).mapv(|x| x.abs()).sum();
69 (-gamma * l1_dist).exp()
70 }
71 KernelFunction::ChiSquared { gamma } => {
72 let mut chi_sq_dist = 0.0;
73 for i in 0..x.len() {
74 let sum = x[i] + y[i];
75 if sum > 1e-12 {
76 let diff = x[i] - y[i];
77 chi_sq_dist += diff * diff / sum;
78 }
79 }
80 (-gamma * chi_sq_dist).exp()
81 }
82 }
83 }
84
85 pub fn compute_matrix(&self, x: &Array2<Float>, y: &Array2<Float>) -> Array2<Float> {
87 let n_x = x.nrows();
88 let n_y = y.nrows();
89 let mut kernel_matrix = Array2::zeros((n_x, n_y));
90
91 for i in 0..n_x {
92 for j in 0..n_y {
93 let x_i = x.row(i).to_owned();
94 let y_j = y.row(j).to_owned();
95 kernel_matrix[[i, j]] = self.compute(&x_i, &y_j);
96 }
97 }
98
99 kernel_matrix
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum KernelApproximation {
106 #[default]
108 Full,
109 Nystrom { n_components: usize },
111 RandomSampling { n_samples: usize },
113}
114
115#[derive(Debug, Clone)]
117pub struct KernelPcaConfig {
118 pub n_components: Option<usize>,
120 pub kernel: KernelFunction,
122 pub tol: Float,
124 pub max_iter: usize,
126 pub center: bool,
128 pub copy: bool,
130 pub approximation: KernelApproximation,
132 pub random_state: Option<u64>,
134}
135
136impl Default for KernelPcaConfig {
137 fn default() -> Self {
138 Self {
139 n_components: None,
140 kernel: KernelFunction::default(),
141 tol: 1e-8,
142 max_iter: 300,
143 center: true,
144 copy: true,
145 approximation: KernelApproximation::default(),
146 random_state: None,
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
180pub struct KernelPCA<State = Untrained> {
181 config: KernelPcaConfig,
182 state: PhantomData<State>,
183 x_fit_: Option<Array2<Float>>,
185 lambdas_: Option<Array1<Float>>,
186 alphas_: Option<Array2<Float>>,
187 n_components_: Option<usize>,
188 n_features_in_: Option<usize>,
189 n_samples_: Option<usize>,
190}
191
192impl KernelPCA<Untrained> {
193 pub fn new() -> Self {
195 Self {
196 config: KernelPcaConfig::default(),
197 state: PhantomData,
198 x_fit_: None,
199 lambdas_: None,
200 alphas_: None,
201 n_components_: None,
202 n_features_in_: None,
203 n_samples_: None,
204 }
205 }
206
207 pub fn n_components(mut self, n_components: usize) -> Self {
209 self.config.n_components = Some(n_components);
210 self
211 }
212
213 pub fn kernel(mut self, kernel: KernelFunction) -> Self {
215 self.config.kernel = kernel;
216 self
217 }
218
219 pub fn tol(mut self, tol: Float) -> Self {
221 self.config.tol = tol;
222 self
223 }
224
225 pub fn max_iter(mut self, max_iter: usize) -> Self {
227 self.config.max_iter = max_iter;
228 self
229 }
230
231 pub fn center(mut self, center: bool) -> Self {
233 self.config.center = center;
234 self
235 }
236
237 pub fn copy(mut self, copy: bool) -> Self {
239 self.config.copy = copy;
240 self
241 }
242
243 pub fn approximation(mut self, approximation: KernelApproximation) -> Self {
245 self.config.approximation = approximation;
246 self
247 }
248
249 pub fn random_state(mut self, random_state: u64) -> Self {
251 self.config.random_state = Some(random_state);
252 self
253 }
254}
255
256impl Default for KernelPCA<Untrained> {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262impl Fit<Array2<Float>, ()> for KernelPCA<Untrained> {
263 type Fitted = KernelPCA<Trained>;
264
265 fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
266 let (n_samples, n_features) = x.dim();
267
268 if n_samples == 0 {
269 return Err(SklearsError::InvalidInput("Empty dataset".to_string()));
270 }
271
272 if n_features == 0 {
273 return Err(SklearsError::InvalidInput(
274 "Dataset has no features".to_string(),
275 ));
276 }
277
278 let n_components = self
279 .config
280 .n_components
281 .unwrap_or(n_samples.min(n_features))
282 .min(n_samples);
283
284 if n_components == 0 {
285 return Err(SklearsError::InvalidInput(
286 "Number of components must be positive".to_string(),
287 ));
288 }
289
290 let x_fit = if self.config.copy {
292 x.clone()
293 } else {
294 x.to_owned()
295 };
296
297 let (lambdas, alphas) = match self.config.approximation {
299 KernelApproximation::Full => {
300 let mut k = self.config.kernel.compute_matrix(&x_fit, &x_fit);
302
303 if self.config.center {
305 self.center_kernel_matrix(&mut k)?;
306 }
307
308 self.solve_eigenvalue_problem(&k, n_components)?
310 }
311 KernelApproximation::Nystrom {
312 n_components: nystrom_components,
313 } => {
314 self.nystrom_approximation(&x_fit, n_components, nystrom_components)?
316 }
317 KernelApproximation::RandomSampling { n_samples } => {
318 self.random_sampling_approximation(&x_fit, n_components, n_samples)?
320 }
321 };
322
323 Ok(KernelPCA {
324 config: self.config,
325 state: PhantomData,
326 x_fit_: Some(x_fit),
327 lambdas_: Some(lambdas),
328 alphas_: Some(alphas),
329 n_components_: Some(n_components),
330 n_features_in_: Some(n_features),
331 n_samples_: Some(n_samples),
332 })
333 }
334}
335
336impl KernelPCA<Untrained> {
337 fn center_kernel_matrix(&self, k: &mut Array2<Float>) -> Result<()> {
339 let n = k.nrows();
340 if n != k.ncols() {
341 return Err(SklearsError::InvalidInput(
342 "Kernel matrix must be square".to_string(),
343 ));
344 }
345
346 let row_means = k.mean_axis(Axis(1)).ok_or_else(|| {
348 SklearsError::NumericalError(
349 "cannot compute row means of empty kernel matrix".to_string(),
350 )
351 })?;
352 let overall_mean = k.mean().ok_or_else(|| {
354 SklearsError::NumericalError("cannot compute mean of empty kernel matrix".to_string())
355 })?;
356
357 for i in 0..n {
359 for j in 0..n {
360 k[[i, j]] = k[[i, j]] - row_means[i] - row_means[j] + overall_mean;
361 }
362 }
363
364 Ok(())
365 }
366
367 fn solve_eigenvalue_problem(
372 &self,
373 k: &Array2<Float>,
374 n_components: usize,
375 ) -> Result<(Array1<Float>, Array2<Float>)> {
376 let n = k.nrows();
377 let n_comp = n_components.min(n);
378
379 let (eigenvalues, eigenvectors) = k.eigh(UPLO::Lower).map_err(|e| {
382 SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
383 })?;
384
385 let mut eigen_pairs: Vec<(Float, usize)> = eigenvalues
388 .iter()
389 .enumerate()
390 .map(|(i, &val)| (val, i))
391 .collect();
392
393 eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
395
396 let mut lambdas = Array1::zeros(n_comp);
398 let mut alphas = Array2::zeros((n, n_comp));
399
400 for comp in 0..n_comp {
401 if comp < eigen_pairs.len() {
402 let (eigenval, idx) = eigen_pairs[comp];
403
404 lambdas[comp] = eigenval.max(0.0);
406
407 for i in 0..n {
409 alphas[[i, comp]] = eigenvectors[[i, idx]];
410 }
411
412 let mut norm = 0.0;
414 for i in 0..n {
415 norm += alphas[[i, comp]] * alphas[[i, comp]];
416 }
417 norm = norm.sqrt();
418
419 if norm > 1e-12 {
420 for i in 0..n {
421 alphas[[i, comp]] /= norm;
422 }
423 }
424 }
425 }
426
427 Ok((lambdas, alphas))
428 }
429
430 fn nystrom_approximation(
440 &self,
441 x: &Array2<Float>,
442 n_components: usize,
443 nystrom_components: usize,
444 ) -> Result<(Array1<Float>, Array2<Float>)> {
445 let (n_samples, _) = x.dim();
446 let m = nystrom_components.min(n_samples);
447
448 let mut rng = if let Some(seed) = self.config.random_state {
450 StdRng::seed_from_u64(seed)
451 } else {
452 StdRng::from_rng(&mut make_rng())
453 };
454
455 let mut landmark_indices: Vec<usize> = (0..n_samples).collect();
457 landmark_indices.shuffle(&mut rng);
458 landmark_indices.truncate(m);
459
460 let mut landmarks = Array2::zeros((m, x.ncols()));
462 for (i, &idx) in landmark_indices.iter().enumerate() {
463 landmarks.row_mut(i).assign(&x.row(idx));
464 }
465
466 let mut w = self.config.kernel.compute_matrix(&landmarks, &landmarks);
469
470 let c = self.config.kernel.compute_matrix(x, &landmarks);
472
473 if self.config.center {
475 self.center_kernel_matrix(&mut w)?;
476 }
478
479 let (w_eigenvals, w_eigenvecs) = self.solve_eigenvalue_problem(&w, m)?;
481
482 let mut valid_components = Vec::new();
484 for i in 0..m {
485 if w_eigenvals[i] > 1e-10 {
486 valid_components.push(i);
487 }
488 }
489 let k = valid_components.len().min(n_components);
490
491 let mut nystrom_eigenvals = Array1::zeros(k);
493 let mut nystrom_eigenvecs = Array2::zeros((n_samples, k));
494
495 for (comp_idx, &w_idx) in valid_components.iter().take(k).enumerate() {
496 nystrom_eigenvals[comp_idx] = w_eigenvals[w_idx] * (n_samples as Float) / (m as Float);
498
499 let w_eigenvec = w_eigenvecs.column(w_idx);
501 let scale = 1.0 / (m as Float * w_eigenvals[w_idx]).sqrt();
502
503 for i in 0..n_samples {
504 let mut eigenvec_val = 0.0;
505 for j in 0..m {
506 eigenvec_val += c[[i, j]] * w_eigenvec[j];
507 }
508 nystrom_eigenvecs[[i, comp_idx]] = eigenvec_val * scale;
509 }
510 }
511
512 Ok((nystrom_eigenvals, nystrom_eigenvecs))
513 }
514
515 fn random_sampling_approximation(
525 &self,
526 x: &Array2<Float>,
527 n_components: usize,
528 n_samples_approx: usize,
529 ) -> Result<(Array1<Float>, Array2<Float>)> {
530 let (n_samples, n_features) = x.dim();
531 let m = n_samples_approx.min(n_samples);
532
533 let mut rng = if let Some(seed) = self.config.random_state {
535 StdRng::seed_from_u64(seed)
536 } else {
537 StdRng::from_rng(&mut make_rng())
538 };
539
540 let mut sample_indices: Vec<usize> = (0..n_samples).collect();
542 sample_indices.shuffle(&mut rng);
543 sample_indices.truncate(m);
544
545 let mut x_sampled = Array2::zeros((m, n_features));
547 for (i, &idx) in sample_indices.iter().enumerate() {
548 x_sampled.row_mut(i).assign(&x.row(idx));
549 }
550
551 let mut k_sampled = self.config.kernel.compute_matrix(&x_sampled, &x_sampled);
553
554 if self.config.center {
556 self.center_kernel_matrix(&mut k_sampled)?;
557 }
558
559 let (eigenvals_sampled, eigenvecs_sampled) =
561 self.solve_eigenvalue_problem(&k_sampled, n_components.min(m))?;
562
563 let mut full_eigenvecs = Array2::zeros((n_samples, eigenvals_sampled.len()));
567
568 for comp in 0..eigenvals_sampled.len() {
570 for i in 0..n_samples {
571 let mut projection = 0.0;
572 for (j, &sample_idx) in sample_indices.iter().enumerate() {
573 let x_i = x.row(i).to_owned();
574 let x_sample_j = x.row(sample_idx).to_owned();
575 let k_val = self.config.kernel.compute(&x_i, &x_sample_j);
576 projection += k_val * eigenvecs_sampled[[j, comp]];
577 }
578
579 if eigenvals_sampled[comp] > 1e-10 {
581 projection /= eigenvals_sampled[comp].sqrt();
582 }
583
584 full_eigenvecs[[i, comp]] = projection;
585 }
586 }
587
588 Ok((eigenvals_sampled, full_eigenvecs))
589 }
590}
591
592impl Transform<Array2<Float>, Array2<Float>> for KernelPCA<Trained> {
593 fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
594 let (n_samples, n_features) = x.dim();
595
596 if n_features != self.n_features_in() {
597 return Err(SklearsError::FeatureMismatch {
598 expected: self.n_features_in(),
599 actual: n_features,
600 });
601 }
602
603 let x_fit = self
604 .x_fit_
605 .as_ref()
606 .expect("invariant: x_fit_ is Some in Trained state");
607 let alphas = self
608 .alphas_
609 .as_ref()
610 .expect("invariant: alphas_ is Some in Trained state");
611 let lambdas = self
612 .lambdas_
613 .as_ref()
614 .expect("invariant: lambdas_ is Some in Trained state");
615 let n_components = self.n_components();
616
617 let mut k_test = self.config.kernel.compute_matrix(x, x_fit);
619
620 if self.config.center {
622 let n_train = x_fit.nrows();
625 let row_means_train: Array1<Float> = Array1::zeros(n_train); let overall_mean_train: Float = 0.0; for i in 0..n_samples {
629 for j in 0..n_train {
630 k_test[[i, j]] = k_test[[i, j]] - row_means_train[j] - overall_mean_train;
631 }
632 }
633 }
634
635 let mut x_transformed = Array2::zeros((n_samples, n_components));
637
638 for i in 0..n_samples {
639 for comp in 0..n_components {
640 let mut projection = 0.0;
641 for j in 0..x_fit.nrows() {
642 projection += k_test[[i, j]] * alphas[[j, comp]];
643 }
644
645 if lambdas[comp] > 1e-10 {
647 projection /= lambdas[comp].sqrt();
648 }
649
650 x_transformed[[i, comp]] = projection;
651 }
652 }
653
654 Ok(x_transformed)
655 }
656}
657
658impl KernelPCA<Trained> {
659 pub fn eigenvalues(&self) -> &Array1<Float> {
661 self.lambdas_
662 .as_ref()
663 .expect("invariant: lambdas_ is Some in Trained state")
664 }
665
666 pub fn eigenvectors(&self) -> &Array2<Float> {
668 self.alphas_
669 .as_ref()
670 .expect("invariant: alphas_ is Some in Trained state")
671 }
672
673 pub fn n_components(&self) -> usize {
675 self.n_components_
676 .expect("invariant: n_components_ is Some in Trained state")
677 }
678
679 pub fn n_features_in(&self) -> usize {
681 self.n_features_in_
682 .expect("invariant: n_features_in_ is Some in Trained state")
683 }
684
685 pub fn n_samples(&self) -> usize {
687 self.n_samples_
688 .expect("invariant: n_samples_ is Some in Trained state")
689 }
690
691 pub fn x_fit(&self) -> &Array2<Float> {
693 self.x_fit_
694 .as_ref()
695 .expect("invariant: x_fit_ is Some in Trained state")
696 }
697
698 pub fn inverse_transform(
712 &self,
713 x_transformed: &Array2<Float>,
714 max_iter: usize,
715 tol: Float,
716 ) -> Result<Array2<Float>> {
717 let (_n_samples, n_components_in) = x_transformed.dim();
718 let n_components = self.n_components();
719 let _n_features = self.n_features_in();
720 let _x_fit = self.x_fit();
721 let _alphas = self.eigenvectors();
722 let _lambdas = self.eigenvalues();
723
724 if n_components_in != n_components {
725 return Err(SklearsError::FeatureMismatch {
726 expected: n_components,
727 actual: n_components_in,
728 });
729 }
730
731 match &self.config.kernel {
733 KernelFunction::Linear => self.linear_preimage(x_transformed),
734 KernelFunction::Rbf { gamma: _ } => {
735 self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
736 }
737 KernelFunction::Polynomial { .. } => {
738 self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
739 }
740 KernelFunction::Sigmoid { .. } => {
741 self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
742 }
743 KernelFunction::Laplacian { .. } => {
744 self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
745 }
746 KernelFunction::ChiSquared { .. } => {
747 self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
748 }
749 }
750 }
751
752 fn linear_preimage(&self, x_transformed: &Array2<Float>) -> Result<Array2<Float>> {
754 let x_fit = self.x_fit();
755 let alphas = self.eigenvectors();
756 let lambdas = self.eigenvalues();
757 let (n_samples, n_components) = x_transformed.dim();
758 let n_features = self.n_features_in();
759
760 let mut x_reconstructed = Array2::zeros((n_samples, n_features));
762
763 for i in 0..n_samples {
764 for k in 0..n_features {
765 let mut value = 0.0;
766 for comp in 0..n_components {
767 if lambdas[comp] > 1e-10 {
768 let component_contrib = x_transformed[[i, comp]] * lambdas[comp].sqrt();
769 for j in 0..x_fit.nrows() {
770 value += alphas[[j, comp]] * component_contrib * x_fit[[j, k]];
771 }
772 }
773 }
774 x_reconstructed[[i, k]] = value;
775 }
776 }
777
778 Ok(x_reconstructed)
779 }
780
781 fn nonlinear_preimage_fixed_point(
783 &self,
784 x_transformed: &Array2<Float>,
785 max_iter: usize,
786 tol: Float,
787 ) -> Result<Array2<Float>> {
788 let (n_samples, _n_components) = x_transformed.dim();
789 let n_features = self.n_features_in();
790 let x_fit = self.x_fit();
791 let alphas = self.eigenvectors();
792 let lambdas = self.eigenvalues();
793
794 let mut x_reconstructed = Array2::zeros((n_samples, n_features));
795
796 for i in 0..n_samples {
797 let mut x_current = x_fit.mean_axis(Axis(0)).ok_or_else(|| {
799 SklearsError::NumericalError(
800 "cannot compute mean of empty training data".to_string(),
801 )
802 })?;
803 let target_transformed = x_transformed.slice(scirs2_core::ndarray::s![i, ..]);
804
805 for _iter in 0..max_iter {
806 let x_old = x_current.clone();
807
808 x_current =
810 self.fixed_point_step(&x_old, &target_transformed, x_fit, alphas, lambdas)?;
811
812 let diff = (&x_current - &x_old)
814 .mapv(|x| x.abs())
815 .fold(0.0f64, |acc, &x| acc.max(x));
816 if diff < tol {
817 break;
818 }
819 }
820
821 for j in 0..n_features {
823 x_reconstructed[[i, j]] = x_current[j];
824 }
825 }
826
827 Ok(x_reconstructed)
828 }
829
830 fn fixed_point_step(
832 &self,
833 x_current: &Array1<Float>,
834 target_transformed: &scirs2_core::ndarray::ArrayView1<Float>,
835 x_fit: &Array2<Float>,
836 alphas: &Array2<Float>,
837 lambdas: &Array1<Float>,
838 ) -> Result<Array1<Float>> {
839 let n_features = x_current.len();
840 let n_train = x_fit.nrows();
841 let n_components = target_transformed.len();
842
843 let mut numerator = Array1::<Float>::zeros(n_features);
845 let mut denominator = Array1::<Float>::zeros(n_features);
846
847 for j in 0..n_train {
848 let x_train_j = x_fit.slice(scirs2_core::ndarray::s![j, ..]).to_owned();
849 let _k_val = self.config.kernel.compute(x_current, &x_train_j);
850 let k_deriv = self.kernel_derivative(x_current, &x_train_j);
851
852 let mut weight = 0.0;
854 for comp in 0..n_components {
855 if lambdas[comp] > 1e-10 {
856 weight += target_transformed[comp] * alphas[[j, comp]] / lambdas[comp].sqrt();
857 }
858 }
859
860 for k in 0..n_features {
861 numerator[k] += weight * k_deriv[k] * x_train_j[k];
862 denominator[k] += weight * k_deriv[k];
863 }
864 }
865
866 let mut x_new = Array1::zeros(n_features);
868 for k in 0..n_features {
869 if denominator[k].abs() > 1e-12 {
870 x_new[k] = numerator[k] / denominator[k];
871 } else {
872 x_new[k] = x_current[k]; }
874 }
875
876 Ok(x_new)
877 }
878
879 fn kernel_derivative(&self, x: &Array1<Float>, y: &Array1<Float>) -> Array1<Float> {
881 match &self.config.kernel {
882 KernelFunction::Linear => {
883 y.clone()
885 }
886 KernelFunction::Rbf { gamma } => {
887 let k_val = self.config.kernel.compute(x, y);
889 let diff = x - y;
890 diff.mapv(|d| -2.0 * gamma * d * k_val)
891 }
892 KernelFunction::Polynomial {
893 degree,
894 gamma,
895 coef0,
896 } => {
897 let dot_product = x.dot(y);
899 let base = gamma * dot_product + coef0;
900 if *degree == 1 || base.abs() < 1e-12 {
901 y.mapv(|yi| *gamma * yi)
902 } else {
903 let factor = (*degree as Float) * gamma * base.powf(*degree as Float - 1.0);
904 y.mapv(|yi| factor * yi)
905 }
906 }
907 KernelFunction::Sigmoid { gamma, coef0 } => {
908 let dot_product = x.dot(y);
910 let tanh_val = (gamma * dot_product + coef0).tanh();
911 let factor = gamma * (1.0 - tanh_val * tanh_val);
912 y.mapv(|yi| factor * yi)
913 }
914 KernelFunction::Laplacian { gamma } => {
915 let k_val = self.config.kernel.compute(x, y);
917 let diff = x - y;
918 diff.mapv(|d| -gamma * d.signum() * k_val)
919 }
920 KernelFunction::ChiSquared { gamma } => {
921 let k_val = self.config.kernel.compute(x, y);
923 let mut derivative = Array1::zeros(x.len());
924 for i in 0..x.len() {
925 let sum = x[i] + y[i];
926 if sum > 1e-12 {
927 let diff = x[i] - y[i];
928 derivative[i] = -2.0 * gamma * k_val * diff / sum;
929 }
930 }
931 derivative
932 }
933 }
934 }
935
936 pub fn mds_preimage(&self, x_transformed: &Array2<Float>) -> Result<Array2<Float>> {
941 let (n_samples, _n_components) = x_transformed.dim();
942 let n_features = self.n_features_in();
943 let x_fit = self.x_fit();
944
945 let mut dist_transformed = Array2::zeros((n_samples, n_samples));
947 for i in 0..n_samples {
948 for j in 0..n_samples {
949 let diff = &x_transformed.slice(scirs2_core::ndarray::s![i, ..])
950 - &x_transformed.slice(scirs2_core::ndarray::s![j, ..]);
951 dist_transformed[[i, j]] = diff.mapv(|x| x * x).sum().sqrt();
952 }
953 }
954
955 let mut x_reconstructed = Array2::zeros((n_samples, n_features));
957
958 for i in 0..n_samples {
959 let k = 5.min(x_fit.nrows()); let mut distances = Vec::new();
962
963 for j in 0..x_fit.nrows() {
964 let x_train_j_transformed =
966 self.transform_single_sample(&x_fit.slice(scirs2_core::ndarray::s![j, ..]))?;
967 let diff =
968 &x_transformed.slice(scirs2_core::ndarray::s![i, ..]) - &x_train_j_transformed;
969 let dist = diff.mapv(|x| x * x).sum().sqrt();
970 distances.push((dist, j));
971 }
972
973 distances.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
975
976 let mut weight_sum = 0.0;
978 for feat in 0..n_features {
979 let mut weighted_value = 0.0;
980 for &(dist, idx) in distances.iter().take(k) {
981 let weight = if dist > 1e-12 {
982 1.0 / (dist + 1e-6)
983 } else {
984 1e6
985 };
986 weighted_value += weight * x_fit[[idx, feat]];
987 weight_sum += weight;
988 }
989 x_reconstructed[[i, feat]] = weighted_value / weight_sum;
990 }
991 }
992
993 Ok(x_reconstructed)
994 }
995
996 fn transform_single_sample(
998 &self,
999 x_sample: &scirs2_core::ndarray::ArrayView1<Float>,
1000 ) -> Result<Array1<Float>> {
1001 let x_fit = self.x_fit();
1002 let alphas = self.eigenvectors();
1003 let lambdas = self.eigenvalues();
1004 let n_components = self.n_components();
1005
1006 let mut k_test = Array1::zeros(x_fit.nrows());
1008 for j in 0..x_fit.nrows() {
1009 let x_train_j = x_fit.slice(scirs2_core::ndarray::s![j, ..]).to_owned();
1010 k_test[j] = self.config.kernel.compute(&x_sample.to_owned(), &x_train_j);
1011 }
1012
1013 if self.config.center {
1015 let mean_k = k_test.mean().ok_or_else(|| {
1017 SklearsError::NumericalError(
1018 "cannot compute mean of empty k_test array".to_string(),
1019 )
1020 })?;
1021 for val in k_test.iter_mut() {
1022 *val -= mean_k;
1023 }
1024 }
1025
1026 let mut x_transformed = Array1::zeros(n_components);
1028 for comp in 0..n_components {
1029 let mut projection = 0.0;
1030 for j in 0..x_fit.nrows() {
1031 projection += k_test[j] * alphas[[j, comp]];
1032 }
1033
1034 if lambdas[comp] > 1e-10 {
1036 projection /= lambdas[comp].sqrt();
1037 }
1038
1039 x_transformed[comp] = projection;
1040 }
1041
1042 Ok(x_transformed)
1043 }
1044}
1045
1046impl KernelFunction {
1048 pub fn validate(&self) -> Result<()> {
1050 match self {
1051 KernelFunction::Linear => Ok(()),
1052 KernelFunction::Rbf { gamma } => {
1053 if *gamma <= 0.0 {
1054 return Err(SklearsError::InvalidParameter {
1055 name: "gamma".to_string(),
1056 reason: "must be positive for RBF kernel".to_string(),
1057 });
1058 }
1059 if *gamma > 1e6 {
1060 return Err(SklearsError::InvalidParameter {
1061 name: "gamma".to_string(),
1062 reason: "too large, may cause numerical instability".to_string(),
1063 });
1064 }
1065 Ok(())
1066 }
1067 KernelFunction::Polynomial {
1068 degree,
1069 gamma,
1070 coef0: _,
1071 } => {
1072 if *degree <= 0 {
1073 return Err(SklearsError::InvalidParameter {
1074 name: "degree".to_string(),
1075 reason: "must be positive for polynomial kernel".to_string(),
1076 });
1077 }
1078 if *degree > 10 {
1079 return Err(SklearsError::InvalidParameter {
1080 name: "degree".to_string(),
1081 reason: "too large, may cause numerical overflow".to_string(),
1082 });
1083 }
1084 if *gamma <= 0.0 {
1085 return Err(SklearsError::InvalidParameter {
1086 name: "gamma".to_string(),
1087 reason: "must be positive for polynomial kernel".to_string(),
1088 });
1089 }
1090 Ok(())
1091 }
1092 KernelFunction::Sigmoid { gamma, coef0: _ } => {
1093 if *gamma <= 0.0 {
1094 return Err(SklearsError::InvalidParameter {
1095 name: "gamma".to_string(),
1096 reason: "must be positive for sigmoid kernel".to_string(),
1097 });
1098 }
1099 Ok(())
1100 }
1101 KernelFunction::Laplacian { gamma } => {
1102 if *gamma <= 0.0 {
1103 return Err(SklearsError::InvalidParameter {
1104 name: "gamma".to_string(),
1105 reason: "must be positive for Laplacian kernel".to_string(),
1106 });
1107 }
1108 if *gamma > 1e6 {
1109 return Err(SklearsError::InvalidParameter {
1110 name: "gamma".to_string(),
1111 reason: "too large, may cause numerical instability".to_string(),
1112 });
1113 }
1114 Ok(())
1115 }
1116 KernelFunction::ChiSquared { gamma } => {
1117 if *gamma <= 0.0 {
1118 return Err(SklearsError::InvalidParameter {
1119 name: "gamma".to_string(),
1120 reason: "must be positive for Chi-squared kernel".to_string(),
1121 });
1122 }
1123 Ok(())
1124 }
1125 }
1126 }
1127
1128 pub fn evaluate_performance(
1130 &self,
1131 x: &Array2<Float>,
1132 n_components: usize,
1133 cv_folds: usize,
1134 ) -> Result<Float> {
1135 let (n_samples, _) = x.dim();
1136 if n_samples < cv_folds {
1137 return Err(SklearsError::InvalidParameter {
1138 name: "cv_folds".to_string(),
1139 reason: "Number of samples must be >= cv_folds".to_string(),
1140 });
1141 }
1142
1143 self.validate()?;
1144
1145 let fold_size = n_samples / cv_folds;
1146 let mut reconstruction_errors = Vec::new();
1147
1148 for fold in 0..cv_folds {
1149 let start_idx = fold * fold_size;
1150 let end_idx = if fold == cv_folds - 1 {
1151 n_samples
1152 } else {
1153 (fold + 1) * fold_size
1154 };
1155
1156 let mut train_indices = Vec::new();
1158 let mut val_indices = Vec::new();
1159
1160 for i in 0..n_samples {
1161 if i >= start_idx && i < end_idx {
1162 val_indices.push(i);
1163 } else {
1164 train_indices.push(i);
1165 }
1166 }
1167
1168 if train_indices.is_empty() || val_indices.is_empty() {
1169 continue;
1170 }
1171
1172 let x_train = x.select(scirs2_core::ndarray::Axis(0), &train_indices);
1174 let x_val = x.select(scirs2_core::ndarray::Axis(0), &val_indices);
1175
1176 let kpca = KernelPCA::new()
1178 .n_components(n_components)
1179 .kernel(*self)
1180 .fit(&x_train, &())?;
1181
1182 let x_val_transformed = kpca.transform(&x_val)?;
1184
1185 let error = self.compute_reconstruction_error(&x_val, &x_val_transformed, &kpca)?;
1187 reconstruction_errors.push(error);
1188 }
1189
1190 if reconstruction_errors.is_empty() {
1191 return Err(SklearsError::InvalidParameter {
1192 name: "cv_folds".to_string(),
1193 reason: "No valid cross-validation folds".to_string(),
1194 });
1195 }
1196
1197 let mean_error =
1199 reconstruction_errors.iter().sum::<Float>() / reconstruction_errors.len() as Float;
1200 Ok(mean_error)
1201 }
1202
1203 fn compute_reconstruction_error(
1205 &self,
1206 x_original: &Array2<Float>,
1207 x_transformed: &Array2<Float>,
1208 _kpca: &KernelPCA<sklears_core::traits::Trained>,
1209 ) -> Result<Float> {
1210 let original_var = self.compute_total_variance(x_original);
1212 let transformed_var = self.compute_total_variance(x_transformed);
1213
1214 let error = (original_var - transformed_var).abs() / original_var.max(1e-10);
1216 Ok(error)
1217 }
1218
1219 fn compute_total_variance(&self, x: &Array2<Float>) -> Float {
1221 let (n_samples, n_features) = x.dim();
1222 if n_samples == 0 || n_features == 0 {
1223 return 0.0;
1224 }
1225
1226 let mut total_var = 0.0;
1227 for j in 0..n_features {
1228 let col = x.column(j);
1229 let mean = col.mean().unwrap_or(0.0);
1230 let var = col.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0);
1231 total_var += var;
1232 }
1233 total_var
1234 }
1235
1236 pub fn select_best_kernel(
1238 kernels: &[KernelFunction],
1239 x: &Array2<Float>,
1240 n_components: usize,
1241 cv_folds: usize,
1242 ) -> Result<(KernelFunction, Float)> {
1243 if kernels.is_empty() {
1244 return Err(SklearsError::InvalidParameter {
1245 name: "kernels".to_string(),
1246 reason: "Must provide at least one kernel".to_string(),
1247 });
1248 }
1249
1250 let mut best_kernel = kernels[0];
1251 let mut best_score = Float::INFINITY;
1252
1253 for &kernel in kernels {
1254 match kernel.evaluate_performance(x, n_components, cv_folds) {
1255 Ok(score) => {
1256 if score < best_score {
1257 best_score = score;
1258 best_kernel = kernel;
1259 }
1260 }
1261 Err(_) => {
1262 continue;
1264 }
1265 }
1266 }
1267
1268 if best_score == Float::INFINITY {
1269 return Err(SklearsError::InvalidParameter {
1270 name: "kernels".to_string(),
1271 reason: "All kernels failed validation".to_string(),
1272 });
1273 }
1274
1275 Ok((best_kernel, best_score))
1276 }
1277
1278 pub fn generate_kernel_grid() -> Vec<KernelFunction> {
1280 let mut kernels = Vec::new();
1281
1282 kernels.push(KernelFunction::Linear);
1284
1285 for &gamma in &[0.001, 0.01, 0.1, 1.0, 10.0, 100.0] {
1287 kernels.push(KernelFunction::Rbf { gamma });
1288 }
1289
1290 for °ree in &[2, 3, 4] {
1292 for &gamma in &[0.1, 1.0] {
1293 for &coef0 in &[0.0, 1.0] {
1294 kernels.push(KernelFunction::Polynomial {
1295 degree,
1296 gamma,
1297 coef0,
1298 });
1299 }
1300 }
1301 }
1302
1303 for &gamma in &[0.001, 0.01, 0.1] {
1305 for &coef0 in &[0.0, 1.0] {
1306 kernels.push(KernelFunction::Sigmoid { gamma, coef0 });
1307 }
1308 }
1309
1310 for &gamma in &[0.01, 0.1, 1.0, 10.0] {
1312 kernels.push(KernelFunction::Laplacian { gamma });
1313 }
1314
1315 for &gamma in &[0.1, 1.0, 10.0] {
1317 kernels.push(KernelFunction::ChiSquared { gamma });
1318 }
1319
1320 kernels
1321 }
1322}
1323
1324impl KernelPCA<Untrained> {
1326 pub fn fit_with_kernel_selection(
1328 mut self,
1329 x: &Array2<Float>,
1330 kernel_candidates: Option<&[KernelFunction]>,
1331 cv_folds: usize,
1332 ) -> Result<KernelPCA<sklears_core::traits::Trained>> {
1333 let kernels = kernel_candidates
1334 .map(|k| k.to_vec())
1335 .unwrap_or_else(KernelFunction::generate_kernel_grid);
1336
1337 let n_components = self.config.n_components.unwrap_or(x.ncols().min(x.nrows()));
1338
1339 let (best_kernel, _score) =
1340 KernelFunction::select_best_kernel(&kernels, x, n_components, cv_folds)?;
1341
1342 self.config.kernel = best_kernel;
1343 self.fit(x, &())
1344 }
1345}
1346
1347#[allow(non_snake_case)]
1348#[cfg(test)]
1349mod tests {
1350 use super::*;
1351 use approx::assert_abs_diff_eq;
1352 use scirs2_core::ndarray::array;
1353
1354 #[test]
1355 fn test_kernel_functions() {
1356 let x = array![1.0, 2.0];
1357 let y = array![3.0, 4.0];
1358
1359 let linear = KernelFunction::Linear;
1361 let linear_result = linear.compute(&x, &y);
1362 assert_abs_diff_eq!(linear_result, 11.0, epsilon = 1e-10); let rbf = KernelFunction::Rbf { gamma: 1.0 };
1366 let rbf_result = rbf.compute(&x, &y);
1367 assert!(rbf_result > 0.0 && rbf_result < 1.0);
1369
1370 let poly = KernelFunction::Polynomial {
1372 degree: 2,
1373 gamma: 1.0,
1374 coef0: 1.0,
1375 };
1376 let poly_result = poly.compute(&x, &y);
1377 assert_abs_diff_eq!(poly_result, 144.0, epsilon = 1e-10);
1379
1380 let sigmoid = KernelFunction::Sigmoid {
1382 gamma: 1.0,
1383 coef0: 0.0,
1384 };
1385 let sigmoid_result = sigmoid.compute(&x, &y);
1386 assert!(sigmoid_result > 0.9 && sigmoid_result <= 1.0);
1388 }
1389
1390 #[test]
1391 fn test_kernel_matrix() {
1392 let x = array![[1.0, 2.0], [3.0, 4.0]];
1393 let y = array![[5.0, 6.0], [7.0, 8.0]];
1394
1395 let kernel = KernelFunction::Linear;
1396 let k = kernel.compute_matrix(&x, &y);
1397
1398 assert_eq!(k.dim(), (2, 2));
1399 assert_abs_diff_eq!(k[[0, 0]], 17.0, epsilon = 1e-10);
1401 assert_abs_diff_eq!(k[[0, 1]], 23.0, epsilon = 1e-10);
1403 assert_abs_diff_eq!(k[[1, 0]], 39.0, epsilon = 1e-10);
1405 assert_abs_diff_eq!(k[[1, 1]], 53.0, epsilon = 1e-10);
1407 }
1408
1409 #[test]
1410 fn test_kernel_pca_creation() {
1411 let kpca = KernelPCA::new()
1412 .n_components(2)
1413 .kernel(KernelFunction::Rbf { gamma: 0.1 })
1414 .tol(1e-6)
1415 .center(false);
1416
1417 assert_eq!(kpca.config.n_components, Some(2));
1418 assert_eq!(kpca.config.tol, 1e-6);
1419 assert!(!kpca.config.center);
1420 }
1421
1422 #[test]
1423 fn test_kernel_pca_fit_transform() {
1424 let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
1425
1426 let kpca = KernelPCA::new()
1427 .n_components(2)
1428 .kernel(KernelFunction::Linear)
1429 .fit(&x, &())
1430 .expect("operation should succeed");
1431
1432 assert_eq!(kpca.n_components(), 2);
1433 assert_eq!(kpca.n_features_in(), 2);
1434 assert_eq!(kpca.n_samples(), 4);
1435
1436 let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1437 assert_eq!(x_transformed.dim(), (4, 2));
1438
1439 for &val in x_transformed.iter() {
1441 assert!(val.is_finite());
1442 }
1443 }
1444
1445 #[test]
1446 fn test_kernel_pca_rbf() {
1447 let x = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0],];
1448
1449 let kpca = KernelPCA::new()
1450 .n_components(2)
1451 .kernel(KernelFunction::Rbf { gamma: 0.5 })
1452 .fit(&x, &())
1453 .expect("operation should succeed");
1454
1455 let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1456 assert_eq!(x_transformed.dim(), (4, 2));
1457
1458 let eigenvalues = kpca.eigenvalues();
1460 for &val in eigenvalues.iter() {
1461 assert!(val >= 0.0, "Eigenvalue should be non-negative: {}", val);
1462 }
1463 }
1464
1465 #[test]
1466 fn test_kernel_pca_polynomial() {
1467 let x = array![[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0],];
1468
1469 let kpca = KernelPCA::new()
1470 .n_components(3)
1471 .kernel(KernelFunction::Polynomial {
1472 degree: 2,
1473 gamma: 1.0,
1474 coef0: 1.0,
1475 })
1476 .fit(&x, &())
1477 .expect("operation should succeed");
1478
1479 let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1480 assert_eq!(x_transformed.dim(), (4, 3));
1481
1482 for &val in x_transformed.iter() {
1484 assert!(val.is_finite());
1485 }
1486 }
1487
1488 #[test]
1489 fn test_kernel_pca_errors() {
1490 let empty_x: Array2<f64> = Array2::zeros((0, 2));
1492 let result = KernelPCA::new().fit(&empty_x, &());
1493 assert!(result.is_err());
1494
1495 let zero_features_x: Array2<f64> = Array2::zeros((2, 0));
1497 let result = KernelPCA::new().fit(&zero_features_x, &());
1498 assert!(result.is_err());
1499
1500 let x = array![[1.0, 2.0], [3.0, 4.0]];
1502 let result = KernelPCA::new().n_components(0).fit(&x, &());
1503 assert!(result.is_err());
1504 }
1505
1506 #[test]
1507 fn test_kernel_pca_feature_mismatch() {
1508 let x_train = array![[1.0, 2.0], [3.0, 4.0]];
1509 let x_test = array![[1.0, 2.0, 3.0]]; let kpca = KernelPCA::new()
1512 .fit(&x_train, &())
1513 .expect("model fitting should succeed");
1514 let result = kpca.transform(&x_test);
1515
1516 assert!(result.is_err());
1517 assert!(result.unwrap_err().to_string().contains("Feature"));
1518 }
1519
1520 #[test]
1521 fn test_kernel_pca_default() {
1522 let x = array![[1.0, 2.0], [3.0, 4.0]];
1523
1524 let kpca = KernelPCA::default()
1525 .fit(&x, &())
1526 .expect("model fitting should succeed");
1527
1528 assert_eq!(kpca.n_components(), 2); assert_eq!(kpca.n_features_in(), 2);
1531 }
1532
1533 #[test]
1534 fn test_new_kernel_functions() {
1535 let x = array![1.0, 2.0];
1536 let y = array![3.0, 4.0];
1537
1538 let laplacian = KernelFunction::Laplacian { gamma: 0.5 };
1540 let laplacian_result = laplacian.compute(&x, &y);
1541 assert!(laplacian_result > 0.0 && laplacian_result < 1.0);
1543
1544 let chi_squared = KernelFunction::ChiSquared { gamma: 1.0 };
1546 let chi_result = chi_squared.compute(&x, &y);
1547 assert!(chi_result > 0.0 && chi_result <= 1.0);
1548 }
1549
1550 #[test]
1551 fn test_kernel_pca_nystrom_approximation() {
1552 let x = array![
1553 [1.0, 2.0],
1554 [3.0, 4.0],
1555 [5.0, 6.0],
1556 [7.0, 8.0],
1557 [9.0, 10.0],
1558 [11.0, 12.0],
1559 ];
1560
1561 let kpca = KernelPCA::new()
1562 .n_components(2)
1563 .kernel(KernelFunction::Rbf { gamma: 0.1 })
1564 .approximation(KernelApproximation::Nystrom { n_components: 4 })
1565 .random_state(42)
1566 .fit(&x, &())
1567 .expect("operation should succeed");
1568
1569 let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1570 assert_eq!(x_transformed.dim(), (6, 2));
1571
1572 let eigenvalues = kpca.eigenvalues();
1574 for &val in eigenvalues.iter() {
1575 assert!(val >= 0.0, "Eigenvalue should be non-negative: {}", val);
1576 }
1577 }
1578
1579 #[test]
1580 fn test_kernel_pca_random_sampling() {
1581 let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0],];
1582
1583 let kpca = KernelPCA::new()
1584 .n_components(2)
1585 .kernel(KernelFunction::Linear)
1586 .approximation(KernelApproximation::RandomSampling { n_samples: 3 })
1587 .random_state(123)
1588 .fit(&x, &())
1589 .expect("operation should succeed");
1590
1591 let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1592 assert_eq!(x_transformed.dim(), (5, 2));
1593
1594 for &val in x_transformed.iter() {
1596 assert!(val.is_finite());
1597 }
1598 }
1599
1600 #[test]
1601 fn test_kernel_pca_laplacian_kernel() {
1602 let x = array![[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]];
1603
1604 let kpca = KernelPCA::new()
1605 .n_components(2)
1606 .kernel(KernelFunction::Laplacian { gamma: 0.1 })
1607 .fit(&x, &())
1608 .expect("operation should succeed");
1609
1610 let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1611 assert_eq!(x_transformed.dim(), (4, 2));
1612
1613 for &val in x_transformed.iter() {
1615 assert!(val.is_finite());
1616 }
1617 }
1618
1619 #[test]
1620 fn test_kernel_pca_chi_squared_kernel() {
1621 let x = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
1623
1624 let kpca = KernelPCA::new()
1625 .n_components(2)
1626 .kernel(KernelFunction::ChiSquared { gamma: 0.5 })
1627 .fit(&x, &())
1628 .expect("operation should succeed");
1629
1630 let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1631 assert_eq!(x_transformed.dim(), (4, 2));
1632
1633 for &val in x_transformed.iter() {
1635 assert!(val.is_finite());
1636 }
1637 }
1638}