use scirs2_core::ndarray::{Array, Dimension, IxDyn, ScalarOperand, Zip};
use scirs2_core::numeric::Float;
use std::fmt::Debug;
use crate::error::{OptimError, Result};
use crate::optimizers::Optimizer;
#[derive(Debug, Clone)]
pub struct SGD<A: Float + ScalarOperand + Debug> {
learning_rate: A,
momentum: A,
weight_decay: A,
velocity: Option<Vec<Array<A, IxDyn>>>,
}
impl<A: Float + ScalarOperand + Debug + Send + Sync> SGD<A> {
pub fn new(learning_rate: A) -> Self {
Self {
learning_rate,
momentum: A::zero(),
weight_decay: A::zero(),
velocity: None,
}
}
pub fn new_with_config(learning_rate: A, momentum: A, weight_decay: A) -> Self {
Self {
learning_rate,
momentum,
weight_decay,
velocity: None,
}
}
pub fn set_momentum(&mut self, momentum: A) -> &mut Self {
self.momentum = momentum;
self
}
pub fn with_momentum(mut self, momentum: A) -> Self {
self.momentum = momentum;
self
}
pub fn get_momentum(&self) -> A {
self.momentum
}
pub fn learning_rate(&self) -> A {
self.learning_rate
}
pub fn set_weight_decay(&mut self, weight_decay: A) -> &mut Self {
self.weight_decay = weight_decay;
self
}
pub fn with_weight_decay(mut self, weight_decay: A) -> Self {
self.weight_decay = weight_decay;
self
}
pub fn get_weight_decay(&self) -> A {
self.weight_decay
}
pub fn reset(&mut self) {
self.velocity = None;
}
fn ensure_state(&mut self, index: usize, dim: &IxDyn) {
let velocity = self.velocity.get_or_insert_with(Vec::new);
while velocity.len() <= index {
velocity.push(Array::zeros(dim.clone()));
}
if velocity[index].raw_dim() != *dim {
velocity[index] = Array::zeros(dim.clone());
}
}
pub fn step_inplace_indexed<D: Dimension>(
&mut self,
index: usize,
params: &mut Array<A, D>,
gradients: &Array<A, D>,
) -> Result<()> {
if params.shape() != gradients.shape() {
return Err(OptimError::DimensionMismatch(format!(
"Incompatible shapes: parameters have shape {:?}, gradients have shape {:?}",
params.shape(),
gradients.shape()
)));
}
let dim = params.raw_dim().into_dyn();
self.ensure_state(index, &dim);
let momentum = self.momentum;
let lr = self.learning_rate;
let weight_decay = self.weight_decay;
let use_weight_decay = weight_decay > A::zero();
let use_momentum = momentum > A::zero();
let velocity = self
.velocity
.as_mut()
.ok_or_else(|| OptimError::InvalidConfig("SGD state not initialized".to_string()))?;
let mut params_view = params.view_mut().into_dyn();
let gradients_view = gradients.view().into_dyn();
Zip::from(&mut params_view)
.and(&gradients_view)
.and(&mut velocity[index])
.for_each(|p, &g, v| {
let grad = if use_weight_decay {
g + weight_decay * *p
} else {
g
};
*v = if use_momentum {
*v * momentum + grad * lr
} else {
grad * lr
};
*p = *p - *v;
});
Ok(())
}
pub fn step_inplace<D: Dimension>(
&mut self,
params: &mut Array<A, D>,
gradients: &Array<A, D>,
) -> Result<()> {
self.step_inplace_indexed(0, params, gradients)
}
pub fn step_indexed<D: Dimension>(
&mut self,
index: usize,
params: &Array<A, D>,
gradients: &Array<A, D>,
) -> Result<Array<A, D>> {
let mut updated = params.to_owned();
self.step_inplace_indexed(index, &mut updated, gradients)?;
Ok(updated)
}
}
impl<A, D> Optimizer<A, D> for SGD<A>
where
A: Float + ScalarOperand + Debug + Send + Sync,
D: Dimension,
{
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 get_learning_rate(&self) -> A {
self.learning_rate
}
fn set_learning_rate(&mut self, learning_rate: A) {
self.learning_rate = learning_rate;
}
}