geographdb_core/algorithms/
adam.rs1pub const DEFAULT_CLIP_NORM: f32 = 1.0;
10
11#[derive(Debug, Clone)]
16pub struct Adam {
17 learning_rate: f32,
18 beta1: f32,
19 beta2: f32,
20 eps: f32,
21 t: u64,
22 m: Vec<f32>,
23 v: Vec<f32>,
24}
25
26impl Adam {
27 pub fn new(param_count: usize) -> Self {
32 Self::with_lr(0.001, param_count)
33 }
34
35 pub fn with_lr(learning_rate: f32, param_count: usize) -> Self {
37 Self::with_hyperparams(learning_rate, 0.9, 0.999, 1e-8, param_count)
38 }
39
40 pub fn with_hyperparams(
42 learning_rate: f32,
43 beta1: f32,
44 beta2: f32,
45 eps: f32,
46 param_count: usize,
47 ) -> Self {
48 assert!(learning_rate > 0.0, "Adam: learning_rate must be positive");
49 assert!(beta1 > 0.0 && beta1 < 1.0, "Adam: beta1 must be in (0, 1)");
50 assert!(beta2 > 0.0 && beta2 < 1.0, "Adam: beta2 must be in (0, 1)");
51 assert!(eps > 0.0, "Adam: eps must be positive");
52
53 Self {
54 learning_rate,
55 beta1,
56 beta2,
57 eps,
58 t: 0,
59 m: vec![0.0f32; param_count],
60 v: vec![0.0f32; param_count],
61 }
62 }
63
64 pub fn param_count(&self) -> usize {
66 self.m.len()
67 }
68
69 pub fn timestep(&self) -> u64 {
71 self.t
72 }
73
74 pub fn step(&mut self, params: &mut [f32], grad: &[f32]) {
79 assert_eq!(
80 params.len(),
81 self.m.len(),
82 "Adam::step: parameter count mismatch"
83 );
84 assert_eq!(
85 grad.len(),
86 self.m.len(),
87 "Adam::step: gradient count mismatch"
88 );
89
90 self.t += 1;
91 let t = self.t as f32;
92 let lr = self.learning_rate;
93 let beta1 = self.beta1;
94 let beta2 = self.beta2;
95 let eps = self.eps;
96 let bias_correction1 = 1.0 - beta1.powf(t);
97 let bias_correction2 = 1.0 - beta2.powf(t);
98
99 for i in 0..params.len() {
100 let g = grad[i];
101 self.m[i] = beta1 * self.m[i] + (1.0 - beta1) * g;
102 self.v[i] = beta2 * self.v[i] + (1.0 - beta2) * g * g;
103
104 let m_hat = self.m[i] / bias_correction1;
105 let v_hat = self.v[i] / bias_correction2;
106 params[i] -= lr * m_hat / (v_hat.sqrt() + eps);
107 }
108 }
109
110 pub fn step_clipped(&mut self, params: &mut [f32], grad: &[f32], max_norm: f32) {
116 assert!(
117 max_norm > 0.0,
118 "Adam::step_clipped: max_norm must be positive"
119 );
120 let mut clipped = grad.to_vec();
121 clip_gradients(&mut clipped, max_norm);
122 self.step(params, &clipped);
123 }
124
125 pub fn step_slices(&mut self, params: &mut [&mut [f32]], grads: &[&[f32]]) {
131 assert_eq!(
132 params.len(),
133 grads.len(),
134 "Adam::step_slices: parameter and gradient slice counts mismatch"
135 );
136
137 let total_len: usize = params.iter().map(|p| p.len()).sum();
138 assert_eq!(
139 total_len,
140 self.m.len(),
141 "Adam::step_slices: total parameter count mismatch"
142 );
143
144 self.t += 1;
145 let t = self.t as f32;
146 let lr = self.learning_rate;
147 let beta1 = self.beta1;
148 let beta2 = self.beta2;
149 let eps = self.eps;
150 let bias_correction1 = 1.0 - beta1.powf(t);
151 let bias_correction2 = 1.0 - beta2.powf(t);
152
153 let mut mom_off = 0;
154 for (param_slice, grad_slice) in params.iter_mut().zip(grads.iter()) {
155 assert_eq!(
156 param_slice.len(),
157 grad_slice.len(),
158 "Adam::step_slices: slice length mismatch"
159 );
160 for i in 0..param_slice.len() {
161 let g = grad_slice[i];
162 self.m[mom_off] = beta1 * self.m[mom_off] + (1.0 - beta1) * g;
163 self.v[mom_off] = beta2 * self.v[mom_off] + (1.0 - beta2) * g * g;
164
165 let m_hat = self.m[mom_off] / bias_correction1;
166 let v_hat = self.v[mom_off] / bias_correction2;
167 param_slice[i] -= lr * m_hat / (v_hat.sqrt() + eps);
168
169 mom_off += 1;
170 }
171 }
172 }
173
174 pub fn step_slices_clipped(
176 &mut self,
177 params: &mut [&mut [f32]],
178 grads: &[&[f32]],
179 max_norm: f32,
180 ) {
181 assert!(
182 max_norm > 0.0,
183 "Adam::step_slices_clipped: max_norm must be positive"
184 );
185
186 let total_len: usize = params.iter().map(|p| p.len()).sum();
187 let mut clipped = Vec::with_capacity(total_len);
188 for grad_slice in grads {
189 clipped.extend_from_slice(grad_slice);
190 }
191 clip_gradients(&mut clipped, max_norm);
192
193 let mut grad_slices: Vec<&[f32]> = Vec::with_capacity(grads.len());
194 let mut off = 0;
195 for grad_slice in grads {
196 let len = grad_slice.len();
197 grad_slices.push(&clipped[off..off + len]);
198 off += len;
199 }
200
201 self.step_slices(params, &grad_slices);
202 }
203
204 pub fn reset(&mut self) {
206 self.t = 0;
207 self.m.fill(0.0);
208 self.v.fill(0.0);
209 }
210}
211
212pub fn clip_gradients(grad: &mut [f32], max_norm: f32) {
217 assert!(max_norm > 0.0, "clip_gradients: max_norm must be positive");
218 let norm_sq: f32 = grad.iter().map(|g| g * g).sum();
219 let norm = norm_sq.sqrt();
220 if norm > max_norm {
221 let scale = max_norm / norm;
222 for g in grad.iter_mut() {
223 *g *= scale;
224 }
225 }
226}
227
228pub fn grad_norm(grad: &[f32]) -> f32 {
230 grad.iter().map(|g| g * g).sum::<f32>().sqrt()
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn adam_decreases_simple_quadratic() {
239 let mut x = [0.0f32];
241 let mut opt = Adam::with_lr(0.1, 1);
242 for _ in 0..200 {
243 let grad = [2.0 * (x[0] - 3.0)];
244 opt.step(&mut x, &grad);
245 }
246 assert!(
247 (x[0] - 3.0).abs() < 0.01,
248 "x converged to {}, expected 3.0",
249 x[0]
250 );
251 }
252
253 #[test]
254 fn clipped_step_respects_norm() {
255 let mut params = [0.0f32, 0.0f32];
256 let grad = [10.0f32, 0.0f32];
257 let mut opt = Adam::with_lr(0.1, 2);
258 opt.step_clipped(&mut params, &grad, 1.0);
259 assert!(params[0] < 0.0);
263 assert!(
264 params[0].abs() < 0.5,
265 "clipped step moved too far: {}",
266 params[0]
267 );
268 }
269
270 #[test]
271 fn clip_gradients_scales_large_norm() {
272 let mut g = vec![3.0f32, 4.0f32];
273 clip_gradients(&mut g, 1.0);
274 assert!(
275 (grad_norm(&g) - 1.0).abs() < 1e-5,
276 "norm after clipping should be 1.0"
277 );
278 assert!((g[0] - 0.6).abs() < 1e-5);
279 assert!((g[1] - 0.8).abs() < 1e-5);
280 }
281
282 #[test]
283 fn reset_clears_momentum() {
284 let mut opt = Adam::with_lr(0.1, 2);
285 let mut params = [1.0f32, 1.0f32];
286 opt.step(&mut params, &[1.0f32, 1.0f32]);
287 assert!(opt.m.iter().any(|&v| v != 0.0));
288 opt.reset();
289 assert!(opt.m.iter().all(|&v| v == 0.0));
290 assert!(opt.v.iter().all(|&v| v == 0.0));
291 assert_eq!(opt.timestep(), 0);
292 }
293
294 #[test]
295 fn step_slices_matches_flat_step() {
296 let mut opt = Adam::with_lr(0.1, 4);
297
298 let mut p_flat = [1.0f32, 2.0, 3.0, 4.0];
299 let g_flat = [0.1f32, 0.2, 0.3, 0.4];
300 opt.step(&mut p_flat, &g_flat);
301
302 let mut opt2 = Adam::with_lr(0.1, 4);
303 let mut a = [1.0f32, 2.0];
304 let mut b = [3.0f32, 4.0];
305 let g_a = [0.1f32, 0.2];
306 let g_b = [0.3f32, 0.4];
307 opt2.step_slices(&mut [&mut a, &mut b], &[&g_a, &g_b]);
308
309 let tol = 1e-6;
310 assert!((p_flat[0] - a[0]).abs() < tol);
311 assert!((p_flat[1] - a[1]).abs() < tol);
312 assert!((p_flat[2] - b[0]).abs() < tol);
313 assert!((p_flat[3] - b[1]).abs() < tol);
314 }
315
316 #[test]
317 fn step_slices_clipped_matches_flat_clipped() {
318 let mut opt = Adam::with_lr(0.1, 2);
319 let mut p_flat = [1.0f32, 1.0];
320 let g_flat = [10.0f32, 0.0];
321 opt.step_clipped(&mut p_flat, &g_flat, 1.0);
322
323 let mut opt2 = Adam::with_lr(0.1, 2);
324 let mut a = [1.0f32];
325 let mut b = [1.0f32];
326 let g_a = [10.0f32];
327 let g_b = [0.0f32];
328 opt2.step_slices_clipped(&mut [&mut a, &mut b], &[&g_a, &g_b], 1.0);
329
330 let tol = 1e-6;
331 assert!((p_flat[0] - a[0]).abs() < tol);
332 assert!((p_flat[1] - b[0]).abs() < tol);
333 }
334}