use scirs2_core::ndarray::{Array1, Array2, Array3};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{rng as make_rng, RngExt, SeedableRng};
use scirs2_linalg::compat::{svd, ArrayLinalgExt};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use sklears_core::traits::Fit;
use sklears_core::{
error::{Result, SklearsError},
traits::Untrained,
};
type CPResult = Result<(Vec<Array2<f64>>, Array1<f64>, usize, f64)>;
type TuckerResult = Result<(Array3<f64>, Vec<Array2<f64>>, usize, f64)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CPAlgorithm {
#[default]
ALS,
NonNegative,
Robust,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TuckerAlgorithm {
#[default]
HOSVD,
ALS,
SequentialSVD,
}
#[derive(Debug, Clone)]
pub struct CPDecomposition<State = Untrained> {
pub n_components: usize,
pub algorithm: CPAlgorithm,
pub max_iter: usize,
pub tol: f64,
pub random_state: Option<u64>,
pub learning_rate: f64,
pub regularization: f64,
pub normalize_factors: bool,
#[allow(dead_code)]
state: State,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TrainedCP {
pub factors: Vec<Array2<f64>>,
pub weights: Array1<f64>,
pub tensor_shape: Vec<usize>,
pub n_components: usize,
pub n_iter: usize,
pub reconstruction_error: f64,
}
#[derive(Debug, Clone)]
pub struct TuckerDecomposition<State = Untrained> {
pub n_components: Vec<usize>,
pub algorithm: TuckerAlgorithm,
pub max_iter: usize,
pub tol: f64,
pub random_state: Option<u64>,
pub center: bool,
pub init_method: String,
#[allow(dead_code)]
state: State,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TrainedTucker {
pub core: Array3<f64>,
pub factors: Vec<Array2<f64>>,
pub mean: Option<Array3<f64>>,
pub tensor_shape: Vec<usize>,
pub n_components: Vec<usize>,
pub n_iter: usize,
pub reconstruction_error: f64,
}
impl CPDecomposition<Untrained> {
pub fn new(n_components: usize) -> Self {
Self {
n_components,
algorithm: CPAlgorithm::ALS,
max_iter: 100,
tol: 1e-6,
random_state: None,
learning_rate: 0.01,
regularization: 0.0,
normalize_factors: true,
state: Untrained,
}
}
pub fn algorithm(mut self, algorithm: CPAlgorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
pub fn learning_rate(mut self, learning_rate: f64) -> Self {
self.learning_rate = learning_rate;
self
}
pub fn regularization(mut self, regularization: f64) -> Self {
self.regularization = regularization;
self
}
pub fn normalize_factors(mut self, normalize_factors: bool) -> Self {
self.normalize_factors = normalize_factors;
self
}
}
impl Fit<Array3<f64>, ()> for CPDecomposition<Untrained> {
type Fitted = CPDecomposition<TrainedCP>;
fn fit(self, tensor: &Array3<f64>, _y: &()) -> Result<Self::Fitted> {
let tensor_shape = tensor.shape().to_vec();
if tensor_shape.len() != 3 {
return Err(SklearsError::InvalidInput(
"CP decomposition currently supports 3D tensors only".to_string(),
));
}
let (_n_mode1, _n_mode2, _n_mode3) = (tensor_shape[0], tensor_shape[1], tensor_shape[2]);
let mut rng: StdRng = match self.random_state {
Some(seed) => StdRng::seed_from_u64(seed),
None => StdRng::from_rng(&mut make_rng()),
};
let (factors, weights, n_iter, reconstruction_error) = match self.algorithm {
CPAlgorithm::ALS => self.cp_als(tensor, &mut rng)?,
CPAlgorithm::NonNegative => self.cp_nonnegative(tensor, &mut rng)?,
CPAlgorithm::Robust => self.cp_robust(tensor, &mut rng)?,
};
Ok(CPDecomposition {
n_components: self.n_components,
algorithm: self.algorithm,
max_iter: self.max_iter,
tol: self.tol,
random_state: self.random_state,
learning_rate: self.learning_rate,
regularization: self.regularization,
normalize_factors: self.normalize_factors,
state: TrainedCP {
factors,
weights,
tensor_shape,
n_components: self.n_components,
n_iter,
reconstruction_error,
},
})
}
}
impl CPDecomposition<Untrained> {
fn cp_als(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> CPResult {
let (n_mode1, n_mode2, n_mode3) = tensor.dim();
let r = self.n_components;
let mut factors: Vec<Array2<f64>> = vec![
{
let mut arr: Array2<f64> = Array2::zeros((n_mode1, r));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>() - 0.5;
}
arr
},
{
let mut arr: Array2<f64> = Array2::zeros((n_mode2, r));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>() - 0.5;
}
arr
},
{
let mut arr: Array2<f64> = Array2::zeros((n_mode3, r));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>() - 0.5;
}
arr
},
];
if self.normalize_factors {
for factor in &mut factors {
for j in 0..r {
let mut col = factor.column_mut(j);
let norm = col.dot(&col).sqrt();
if norm > 1e-12 {
col /= norm;
}
}
}
}
let mut prev_error = f64::INFINITY;
let mut n_iter = 0;
for iter in 0..self.max_iter {
n_iter = iter + 1;
for mode in 0..3 {
let khatri_rao = match mode {
0 => self.khatri_rao_product(&factors[2], &factors[1]),
1 => self.khatri_rao_product(&factors[2], &factors[0]),
2 => self.khatri_rao_product(&factors[1], &factors[0]),
_ => unreachable!(),
};
let unfolded = self.unfold_tensor(tensor, mode)?;
let gram = khatri_rao.t().dot(&khatri_rao);
let mut gram_reg = gram;
for i in 0..r {
gram_reg[[i, i]] += self.regularization;
}
let rhs = unfolded.dot(&khatri_rao);
let rhs_t = rhs.t().to_owned();
factors[mode] = self.solve_linear_system(&gram_reg, &rhs_t)?;
if self.normalize_factors {
for j in 0..r {
let mut col = factors[mode].column_mut(j);
let norm = col.dot(&col).sqrt();
if norm > 1e-12 {
col /= norm;
}
}
}
}
let reconstructed = self.reconstruct_tensor(&factors)?;
let error = self.frobenius_norm(&(tensor - &reconstructed));
if (prev_error - error).abs() < self.tol {
break;
}
prev_error = error;
}
let mut weights = Array1::ones(r);
if self.normalize_factors {
for j in 0..r {
let mut weight = 1.0;
for factor in &factors {
let col_norm = factor.column(j).dot(&factor.column(j)).sqrt();
weight *= col_norm;
}
weights[j] = weight;
}
}
let final_error = self.frobenius_norm(&(tensor - &self.reconstruct_tensor(&factors)?));
Ok((factors, weights, n_iter, final_error))
}
fn cp_nonnegative(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> CPResult {
let (n_mode1, n_mode2, n_mode3) = tensor.dim();
let r = self.n_components;
let mut factors: Vec<Array2<f64>> = vec![
{
let mut arr: Array2<f64> = Array2::zeros((n_mode1, r));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>().abs();
}
arr
},
{
let mut arr: Array2<f64> = Array2::zeros((n_mode2, r));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>().abs();
}
arr
},
{
let mut arr: Array2<f64> = Array2::zeros((n_mode3, r));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>().abs();
}
arr
},
];
let mut prev_error = f64::INFINITY;
let mut n_iter = 0;
for iter in 0..self.max_iter {
n_iter = iter + 1;
for mode in 0..3 {
let khatri_rao = match mode {
0 => self.khatri_rao_product(&factors[2], &factors[1]),
1 => self.khatri_rao_product(&factors[2], &factors[0]),
2 => self.khatri_rao_product(&factors[1], &factors[0]),
_ => unreachable!(),
};
let unfolded = self.unfold_tensor(tensor, mode)?;
let numerator = unfolded.dot(&khatri_rao);
let denominator = factors[mode].dot(&khatri_rao.t().dot(&khatri_rao));
for i in 0..factors[mode].nrows() {
for j in 0..factors[mode].ncols() {
if denominator[[i, j]] > 1e-12 {
factors[mode][[i, j]] *= numerator[[i, j]] / denominator[[i, j]];
}
factors[mode][[i, j]] = factors[mode][[i, j]].max(1e-12);
}
}
if self.normalize_factors {
for j in 0..r {
let mut col = factors[mode].column_mut(j);
let norm = col.dot(&col).sqrt();
if norm > 1e-12 {
col /= norm;
}
}
}
}
let reconstructed = self.reconstruct_tensor(&factors)?;
let error = self.frobenius_norm(&(tensor - &reconstructed));
if (prev_error - error).abs() < self.tol {
break;
}
prev_error = error;
}
let weights = Array1::ones(r);
let final_error = self.frobenius_norm(&(tensor - &self.reconstruct_tensor(&factors)?));
Ok((factors, weights, n_iter, final_error))
}
fn cp_robust(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> CPResult {
self.cp_als(tensor, rng)
}
fn khatri_rao_product(&self, a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
let (n_a, r) = a.dim();
let (n_b, r_b) = b.dim();
assert_eq!(r, r_b, "Matrices must have same number of columns");
let mut result = Array2::zeros((n_a * n_b, r));
for j in 0..r {
let a_col = a.column(j);
let b_col = b.column(j);
for i in 0..n_a {
for k in 0..n_b {
result[[i * n_b + k, j]] = a_col[i] * b_col[k];
}
}
}
result
}
fn unfold_tensor(&self, tensor: &Array3<f64>, mode: usize) -> Result<Array2<f64>> {
let (n1, n2, n3) = tensor.dim();
match mode {
0 => {
let mut unfolded = Array2::zeros((n1, n2 * n3));
for i in 0..n1 {
for j in 0..n2 {
for k in 0..n3 {
unfolded[[i, j * n3 + k]] = tensor[[i, j, k]];
}
}
}
Ok(unfolded)
}
1 => {
let mut unfolded = Array2::zeros((n2, n3 * n1));
for j in 0..n2 {
for k in 0..n3 {
for i in 0..n1 {
unfolded[[j, k * n1 + i]] = tensor[[i, j, k]];
}
}
}
Ok(unfolded)
}
2 => {
let mut unfolded = Array2::zeros((n3, n1 * n2));
for k in 0..n3 {
for i in 0..n1 {
for j in 0..n2 {
unfolded[[k, i * n2 + j]] = tensor[[i, j, k]];
}
}
}
Ok(unfolded)
}
_ => Err(SklearsError::InvalidParameter {
name: "mode".to_string(),
reason: "must be 0, 1, or 2 for 3D tensors".to_string(),
}),
}
}
fn solve_linear_system(&self, a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
let (u, s, vt) = svd(&a.view(), true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
let tolerance = 1e-12;
let k = s.len();
let mut s_inv = Array2::zeros((k, k));
for i in 0..k {
if s[i] > tolerance {
s_inv[[i, i]] = 1.0 / s[i];
}
}
let vt_t = vt.t();
let u_t = u.t();
let temp = s_inv.dot(&u_t);
let a_pinv = vt_t.dot(&temp);
let result = a_pinv.dot(b);
Ok(result)
}
fn reconstruct_tensor(&self, factors: &[Array2<f64>]) -> Result<Array3<f64>> {
let (n1, r) = factors[0].dim();
let (n2, r2) = factors[1].dim();
let (n3, r3) = factors[2].dim();
if r != r2 || r != r3 {
return Err(SklearsError::InvalidInput(
"All factor matrices must have the same number of columns".to_string(),
));
}
let mut reconstructed = Array3::zeros((n1, n2, n3));
for k in 0..r {
let a_k = factors[0].column(k);
let b_k = factors[1].column(k);
let c_k = factors[2].column(k);
for i in 0..n1 {
for j in 0..n2 {
for l in 0..n3 {
reconstructed[[i, j, l]] += a_k[i] * b_k[j] * c_k[l];
}
}
}
}
Ok(reconstructed)
}
fn frobenius_norm(&self, tensor: &Array3<f64>) -> f64 {
tensor.iter().map(|&x| x * x).sum::<f64>().sqrt()
}
}
impl TuckerDecomposition<Untrained> {
pub fn new(n_components: Vec<usize>) -> Self {
Self {
n_components,
algorithm: TuckerAlgorithm::HOSVD,
max_iter: 100,
tol: 1e-6,
random_state: None,
center: true,
init_method: "random".to_string(),
state: Untrained,
}
}
pub fn algorithm(mut self, algorithm: TuckerAlgorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
pub fn center(mut self, center: bool) -> Self {
self.center = center;
self
}
pub fn init_method(mut self, init_method: String) -> Self {
self.init_method = init_method;
self
}
}
impl Fit<Array3<f64>, ()> for TuckerDecomposition<Untrained> {
type Fitted = TuckerDecomposition<TrainedTucker>;
fn fit(self, tensor: &Array3<f64>, _y: &()) -> Result<Self::Fitted> {
let tensor_shape = tensor.shape().to_vec();
if tensor_shape.len() != 3 {
return Err(SklearsError::InvalidInput(
"Tucker decomposition currently supports 3D tensors only".to_string(),
));
}
if self.n_components.len() != 3 {
return Err(SklearsError::InvalidInput(
"n_components must have 3 elements for 3D tensors".to_string(),
));
}
let (centered_tensor, mean) = if self.center {
let mean_val = tensor.mean().ok_or_else(|| {
SklearsError::NumericalError("cannot compute mean of empty tensor".to_string())
})?;
let centered = tensor.mapv(|x| x - mean_val);
let mean_tensor = Array3::from_elem(tensor.dim(), mean_val);
(centered, Some(mean_tensor))
} else {
(tensor.clone(), None)
};
let mut rng: StdRng = match self.random_state {
Some(seed) => StdRng::seed_from_u64(seed),
None => StdRng::from_rng(&mut make_rng()),
};
let (core, factors, n_iter, reconstruction_error) = match self.algorithm {
TuckerAlgorithm::HOSVD => self.tucker_hosvd(¢ered_tensor)?,
TuckerAlgorithm::ALS => self.tucker_als(¢ered_tensor, &mut rng)?,
TuckerAlgorithm::SequentialSVD => self.tucker_sequential_svd(¢ered_tensor)?,
};
Ok(TuckerDecomposition {
n_components: self.n_components.clone(),
algorithm: self.algorithm,
max_iter: self.max_iter,
tol: self.tol,
random_state: self.random_state,
center: self.center,
init_method: self.init_method,
state: TrainedTucker {
core,
factors,
mean,
tensor_shape,
n_components: self.n_components,
n_iter,
reconstruction_error,
},
})
}
}
impl TuckerDecomposition<Untrained> {
fn tucker_hosvd(&self, tensor: &Array3<f64>) -> TuckerResult {
let mut factors = Vec::new();
for mode in 0..3 {
let unfolded = self.unfold_tensor_tucker(tensor, mode)?;
let svd_result = self.compute_svd(&unfolded)?;
let n_comp = self.n_components[mode]
.min(svd_result.0.ncols())
.min(unfolded.nrows());
let factor = svd_result
.0
.slice(scirs2_core::ndarray::s![.., ..n_comp])
.to_owned();
factors.push(factor);
}
let core = self.compute_core_tensor(tensor, &factors)?;
let reconstructed = self.reconstruct_tucker_tensor(&core, &factors)?;
let error = self.frobenius_norm_tucker(&(tensor - &reconstructed));
Ok((core, factors, 1, error))
}
fn tucker_als(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> TuckerResult {
let (n1, n2, n3) = tensor.dim();
let mut factors: Vec<Array2<f64>> = vec![
{
let mut arr: Array2<f64> = Array2::zeros((n1, self.n_components[0]));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>() - 0.5;
}
arr
},
{
let mut arr: Array2<f64> = Array2::zeros((n2, self.n_components[1]));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>() - 0.5;
}
arr
},
{
let mut arr: Array2<f64> = Array2::zeros((n3, self.n_components[2]));
for elem in arr.iter_mut() {
*elem = rng.random::<f64>() - 0.5;
}
arr
},
];
for factor in &mut factors {
*factor = self.orthogonalize_matrix(factor)?;
}
let mut prev_error = f64::INFINITY;
let mut n_iter = 0;
for iter in 0..self.max_iter {
n_iter = iter + 1;
for mode in 0..3 {
let unfolded = self.unfold_tensor_tucker(tensor, mode)?;
let other_factors = match mode {
0 => self.kronecker_product(&factors[2], &factors[1]),
1 => self.kronecker_product(&factors[2], &factors[0]),
2 => self.kronecker_product(&factors[1], &factors[0]),
_ => unreachable!(),
};
let rhs = unfolded.dot(&other_factors);
let gram = other_factors.t().dot(&other_factors);
let rhs_t = rhs.t().to_owned();
let solved = self.solve_linear_system_tucker(&gram, &rhs_t)?;
let _expected_shape = (
tensor.dim().0.max(tensor.dim().1.max(tensor.dim().2)),
self.n_components[mode],
);
let n_mode = match mode {
0 => tensor.dim().0,
1 => tensor.dim().1,
2 => tensor.dim().2,
_ => unreachable!(),
};
let solved_t = solved.t().to_owned();
let factor_shape = (n_mode, self.n_components[mode]);
if solved_t.dim() == factor_shape {
factors[mode] = solved_t;
} else {
let max_rows = solved_t.nrows().min(n_mode);
let max_cols = solved_t.ncols().min(self.n_components[mode]);
let mut factor = Array2::zeros(factor_shape);
for i in 0..max_rows {
for j in 0..max_cols {
factor[[i, j]] = solved_t[[i, j]];
}
}
factors[mode] = factor;
}
factors[mode] = self.orthogonalize_matrix(&factors[mode])?;
}
let core = self.compute_core_tensor(tensor, &factors)?;
let reconstructed = self.reconstruct_tucker_tensor(&core, &factors)?;
let error = self.frobenius_norm_tucker(&(tensor - &reconstructed));
if (prev_error - error).abs() < self.tol {
break;
}
prev_error = error;
}
let core = self.compute_core_tensor(tensor, &factors)?;
let final_error = prev_error;
Ok((core, factors, n_iter, final_error))
}
fn tucker_sequential_svd(&self, tensor: &Array3<f64>) -> TuckerResult {
self.tucker_hosvd(tensor)
}
fn unfold_tensor_tucker(&self, tensor: &Array3<f64>, mode: usize) -> Result<Array2<f64>> {
let (n1, n2, n3) = tensor.dim();
match mode {
0 => {
let mut unfolded = Array2::zeros((n1, n2 * n3));
for i in 0..n1 {
for j in 0..n2 {
for k in 0..n3 {
unfolded[[i, j * n3 + k]] = tensor[[i, j, k]];
}
}
}
Ok(unfolded)
}
1 => {
let mut unfolded = Array2::zeros((n2, n1 * n3));
for j in 0..n2 {
for i in 0..n1 {
for k in 0..n3 {
unfolded[[j, i * n3 + k]] = tensor[[i, j, k]];
}
}
}
Ok(unfolded)
}
2 => {
let mut unfolded = Array2::zeros((n3, n1 * n2));
for k in 0..n3 {
for i in 0..n1 {
for j in 0..n2 {
unfolded[[k, i * n2 + j]] = tensor[[i, j, k]];
}
}
}
Ok(unfolded)
}
_ => Err(SklearsError::InvalidParameter {
name: "mode".to_string(),
reason: "must be 0, 1, or 2 for 3D tensors".to_string(),
}),
}
}
fn compute_svd(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>, Array1<f64>)> {
let (u, s, vt) = svd(&matrix.view(), true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
Ok((u, vt, s))
}
fn compute_core_tensor(
&self,
tensor: &Array3<f64>,
factors: &[Array2<f64>],
) -> Result<Array3<f64>> {
let (r1, r2, r3) = (factors[0].ncols(), factors[1].ncols(), factors[2].ncols());
let mut core = Array3::zeros((r1, r2, r3));
for i in 0..r1 {
for j in 0..r2 {
for k in 0..r3 {
let mut value = 0.0;
let (n1, n2, n3) = tensor.dim();
for ii in 0..n1 {
for jj in 0..n2 {
for kk in 0..n3 {
value += tensor[[ii, jj, kk]]
* factors[0][[ii, i]]
* factors[1][[jj, j]]
* factors[2][[kk, k]];
}
}
}
core[[i, j, k]] = value;
}
}
}
Ok(core)
}
fn reconstruct_tucker_tensor(
&self,
core: &Array3<f64>,
factors: &[Array2<f64>],
) -> Result<Array3<f64>> {
let (n1, n2, n3) = (factors[0].nrows(), factors[1].nrows(), factors[2].nrows());
let mut reconstructed = Array3::zeros((n1, n2, n3));
let (r1, r2, r3) = core.dim();
for i in 0..n1 {
for j in 0..n2 {
for k in 0..n3 {
let mut value = 0.0;
for ii in 0..r1 {
for jj in 0..r2 {
for kk in 0..r3 {
value += core[[ii, jj, kk]]
* factors[0][[i, ii]]
* factors[1][[j, jj]]
* factors[2][[k, kk]];
}
}
}
reconstructed[[i, j, k]] = value;
}
}
}
Ok(reconstructed)
}
fn orthogonalize_matrix(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
let (m, n) = matrix.dim();
let mut q = matrix.clone();
for j in 0..n {
for i in 0..j {
let qi = q.column(i).to_owned();
let qj = q.column(j).to_owned();
let proj = qi.dot(&qj);
for k in 0..m {
q[[k, j]] -= proj * qi[k];
}
}
let norm = {
let col = q.column(j);
col.dot(&col).sqrt()
};
if norm > 1e-12 {
let mut col = q.column_mut(j);
col /= norm;
}
}
Ok(q)
}
fn kronecker_product(&self, a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
let (m_a, n_a) = a.dim();
let (m_b, n_b) = b.dim();
let mut result = Array2::zeros((m_a * m_b, n_a * n_b));
for i in 0..m_a {
for j in 0..n_a {
for k in 0..m_b {
for l in 0..n_b {
result[[i * m_b + k, j * n_b + l]] = a[[i, j]] * b[[k, l]];
}
}
}
}
result
}
fn solve_linear_system_tucker(&self, a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
if let Ok(chol) = a.cholesky() {
let mut result = Array2::zeros((a.ncols(), b.ncols()));
for (j, col) in b.axis_iter(scirs2_core::ndarray::Axis(1)).enumerate() {
let sol = chol.solve(&col.to_owned()).map_err(|e| {
SklearsError::NumericalError(format!("Cholesky solve failed: {}", e))
})?;
for (i, &val) in sol.iter().enumerate() {
result[[i, j]] = val;
}
}
return Ok(result);
}
let (u, s, vt) = svd(&a.view(), true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
let tolerance = 1e-12;
let k = s.len();
let mut s_inv = Array2::zeros((k, k));
for i in 0..k {
if s[i] > tolerance {
s_inv[[i, i]] = 1.0 / s[i];
}
}
let vt_t = vt.t();
let u_t = u.t();
let temp = s_inv.dot(&u_t);
let a_pinv = vt_t.dot(&temp);
let result = a_pinv.dot(b);
Ok(result)
}
fn frobenius_norm_tucker(&self, tensor: &Array3<f64>) -> f64 {
tensor.iter().map(|&x| x * x).sum::<f64>().sqrt()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_cp_decomposition_basic() {
let tensor = array![[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]],];
let cp = CPDecomposition::new(2).max_iter(10).random_state(42);
let result = cp.fit(&tensor, &());
if let Err(ref e) = result {
println!("CP decomposition error: {e:?}");
}
assert!(result.is_ok());
let trained = result.expect("operation should succeed");
assert_eq!(trained.state.n_components, 2);
assert_eq!(trained.state.factors.len(), 3);
assert!(trained.state.reconstruction_error.is_finite());
}
#[test]
fn test_tucker_decomposition_basic() {
let tensor = 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 tucker = TuckerDecomposition::new(vec![2, 2, 1])
.algorithm(TuckerAlgorithm::HOSVD)
.random_state(42);
let result = tucker.fit(&tensor, &());
if let Err(ref e) = result {
println!("Tucker decomposition error: {e:?}");
}
assert!(result.is_ok());
let trained = result.expect("operation should succeed");
assert_eq!(trained.state.n_components, vec![2, 2, 1]);
assert_eq!(trained.state.factors.len(), 3);
assert!(trained.state.reconstruction_error.is_finite());
}
#[test]
fn test_cp_nonnegative() {
let tensor = array![[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]],];
let cp = CPDecomposition::new(1)
.algorithm(CPAlgorithm::NonNegative)
.max_iter(5)
.random_state(42);
let result = cp.fit(&tensor, &());
assert!(result.is_ok());
let trained = result.expect("operation should succeed");
for factor in &trained.state.factors {
for &val in factor.iter() {
assert!(val >= 0.0, "Factor value should be non-negative: {}", val);
}
}
}
#[test]
fn test_tucker_als() {
let tensor = array![[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]],];
let tucker = TuckerDecomposition::new(vec![1, 2, 1])
.algorithm(TuckerAlgorithm::ALS)
.max_iter(5)
.random_state(42);
let result = tucker.fit(&tensor, &());
if let Err(ref e) = result {
println!("Tucker ALS error: {e:?}");
}
assert!(result.is_ok());
let trained = result.expect("operation should succeed");
assert_eq!(trained.state.n_components, vec![1, 2, 1]);
assert!(trained.state.n_iter > 0);
}
#[test]
fn test_cp_parameters() {
let cp = CPDecomposition::new(3)
.algorithm(CPAlgorithm::Robust)
.max_iter(200)
.tol(1e-8)
.learning_rate(0.05)
.regularization(0.1)
.normalize_factors(false);
assert_eq!(cp.n_components, 3);
assert_eq!(cp.algorithm, CPAlgorithm::Robust);
assert_eq!(cp.max_iter, 200);
assert_eq!(cp.tol, 1e-8);
assert_eq!(cp.learning_rate, 0.05);
assert_eq!(cp.regularization, 0.1);
assert!(!cp.normalize_factors);
}
#[test]
fn test_tucker_parameters() {
let tucker = TuckerDecomposition::new(vec![2, 3, 4])
.algorithm(TuckerAlgorithm::SequentialSVD)
.max_iter(150)
.tol(1e-7)
.center(false)
.init_method("svd".to_string());
assert_eq!(tucker.n_components, vec![2, 3, 4]);
assert_eq!(tucker.algorithm, TuckerAlgorithm::SequentialSVD);
assert_eq!(tucker.max_iter, 150);
assert_eq!(tucker.tol, 1e-7);
assert!(!tucker.center);
assert_eq!(tucker.init_method, "svd");
}
}