Skip to main content

optirs_core/optimizers/
adagrad.rs

1// Adagrad optimizer implementation
2
3use scirs2_core::ndarray::{Array, Dimension, IxDyn, ScalarOperand, Zip};
4use scirs2_core::numeric::Float;
5use std::fmt::Debug;
6
7use crate::error::{OptimError, Result};
8use crate::optimizers::Optimizer;
9
10/// Adagrad optimizer
11///
12/// Implements the Adagrad optimization algorithm from the paper:
13/// "Adaptive Subgradient Methods for Online Learning and Stochastic Optimization" by Duchi et al. (2011)
14///
15/// Adagrad adapts the learning rate to the parameters, performing larger updates for
16/// infrequently updated parameters and smaller updates for frequently updated parameters.
17///
18/// Formula:
19/// G_t = G_{t-1} + g_t^2
20/// param_t = param_{t-1} - learning_rate * g_t / (sqrt(G_t) + epsilon)
21///
22/// # Examples
23///
24/// ```
25/// use scirs2_core::ndarray::Array1;
26/// use optirs_core::optimizers::{Adagrad, Optimizer};
27///
28/// // Initialize parameters and gradients
29/// let params = Array1::zeros(5);
30/// let gradients = Array1::from_vec(vec![0.1, 0.2, -0.3, 0.0, 0.5]);
31///
32/// // Create an Adagrad optimizer with learning rate 0.01
33/// let mut optimizer = Adagrad::new(0.01);
34///
35/// // Update parameters
36/// let new_params = optimizer.step(&params, &gradients).expect("optimizer.step succeeds");
37/// ```
38#[derive(Debug, Clone)]
39pub struct Adagrad<A: Float + ScalarOperand + Debug> {
40    /// Learning rate
41    learning_rate: A,
42    /// Small constant for numerical stability
43    epsilon: A,
44    /// Weight decay factor (L2 regularization)
45    weight_decay: A,
46    /// Sum of squared gradients, one slot per parameter-tensor index
47    sum_squared_grads: Option<Vec<Array<A, IxDyn>>>,
48}
49
50impl<A: Float + ScalarOperand + Debug + Send + Sync> Adagrad<A> {
51    /// Creates a new Adagrad optimizer with the given learning rate and default settings
52    ///
53    /// # Arguments
54    ///
55    /// * `learning_rate` - The learning rate for parameter updates
56    pub fn new(learning_rate: A) -> Self {
57        Self {
58            learning_rate,
59            epsilon: A::from(1e-10).expect("Adagrad: default epsilon (1e-10) must fit in A"),
60            weight_decay: A::zero(),
61            sum_squared_grads: None,
62        }
63    }
64
65    /// Creates a new Adagrad optimizer with the full configuration
66    ///
67    /// # Arguments
68    ///
69    /// * `learning_rate` - The learning rate for parameter updates
70    /// * `epsilon` - Small constant for numerical stability (default: 1e-10)
71    /// * `weight_decay` - Weight decay factor for L2 regularization (default: 0.0)
72    pub fn new_with_config(learning_rate: A, epsilon: A, weight_decay: A) -> Self {
73        Self {
74            learning_rate,
75            epsilon,
76            weight_decay,
77            sum_squared_grads: None,
78        }
79    }
80
81    /// Sets the epsilon parameter
82    pub fn set_epsilon(&mut self, epsilon: A) -> &mut Self {
83        self.epsilon = epsilon;
84        self
85    }
86
87    /// Gets the epsilon parameter
88    pub fn get_epsilon(&self) -> A {
89        self.epsilon
90    }
91
92    /// Sets the weight decay parameter
93    pub fn set_weight_decay(&mut self, weight_decay: A) -> &mut Self {
94        self.weight_decay = weight_decay;
95        self
96    }
97
98    /// Gets the weight decay parameter
99    pub fn get_weight_decay(&self) -> A {
100        self.weight_decay
101    }
102
103    /// Resets the internal state of the optimizer
104    pub fn reset(&mut self) {
105        self.sum_squared_grads = None;
106    }
107
108    /// Ensures an accumulator slot exists for `index` and matches `dim`
109    fn ensure_state(&mut self, index: usize, dim: &IxDyn) {
110        let accumulators = self.sum_squared_grads.get_or_insert_with(Vec::new);
111        while accumulators.len() <= index {
112            accumulators.push(Array::zeros(dim.clone()));
113        }
114        if accumulators[index].raw_dim() != *dim {
115            accumulators[index] = Array::zeros(dim.clone());
116        }
117    }
118
119    /// Performs an Adagrad update for the parameter tensor at `index`
120    ///
121    /// Each `index` owns an independent accumulator, so several parameter tensors can
122    /// be optimized by a single `Adagrad` instance without their histories mixing.
123    pub fn step_indexed<D: Dimension>(
124        &mut self,
125        index: usize,
126        params: &Array<A, D>,
127        gradients: &Array<A, D>,
128    ) -> Result<Array<A, D>> {
129        if params.shape() != gradients.shape() {
130            return Err(OptimError::DimensionMismatch(format!(
131                "Incompatible shapes: parameters have shape {:?}, gradients have shape {:?}",
132                params.shape(),
133                gradients.shape()
134            )));
135        }
136
137        let dim = params.raw_dim().into_dyn();
138        self.ensure_state(index, &dim);
139
140        let lr = self.learning_rate;
141        let eps = self.epsilon;
142        let weight_decay = self.weight_decay;
143        let use_weight_decay = weight_decay > A::zero();
144
145        let accumulators = self.sum_squared_grads.as_mut().ok_or_else(|| {
146            OptimError::InvalidConfig("Adagrad state not initialized".to_string())
147        })?;
148
149        let mut updated = params.to_owned();
150        let mut params_view = updated.view_mut().into_dyn();
151        let gradients_view = gradients.view().into_dyn();
152
153        Zip::from(&mut params_view)
154            .and(&gradients_view)
155            .and(&mut accumulators[index])
156            .for_each(|p, &g, acc| {
157                let grad = if use_weight_decay {
158                    g + weight_decay * *p
159                } else {
160                    g
161                };
162                *acc = *acc + grad * grad;
163                *p = *p - lr * grad / (acc.sqrt() + eps);
164            });
165        drop(params_view);
166
167        Ok(updated)
168    }
169}
170
171impl<A, D> Optimizer<A, D> for Adagrad<A>
172where
173    A: Float + ScalarOperand + Debug + Send + Sync,
174    D: Dimension,
175{
176    fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
177        self.step_indexed(0, params, gradients)
178    }
179
180    fn step_list(
181        &mut self,
182        params_list: &[&Array<A, D>],
183        gradients_list: &[&Array<A, D>],
184    ) -> Result<Vec<Array<A, D>>> {
185        if params_list.len() != gradients_list.len() {
186            return Err(OptimError::InvalidConfig(format!(
187                "Number of parameter arrays ({}) does not match number of gradient arrays ({})",
188                params_list.len(),
189                gradients_list.len()
190            )));
191        }
192
193        let mut results = Vec::with_capacity(params_list.len());
194        for (index, (params, grads)) in params_list.iter().zip(gradients_list.iter()).enumerate() {
195            results.push(self.step_indexed(index, params, grads)?);
196        }
197        Ok(results)
198    }
199
200    fn get_learning_rate(&self) -> A {
201        self.learning_rate
202    }
203
204    fn set_learning_rate(&mut self, learning_rate: A) {
205        self.learning_rate = learning_rate;
206    }
207}