1use crate::error::{RillError, checked_finite_add, checked_increment, ensure_finite};
7
8#[derive(Debug, Clone)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct AdaGradConfig {
12 pub learning_rate: f64,
14 pub l2: f64,
16 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#[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 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 pub const fn param_count(&self) -> usize {
82 self.feature_count + 1
83 }
84
85 pub const fn samples_seen(&self) -> u64 {
87 self.samples_seen
88 }
89
90 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 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 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 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}