use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Predict, Untrained},
types::Float,
};
use std::collections::HashMap;
use super::simd_ops;
#[derive(Debug, Clone)]
pub struct GroupLasso<S = Untrained> {
#[allow(dead_code)]
pub(crate) state: S,
pub(crate) alpha: Float,
pub(crate) feature_groups: Vec<Vec<usize>>,
pub(crate) max_iter: usize,
pub(crate) tolerance: Float,
pub(crate) learning_rate: Float,
pub(crate) task_outputs: HashMap<String, usize>,
pub(crate) fit_intercept: bool,
}
#[derive(Debug, Clone)]
pub struct GroupLassoTrained {
pub(crate) coefficients: HashMap<String, Array2<Float>>,
pub(crate) intercepts: HashMap<String, Array1<Float>>,
pub(crate) n_features: usize,
#[allow(dead_code)]
pub(crate) task_outputs: HashMap<String, usize>,
pub(crate) feature_groups: Vec<Vec<usize>>,
pub(crate) n_iter: usize,
#[allow(dead_code)]
pub(crate) alpha: Float,
}
impl GroupLasso<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
alpha: 1.0,
feature_groups: Vec::new(),
max_iter: 1000,
tolerance: 1e-4,
learning_rate: 0.01,
task_outputs: HashMap::new(),
fit_intercept: true,
}
}
pub fn alpha(mut self, alpha: Float) -> Self {
self.alpha = alpha;
self
}
pub fn feature_groups(mut self, groups: Vec<Vec<usize>>) -> Self {
self.feature_groups = groups;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tolerance(mut self, tolerance: Float) -> Self {
self.tolerance = tolerance;
self
}
pub fn learning_rate(mut self, lr: Float) -> Self {
self.learning_rate = lr;
self
}
pub fn task_outputs(mut self, tasks: &[(&str, usize)]) -> Self {
for (task_name, output_size) in tasks {
self.task_outputs
.insert(task_name.to_string(), *output_size);
}
self
}
pub fn fit_intercept(mut self, fit_intercept: bool) -> Self {
self.fit_intercept = fit_intercept;
self
}
}
impl Default for GroupLasso<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for GroupLasso<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>> for GroupLasso<Untrained> {
type Fitted = GroupLassoTrained;
fn fit(
self,
x: &ArrayView2<Float>,
y: &HashMap<String, Array2<Float>>,
) -> SklResult<Self::Fitted> {
if x.nrows() == 0 || x.ncols() == 0 {
return Err(SklearsError::InvalidInput("Empty input data".to_string()));
}
if y.is_empty() {
return Err(SklearsError::InvalidInput("No tasks provided".to_string()));
}
let n_samples = x.nrows();
let n_features = x.ncols();
if !self.feature_groups.is_empty() {
for group in &self.feature_groups {
for &feature_idx in group {
if feature_idx >= n_features {
return Err(SklearsError::InvalidInput(format!(
"Feature index {} out of range for {} features",
feature_idx, n_features
)));
}
}
}
}
let mut coefficients = HashMap::new();
let mut intercepts = HashMap::new();
for (task_name, task_targets) in y {
if task_targets.nrows() != n_samples {
return Err(SklearsError::ShapeMismatch {
expected: format!("{}", n_samples),
actual: format!("{}", task_targets.nrows()),
});
}
let n_outputs = task_targets.ncols();
coefficients.insert(
task_name.clone(),
Array2::<Float>::zeros((n_features, n_outputs)),
);
intercepts.insert(task_name.clone(), Array1::<Float>::zeros(n_outputs));
}
for iteration in 0..self.max_iter {
let mut max_change: Float = 0.0;
for (task_name, task_targets) in y {
let task_coefs = coefficients
.get_mut(task_name)
.expect("operation should succeed");
let task_intercepts = intercepts
.get_mut(task_name)
.expect("operation should succeed");
let mut predictions = x.dot(task_coefs);
let intercepts_ref = task_intercepts.view();
for mut row in predictions.rows_mut() {
row += &intercepts_ref;
}
let residuals = &predictions - task_targets;
let grad_coefs = x.t().dot(&residuals) / (n_samples as Float);
let grad_intercepts = residuals
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let old_coefs = task_coefs.clone();
*task_coefs = &*task_coefs - &(self.learning_rate * &grad_coefs);
*task_intercepts = &*task_intercepts - &(self.learning_rate * &grad_intercepts);
if !self.feature_groups.is_empty() {
self.apply_group_lasso_proximal(task_coefs);
}
let old_coefs_flat: Vec<f64> = old_coefs.iter().cloned().collect();
let new_coefs_flat: Vec<f64> = task_coefs.iter().cloned().collect();
let coef_change = simd_ops::simd_max_change(&old_coefs_flat, &new_coefs_flat);
max_change = max_change.max(coef_change);
}
if max_change < self.tolerance {
return Ok(GroupLassoTrained {
coefficients,
intercepts,
n_features,
task_outputs: self.task_outputs,
feature_groups: self.feature_groups,
n_iter: iteration + 1,
alpha: self.alpha,
});
}
}
Ok(GroupLassoTrained {
coefficients,
intercepts,
n_features,
task_outputs: self.task_outputs,
feature_groups: self.feature_groups,
n_iter: self.max_iter,
alpha: self.alpha,
})
}
}
impl GroupLasso<Untrained> {
fn apply_group_lasso_proximal(&self, coefficients: &mut Array2<Float>) {
for group in &self.feature_groups {
for &feature_idx in group {
if feature_idx < coefficients.nrows() {
let mut feature_coefs = coefficients.row_mut(feature_idx);
let coef_slice = feature_coefs
.as_slice()
.expect("slice operation should succeed");
let group_norm = simd_ops::simd_l2_norm(coef_slice);
if group_norm > 0.0 {
let shrinkage =
(1.0 - self.alpha * self.learning_rate / group_norm).max(0.0);
feature_coefs *= shrinkage;
}
}
}
}
}
}
impl Predict<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>> for GroupLassoTrained {
fn predict(&self, x: &ArrayView2<Float>) -> SklResult<HashMap<String, Array2<Float>>> {
if x.ncols() != self.n_features {
return Err(SklearsError::ShapeMismatch {
expected: format!("{}", self.n_features),
actual: format!("{}", x.ncols()),
});
}
let mut predictions = HashMap::new();
for (task_name, task_coefs) in &self.coefficients {
let task_intercepts = &self.intercepts[task_name];
let task_pred = x.dot(task_coefs) + task_intercepts;
predictions.insert(task_name.clone(), task_pred);
}
Ok(predictions)
}
}
impl GroupLassoTrained {
pub fn group_sparsity(&self) -> Float {
if self.feature_groups.is_empty() {
return 0.0;
}
let mut zero_groups = 0;
let total_groups = self.feature_groups.len();
for group in &self.feature_groups {
let mut group_is_zero = true;
for task_coefs in self.coefficients.values() {
for &feature_idx in group {
if feature_idx < task_coefs.nrows() {
let coef_row = task_coefs.row(feature_idx);
for &coef in coef_row {
if coef.abs() > 1e-8 {
group_is_zero = false;
break;
}
}
if !group_is_zero {
break;
}
}
}
if !group_is_zero {
break;
}
}
if group_is_zero {
zero_groups += 1;
}
}
zero_groups as Float / total_groups as Float
}
pub fn task_coefficients(&self, task_name: &str) -> Option<&Array2<Float>> {
self.coefficients.get(task_name)
}
pub fn task_intercepts(&self, task_name: &str) -> Option<&Array1<Float>> {
self.intercepts.get(task_name)
}
pub fn n_iter(&self) -> usize {
self.n_iter
}
}