use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_core::random::rng as make_rng;
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{SeedableRng, SliceRandom};
use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
use sklears_core::{
error::{Result, SklearsError},
traits::{Fit, Trained, Transform, Untrained},
types::Float,
};
use std::marker::PhantomData;
#[derive(Debug, Clone, Copy)]
pub enum KernelFunction {
Linear,
Rbf { gamma: Float },
Polynomial {
degree: i32,
gamma: Float,
coef0: Float,
},
Sigmoid { gamma: Float, coef0: Float },
Laplacian { gamma: Float },
ChiSquared { gamma: Float },
}
impl Default for KernelFunction {
fn default() -> Self {
KernelFunction::Rbf { gamma: 1.0 }
}
}
impl KernelFunction {
pub fn compute(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
match self {
KernelFunction::Linear => x.dot(y),
KernelFunction::Rbf { gamma } => {
let diff = x - y;
let dist_sq = diff.dot(&diff);
(-gamma * dist_sq).exp()
}
KernelFunction::Polynomial {
degree,
gamma,
coef0,
} => {
let dot_product = x.dot(y);
(gamma * dot_product + coef0).powi(*degree)
}
KernelFunction::Sigmoid { gamma, coef0 } => {
let dot_product = x.dot(y);
(gamma * dot_product + coef0).tanh()
}
KernelFunction::Laplacian { gamma } => {
let l1_dist = (x - y).mapv(|x| x.abs()).sum();
(-gamma * l1_dist).exp()
}
KernelFunction::ChiSquared { gamma } => {
let mut chi_sq_dist = 0.0;
for i in 0..x.len() {
let sum = x[i] + y[i];
if sum > 1e-12 {
let diff = x[i] - y[i];
chi_sq_dist += diff * diff / sum;
}
}
(-gamma * chi_sq_dist).exp()
}
}
}
pub fn compute_matrix(&self, x: &Array2<Float>, y: &Array2<Float>) -> Array2<Float> {
let n_x = x.nrows();
let n_y = y.nrows();
let mut kernel_matrix = Array2::zeros((n_x, n_y));
for i in 0..n_x {
for j in 0..n_y {
let x_i = x.row(i).to_owned();
let y_j = y.row(j).to_owned();
kernel_matrix[[i, j]] = self.compute(&x_i, &y_j);
}
}
kernel_matrix
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KernelApproximation {
#[default]
Full,
Nystrom { n_components: usize },
RandomSampling { n_samples: usize },
}
#[derive(Debug, Clone)]
pub struct KernelPcaConfig {
pub n_components: Option<usize>,
pub kernel: KernelFunction,
pub tol: Float,
pub max_iter: usize,
pub center: bool,
pub copy: bool,
pub approximation: KernelApproximation,
pub random_state: Option<u64>,
}
impl Default for KernelPcaConfig {
fn default() -> Self {
Self {
n_components: None,
kernel: KernelFunction::default(),
tol: 1e-8,
max_iter: 300,
center: true,
copy: true,
approximation: KernelApproximation::default(),
random_state: None,
}
}
}
#[derive(Debug, Clone)]
pub struct KernelPCA<State = Untrained> {
config: KernelPcaConfig,
state: PhantomData<State>,
x_fit_: Option<Array2<Float>>,
lambdas_: Option<Array1<Float>>,
alphas_: Option<Array2<Float>>,
n_components_: Option<usize>,
n_features_in_: Option<usize>,
n_samples_: Option<usize>,
}
impl KernelPCA<Untrained> {
pub fn new() -> Self {
Self {
config: KernelPcaConfig::default(),
state: PhantomData,
x_fit_: None,
lambdas_: None,
alphas_: None,
n_components_: None,
n_features_in_: None,
n_samples_: None,
}
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.config.n_components = Some(n_components);
self
}
pub fn kernel(mut self, kernel: KernelFunction) -> Self {
self.config.kernel = kernel;
self
}
pub fn tol(mut self, tol: Float) -> Self {
self.config.tol = tol;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.config.max_iter = max_iter;
self
}
pub fn center(mut self, center: bool) -> Self {
self.config.center = center;
self
}
pub fn copy(mut self, copy: bool) -> Self {
self.config.copy = copy;
self
}
pub fn approximation(mut self, approximation: KernelApproximation) -> Self {
self.config.approximation = approximation;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.config.random_state = Some(random_state);
self
}
}
impl Default for KernelPCA<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Fit<Array2<Float>, ()> for KernelPCA<Untrained> {
type Fitted = KernelPCA<Trained>;
fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
let (n_samples, n_features) = x.dim();
if n_samples == 0 {
return Err(SklearsError::InvalidInput("Empty dataset".to_string()));
}
if n_features == 0 {
return Err(SklearsError::InvalidInput(
"Dataset has no features".to_string(),
));
}
let n_components = self
.config
.n_components
.unwrap_or(n_samples.min(n_features))
.min(n_samples);
if n_components == 0 {
return Err(SklearsError::InvalidInput(
"Number of components must be positive".to_string(),
));
}
let x_fit = if self.config.copy {
x.clone()
} else {
x.to_owned()
};
let (lambdas, alphas) = match self.config.approximation {
KernelApproximation::Full => {
let mut k = self.config.kernel.compute_matrix(&x_fit, &x_fit);
if self.config.center {
self.center_kernel_matrix(&mut k)?;
}
self.solve_eigenvalue_problem(&k, n_components)?
}
KernelApproximation::Nystrom {
n_components: nystrom_components,
} => {
self.nystrom_approximation(&x_fit, n_components, nystrom_components)?
}
KernelApproximation::RandomSampling { n_samples } => {
self.random_sampling_approximation(&x_fit, n_components, n_samples)?
}
};
Ok(KernelPCA {
config: self.config,
state: PhantomData,
x_fit_: Some(x_fit),
lambdas_: Some(lambdas),
alphas_: Some(alphas),
n_components_: Some(n_components),
n_features_in_: Some(n_features),
n_samples_: Some(n_samples),
})
}
}
impl KernelPCA<Untrained> {
fn center_kernel_matrix(&self, k: &mut Array2<Float>) -> Result<()> {
let n = k.nrows();
if n != k.ncols() {
return Err(SklearsError::InvalidInput(
"Kernel matrix must be square".to_string(),
));
}
let row_means = k.mean_axis(Axis(1)).ok_or_else(|| {
SklearsError::NumericalError(
"cannot compute row means of empty kernel matrix".to_string(),
)
})?;
let overall_mean = k.mean().ok_or_else(|| {
SklearsError::NumericalError("cannot compute mean of empty kernel matrix".to_string())
})?;
for i in 0..n {
for j in 0..n {
k[[i, j]] = k[[i, j]] - row_means[i] - row_means[j] + overall_mean;
}
}
Ok(())
}
fn solve_eigenvalue_problem(
&self,
k: &Array2<Float>,
n_components: usize,
) -> Result<(Array1<Float>, Array2<Float>)> {
let n = k.nrows();
let n_comp = n_components.min(n);
let (eigenvalues, eigenvectors) = k.eigh(UPLO::Lower).map_err(|e| {
SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
})?;
let mut eigen_pairs: Vec<(Float, usize)> = eigenvalues
.iter()
.enumerate()
.map(|(i, &val)| (val, i))
.collect();
eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let mut lambdas = Array1::zeros(n_comp);
let mut alphas = Array2::zeros((n, n_comp));
for comp in 0..n_comp {
if comp < eigen_pairs.len() {
let (eigenval, idx) = eigen_pairs[comp];
lambdas[comp] = eigenval.max(0.0);
for i in 0..n {
alphas[[i, comp]] = eigenvectors[[i, idx]];
}
let mut norm = 0.0;
for i in 0..n {
norm += alphas[[i, comp]] * alphas[[i, comp]];
}
norm = norm.sqrt();
if norm > 1e-12 {
for i in 0..n {
alphas[[i, comp]] /= norm;
}
}
}
}
Ok((lambdas, alphas))
}
fn nystrom_approximation(
&self,
x: &Array2<Float>,
n_components: usize,
nystrom_components: usize,
) -> Result<(Array1<Float>, Array2<Float>)> {
let (n_samples, _) = x.dim();
let m = nystrom_components.min(n_samples);
let mut rng = if let Some(seed) = self.config.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut make_rng())
};
let mut landmark_indices: Vec<usize> = (0..n_samples).collect();
landmark_indices.shuffle(&mut rng);
landmark_indices.truncate(m);
let mut landmarks = Array2::zeros((m, x.ncols()));
for (i, &idx) in landmark_indices.iter().enumerate() {
landmarks.row_mut(i).assign(&x.row(idx));
}
let mut w = self.config.kernel.compute_matrix(&landmarks, &landmarks);
let c = self.config.kernel.compute_matrix(x, &landmarks);
if self.config.center {
self.center_kernel_matrix(&mut w)?;
}
let (w_eigenvals, w_eigenvecs) = self.solve_eigenvalue_problem(&w, m)?;
let mut valid_components = Vec::new();
for i in 0..m {
if w_eigenvals[i] > 1e-10 {
valid_components.push(i);
}
}
let k = valid_components.len().min(n_components);
let mut nystrom_eigenvals = Array1::zeros(k);
let mut nystrom_eigenvecs = Array2::zeros((n_samples, k));
for (comp_idx, &w_idx) in valid_components.iter().take(k).enumerate() {
nystrom_eigenvals[comp_idx] = w_eigenvals[w_idx] * (n_samples as Float) / (m as Float);
let w_eigenvec = w_eigenvecs.column(w_idx);
let scale = 1.0 / (m as Float * w_eigenvals[w_idx]).sqrt();
for i in 0..n_samples {
let mut eigenvec_val = 0.0;
for j in 0..m {
eigenvec_val += c[[i, j]] * w_eigenvec[j];
}
nystrom_eigenvecs[[i, comp_idx]] = eigenvec_val * scale;
}
}
Ok((nystrom_eigenvals, nystrom_eigenvecs))
}
fn random_sampling_approximation(
&self,
x: &Array2<Float>,
n_components: usize,
n_samples_approx: usize,
) -> Result<(Array1<Float>, Array2<Float>)> {
let (n_samples, n_features) = x.dim();
let m = n_samples_approx.min(n_samples);
let mut rng = if let Some(seed) = self.config.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut make_rng())
};
let mut sample_indices: Vec<usize> = (0..n_samples).collect();
sample_indices.shuffle(&mut rng);
sample_indices.truncate(m);
let mut x_sampled = Array2::zeros((m, n_features));
for (i, &idx) in sample_indices.iter().enumerate() {
x_sampled.row_mut(i).assign(&x.row(idx));
}
let mut k_sampled = self.config.kernel.compute_matrix(&x_sampled, &x_sampled);
if self.config.center {
self.center_kernel_matrix(&mut k_sampled)?;
}
let (eigenvals_sampled, eigenvecs_sampled) =
self.solve_eigenvalue_problem(&k_sampled, n_components.min(m))?;
let mut full_eigenvecs = Array2::zeros((n_samples, eigenvals_sampled.len()));
for comp in 0..eigenvals_sampled.len() {
for i in 0..n_samples {
let mut projection = 0.0;
for (j, &sample_idx) in sample_indices.iter().enumerate() {
let x_i = x.row(i).to_owned();
let x_sample_j = x.row(sample_idx).to_owned();
let k_val = self.config.kernel.compute(&x_i, &x_sample_j);
projection += k_val * eigenvecs_sampled[[j, comp]];
}
if eigenvals_sampled[comp] > 1e-10 {
projection /= eigenvals_sampled[comp].sqrt();
}
full_eigenvecs[[i, comp]] = projection;
}
}
Ok((eigenvals_sampled, full_eigenvecs))
}
}
impl Transform<Array2<Float>, Array2<Float>> for KernelPCA<Trained> {
fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
let (n_samples, n_features) = x.dim();
if n_features != self.n_features_in() {
return Err(SklearsError::FeatureMismatch {
expected: self.n_features_in(),
actual: n_features,
});
}
let x_fit = self
.x_fit_
.as_ref()
.expect("invariant: x_fit_ is Some in Trained state");
let alphas = self
.alphas_
.as_ref()
.expect("invariant: alphas_ is Some in Trained state");
let lambdas = self
.lambdas_
.as_ref()
.expect("invariant: lambdas_ is Some in Trained state");
let n_components = self.n_components();
let mut k_test = self.config.kernel.compute_matrix(x, x_fit);
if self.config.center {
let n_train = x_fit.nrows();
let row_means_train: Array1<Float> = Array1::zeros(n_train); let overall_mean_train: Float = 0.0;
for i in 0..n_samples {
for j in 0..n_train {
k_test[[i, j]] = k_test[[i, j]] - row_means_train[j] - overall_mean_train;
}
}
}
let mut x_transformed = Array2::zeros((n_samples, n_components));
for i in 0..n_samples {
for comp in 0..n_components {
let mut projection = 0.0;
for j in 0..x_fit.nrows() {
projection += k_test[[i, j]] * alphas[[j, comp]];
}
if lambdas[comp] > 1e-10 {
projection /= lambdas[comp].sqrt();
}
x_transformed[[i, comp]] = projection;
}
}
Ok(x_transformed)
}
}
impl KernelPCA<Trained> {
pub fn eigenvalues(&self) -> &Array1<Float> {
self.lambdas_
.as_ref()
.expect("invariant: lambdas_ is Some in Trained state")
}
pub fn eigenvectors(&self) -> &Array2<Float> {
self.alphas_
.as_ref()
.expect("invariant: alphas_ is Some in Trained state")
}
pub fn n_components(&self) -> usize {
self.n_components_
.expect("invariant: n_components_ is Some in Trained state")
}
pub fn n_features_in(&self) -> usize {
self.n_features_in_
.expect("invariant: n_features_in_ is Some in Trained state")
}
pub fn n_samples(&self) -> usize {
self.n_samples_
.expect("invariant: n_samples_ is Some in Trained state")
}
pub fn x_fit(&self) -> &Array2<Float> {
self.x_fit_
.as_ref()
.expect("invariant: x_fit_ is Some in Trained state")
}
pub fn inverse_transform(
&self,
x_transformed: &Array2<Float>,
max_iter: usize,
tol: Float,
) -> Result<Array2<Float>> {
let (_n_samples, n_components_in) = x_transformed.dim();
let n_components = self.n_components();
let _n_features = self.n_features_in();
let _x_fit = self.x_fit();
let _alphas = self.eigenvectors();
let _lambdas = self.eigenvalues();
if n_components_in != n_components {
return Err(SklearsError::FeatureMismatch {
expected: n_components,
actual: n_components_in,
});
}
match &self.config.kernel {
KernelFunction::Linear => self.linear_preimage(x_transformed),
KernelFunction::Rbf { gamma: _ } => {
self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
}
KernelFunction::Polynomial { .. } => {
self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
}
KernelFunction::Sigmoid { .. } => {
self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
}
KernelFunction::Laplacian { .. } => {
self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
}
KernelFunction::ChiSquared { .. } => {
self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
}
}
}
fn linear_preimage(&self, x_transformed: &Array2<Float>) -> Result<Array2<Float>> {
let x_fit = self.x_fit();
let alphas = self.eigenvectors();
let lambdas = self.eigenvalues();
let (n_samples, n_components) = x_transformed.dim();
let n_features = self.n_features_in();
let mut x_reconstructed = Array2::zeros((n_samples, n_features));
for i in 0..n_samples {
for k in 0..n_features {
let mut value = 0.0;
for comp in 0..n_components {
if lambdas[comp] > 1e-10 {
let component_contrib = x_transformed[[i, comp]] * lambdas[comp].sqrt();
for j in 0..x_fit.nrows() {
value += alphas[[j, comp]] * component_contrib * x_fit[[j, k]];
}
}
}
x_reconstructed[[i, k]] = value;
}
}
Ok(x_reconstructed)
}
fn nonlinear_preimage_fixed_point(
&self,
x_transformed: &Array2<Float>,
max_iter: usize,
tol: Float,
) -> Result<Array2<Float>> {
let (n_samples, _n_components) = x_transformed.dim();
let n_features = self.n_features_in();
let x_fit = self.x_fit();
let alphas = self.eigenvectors();
let lambdas = self.eigenvalues();
let mut x_reconstructed = Array2::zeros((n_samples, n_features));
for i in 0..n_samples {
let mut x_current = x_fit.mean_axis(Axis(0)).ok_or_else(|| {
SklearsError::NumericalError(
"cannot compute mean of empty training data".to_string(),
)
})?;
let target_transformed = x_transformed.slice(scirs2_core::ndarray::s![i, ..]);
for _iter in 0..max_iter {
let x_old = x_current.clone();
x_current =
self.fixed_point_step(&x_old, &target_transformed, x_fit, alphas, lambdas)?;
let diff = (&x_current - &x_old)
.mapv(|x| x.abs())
.fold(0.0f64, |acc, &x| acc.max(x));
if diff < tol {
break;
}
}
for j in 0..n_features {
x_reconstructed[[i, j]] = x_current[j];
}
}
Ok(x_reconstructed)
}
fn fixed_point_step(
&self,
x_current: &Array1<Float>,
target_transformed: &scirs2_core::ndarray::ArrayView1<Float>,
x_fit: &Array2<Float>,
alphas: &Array2<Float>,
lambdas: &Array1<Float>,
) -> Result<Array1<Float>> {
let n_features = x_current.len();
let n_train = x_fit.nrows();
let n_components = target_transformed.len();
let mut numerator = Array1::<Float>::zeros(n_features);
let mut denominator = Array1::<Float>::zeros(n_features);
for j in 0..n_train {
let x_train_j = x_fit.slice(scirs2_core::ndarray::s![j, ..]).to_owned();
let _k_val = self.config.kernel.compute(x_current, &x_train_j);
let k_deriv = self.kernel_derivative(x_current, &x_train_j);
let mut weight = 0.0;
for comp in 0..n_components {
if lambdas[comp] > 1e-10 {
weight += target_transformed[comp] * alphas[[j, comp]] / lambdas[comp].sqrt();
}
}
for k in 0..n_features {
numerator[k] += weight * k_deriv[k] * x_train_j[k];
denominator[k] += weight * k_deriv[k];
}
}
let mut x_new = Array1::zeros(n_features);
for k in 0..n_features {
if denominator[k].abs() > 1e-12 {
x_new[k] = numerator[k] / denominator[k];
} else {
x_new[k] = x_current[k]; }
}
Ok(x_new)
}
fn kernel_derivative(&self, x: &Array1<Float>, y: &Array1<Float>) -> Array1<Float> {
match &self.config.kernel {
KernelFunction::Linear => {
y.clone()
}
KernelFunction::Rbf { gamma } => {
let k_val = self.config.kernel.compute(x, y);
let diff = x - y;
diff.mapv(|d| -2.0 * gamma * d * k_val)
}
KernelFunction::Polynomial {
degree,
gamma,
coef0,
} => {
let dot_product = x.dot(y);
let base = gamma * dot_product + coef0;
if *degree == 1 || base.abs() < 1e-12 {
y.mapv(|yi| *gamma * yi)
} else {
let factor = (*degree as Float) * gamma * base.powf(*degree as Float - 1.0);
y.mapv(|yi| factor * yi)
}
}
KernelFunction::Sigmoid { gamma, coef0 } => {
let dot_product = x.dot(y);
let tanh_val = (gamma * dot_product + coef0).tanh();
let factor = gamma * (1.0 - tanh_val * tanh_val);
y.mapv(|yi| factor * yi)
}
KernelFunction::Laplacian { gamma } => {
let k_val = self.config.kernel.compute(x, y);
let diff = x - y;
diff.mapv(|d| -gamma * d.signum() * k_val)
}
KernelFunction::ChiSquared { gamma } => {
let k_val = self.config.kernel.compute(x, y);
let mut derivative = Array1::zeros(x.len());
for i in 0..x.len() {
let sum = x[i] + y[i];
if sum > 1e-12 {
let diff = x[i] - y[i];
derivative[i] = -2.0 * gamma * k_val * diff / sum;
}
}
derivative
}
}
}
pub fn mds_preimage(&self, x_transformed: &Array2<Float>) -> Result<Array2<Float>> {
let (n_samples, _n_components) = x_transformed.dim();
let n_features = self.n_features_in();
let x_fit = self.x_fit();
let mut dist_transformed = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in 0..n_samples {
let diff = &x_transformed.slice(scirs2_core::ndarray::s![i, ..])
- &x_transformed.slice(scirs2_core::ndarray::s![j, ..]);
dist_transformed[[i, j]] = diff.mapv(|x| x * x).sum().sqrt();
}
}
let mut x_reconstructed = Array2::zeros((n_samples, n_features));
for i in 0..n_samples {
let k = 5.min(x_fit.nrows()); let mut distances = Vec::new();
for j in 0..x_fit.nrows() {
let x_train_j_transformed =
self.transform_single_sample(&x_fit.slice(scirs2_core::ndarray::s![j, ..]))?;
let diff =
&x_transformed.slice(scirs2_core::ndarray::s![i, ..]) - &x_train_j_transformed;
let dist = diff.mapv(|x| x * x).sum().sqrt();
distances.push((dist, j));
}
distances.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut weight_sum = 0.0;
for feat in 0..n_features {
let mut weighted_value = 0.0;
for &(dist, idx) in distances.iter().take(k) {
let weight = if dist > 1e-12 {
1.0 / (dist + 1e-6)
} else {
1e6
};
weighted_value += weight * x_fit[[idx, feat]];
weight_sum += weight;
}
x_reconstructed[[i, feat]] = weighted_value / weight_sum;
}
}
Ok(x_reconstructed)
}
fn transform_single_sample(
&self,
x_sample: &scirs2_core::ndarray::ArrayView1<Float>,
) -> Result<Array1<Float>> {
let x_fit = self.x_fit();
let alphas = self.eigenvectors();
let lambdas = self.eigenvalues();
let n_components = self.n_components();
let mut k_test = Array1::zeros(x_fit.nrows());
for j in 0..x_fit.nrows() {
let x_train_j = x_fit.slice(scirs2_core::ndarray::s![j, ..]).to_owned();
k_test[j] = self.config.kernel.compute(&x_sample.to_owned(), &x_train_j);
}
if self.config.center {
let mean_k = k_test.mean().ok_or_else(|| {
SklearsError::NumericalError(
"cannot compute mean of empty k_test array".to_string(),
)
})?;
for val in k_test.iter_mut() {
*val -= mean_k;
}
}
let mut x_transformed = Array1::zeros(n_components);
for comp in 0..n_components {
let mut projection = 0.0;
for j in 0..x_fit.nrows() {
projection += k_test[j] * alphas[[j, comp]];
}
if lambdas[comp] > 1e-10 {
projection /= lambdas[comp].sqrt();
}
x_transformed[comp] = projection;
}
Ok(x_transformed)
}
}
impl KernelFunction {
pub fn validate(&self) -> Result<()> {
match self {
KernelFunction::Linear => Ok(()),
KernelFunction::Rbf { gamma } => {
if *gamma <= 0.0 {
return Err(SklearsError::InvalidParameter {
name: "gamma".to_string(),
reason: "must be positive for RBF kernel".to_string(),
});
}
if *gamma > 1e6 {
return Err(SklearsError::InvalidParameter {
name: "gamma".to_string(),
reason: "too large, may cause numerical instability".to_string(),
});
}
Ok(())
}
KernelFunction::Polynomial {
degree,
gamma,
coef0: _,
} => {
if *degree <= 0 {
return Err(SklearsError::InvalidParameter {
name: "degree".to_string(),
reason: "must be positive for polynomial kernel".to_string(),
});
}
if *degree > 10 {
return Err(SklearsError::InvalidParameter {
name: "degree".to_string(),
reason: "too large, may cause numerical overflow".to_string(),
});
}
if *gamma <= 0.0 {
return Err(SklearsError::InvalidParameter {
name: "gamma".to_string(),
reason: "must be positive for polynomial kernel".to_string(),
});
}
Ok(())
}
KernelFunction::Sigmoid { gamma, coef0: _ } => {
if *gamma <= 0.0 {
return Err(SklearsError::InvalidParameter {
name: "gamma".to_string(),
reason: "must be positive for sigmoid kernel".to_string(),
});
}
Ok(())
}
KernelFunction::Laplacian { gamma } => {
if *gamma <= 0.0 {
return Err(SklearsError::InvalidParameter {
name: "gamma".to_string(),
reason: "must be positive for Laplacian kernel".to_string(),
});
}
if *gamma > 1e6 {
return Err(SklearsError::InvalidParameter {
name: "gamma".to_string(),
reason: "too large, may cause numerical instability".to_string(),
});
}
Ok(())
}
KernelFunction::ChiSquared { gamma } => {
if *gamma <= 0.0 {
return Err(SklearsError::InvalidParameter {
name: "gamma".to_string(),
reason: "must be positive for Chi-squared kernel".to_string(),
});
}
Ok(())
}
}
}
pub fn evaluate_performance(
&self,
x: &Array2<Float>,
n_components: usize,
cv_folds: usize,
) -> Result<Float> {
let (n_samples, _) = x.dim();
if n_samples < cv_folds {
return Err(SklearsError::InvalidParameter {
name: "cv_folds".to_string(),
reason: "Number of samples must be >= cv_folds".to_string(),
});
}
self.validate()?;
let fold_size = n_samples / cv_folds;
let mut reconstruction_errors = Vec::new();
for fold in 0..cv_folds {
let start_idx = fold * fold_size;
let end_idx = if fold == cv_folds - 1 {
n_samples
} else {
(fold + 1) * fold_size
};
let mut train_indices = Vec::new();
let mut val_indices = Vec::new();
for i in 0..n_samples {
if i >= start_idx && i < end_idx {
val_indices.push(i);
} else {
train_indices.push(i);
}
}
if train_indices.is_empty() || val_indices.is_empty() {
continue;
}
let x_train = x.select(scirs2_core::ndarray::Axis(0), &train_indices);
let x_val = x.select(scirs2_core::ndarray::Axis(0), &val_indices);
let kpca = KernelPCA::new()
.n_components(n_components)
.kernel(*self)
.fit(&x_train, &())?;
let x_val_transformed = kpca.transform(&x_val)?;
let error = self.compute_reconstruction_error(&x_val, &x_val_transformed, &kpca)?;
reconstruction_errors.push(error);
}
if reconstruction_errors.is_empty() {
return Err(SklearsError::InvalidParameter {
name: "cv_folds".to_string(),
reason: "No valid cross-validation folds".to_string(),
});
}
let mean_error =
reconstruction_errors.iter().sum::<Float>() / reconstruction_errors.len() as Float;
Ok(mean_error)
}
fn compute_reconstruction_error(
&self,
x_original: &Array2<Float>,
x_transformed: &Array2<Float>,
_kpca: &KernelPCA<sklears_core::traits::Trained>,
) -> Result<Float> {
let original_var = self.compute_total_variance(x_original);
let transformed_var = self.compute_total_variance(x_transformed);
let error = (original_var - transformed_var).abs() / original_var.max(1e-10);
Ok(error)
}
fn compute_total_variance(&self, x: &Array2<Float>) -> Float {
let (n_samples, n_features) = x.dim();
if n_samples == 0 || n_features == 0 {
return 0.0;
}
let mut total_var = 0.0;
for j in 0..n_features {
let col = x.column(j);
let mean = col.mean().unwrap_or(0.0);
let var = col.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0);
total_var += var;
}
total_var
}
pub fn select_best_kernel(
kernels: &[KernelFunction],
x: &Array2<Float>,
n_components: usize,
cv_folds: usize,
) -> Result<(KernelFunction, Float)> {
if kernels.is_empty() {
return Err(SklearsError::InvalidParameter {
name: "kernels".to_string(),
reason: "Must provide at least one kernel".to_string(),
});
}
let mut best_kernel = kernels[0];
let mut best_score = Float::INFINITY;
for &kernel in kernels {
match kernel.evaluate_performance(x, n_components, cv_folds) {
Ok(score) => {
if score < best_score {
best_score = score;
best_kernel = kernel;
}
}
Err(_) => {
continue;
}
}
}
if best_score == Float::INFINITY {
return Err(SklearsError::InvalidParameter {
name: "kernels".to_string(),
reason: "All kernels failed validation".to_string(),
});
}
Ok((best_kernel, best_score))
}
pub fn generate_kernel_grid() -> Vec<KernelFunction> {
let mut kernels = Vec::new();
kernels.push(KernelFunction::Linear);
for &gamma in &[0.001, 0.01, 0.1, 1.0, 10.0, 100.0] {
kernels.push(KernelFunction::Rbf { gamma });
}
for °ree in &[2, 3, 4] {
for &gamma in &[0.1, 1.0] {
for &coef0 in &[0.0, 1.0] {
kernels.push(KernelFunction::Polynomial {
degree,
gamma,
coef0,
});
}
}
}
for &gamma in &[0.001, 0.01, 0.1] {
for &coef0 in &[0.0, 1.0] {
kernels.push(KernelFunction::Sigmoid { gamma, coef0 });
}
}
for &gamma in &[0.01, 0.1, 1.0, 10.0] {
kernels.push(KernelFunction::Laplacian { gamma });
}
for &gamma in &[0.1, 1.0, 10.0] {
kernels.push(KernelFunction::ChiSquared { gamma });
}
kernels
}
}
impl KernelPCA<Untrained> {
pub fn fit_with_kernel_selection(
mut self,
x: &Array2<Float>,
kernel_candidates: Option<&[KernelFunction]>,
cv_folds: usize,
) -> Result<KernelPCA<sklears_core::traits::Trained>> {
let kernels = kernel_candidates
.map(|k| k.to_vec())
.unwrap_or_else(KernelFunction::generate_kernel_grid);
let n_components = self.config.n_components.unwrap_or(x.ncols().min(x.nrows()));
let (best_kernel, _score) =
KernelFunction::select_best_kernel(&kernels, x, n_components, cv_folds)?;
self.config.kernel = best_kernel;
self.fit(x, &())
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_kernel_functions() {
let x = array![1.0, 2.0];
let y = array![3.0, 4.0];
let linear = KernelFunction::Linear;
let linear_result = linear.compute(&x, &y);
assert_abs_diff_eq!(linear_result, 11.0, epsilon = 1e-10);
let rbf = KernelFunction::Rbf { gamma: 1.0 };
let rbf_result = rbf.compute(&x, &y);
assert!(rbf_result > 0.0 && rbf_result < 1.0);
let poly = KernelFunction::Polynomial {
degree: 2,
gamma: 1.0,
coef0: 1.0,
};
let poly_result = poly.compute(&x, &y);
assert_abs_diff_eq!(poly_result, 144.0, epsilon = 1e-10);
let sigmoid = KernelFunction::Sigmoid {
gamma: 1.0,
coef0: 0.0,
};
let sigmoid_result = sigmoid.compute(&x, &y);
assert!(sigmoid_result > 0.9 && sigmoid_result <= 1.0);
}
#[test]
fn test_kernel_matrix() {
let x = array![[1.0, 2.0], [3.0, 4.0]];
let y = array![[5.0, 6.0], [7.0, 8.0]];
let kernel = KernelFunction::Linear;
let k = kernel.compute_matrix(&x, &y);
assert_eq!(k.dim(), (2, 2));
assert_abs_diff_eq!(k[[0, 0]], 17.0, epsilon = 1e-10);
assert_abs_diff_eq!(k[[0, 1]], 23.0, epsilon = 1e-10);
assert_abs_diff_eq!(k[[1, 0]], 39.0, epsilon = 1e-10);
assert_abs_diff_eq!(k[[1, 1]], 53.0, epsilon = 1e-10);
}
#[test]
fn test_kernel_pca_creation() {
let kpca = KernelPCA::new()
.n_components(2)
.kernel(KernelFunction::Rbf { gamma: 0.1 })
.tol(1e-6)
.center(false);
assert_eq!(kpca.config.n_components, Some(2));
assert_eq!(kpca.config.tol, 1e-6);
assert!(!kpca.config.center);
}
#[test]
fn test_kernel_pca_fit_transform() {
let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
let kpca = KernelPCA::new()
.n_components(2)
.kernel(KernelFunction::Linear)
.fit(&x, &())
.expect("operation should succeed");
assert_eq!(kpca.n_components(), 2);
assert_eq!(kpca.n_features_in(), 2);
assert_eq!(kpca.n_samples(), 4);
let x_transformed = kpca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (4, 2));
for &val in x_transformed.iter() {
assert!(val.is_finite());
}
}
#[test]
fn test_kernel_pca_rbf() {
let x = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0],];
let kpca = KernelPCA::new()
.n_components(2)
.kernel(KernelFunction::Rbf { gamma: 0.5 })
.fit(&x, &())
.expect("operation should succeed");
let x_transformed = kpca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (4, 2));
let eigenvalues = kpca.eigenvalues();
for &val in eigenvalues.iter() {
assert!(val >= 0.0, "Eigenvalue should be non-negative: {}", val);
}
}
#[test]
fn test_kernel_pca_polynomial() {
let x = array![[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0],];
let kpca = KernelPCA::new()
.n_components(3)
.kernel(KernelFunction::Polynomial {
degree: 2,
gamma: 1.0,
coef0: 1.0,
})
.fit(&x, &())
.expect("operation should succeed");
let x_transformed = kpca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (4, 3));
for &val in x_transformed.iter() {
assert!(val.is_finite());
}
}
#[test]
fn test_kernel_pca_errors() {
let empty_x: Array2<f64> = Array2::zeros((0, 2));
let result = KernelPCA::new().fit(&empty_x, &());
assert!(result.is_err());
let zero_features_x: Array2<f64> = Array2::zeros((2, 0));
let result = KernelPCA::new().fit(&zero_features_x, &());
assert!(result.is_err());
let x = array![[1.0, 2.0], [3.0, 4.0]];
let result = KernelPCA::new().n_components(0).fit(&x, &());
assert!(result.is_err());
}
#[test]
fn test_kernel_pca_feature_mismatch() {
let x_train = array![[1.0, 2.0], [3.0, 4.0]];
let x_test = array![[1.0, 2.0, 3.0]];
let kpca = KernelPCA::new()
.fit(&x_train, &())
.expect("model fitting should succeed");
let result = kpca.transform(&x_test);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Feature"));
}
#[test]
fn test_kernel_pca_default() {
let x = array![[1.0, 2.0], [3.0, 4.0]];
let kpca = KernelPCA::default()
.fit(&x, &())
.expect("model fitting should succeed");
assert_eq!(kpca.n_components(), 2); assert_eq!(kpca.n_features_in(), 2);
}
#[test]
fn test_new_kernel_functions() {
let x = array![1.0, 2.0];
let y = array![3.0, 4.0];
let laplacian = KernelFunction::Laplacian { gamma: 0.5 };
let laplacian_result = laplacian.compute(&x, &y);
assert!(laplacian_result > 0.0 && laplacian_result < 1.0);
let chi_squared = KernelFunction::ChiSquared { gamma: 1.0 };
let chi_result = chi_squared.compute(&x, &y);
assert!(chi_result > 0.0 && chi_result <= 1.0);
}
#[test]
fn test_kernel_pca_nystrom_approximation() {
let x = array![
[1.0, 2.0],
[3.0, 4.0],
[5.0, 6.0],
[7.0, 8.0],
[9.0, 10.0],
[11.0, 12.0],
];
let kpca = KernelPCA::new()
.n_components(2)
.kernel(KernelFunction::Rbf { gamma: 0.1 })
.approximation(KernelApproximation::Nystrom { n_components: 4 })
.random_state(42)
.fit(&x, &())
.expect("operation should succeed");
let x_transformed = kpca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (6, 2));
let eigenvalues = kpca.eigenvalues();
for &val in eigenvalues.iter() {
assert!(val >= 0.0, "Eigenvalue should be non-negative: {}", val);
}
}
#[test]
fn test_kernel_pca_random_sampling() {
let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0],];
let kpca = KernelPCA::new()
.n_components(2)
.kernel(KernelFunction::Linear)
.approximation(KernelApproximation::RandomSampling { n_samples: 3 })
.random_state(123)
.fit(&x, &())
.expect("operation should succeed");
let x_transformed = kpca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (5, 2));
for &val in x_transformed.iter() {
assert!(val.is_finite());
}
}
#[test]
fn test_kernel_pca_laplacian_kernel() {
let x = array![[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]];
let kpca = KernelPCA::new()
.n_components(2)
.kernel(KernelFunction::Laplacian { gamma: 0.1 })
.fit(&x, &())
.expect("operation should succeed");
let x_transformed = kpca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (4, 2));
for &val in x_transformed.iter() {
assert!(val.is_finite());
}
}
#[test]
fn test_kernel_pca_chi_squared_kernel() {
let x = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let kpca = KernelPCA::new()
.n_components(2)
.kernel(KernelFunction::ChiSquared { gamma: 0.5 })
.fit(&x, &())
.expect("operation should succeed");
let x_transformed = kpca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (4, 2));
for &val in x_transformed.iter() {
assert!(val.is_finite());
}
}
}