Skip to main content

rill_ml/optim/
adagrad.rs

1//! AdaGrad optimizer.
2//!
3//! Maintains a per-parameter sum of squared gradients and scales the learning
4//! rate accordingly. Time complexity per step: `O(d)`. Space: `O(d)`.
5
6use crate::error::{RillError, checked_finite_add, checked_increment, ensure_finite};
7
8/// Configuration for [`AdaGrad`].
9#[derive(Debug, Clone)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct AdaGradConfig {
12    /// Global learning rate. Must be finite and strictly positive.
13    pub learning_rate: f64,
14    /// L2 regularization strength.
15    pub l2: f64,
16    /// Small constant added to the denominator for numerical stability.
17    pub epsilon: f64,
18}
19
20impl Default for AdaGradConfig {
21    fn default() -> Self {
22        Self {
23            learning_rate: 0.1,
24            l2: 0.0,
25            epsilon: 1e-8,
26        }
27    }
28}
29
30/// AdaGrad optimizer.
31///
32/// Update rule:
33/// ```text
34/// g2_i += grad_i^2
35/// w_i -= lr / sqrt(g2_i + epsilon) * (grad_i + l2 * w_i)
36/// ```
37#[derive(Debug, Clone)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct AdaGrad {
40    feature_count: usize,
41    config: AdaGradConfig,
42    grad_sq_weights: Vec<f64>,
43    grad_sq_intercept: f64,
44    samples_seen: u64,
45}
46
47impl AdaGrad {
48    /// Create a new AdaGrad optimizer.
49    pub fn new(feature_count: usize, config: AdaGradConfig) -> Result<Self, RillError> {
50        if feature_count == 0 {
51            return Err(RillError::EmptyFeatures);
52        }
53        ensure_finite("learning_rate", config.learning_rate)?;
54        ensure_finite("l2", config.l2)?;
55        ensure_finite("epsilon", config.epsilon)?;
56        if config.learning_rate <= 0.0 {
57            return Err(RillError::InvalidLearningRate(config.learning_rate));
58        }
59        if config.l2 < 0.0 {
60            return Err(RillError::InvalidParameter {
61                name: "l2",
62                value: config.l2,
63            });
64        }
65        if config.epsilon <= 0.0 {
66            return Err(RillError::InvalidParameter {
67                name: "epsilon",
68                value: config.epsilon,
69            });
70        }
71        Ok(Self {
72            feature_count,
73            config,
74            grad_sq_weights: vec![0.0; feature_count],
75            grad_sq_intercept: 0.0,
76            samples_seen: 0,
77        })
78    }
79
80    /// Number of parameters (features + intercept).
81    pub const fn param_count(&self) -> usize {
82        self.feature_count + 1
83    }
84
85    /// Number of samples processed.
86    pub const fn samples_seen(&self) -> u64 {
87        self.samples_seen
88    }
89
90    /// Apply one gradient step.
91    pub fn step(
92        &mut self,
93        weights: &mut [f64],
94        intercept: &mut f64,
95        grad_weights: &[f64],
96        grad_intercept: f64,
97    ) -> Result<(), RillError> {
98        if weights.len() != self.feature_count {
99            return Err(RillError::DimensionMismatch {
100                expected: self.feature_count,
101                actual: weights.len(),
102            });
103        }
104        if grad_weights.len() != self.feature_count {
105            return Err(RillError::DimensionMismatch {
106                expected: self.feature_count,
107                actual: grad_weights.len(),
108            });
109        }
110        for &gradient in grad_weights {
111            ensure_finite("grad_weight", gradient)?;
112        }
113        ensure_finite("grad_intercept", grad_intercept)?;
114        let next_samples = checked_increment(self.samples_seen, "AdaGrad sample")?;
115        let lr = self.config.learning_rate;
116        let l2 = self.config.l2;
117        let eps = self.config.epsilon;
118
119        let mut next_grad_sq_weights = Vec::with_capacity(self.feature_count);
120        let mut next_weights = Vec::with_capacity(self.feature_count);
121        for (i, (&weight, &gradient)) in weights.iter().zip(grad_weights).enumerate() {
122            let squared_gradient = gradient * gradient;
123            ensure_finite("squared gradient", squared_gradient)?;
124            let accumulator = checked_finite_add(
125                self.grad_sq_weights[i],
126                squared_gradient,
127                "AdaGrad accumulator",
128            )?;
129            let scale = (accumulator + eps).sqrt();
130            ensure_finite("AdaGrad scale", scale)?;
131            let regularized_gradient = gradient + l2 * weight;
132            ensure_finite("regularized gradient", regularized_gradient)?;
133            let next_weight = weight - lr / scale * regularized_gradient;
134            ensure_finite("weight", next_weight)?;
135            next_grad_sq_weights.push(accumulator);
136            next_weights.push(next_weight);
137        }
138
139        let squared_intercept_gradient = grad_intercept * grad_intercept;
140        ensure_finite("squared intercept gradient", squared_intercept_gradient)?;
141        let next_grad_sq_intercept = checked_finite_add(
142            self.grad_sq_intercept,
143            squared_intercept_gradient,
144            "AdaGrad intercept accumulator",
145        )?;
146        let intercept_scale = (next_grad_sq_intercept + eps).sqrt();
147        ensure_finite("AdaGrad intercept scale", intercept_scale)?;
148        let next_intercept = *intercept - lr / intercept_scale * grad_intercept;
149        ensure_finite("intercept", next_intercept)?;
150
151        self.grad_sq_weights = next_grad_sq_weights;
152        self.grad_sq_intercept = next_grad_sq_intercept;
153        weights.copy_from_slice(&next_weights);
154        *intercept = next_intercept;
155        self.samples_seen = next_samples;
156        Ok(())
157    }
158
159    /// Reset to initial state.
160    pub fn reset(&mut self) {
161        for g in &mut self.grad_sq_weights {
162            *g = 0.0;
163        }
164        self.grad_sq_intercept = 0.0;
165        self.samples_seen = 0;
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn adagrad_decreases_weights() {
175        let mut opt = AdaGrad::new(2, AdaGradConfig::default()).unwrap();
176        let mut w = vec![0.0, 0.0];
177        let mut b = 0.0;
178        opt.step(&mut w, &mut b, &[1.0, 2.0], 1.0).unwrap();
179        // first step: scale = sqrt(g^2 + eps) ≈ |g|
180        // w0 -= 0.1 / 1.0 * 1.0 = -0.1
181        assert!(w[0] < 0.0);
182        assert!(w[1] < 0.0);
183    }
184
185    #[test]
186    fn adagrad_learning_rate_decreases() {
187        let mut opt = AdaGrad::new(
188            1,
189            AdaGradConfig {
190                learning_rate: 1.0,
191                l2: 0.0,
192                epsilon: 1e-12,
193            },
194        )
195        .unwrap();
196        let mut w = vec![0.0];
197        let mut b = 0.0;
198        opt.step(&mut w, &mut b, &[1.0], 0.0).unwrap();
199        let step1 = w[0].abs();
200        opt.step(&mut w, &mut b, &[1.0], 0.0).unwrap();
201        let step2 = w[0].abs() - step1;
202        // second step should be smaller than first
203        assert!(step2 < step1);
204    }
205
206    #[test]
207    fn invalid_config_rejected() {
208        assert!(
209            AdaGrad::new(
210                1,
211                AdaGradConfig {
212                    learning_rate: 0.0,
213                    l2: 0.0,
214                    epsilon: 1e-8
215                }
216            )
217            .is_err()
218        );
219        assert!(
220            AdaGrad::new(
221                1,
222                AdaGradConfig {
223                    learning_rate: 0.1,
224                    l2: -1.0,
225                    epsilon: 1e-8
226                }
227            )
228            .is_err()
229        );
230        assert!(
231            AdaGrad::new(
232                1,
233                AdaGradConfig {
234                    learning_rate: 0.1,
235                    l2: 0.0,
236                    epsilon: 0.0
237                }
238            )
239            .is_err()
240        );
241    }
242
243    #[test]
244    fn failed_step_is_atomic() {
245        let mut opt = AdaGrad::new(2, AdaGradConfig::default()).unwrap();
246        let mut weights = vec![1.0, 2.0];
247        let mut intercept = 3.0;
248        let result = opt.step(&mut weights, &mut intercept, &[1.0, f64::MAX], 1.0);
249        assert!(result.is_err());
250        assert_eq!(weights, vec![1.0, 2.0]);
251        assert_eq!(intercept, 3.0);
252        assert_eq!(opt.samples_seen(), 0);
253        assert_eq!(opt.grad_sq_weights, vec![0.0, 0.0]);
254    }
255}