use scirs2_core::ndarray::Array2;
use scirs2_core::random::thread_rng;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use sklears_core::{
error::{Result, SklearsError},
traits::{Estimator, Fit, Trained, Transform, Untrained},
types::Float,
};
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DictionaryTransformAlgorithm {
OMP,
LARS,
CoordinateDescent,
Threshold,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DictionaryLearningConfig {
pub n_components: usize,
pub max_iter: usize,
pub tol: Float,
pub transform_algorithm: DictionaryTransformAlgorithm,
pub alpha: Float,
pub random_state: Option<u64>,
}
impl Default for DictionaryLearningConfig {
fn default() -> Self {
Self {
n_components: 100,
max_iter: 1000,
tol: 1e-8,
transform_algorithm: DictionaryTransformAlgorithm::OMP,
alpha: 1.0,
random_state: None,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DictionaryLearning<State = Untrained> {
config: DictionaryLearningConfig,
state: std::marker::PhantomData<State>,
components_: Option<Array2<Float>>,
n_iter_: Option<usize>,
}
impl DictionaryLearning<Untrained> {
pub fn new(config: DictionaryLearningConfig) -> Self {
Self {
config,
state: std::marker::PhantomData,
components_: None,
n_iter_: None,
}
}
pub fn builder() -> DictionaryLearningBuilder {
DictionaryLearningBuilder::default()
}
}
impl DictionaryLearning<Trained> {
pub fn components(&self) -> &Array2<Float> {
self.components_.as_ref().expect("Model is trained")
}
pub fn n_iter(&self) -> usize {
self.n_iter_.expect("Model is trained")
}
}
impl Estimator for DictionaryLearning<Untrained> {
type Config = DictionaryLearningConfig;
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&self.config
}
}
impl Fit<Array2<Float>, ()> for DictionaryLearning<Untrained> {
type Fitted = DictionaryLearning<Trained>;
fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
let (_n_samples, n_features) = x.dim();
if self.config.n_components > n_features {
return Err(SklearsError::InvalidInput(
"n_components cannot be larger than n_features".to_string(),
));
}
let mut rng = thread_rng();
let mut components = Array2::zeros((self.config.n_components, n_features));
for i in 0..self.config.n_components {
for j in 0..n_features {
components[[i, j]] = rng.random::<Float>() - 0.5;
}
}
for mut row in components.rows_mut() {
let norm = row.mapv(|x| x * x).sum().sqrt();
if norm > 1e-10 {
row.mapv_inplace(|x| x / norm);
}
}
use super::omp_algorithms::{OMPConfig, OMPEncoder};
let n_samples = x.nrows();
let n_atoms = self.config.n_components;
let omp_config = OMPConfig {
n_nonzero_coefs: Some((self.config.alpha * n_atoms as Float).max(1.0) as usize),
tol: Some(self.config.tol * 0.1), };
let mut n_iter = 0;
let mut prev_error = Float::INFINITY;
for iter in 0..self.config.max_iter {
n_iter = iter + 1;
let mut codes = Array2::zeros((n_samples, n_atoms));
let encoder = OMPEncoder::new(omp_config.clone());
let dict_transposed = components.t().to_owned();
for i in 0..n_samples {
let signal = x.row(i).to_owned();
if let Ok(result) = encoder.encode(&dict_transposed, &signal) {
for j in 0..n_atoms {
codes[[i, j]] = result.coefficients[j];
}
}
}
for atom_idx in 0..n_atoms {
let mut residual = x.clone();
for i in 0..n_samples {
for k in 0..n_atoms {
if k != atom_idx {
let coef = codes[[i, k]];
for j in 0..n_features {
residual[[i, j]] -= coef * components[[k, j]];
}
}
}
}
let codes_col_norm_sq: Float = (0..n_samples)
.map(|i| codes[[i, atom_idx]] * codes[[i, atom_idx]])
.sum();
if codes_col_norm_sq > 1e-10 {
for j in 0..n_features {
let mut numerator = 0.0;
for i in 0..n_samples {
numerator += residual[[i, j]] * codes[[i, atom_idx]];
}
components[[atom_idx, j]] = numerator / codes_col_norm_sq;
}
let atom_norm: Float = (0..n_features)
.map(|j| components[[atom_idx, j]] * components[[atom_idx, j]])
.sum::<Float>()
.sqrt();
if atom_norm > 1e-10 {
for j in 0..n_features {
components[[atom_idx, j]] /= atom_norm;
}
}
}
}
let mut reconstruction_error = 0.0;
for i in 0..n_samples {
for j in 0..n_features {
let mut pred = 0.0;
for k in 0..n_atoms {
pred += codes[[i, k]] * components[[k, j]];
}
let diff = x[[i, j]] - pred;
reconstruction_error += diff * diff;
}
}
reconstruction_error =
(reconstruction_error / (n_samples * n_features) as Float).sqrt();
if (prev_error - reconstruction_error).abs() < self.config.tol {
break;
}
prev_error = reconstruction_error;
}
Ok(DictionaryLearning {
config: self.config,
state: std::marker::PhantomData,
components_: Some(components),
n_iter_: Some(n_iter),
})
}
}
impl Transform<Array2<Float>, Array2<Float>> for DictionaryLearning<Trained> {
fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
use super::omp_algorithms::{OMPConfig, OMPEncoder};
let (n_samples, n_features) = x.dim();
let components = self.components();
let (n_atoms, dict_features) = components.dim();
if n_features != dict_features {
return Err(SklearsError::InvalidInput(format!(
"Input features {} don't match dictionary features {}",
n_features, dict_features
)));
}
let omp_config = match self.config.transform_algorithm {
DictionaryTransformAlgorithm::OMP => OMPConfig {
n_nonzero_coefs: Some((self.config.alpha * n_atoms as Float) as usize),
tol: Some(self.config.tol),
},
_ => {
OMPConfig {
n_nonzero_coefs: None,
tol: Some(self.config.tol),
}
}
};
let encoder = OMPEncoder::new(omp_config);
let mut codes = Array2::zeros((n_samples, n_atoms));
let dict_transposed = components.t().to_owned();
for i in 0..n_samples {
let signal = x.row(i).to_owned();
let result = encoder.encode(&dict_transposed, &signal)?;
for j in 0..n_atoms {
codes[[i, j]] = result.coefficients[j];
}
}
Ok(codes)
}
}
#[derive(Debug, Clone, Default)]
pub struct DictionaryLearningBuilder {
config: DictionaryLearningConfig,
}
impl DictionaryLearningBuilder {
pub fn n_components(mut self, n_components: usize) -> Self {
self.config.n_components = n_components;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.config.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: Float) -> Self {
self.config.tol = tol;
self
}
pub fn transform_algorithm(mut self, algorithm: DictionaryTransformAlgorithm) -> Self {
self.config.transform_algorithm = algorithm;
self
}
pub fn alpha(mut self, alpha: Float) -> Self {
self.config.alpha = alpha;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.config.random_state = random_state;
self
}
pub fn build(self) -> DictionaryLearning<Untrained> {
DictionaryLearning::new(self.config)
}
}
pub type TrainedDictionaryLearning = DictionaryLearning<Trained>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dictionary_learning_transform() {
let x = Array2::from_shape_vec(
(10, 5),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 4.0, 5.0, 6.0, 3.0, 4.0, 5.0, 6.0, 7.0, 1.5,
2.5, 3.5, 4.5, 5.5, 2.5, 3.5, 4.5, 5.5, 6.5, 1.2, 2.2, 3.2, 4.2, 5.2, 2.2, 3.2,
4.2, 5.2, 6.2, 1.8, 2.8, 3.8, 4.8, 5.8, 2.8, 3.8, 4.8, 5.8, 6.8, 1.1, 2.1, 3.1,
4.1, 5.1,
],
)
.expect("operation should succeed");
let config = DictionaryLearningConfig {
n_components: 3,
max_iter: 50,
tol: 1e-4,
transform_algorithm: DictionaryTransformAlgorithm::OMP,
alpha: 0.5,
random_state: Some(42),
};
let model = DictionaryLearning::new(config);
let fitted_model = model.fit(&x, &()).expect("model fitting should succeed");
let codes = fitted_model
.transform(&x)
.expect("transformation should succeed");
assert_eq!(codes.nrows(), 10); assert_eq!(codes.ncols(), 3);
let sum: Float = codes.iter().map(|&x| x.abs()).sum();
assert!(sum > 0.0, "Codes should not be all zeros");
}
#[test]
fn test_dictionary_learning_dimension_validation() {
let x_train = Array2::from_shape_vec((5, 3), vec![1.0; 15])
.expect("shape and data length should match");
let x_test = Array2::from_shape_vec((3, 4), vec![1.0; 12])
.expect("shape and data length should match");
let config = DictionaryLearningConfig {
n_components: 2, ..Default::default()
};
let model = DictionaryLearning::new(config);
let fitted_model = model
.fit(&x_train, &())
.expect("model fitting should succeed");
let result = fitted_model.transform(&x_test);
assert!(result.is_err());
}
#[test]
fn test_dictionary_learning_builder() {
let model = DictionaryLearning::builder()
.n_components(5)
.max_iter(100)
.tol(1e-5)
.alpha(0.3)
.random_state(Some(123))
.build();
assert_eq!(model.config.n_components, 5);
assert_eq!(model.config.max_iter, 100);
assert_eq!(model.config.tol, 1e-5);
assert_eq!(model.config.alpha, 0.3);
assert_eq!(model.config.random_state, Some(123));
}
#[test]
fn test_dictionary_learning_convergence() {
let x = Array2::from_shape_vec(
(20, 8),
vec![
5.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, 5.5, 5.5, 5.5, 5.5, 0.5, 0.5, 0.5, 0.5, 4.5,
4.5, 4.5, 4.5, 1.5, 1.5, 1.5, 1.5, 5.2, 5.2, 5.2, 5.2, 0.8, 0.8, 0.8, 0.8, 4.8,
4.8, 4.8, 4.8, 1.2, 1.2, 1.2, 1.2,
1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0, 0.5, 0.5, 0.5, 0.5, 5.5, 5.5, 5.5, 5.5, 1.5,
1.5, 1.5, 1.5, 4.5, 4.5, 4.5, 4.5, 0.8, 0.8, 0.8, 0.8, 5.2, 5.2, 5.2, 5.2, 1.2,
1.2, 1.2, 1.2, 4.8, 4.8, 4.8, 4.8, 5.0, 1.0, 5.0, 1.0, 5.0, 1.0, 5.0, 1.0, 5.5, 0.5, 5.5, 0.5, 5.5, 0.5, 5.5, 0.5,
4.5, 1.5, 4.5, 1.5, 4.5, 1.5, 4.5, 1.5, 5.2, 0.8, 5.2, 0.8, 5.2, 0.8, 5.2, 0.8,
4.8, 1.2, 4.8, 1.2, 4.8, 1.2, 4.8, 1.2, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.5, 2.5, 3.5, 2.5, 3.5, 2.5, 3.5, 2.5,
2.5, 3.5, 2.5, 3.5, 2.5, 3.5, 2.5, 3.5, 4.0, 2.0, 3.0, 3.0, 2.0, 4.0, 3.0, 3.0,
2.0, 4.0, 3.0, 3.0, 4.0, 2.0, 3.0, 3.0,
],
)
.expect("operation should succeed");
let config = DictionaryLearningConfig {
n_components: 4,
max_iter: 30,
tol: 1e-3,
transform_algorithm: DictionaryTransformAlgorithm::OMP,
alpha: 0.3,
random_state: Some(42),
};
let model = DictionaryLearning::new(config);
let fitted_model = model.fit(&x, &()).expect("model fitting should succeed");
let components = fitted_model.components();
assert_eq!(components.nrows(), 4);
assert_eq!(components.ncols(), 8);
for atom_idx in 0..4 {
let norm: Float = (0..8)
.map(|j| components[[atom_idx, j]] * components[[atom_idx, j]])
.sum::<Float>()
.sqrt();
assert!(
(norm - 1.0).abs() < 1e-6,
"Atom {} not normalized: {}",
atom_idx,
norm
);
}
let codes = fitted_model
.transform(&x)
.expect("transformation should succeed");
assert_eq!(codes.nrows(), 20);
assert_eq!(codes.ncols(), 4);
let mut nonzero_count = 0;
for i in 0..codes.nrows() {
for j in 0..codes.ncols() {
if codes[[i, j]].abs() > 1e-10 {
nonzero_count += 1;
}
}
}
let sparsity_ratio = nonzero_count as f64 / (codes.nrows() * codes.ncols()) as f64;
assert!(
sparsity_ratio < 0.5,
"Codes should be sparse, but {}% are nonzero",
sparsity_ratio * 100.0
);
let mut reconstruction_error = 0.0;
for i in 0..x.nrows() {
for j in 0..x.ncols() {
let mut reconstruction = 0.0;
for k in 0..components.nrows() {
reconstruction += codes[[i, k]] * components[[k, j]];
}
let diff = x[[i, j]] - reconstruction;
reconstruction_error += diff * diff;
}
}
reconstruction_error = (reconstruction_error / (x.nrows() * x.ncols()) as Float).sqrt();
assert!(
reconstruction_error < 3.0,
"Reconstruction error too high: {}",
reconstruction_error
);
}
}