use crate::error::{OptimError, Result};
use crate::optimizers::Optimizer;
use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
use scirs2_core::numeric::Float;
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct LARS<A: Float> {
learning_rate: A,
momentum: A,
weight_decay: A,
trust_coefficient: A,
eps: A,
exclude_bias_and_norm: bool,
velocity: Option<Vec<Vec<A>>>,
}
impl<A: Float + ScalarOperand + Debug + Send + Sync> LARS<A> {
pub fn new(learning_rate: A) -> Self {
Self {
learning_rate,
momentum: A::from(0.9).expect("LARS: default momentum (0.9) must fit in A"),
weight_decay: A::from(0.0001)
.expect("LARS: default weight_decay (0.0001) must fit in A"),
trust_coefficient: A::from(0.001)
.expect("LARS: default trust_coefficient (0.001) must fit in A"),
eps: A::from(1e-8).expect("LARS: default eps (1e-8) must fit in A"),
exclude_bias_and_norm: true,
velocity: None,
}
}
pub fn with_momentum(mut self, momentum: A) -> Self {
self.momentum = momentum;
self
}
pub fn with_weight_decay(mut self, weight_decay: A) -> Self {
self.weight_decay = weight_decay;
self
}
pub fn with_trust_coefficient(mut self, trust_coefficient: A) -> Self {
self.trust_coefficient = trust_coefficient;
self
}
pub fn with_eps(mut self, eps: A) -> Self {
self.eps = eps;
self
}
pub fn with_exclude_bias_and_norm(mut self, exclude_bias_and_norm: bool) -> Self {
self.exclude_bias_and_norm = exclude_bias_and_norm;
self
}
pub fn reset(&mut self) {
self.velocity = None;
}
fn ensure_state(&mut self, index: usize, len: usize) {
let velocity = self.velocity.get_or_insert_with(Vec::new);
while velocity.len() <= index {
velocity.push(vec![A::zero(); len]);
}
if velocity[index].len() != len {
velocity[index] = vec![A::zero(); len];
}
}
pub fn step_indexed<D: Dimension>(
&mut self,
index: usize,
params: &Array<A, D>,
gradients: &Array<A, D>,
) -> Result<Array<A, D>> {
if params.shape() != gradients.shape() {
return Err(OptimError::DimensionMismatch(format!(
"Incompatible shapes: parameters have shape {:?}, gradients have shape {:?}",
params.shape(),
gradients.shape()
)));
}
let is_bias_or_norm = params.ndim() <= 1;
let n_params = gradients.len();
self.ensure_state(index, n_params);
let weight_norm = params.mapv(|x| x * x).sum().sqrt();
let grad_norm = gradients.mapv(|x| x * x).sum().sqrt();
let should_apply_lars = !(self.exclude_bias_and_norm && is_bias_or_norm);
let local_lr = if should_apply_lars && weight_norm > A::zero() && grad_norm > A::zero() {
self.trust_coefficient * weight_norm
/ (grad_norm + self.weight_decay * weight_norm + self.eps)
} else {
A::one()
};
let scaled_lr = self.learning_rate * local_lr;
let momentum = self.momentum;
let weight_decay = self.weight_decay;
let use_weight_decay = weight_decay > A::zero();
let velocity = self
.velocity
.as_mut()
.ok_or_else(|| OptimError::InvalidConfig("LARS state not initialized".to_string()))?;
let buffer = velocity.get_mut(index).ok_or_else(|| {
OptimError::InvalidConfig(format!("LARS has no velocity buffer for index {}", index))
})?;
let mut updated_params = params.clone();
for (slot, (p, g)) in buffer
.iter_mut()
.zip(updated_params.iter_mut().zip(gradients.iter()))
{
let grad = if use_weight_decay {
*g + weight_decay * *p
} else {
*g
};
*slot = momentum * *slot + grad * scaled_lr;
*p = *p - *slot;
}
Ok(updated_params)
}
}
impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync> Optimizer<A, D>
for LARS<A>
{
fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
self.step_indexed(0, params, gradients)
}
fn step_list(
&mut self,
params_list: &[&Array<A, D>],
gradients_list: &[&Array<A, D>],
) -> Result<Vec<Array<A, D>>> {
if params_list.len() != gradients_list.len() {
return Err(OptimError::InvalidConfig(format!(
"Number of parameter arrays ({}) does not match number of gradient arrays ({})",
params_list.len(),
gradients_list.len()
)));
}
let mut results = Vec::with_capacity(params_list.len());
for (index, (params, grads)) in params_list.iter().zip(gradients_list.iter()).enumerate() {
results.push(self.step_indexed(index, params, grads)?);
}
Ok(results)
}
fn set_learning_rate(&mut self, learning_rate: A) {
self.learning_rate = learning_rate;
}
fn get_learning_rate(&self) -> A {
self.learning_rate
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::Array1;
#[test]
fn test_lars_creation() {
let optimizer = LARS::new(0.01);
assert_abs_diff_eq!(optimizer.learning_rate, 0.01);
assert_abs_diff_eq!(optimizer.momentum, 0.9);
assert_abs_diff_eq!(optimizer.weight_decay, 0.0001);
assert_abs_diff_eq!(optimizer.trust_coefficient, 0.001);
assert_abs_diff_eq!(optimizer.eps, 1e-8);
assert!(optimizer.exclude_bias_and_norm);
}
#[test]
fn test_lars_builder() {
let optimizer = LARS::new(0.01)
.with_momentum(0.95)
.with_weight_decay(0.0005)
.with_trust_coefficient(0.01)
.with_eps(1e-6)
.with_exclude_bias_and_norm(false);
assert_abs_diff_eq!(optimizer.momentum, 0.95);
assert_abs_diff_eq!(optimizer.weight_decay, 0.0005);
assert_abs_diff_eq!(optimizer.trust_coefficient, 0.01);
assert_abs_diff_eq!(optimizer.eps, 1e-6);
assert!(!optimizer.exclude_bias_and_norm);
}
#[test]
fn test_lars_update() {
let mut optimizer = LARS::new(0.1)
.with_momentum(0.9)
.with_weight_decay(0.0)
.with_trust_coefficient(1.0)
.with_exclude_bias_and_norm(false);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
let updated_params = optimizer
.step(¶ms, &gradients)
.expect("optimizer.step succeeds in test_lars_update");
let weight_norm = params.mapv(|x| x * x).sum().sqrt();
let grad_norm = gradients.mapv(|x| x * x).sum().sqrt();
let scale = weight_norm / grad_norm;
assert_abs_diff_eq!(updated_params[0], 1.0 - 0.1 * scale * 0.1, epsilon = 1e-5);
assert_abs_diff_eq!(updated_params[1], 2.0 - 0.1 * scale * 0.2, epsilon = 1e-5);
assert_abs_diff_eq!(updated_params[2], 3.0 - 0.1 * scale * 0.3, epsilon = 1e-5);
let updated_params2 = optimizer
.step(&updated_params, &gradients)
.expect("step succeeds in test_lars_update");
assert!(updated_params2[0] < updated_params[0]);
assert!(updated_params2[1] < updated_params[1]);
assert!(updated_params2[2] < updated_params[2]);
}
#[test]
fn test_lars_weight_decay() {
let mut optimizer = LARS::new(0.01)
.with_momentum(0.0) .with_weight_decay(0.1)
.with_trust_coefficient(1.0)
.with_exclude_bias_and_norm(false);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
let updated_params = optimizer
.step(¶ms, &gradients)
.expect("optimizer.step succeeds in test_lars_weight_decay");
let weight_norm = params.mapv(|x| x * x).sum().sqrt();
let grad_norm = gradients.mapv(|x| x * x).sum().sqrt();
let expected_scale = weight_norm / (grad_norm + 0.1 * weight_norm);
let expected_p0 = 1.0 - 0.01 * expected_scale * (0.1 + 0.1 * 1.0);
let expected_p1 = 2.0 - 0.01 * expected_scale * (0.2 + 0.1 * 2.0);
let expected_p2 = 3.0 - 0.01 * expected_scale * (0.3 + 0.1 * 3.0);
assert_abs_diff_eq!(updated_params[0], expected_p0, epsilon = 1e-5);
assert_abs_diff_eq!(updated_params[1], expected_p1, epsilon = 1e-5);
assert_abs_diff_eq!(updated_params[2], expected_p2, epsilon = 1e-5);
}
#[test]
fn test_zero_gradients() {
let mut optimizer = LARS::new(0.01);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let zero_gradients = Array1::zeros(3);
let updated_params = optimizer
.step(¶ms, &zero_gradients)
.expect("step succeeds in test_zero_gradients");
assert_abs_diff_eq!(updated_params[0], params[0], epsilon = 1e-3);
assert_abs_diff_eq!(updated_params[1], params[1], epsilon = 1e-3);
assert_abs_diff_eq!(updated_params[2], params[2], epsilon = 1e-3);
}
#[test]
fn test_exclude_bias_and_norm() {
let mut optimizer_excluded = LARS::new(0.01)
.with_momentum(0.0)
.with_weight_decay(0.0)
.with_exclude_bias_and_norm(true);
let mut optimizer_included = LARS::new(0.01)
.with_momentum(0.0)
.with_weight_decay(0.0)
.with_exclude_bias_and_norm(false);
let bias_params = Array1::from_vec(vec![0.1, 0.2]);
let bias_grads = Array1::from_vec(vec![0.01, 0.02]);
let updated_excluded = optimizer_excluded
.step(&bias_params, &bias_grads)
.expect("step succeeds in test_exclude_bias_and_norm");
let updated_included = optimizer_included
.step(&bias_params, &bias_grads)
.expect("step succeeds in test_exclude_bias_and_norm");
assert_abs_diff_eq!(updated_excluded[0], 0.1 - 0.01 * 0.01, epsilon = 1e-4);
let weight_norm = (0.1f64.powi(2) + 0.2f64.powi(2)).sqrt();
let grad_norm = (0.01f64.powi(2) + 0.02f64.powi(2)).sqrt();
let expected_factor = 0.001 * weight_norm / grad_norm;
assert_abs_diff_eq!(
updated_included[0],
0.1 - 0.01 * expected_factor * 0.01,
epsilon = 1e-5
);
}
#[test]
fn test_exclude_bias_and_norm_is_decided_by_rank() {
use scirs2_core::ndarray::Array2;
let mut bias_opt = LARS::new(0.01)
.with_momentum(0.0)
.with_weight_decay(0.0)
.with_trust_coefficient(1.0)
.with_exclude_bias_and_norm(true);
let bias = Array1::from_vec(vec![1.0f64, 2.0, 3.0]);
let bias_grads = Array1::from_vec(vec![0.1f64, 0.2, 0.3]);
let updated_bias = bias_opt.step(&bias, &bias_grads).expect("bias step");
assert_abs_diff_eq!(updated_bias[0], 1.0 - 0.01 * 0.1, epsilon = 1e-12);
assert_abs_diff_eq!(updated_bias[2], 3.0 - 0.01 * 0.3, epsilon = 1e-12);
let mut weight_opt = LARS::new(0.01)
.with_momentum(0.0)
.with_weight_decay(0.0)
.with_trust_coefficient(1.0)
.with_exclude_bias_and_norm(true);
let weights =
Array2::from_shape_vec((3, 1), vec![1.0f64, 2.0, 3.0]).expect("valid 3x1 matrix");
let weight_grads =
Array2::from_shape_vec((3, 1), vec![0.1f64, 0.2, 0.3]).expect("valid 3x1 matrix");
let updated_weights = weight_opt
.step(&weights, &weight_grads)
.expect("weight step");
let weight_norm = weights.mapv(|x: f64| x * x).sum().sqrt();
let grad_norm = weight_grads.mapv(|x: f64| x * x).sum().sqrt();
let scale = weight_norm / (grad_norm + 1e-8);
assert_abs_diff_eq!(
updated_weights[[0, 0]],
1.0 - 0.01 * scale * 0.1,
epsilon = 1e-8
);
assert!((updated_bias[0] - updated_weights[[0, 0]]).abs() > 1e-6);
}
}