use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_linalg::compat::svd;
use sklears_core::{
error::{Result, SklearsError},
traits::{Fit, Trained, Transform, Untrained},
types::Float,
};
use std::marker::PhantomData;
#[derive(Debug, Clone)]
pub struct IncrementalPcaConfig {
pub n_components: Option<usize>,
pub whiten: bool,
pub copy: bool,
pub batch_size: Option<usize>,
}
impl Default for IncrementalPcaConfig {
fn default() -> Self {
Self {
n_components: None,
whiten: false,
copy: true,
batch_size: None,
}
}
}
#[derive(Debug, Clone)]
pub struct IncrementalPCA<State = Untrained> {
config: IncrementalPcaConfig,
state: PhantomData<State>,
components_: Option<Array2<Float>>,
explained_variance_: Option<Array1<Float>>,
explained_variance_ratio_: Option<Array1<Float>>,
singular_values_: Option<Array1<Float>>,
mean_: Option<Array1<Float>>,
var_: Option<Array1<Float>>,
n_components_: Option<usize>,
n_features_in_: Option<usize>,
n_samples_seen_: usize,
sum_: Option<Array1<Float>>,
sum_squared_: Option<Array1<Float>>,
}
impl IncrementalPCA<Untrained> {
pub fn new() -> Self {
Self {
config: IncrementalPcaConfig::default(),
state: PhantomData,
components_: None,
explained_variance_: None,
explained_variance_ratio_: None,
singular_values_: None,
mean_: None,
var_: None,
n_components_: None,
n_features_in_: None,
n_samples_seen_: 0,
sum_: None,
sum_squared_: None,
}
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.config.n_components = Some(n_components);
self
}
pub fn whiten(mut self, whiten: bool) -> Self {
self.config.whiten = whiten;
self
}
pub fn copy(mut self, copy: bool) -> Self {
self.config.copy = copy;
self
}
pub fn batch_size(mut self, batch_size: usize) -> Self {
self.config.batch_size = Some(batch_size);
self
}
fn initialize(&mut self, x: &Array2<Float>) -> Result<()> {
let (_, n_features) = x.dim();
if let Some(existing_n_features) = self.n_features_in_ {
if existing_n_features != n_features {
return Err(SklearsError::FeatureMismatch {
expected: existing_n_features,
actual: n_features,
});
}
} else {
self.n_features_in_ = Some(n_features);
self.sum_ = Some(Array1::zeros(n_features));
self.sum_squared_ = Some(Array1::zeros(n_features));
}
Ok(())
}
fn update_statistics(&mut self, x: &Array2<Float>) {
let (n_samples, _) = x.dim();
let batch_sum = x.sum_axis(Axis(0));
let batch_sum_squared = x.mapv(|v| v * v).sum_axis(Axis(0));
if let (Some(sum), Some(sum_squared)) = (&mut self.sum_, &mut self.sum_squared_) {
*sum += &batch_sum;
*sum_squared += &batch_sum_squared;
}
self.n_samples_seen_ += n_samples;
}
fn compute_mean_and_variance(&self) -> (Array1<Float>, Array1<Float>) {
let sum = self
.sum_
.as_ref()
.expect("invariant: sum_ is Some after partial_fit");
let sum_squared = self
.sum_squared_
.as_ref()
.expect("invariant: sum_squared_ is Some after partial_fit");
let n_samples = self.n_samples_seen_ as Float;
let mean = sum / n_samples;
let variance = sum_squared / n_samples - &mean * &mean;
(mean, variance)
}
fn update_components(&mut self, x: &Array2<Float>) -> Result<()> {
let (mean, variance) = self.compute_mean_and_variance();
self.mean_ = Some(mean.clone());
self.var_ = Some(variance);
let x_centered = x - &mean.clone().insert_axis(Axis(0));
let n_components = self
.config
.n_components
.unwrap_or_else(|| x.dim().1.min(self.n_samples_seen_));
let (_n_samples, _n_features) = x_centered.dim();
let (_u, s, vt) = svd(&x_centered.view(), true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
let n_comp = n_components.min(s.len());
let components = vt.slice(scirs2_core::ndarray::s![..n_comp, ..]).to_owned();
let sing_vals = s.slice(scirs2_core::ndarray::s![..n_comp]).to_owned();
let n_samples_f = self.n_samples_seen_ as Float;
let explained_variance = sing_vals.mapv(|s| s * s / (n_samples_f - 1.0));
let total_variance = explained_variance.sum();
let explained_variance_ratio = if total_variance > 0.0 {
explained_variance.mapv(|var| var / total_variance)
} else {
Array1::zeros(explained_variance.len())
};
self.components_ = Some(components);
self.singular_values_ = Some(sing_vals);
self.explained_variance_ = Some(explained_variance);
self.explained_variance_ratio_ = Some(explained_variance_ratio);
self.n_components_ = Some(n_comp);
Ok(())
}
pub fn partial_fit(mut self, x: &Array2<Float>, _y: &()) -> Result<Self> {
let (n_samples, _) = x.dim();
if n_samples == 0 {
return Err(SklearsError::InvalidInput(
"Cannot fit IncrementalPCA on empty batch".to_string(),
));
}
self.initialize(x)?;
self.update_statistics(x);
self.update_components(x)?;
Ok(self)
}
pub fn into_trained(self) -> Result<IncrementalPCA<Trained>> {
if self.n_samples_seen_ == 0 {
return Err(SklearsError::InvalidInput(
"IncrementalPCA has not been fitted yet".to_string(),
));
}
Ok(IncrementalPCA {
config: self.config,
state: PhantomData,
components_: self.components_,
explained_variance_: self.explained_variance_,
explained_variance_ratio_: self.explained_variance_ratio_,
singular_values_: self.singular_values_,
mean_: self.mean_,
var_: self.var_,
n_components_: self.n_components_,
n_features_in_: self.n_features_in_,
n_samples_seen_: self.n_samples_seen_,
sum_: self.sum_,
sum_squared_: self.sum_squared_,
})
}
}
impl IncrementalPCA<Trained> {
pub fn components(&self) -> &Array2<Float> {
self.components_
.as_ref()
.expect("IncrementalPCA should be fitted")
}
pub fn explained_variance(&self) -> &Array1<Float> {
self.explained_variance_
.as_ref()
.expect("IncrementalPCA should be fitted")
}
pub fn explained_variance_ratio(&self) -> &Array1<Float> {
self.explained_variance_ratio_
.as_ref()
.expect("IncrementalPCA should be fitted")
}
pub fn singular_values(&self) -> &Array1<Float> {
self.singular_values_
.as_ref()
.expect("IncrementalPCA should be fitted")
}
pub fn mean(&self) -> &Array1<Float> {
self.mean_
.as_ref()
.expect("IncrementalPCA should be fitted")
}
pub fn var(&self) -> &Array1<Float> {
self.var_.as_ref().expect("IncrementalPCA should be fitted")
}
pub fn n_components(&self) -> usize {
self.n_components_.expect("IncrementalPCA should be fitted")
}
pub fn n_features_in(&self) -> usize {
self.n_features_in_
.expect("IncrementalPCA should be fitted")
}
pub fn n_samples_seen(&self) -> usize {
self.n_samples_seen_
}
pub fn inverse_transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
let (_n_samples, n_components_transformed) = x.dim();
if n_components_transformed != self.n_components() {
return Err(SklearsError::FeatureMismatch {
expected: self.n_components(),
actual: n_components_transformed,
});
}
let components = self.components();
let mean = self.mean();
let reconstructed = x.dot(components) + &mean.clone().insert_axis(Axis(0));
Ok(reconstructed)
}
pub fn partial_fit_more(self, x: &Array2<Float>, _y: &()) -> Result<IncrementalPCA<Trained>> {
let untrained = IncrementalPCA {
config: self.config,
state: PhantomData,
components_: self.components_,
explained_variance_: self.explained_variance_,
explained_variance_ratio_: self.explained_variance_ratio_,
singular_values_: self.singular_values_,
mean_: self.mean_,
var_: self.var_,
n_components_: self.n_components_,
n_features_in_: self.n_features_in_,
n_samples_seen_: self.n_samples_seen_,
sum_: self.sum_,
sum_squared_: self.sum_squared_,
};
let updated = untrained.partial_fit(x, _y)?;
updated.into_trained()
}
}
impl IncrementalPCA<Untrained> {
pub fn fit_minibatch_adaptive(
mut self,
x: &Array2<Float>,
_y: &(),
memory_limit_mb: Option<usize>,
) -> Result<IncrementalPCA<Trained>> {
let (n_samples, n_features) = x.dim();
let batch_size = self.calculate_adaptive_batch_size(n_samples, n_features, memory_limit_mb);
let mut current_pos = 0;
while current_pos < n_samples {
let end_pos = (current_pos + batch_size).min(n_samples);
let batch = x
.slice(scirs2_core::ndarray::s![current_pos..end_pos, ..])
.to_owned();
self = self.partial_fit(&batch, _y)?;
current_pos = end_pos;
}
self.into_trained()
}
fn calculate_adaptive_batch_size(
&self,
n_samples: usize,
n_features: usize,
memory_limit_mb: Option<usize>,
) -> usize {
let memory_limit_bytes = memory_limit_mb.unwrap_or(100) * 1024 * 1024;
let bytes_per_sample = n_features * 8;
let max_samples_from_memory = memory_limit_bytes / bytes_per_sample;
let min_batch_size = 10;
let max_batch_size = 1000;
let adaptive_size = if n_samples <= 100 {
n_samples } else if n_samples <= 1000 {
n_samples / 4 } else {
let efficiency_size = (n_samples as f64).sqrt() as usize;
efficiency_size.min(max_samples_from_memory)
};
adaptive_size
.max(min_batch_size)
.min(max_batch_size)
.min(n_samples)
}
pub fn fit_streaming<I>(
mut self,
batch_iterator: I,
forgetting_factor: Option<Float>,
) -> Result<IncrementalPCA<Trained>>
where
I: Iterator<Item = Array2<Float>>,
{
let forgetting = forgetting_factor.unwrap_or(1.0);
for (batch_idx, batch) in batch_iterator.enumerate() {
if forgetting < 1.0 && batch_idx > 0 {
self = self.apply_forgetting_factor(forgetting)?;
}
self = self.partial_fit(&batch, &())?;
}
self.into_trained()
}
fn apply_forgetting_factor(mut self, factor: Float) -> Result<Self> {
if let (Some(ref mut sum), Some(ref mut sum_squared)) =
(&mut self.sum_, &mut self.sum_squared_)
{
*sum *= factor;
*sum_squared *= factor;
self.n_samples_seen_ = (self.n_samples_seen_ as Float * factor) as usize;
}
Ok(self)
}
pub fn fit_online_ema(
mut self,
x: &Array2<Float>,
_y: &(),
decay_rate: Float,
) -> Result<IncrementalPCA<Trained>> {
let (n_samples, _) = x.dim();
for i in 0..n_samples {
let sample = x.slice(scirs2_core::ndarray::s![i..i + 1, ..]).to_owned();
if i > 0 {
self = self.apply_exponential_decay(decay_rate)?;
}
self = self.partial_fit(&sample, _y)?;
}
self.into_trained()
}
fn apply_exponential_decay(mut self, decay_rate: Float) -> Result<Self> {
if let (Some(ref mut sum), Some(ref mut sum_squared)) =
(&mut self.sum_, &mut self.sum_squared_)
{
*sum *= decay_rate;
*sum_squared *= decay_rate;
self.n_samples_seen_ = (self.n_samples_seen_ as Float * decay_rate) as usize;
}
Ok(self)
}
pub fn fit_early_stopping(
mut self,
x: &Array2<Float>,
_y: &(),
batch_size: Option<usize>,
convergence_tol: Float,
patience: usize,
) -> Result<(IncrementalPCA<Trained>, usize)> {
let (n_samples, n_features) = x.dim();
let actual_batch_size =
batch_size.unwrap_or(self.calculate_adaptive_batch_size(n_samples, n_features, None));
let mut prev_components: Option<Array2<Float>> = None;
let mut no_improvement_count = 0;
let mut batches_processed = 0;
let mut current_pos = 0;
while current_pos < n_samples {
let end_pos = (current_pos + actual_batch_size).min(n_samples);
let batch = x
.slice(scirs2_core::ndarray::s![current_pos..end_pos, ..])
.to_owned();
self = self.partial_fit(&batch, _y)?;
batches_processed += 1;
if let Some(ref current_components) = self.components_ {
if let Some(ref prev) = prev_components {
let component_diff =
self.compute_component_difference(current_components, prev);
if component_diff < convergence_tol {
no_improvement_count += 1;
if no_improvement_count >= patience {
break; }
} else {
no_improvement_count = 0; }
}
prev_components = Some(current_components.clone());
}
current_pos = end_pos;
}
let trained = self.into_trained()?;
Ok((trained, batches_processed))
}
fn compute_component_difference(
&self,
current: &Array2<Float>,
previous: &Array2<Float>,
) -> Float {
if current.dim() != previous.dim() {
return Float::INFINITY; }
let diff = current - previous;
diff.iter()
.map(|x| x.abs())
.fold(0.0f64, |acc, x| acc.max(x))
}
pub fn fit_memory_efficient<F>(
mut self,
x: &Array2<Float>,
_y: &(),
progress_callback: Option<F>,
) -> Result<IncrementalPCA<Trained>>
where
F: Fn(usize, usize, &IncrementalPCA<Untrained>),
{
let (n_samples, n_features) = x.dim();
let batch_size = self.calculate_adaptive_batch_size(n_samples, n_features, Some(50));
let total_batches = n_samples.div_ceil(batch_size);
let mut current_pos = 0;
let mut batch_count = 0;
while current_pos < n_samples {
let end_pos = (current_pos + batch_size).min(n_samples);
let batch = x
.slice(scirs2_core::ndarray::s![current_pos..end_pos, ..])
.to_owned();
self = self.partial_fit(&batch, _y)?;
batch_count += 1;
if let Some(ref callback) = progress_callback {
callback(batch_count, total_batches, &self);
}
current_pos = end_pos;
}
self.into_trained()
}
}
impl Default for IncrementalPCA<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Fit<Array2<Float>, ()> for IncrementalPCA<Untrained> {
type Fitted = IncrementalPCA<Trained>;
fn fit(self, x: &Array2<Float>, y: &()) -> Result<Self::Fitted> {
let updated = self.partial_fit(x, y)?;
updated.into_trained()
}
}
impl Transform<Array2<Float>, Array2<Float>> for IncrementalPCA<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 components = self.components();
let mean = self.mean();
let x_centered = x - &mean.clone().insert_axis(Axis(0));
let x_transformed = x_centered.dot(&components.t());
if self.config.whiten {
let singular_values = self.singular_values();
let sqrt_n_samples = ((self.n_samples_seen_ - 1) as Float).sqrt();
let mut result = x_transformed;
for i in 0..self.n_components() {
if singular_values[i] > 1e-12 {
result
.column_mut(i)
.mapv_inplace(|val| val * sqrt_n_samples / singular_values[i]);
}
}
Ok(result)
} else {
Ok(x_transformed)
}
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_incremental_pca_basic() {
let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0],];
let pca = IncrementalPCA::new()
.n_components(2)
.fit(&x, &())
.expect("model fitting should succeed");
assert_eq!(pca.n_components(), 2);
assert_eq!(pca.n_features_in(), 3);
assert_eq!(pca.n_samples_seen(), 3);
assert_eq!(pca.components().dim(), (2, 3));
let x_transformed = pca.transform(&x).expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (3, 2));
}
#[test]
fn test_incremental_pca_partial_fit() {
let x1 = array![[1.0, 2.0], [2.0, 4.0],];
let x2 = array![[3.0, 6.0], [4.0, 8.0],];
let mut pca = IncrementalPCA::new().n_components(1);
pca = pca.partial_fit(&x1, &()).expect("operation should succeed");
assert_eq!(pca.n_samples_seen_, 2);
pca = pca.partial_fit(&x2, &()).expect("operation should succeed");
assert_eq!(pca.n_samples_seen_, 4);
let trained_pca = pca.into_trained().expect("operation should succeed");
assert_eq!(trained_pca.n_samples_seen(), 4);
let x_all = array![[1.0, 2.0], [2.0, 4.0], [3.0, 6.0], [4.0, 8.0],];
let x_transformed = trained_pca
.transform(&x_all)
.expect("transformation should succeed");
assert_eq!(x_transformed.dim(), (4, 1));
}
#[test]
fn test_incremental_pca_statistics() {
let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
let pca = IncrementalPCA::new()
.fit(&x, &())
.expect("model fitting should succeed");
let expected_mean = array![3.0, 4.0];
assert_abs_diff_eq!(pca.mean(), &expected_mean, epsilon = 1e-10);
for &var in pca.var().iter() {
assert!(var >= 0.0);
}
}
#[test]
fn test_incremental_pca_inverse_transform() {
let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0],];
let pca = IncrementalPCA::new()
.n_components(2)
.fit(&x, &())
.expect("model fitting should succeed");
let x_transformed = pca.transform(&x).expect("transformation should succeed");
let x_reconstructed = pca
.inverse_transform(&x_transformed)
.expect("operation should succeed");
assert_eq!(x_reconstructed.dim(), (3, 3));
}
#[test]
fn test_incremental_pca_empty_batch() {
let x = Array2::<Float>::zeros((0, 3));
let result = IncrementalPCA::new().partial_fit(&x, &());
assert!(result.is_err());
}
#[test]
fn test_incremental_pca_feature_mismatch() {
let x1 = array![[1.0, 2.0, 3.0]];
let x2 = array![[4.0, 5.0]];
let pca = IncrementalPCA::new()
.partial_fit(&x1, &())
.expect("operation should succeed");
let result = pca.partial_fit(&x2, &());
assert!(result.is_err());
}
#[test]
fn test_incremental_pca_partial_fit_more() {
let x1 = array![[1.0, 2.0], [2.0, 4.0],];
let x2 = array![[3.0, 6.0],];
let pca = IncrementalPCA::new()
.fit(&x1, &())
.expect("model fitting should succeed");
assert_eq!(pca.n_samples_seen(), 2);
let updated_pca = pca
.partial_fit_more(&x2, &())
.expect("operation should succeed");
assert_eq!(updated_pca.n_samples_seen(), 3);
}
}