1use scirs2_core::ndarray::{Array, Dimension, IxDyn, ScalarOperand, Zip};
7use scirs2_core::numeric::Float;
8use std::fmt::Debug;
9
10use crate::error::{OptimError, Result};
11use crate::optimizers::Optimizer;
12
13#[derive(Debug, Clone)]
47pub struct LAMB<A: Float + ScalarOperand + Debug> {
48 learning_rate: A,
50 beta1: A,
52 beta2: A,
54 epsilon: A,
56 weight_decay: A,
58 bias_correction: bool,
60 m: Option<Vec<Array<A, IxDyn>>>,
62 v: Option<Vec<Array<A, IxDyn>>>,
64 t: Vec<usize>,
66}
67
68impl<A: Float + ScalarOperand + Debug + Send + Sync> LAMB<A> {
69 pub fn new(learning_rate: A) -> Self {
75 Self {
76 learning_rate,
77 beta1: A::from(0.9).expect("LAMB: default beta1 (0.9) must fit in A"),
78 beta2: A::from(0.999).expect("LAMB: default beta2 (0.999) must fit in A"),
79 epsilon: A::from(1e-6).expect("LAMB: default epsilon (1e-6) must fit in A"),
80 weight_decay: A::zero(),
81 bias_correction: true,
82 m: None,
83 v: None,
84 t: Vec::new(),
85 }
86 }
87
88 pub fn new_with_config(
99 learning_rate: A,
100 beta1: A,
101 beta2: A,
102 epsilon: A,
103 weight_decay: A,
104 bias_correction: bool,
105 ) -> Self {
106 Self {
107 learning_rate,
108 beta1,
109 beta2,
110 epsilon,
111 weight_decay,
112 bias_correction,
113 m: None,
114 v: None,
115 t: Vec::new(),
116 }
117 }
118
119 pub fn set_beta1(&mut self, beta1: A) -> &mut Self {
121 self.beta1 = beta1;
122 self
123 }
124
125 pub fn get_beta1(&self) -> A {
127 self.beta1
128 }
129
130 pub fn set_beta2(&mut self, beta2: A) -> &mut Self {
132 self.beta2 = beta2;
133 self
134 }
135
136 pub fn get_beta2(&self) -> A {
138 self.beta2
139 }
140
141 pub fn set_epsilon(&mut self, epsilon: A) -> &mut Self {
143 self.epsilon = epsilon;
144 self
145 }
146
147 pub fn get_epsilon(&self) -> A {
149 self.epsilon
150 }
151
152 pub fn set_weight_decay(&mut self, weight_decay: A) -> &mut Self {
154 self.weight_decay = weight_decay;
155 self
156 }
157
158 pub fn get_weight_decay(&self) -> A {
160 self.weight_decay
161 }
162
163 pub fn learning_rate(&self) -> A {
165 self.learning_rate
166 }
167
168 pub fn set_lr(&mut self, lr: A) {
170 self.learning_rate = lr;
171 }
172
173 pub fn reset(&mut self) {
175 self.m = None;
176 self.v = None;
177 self.t.clear();
178 }
179
180 pub fn timestep(&self, index: usize) -> usize {
184 self.t.get(index).copied().unwrap_or(0)
185 }
186
187 fn advance_state(&mut self, index: usize, dim: &IxDyn) -> Result<usize> {
189 let m = self.m.get_or_insert_with(Vec::new);
190 let v = self.v.get_or_insert_with(Vec::new);
191 while m.len() <= index {
192 m.push(Array::zeros(dim.clone()));
193 }
194 while v.len() <= index {
195 v.push(Array::zeros(dim.clone()));
196 }
197 while self.t.len() <= index {
198 self.t.push(0);
199 }
200
201 if m[index].raw_dim() != *dim || v[index].raw_dim() != *dim {
203 m[index] = Array::zeros(dim.clone());
204 v[index] = Array::zeros(dim.clone());
205 self.t[index] = 0;
206 }
207
208 let next = self.t[index].checked_add(1).ok_or_else(|| {
209 OptimError::InvalidConfig(
210 "Timestep counter overflow - too many optimization steps".to_string(),
211 )
212 })?;
213 self.t[index] = next;
214 Ok(next)
215 }
216
217 pub fn step_indexed<D: Dimension>(
224 &mut self,
225 index: usize,
226 params: &Array<A, D>,
227 gradients: &Array<A, D>,
228 ) -> Result<Array<A, D>> {
229 if params.shape() != gradients.shape() {
230 return Err(OptimError::DimensionMismatch(format!(
231 "Incompatible shapes: parameters have shape {:?}, gradients have shape {:?}",
232 params.shape(),
233 gradients.shape()
234 )));
235 }
236
237 let dim = params.raw_dim().into_dyn();
238 let t = self.advance_state(index, &dim)?;
239 let exp = i32::try_from(t).map_err(|_| {
240 OptimError::InvalidConfig(
241 "Timestep too large for bias correction calculation".to_string(),
242 )
243 })?;
244
245 let beta1 = self.beta1;
246 let beta2 = self.beta2;
247 let eps = self.epsilon;
248 let weight_decay = self.weight_decay;
249 let use_weight_decay = weight_decay > A::zero();
250 let one = A::one();
251 let (bias_correction1, bias_correction2) = if self.bias_correction {
252 (one - beta1.powi(exp), one - beta2.powi(exp))
253 } else {
254 (one, one)
255 };
256
257 let m = self
258 .m
259 .as_mut()
260 .ok_or_else(|| OptimError::InvalidConfig("LAMB state not initialized".to_string()))?;
261 let v = self
262 .v
263 .as_mut()
264 .ok_or_else(|| OptimError::InvalidConfig("LAMB state not initialized".to_string()))?;
265
266 let params_view = params.view().into_dyn();
267 let gradients_view = gradients.view().into_dyn();
268
269 let mut update: Array<A, IxDyn> = Array::zeros(dim.clone());
272 let mut weight_norm_sq = A::zero();
273 let mut update_norm_sq = A::zero();
274
275 Zip::from(&mut update)
276 .and(¶ms_view)
277 .and(&gradients_view)
278 .and(&mut m[index])
279 .and(&mut v[index])
280 .for_each(|u, &p, &g, m_i, v_i| {
281 *m_i = *m_i * beta1 + g * (one - beta1);
282 *v_i = *v_i * beta2 + g * g * (one - beta2);
283 let m_hat = *m_i / bias_correction1;
284 let v_hat = *v_i / bias_correction2;
285 let mut direction = m_hat / (v_hat.sqrt() + eps);
286 if use_weight_decay {
287 direction = direction + p * weight_decay;
288 }
289 *u = direction;
290 weight_norm_sq = weight_norm_sq + p * p;
291 update_norm_sq = update_norm_sq + direction * direction;
292 });
293
294 let weight_norm = weight_norm_sq.sqrt();
295 let update_norm = update_norm_sq.sqrt();
296 let trust_ratio = if weight_norm > A::zero() && update_norm > A::zero() {
297 weight_norm / update_norm
298 } else {
299 one
300 };
301
302 let scale = self.learning_rate * trust_ratio;
303 let mut updated = params.to_owned();
304 let mut updated_view = updated.view_mut().into_dyn();
305 Zip::from(&mut updated_view).and(&update).for_each(|p, &u| {
306 *p = *p - u * scale;
307 });
308 drop(updated_view);
309
310 Ok(updated)
311 }
312}
313
314impl<A, D> Optimizer<A, D> for LAMB<A>
315where
316 A: Float + ScalarOperand + Debug + Send + Sync,
317 D: Dimension,
318{
319 fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
320 self.step_indexed(0, params, gradients)
321 }
322
323 fn step_list(
324 &mut self,
325 params_list: &[&Array<A, D>],
326 gradients_list: &[&Array<A, D>],
327 ) -> Result<Vec<Array<A, D>>> {
328 if params_list.len() != gradients_list.len() {
329 return Err(OptimError::InvalidConfig(format!(
330 "Number of parameter arrays ({}) does not match number of gradient arrays ({})",
331 params_list.len(),
332 gradients_list.len()
333 )));
334 }
335
336 let mut results = Vec::with_capacity(params_list.len());
337 for (index, (params, grads)) in params_list.iter().zip(gradients_list.iter()).enumerate() {
338 results.push(self.step_indexed(index, params, grads)?);
339 }
340 Ok(results)
341 }
342
343 fn get_learning_rate(&self) -> A {
344 self.learning_rate
345 }
346
347 fn set_learning_rate(&mut self, learning_rate: A) {
348 self.learning_rate = learning_rate;
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use approx::assert_abs_diff_eq;
356 use scirs2_core::ndarray::Array1;
357
358 #[test]
359 fn test_lamb_basic_creation() {
360 let optimizer: LAMB<f64> = LAMB::new(0.001);
361 assert_abs_diff_eq!(optimizer.learning_rate(), 0.001);
362 assert_abs_diff_eq!(optimizer.get_beta1(), 0.9);
363 assert_abs_diff_eq!(optimizer.get_beta2(), 0.999);
364 assert_abs_diff_eq!(optimizer.get_epsilon(), 1e-6);
365 assert_abs_diff_eq!(optimizer.get_weight_decay(), 0.0);
366 assert!(optimizer.bias_correction);
367 }
368
369 #[test]
370 fn test_lamb_convergence() {
371 let mut optimizer: LAMB<f64> = LAMB::new(0.1);
372
373 let mut params = Array1::from_vec(vec![5.0, 3.0]);
375
376 for _ in 0..50 {
377 let gradients = Array1::from_vec(vec![2.0 * params[0], 2.0 * params[1]]);
379 params = optimizer
380 .step(¶ms, &gradients)
381 .expect("optimizer.step succeeds in test_lamb_convergence");
382 }
383
384 assert!(params[0].abs() < 1.0);
386 assert!(params[1].abs() < 1.0);
387 }
388
389 #[test]
390 fn test_lamb_with_weight_decay() {
391 let mut optimizer: LAMB<f64> = LAMB::new_with_config(
392 0.1, 0.9, 0.999, 1e-6, 0.1, true, );
399
400 let mut params = Array1::from_vec(vec![1.0, 1.0]);
402
403 for _ in 0..20 {
405 let gradients = Array1::from_vec(vec![0.1, 0.1]);
406 params = optimizer
407 .step(¶ms, &gradients)
408 .expect("optimizer.step succeeds in test_lamb_with_weight_decay");
409 }
410
411 assert!(params[0] < 1.0);
413 assert!(params[1] < 1.0);
414 }
415
416 #[test]
417 fn test_lamb_reset() {
418 let mut optimizer: LAMB<f64> = LAMB::new(0.1);
419
420 let params = Array1::from_vec(vec![1.0]);
422 let gradients = Array1::from_vec(vec![0.5]);
423 let _ = optimizer
424 .step(¶ms, &gradients)
425 .expect("optimizer.step succeeds in test_lamb_reset");
426
427 assert!(optimizer.m.is_some());
429 assert!(optimizer.v.is_some());
430 assert_eq!(optimizer.timestep(0), 1);
431
432 optimizer.reset();
434
435 assert!(optimizer.m.is_none());
437 assert!(optimizer.v.is_none());
438 assert_eq!(optimizer.timestep(0), 0);
439 }
440
441 #[test]
442 fn test_lamb_trust_ratio() {
443 let mut optimizer: LAMB<f64> = LAMB::new(0.1);
445 let params = Array1::from_vec(vec![2.0, 3.0]);
446 let gradients = Array1::from_vec(vec![0.4, 0.6]);
447
448 let new_params = optimizer
449 .step(¶ms, &gradients)
450 .expect("optimizer.step succeeds in test_lamb_trust_ratio");
451
452 assert_ne!(new_params[0], params[0]);
454 assert_ne!(new_params[1], params[1]);
455
456 assert!(new_params[0] < params[0]); assert!(new_params[1] < params[1]); }
460}