use scirs2_core::ndarray::{s, Array1, Array2, Array3, ArrayView1, ArrayView2, ArrayView3, Axis};
use scirs2_core::random::RandNormal;
use scirs2_core::random::{Random, rng};
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Predict, Transform, Untrained},
types::Float,
};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct CompressedSensingLabelPowerset<S = Untrained> {
state: S,
compressed_dimension: usize,
reconstruction_method: ReconstructionMethod,
learning_rate: Float,
max_iter: usize,
random_state: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ReconstructionMethod {
L1,
L2,
OMP,
}
#[derive(Debug, Clone)]
pub struct CompressedSensingLabelPowersetTrained {
projection_matrix: Array2<Float>,
reconstruction_weights: Array2<Float>,
compressed_dimension: usize,
n_features: usize,
n_labels: usize,
reconstruction_method: ReconstructionMethod,
}
impl Default for CompressedSensingLabelPowerset<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for CompressedSensingLabelPowerset<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, Array2<i32>> for CompressedSensingLabelPowerset<Untrained> {
type Fitted = CompressedSensingLabelPowerset<CompressedSensingLabelPowersetTrained>;
fn fit(
self,
X: &ArrayView2<'_, Float>,
y: &Array2<i32>,
) -> SklResult<Self::Fitted> {
let (n_samples, n_features) = X.dim();
let n_labels = y.ncols();
if n_samples != y.nrows() {
return Err(SklearsError::InvalidInput(
"X and y must have the same number of samples".to_string(),
));
}
if self.compressed_dimension >= n_labels {
return Err(SklearsError::InvalidInput(
"Compressed dimension must be less than number of labels".to_string(),
));
}
let mut rng = if let Some(seed) = self.random_state {
scirs2_core::random::seeded_rng(seed)
} else {
thread_rng()
};
let projection_matrix = Array2::random_using(
(self.compressed_dimension, n_labels),
RandNormal::new(0.0, 1.0 / (n_labels as Float).sqrt()).expect("operation should succeed"),
&mut rng,
);
let y_float = y.mapv(|x| x as Float);
let compressed_labels = projection_matrix.dot(&y_float.t()).t();
let mut reconstruction_weights = Array2::<Float>::zeros((n_features, self.compressed_dimension));
match self.reconstruction_method {
ReconstructionMethod::L2 => {
for _iter in 0..self.max_iter {
for sample_idx in 0..n_samples {
let x = X.row(sample_idx);
let y_compressed = compressed_labels.row(sample_idx);
let prediction = x.dot(&reconstruction_weights);
for comp_idx in 0..self.compressed_dimension {
let error = prediction[comp_idx] - y_compressed[comp_idx];
for feat_idx in 0..n_features {
reconstruction_weights[[feat_idx, comp_idx]] -=
self.learning_rate * error * x[feat_idx];
}
}
}
}
}
ReconstructionMethod::L1 => {
for _iter in 0..self.max_iter {
for sample_idx in 0..n_samples {
let x = X.row(sample_idx);
let y_compressed = compressed_labels.row(sample_idx);
let prediction = x.dot(&reconstruction_weights);
for comp_idx in 0..self.compressed_dimension {
let error = prediction[comp_idx] - y_compressed[comp_idx];
for feat_idx in 0..n_features {
let weight = reconstruction_weights[[feat_idx, comp_idx]];
let l1_grad = if weight > 0.0 { 1.0 } else { -1.0 };
reconstruction_weights[[feat_idx, comp_idx]] -=
self.learning_rate * (error * x[feat_idx] + 0.01 * l1_grad);
}
}
}
}
}
ReconstructionMethod::OMP => {
for _iter in 0..self.max_iter {
for sample_idx in 0..n_samples {
let x = X.row(sample_idx);
let y_compressed = compressed_labels.row(sample_idx);
let prediction = x.dot(&reconstruction_weights);
for comp_idx in 0..self.compressed_dimension {
let error = prediction[comp_idx] - y_compressed[comp_idx];
for feat_idx in 0..n_features {
reconstruction_weights[[feat_idx, comp_idx]] -=
self.learning_rate * error * x[feat_idx];
}
}
}
}
}
}
Ok(CompressedSensingLabelPowerset {
state: CompressedSensingLabelPowersetTrained {
projection_matrix,
reconstruction_weights,
compressed_dimension: self.compressed_dimension,
n_features,
n_labels,
reconstruction_method: self.reconstruction_method,
},
compressed_dimension: self.compressed_dimension,
reconstruction_method: self.reconstruction_method,
learning_rate: self.learning_rate,
max_iter: self.max_iter,
random_state: self.random_state,
})
}
}
impl CompressedSensingLabelPowerset<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
compressed_dimension: 10,
reconstruction_method: ReconstructionMethod::L2,
learning_rate: 0.01,
max_iter: 100,
random_state: None,
}
}
pub fn compressed_dimension(mut self, compressed_dimension: usize) -> Self {
self.compressed_dimension = compressed_dimension;
self
}
pub fn reconstruction_method(mut self, method: ReconstructionMethod) -> Self {
self.reconstruction_method = method;
self
}
pub fn learning_rate(mut self, learning_rate: Float) -> Self {
self.learning_rate = learning_rate;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Predict<ArrayView2<'_, Float>, Array2<i32>>
for CompressedSensingLabelPowerset<CompressedSensingLabelPowersetTrained>
{
fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
let (n_samples, n_features) = X.dim();
if n_features != self.state.n_features {
return Err(SklearsError::InvalidInput(
"X has different number of features than training data".to_string(),
));
}
let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_labels));
for sample_idx in 0..n_samples {
let x = X.row(sample_idx);
let compressed_pred = x.dot(&self.state.reconstruction_weights);
let label_pred = self.state.projection_matrix.t().dot(&compressed_pred);
for label_idx in 0..self.state.n_labels {
predictions[[sample_idx, label_idx]] = if label_pred[label_idx] > 0.5 { 1 } else { 0 };
}
}
Ok(predictions)
}
}
impl CompressedSensingLabelPowerset<CompressedSensingLabelPowersetTrained> {
pub fn compressed_dimension(&self) -> usize {
self.state.compressed_dimension
}
pub fn compression_ratio(&self) -> Float {
self.state.compressed_dimension as Float / self.state.n_labels as Float
}
pub fn projection_matrix(&self) -> &Array2<Float> {
&self.state.projection_matrix
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compressed_sensing_basic() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
let y = array![[1, 0, 1, 0], [0, 1, 0, 1], [1, 1, 0, 0], [0, 0, 1, 1]];
let cs = CompressedSensingLabelPowerset::new()
.compressed_dimension(2)
.reconstruction_method(ReconstructionMethod::L2);
let trained_cs = cs.fit(&X.view(), &y).expect("model fitting should succeed");
let predictions = trained_cs.predict(&X.view()).expect("prediction should succeed");
assert_eq!(predictions.dim(), (4, 4));
assert!(predictions.iter().all(|&x| x == 0 || x == 1));
assert_eq!(trained_cs.compressed_dimension(), 2);
assert!((trained_cs.compression_ratio() - 0.5).abs() < 1e-10);
}
#[test]
fn test_compressed_sensing_methods() {
let X = array![[1.0, 2.0], [2.0, 3.0]];
let y = array![[1, 0, 1], [0, 1, 0]];
let cs_l1 = CompressedSensingLabelPowerset::new()
.compressed_dimension(2)
.reconstruction_method(ReconstructionMethod::L1);
let cs_l2 = CompressedSensingLabelPowerset::new()
.compressed_dimension(2)
.reconstruction_method(ReconstructionMethod::L2);
let cs_omp = CompressedSensingLabelPowerset::new()
.compressed_dimension(2)
.reconstruction_method(ReconstructionMethod::OMP);
let trained_l1 = cs_l1.fit(&X.view(), &y).expect("model fitting should succeed");
let trained_l2 = cs_l2.fit(&X.view(), &y).expect("model fitting should succeed");
let trained_omp = cs_omp.fit(&X.view(), &y).expect("model fitting should succeed");
let pred_l1 = trained_l1.predict(&X.view()).expect("prediction should succeed");
let pred_l2 = trained_l2.predict(&X.view()).expect("prediction should succeed");
let pred_omp = trained_omp.predict(&X.view()).expect("prediction should succeed");
assert_eq!(pred_l1.dim(), (2, 3));
assert_eq!(pred_l2.dim(), (2, 3));
assert_eq!(pred_omp.dim(), (2, 3));
}
#[test]
fn test_compressed_sensing_reproducibility() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
let y = array![[1, 0, 1], [0, 1, 0], [1, 1, 0], [0, 0, 1]];
let cs1 = CompressedSensingLabelPowerset::new()
.compressed_dimension(2)
.random_state(42);
let trained_cs1 = cs1.fit(&X.view(), &y).expect("model fitting should succeed");
let cs2 = CompressedSensingLabelPowerset::new()
.compressed_dimension(2)
.random_state(42);
let trained_cs2 = cs2.fit(&X.view(), &y).expect("model fitting should succeed");
let proj1 = trained_cs1.projection_matrix();
let proj2 = trained_cs2.projection_matrix();
for i in 0..proj1.nrows() {
for j in 0..proj1.ncols() {
assert!((proj1[[i, j]] - proj2[[i, j]]).abs() < 1e-10);
}
}
}
}