1use crate::error::{OptimError, Result};
12use crate::optimizers::Optimizer;
13use scirs2_core::ndarray::{Ix1, ScalarOperand};
14use scirs2_core::ndarray_ext::{Array1, ArrayView1};
15use scirs2_core::numeric::Float;
16use serde::{Deserialize, Serialize};
17use std::fmt::Debug;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AdaDelta<T: Float> {
35 rho: T,
38
39 epsilon: T,
42
43 accumulated_gradients: Option<Array1<T>>,
46
47 accumulated_updates: Option<Array1<T>>,
50
51 step_count: usize,
53
54 warmup_boost: T,
61
62 warmup_steps: usize,
64}
65
66impl<T: Float> Default for AdaDelta<T> {
67 fn default() -> Self {
68 Self::new(
69 T::from(0.95).expect("AdaDelta: default rho (0.95) must be representable in T"),
70 T::from(1e-6).expect("AdaDelta: default epsilon (1e-6) must be representable in T"),
71 )
72 .expect("AdaDelta: default (rho=0.95, epsilon=1e-6) always satisfies validation")
73 }
74}
75
76impl<T: Float> AdaDelta<T> {
77 pub fn new(rho: T, epsilon: T) -> Result<Self> {
93 let rho_f64 = crate::optimizers::scalar_to_f64(rho)?;
94 let epsilon_f64 = crate::optimizers::scalar_to_f64(epsilon)?;
95
96 if rho_f64 <= 0.0 || rho_f64 >= 1.0 {
97 return Err(OptimError::InvalidParameter(format!(
98 "rho must be in (0, 1), got {}",
99 rho_f64
100 )));
101 }
102
103 if epsilon_f64 <= 0.0 {
104 return Err(OptimError::InvalidParameter(format!(
105 "epsilon must be positive, got {}",
106 epsilon_f64
107 )));
108 }
109
110 Ok(Self {
111 rho,
112 epsilon,
113 accumulated_gradients: None,
114 accumulated_updates: None,
115 step_count: 0,
116 warmup_boost: T::one(),
117 warmup_steps: 0,
118 })
119 }
120
121 pub fn with_warmup_boost(mut self, boost: T, steps: usize) -> Result<Self> {
134 let boost_f64 = boost.to_f64().ok_or_else(|| {
135 OptimError::InvalidParameter("boost is not representable".to_string())
136 })?;
137 if boost_f64 <= 0.0 {
138 return Err(OptimError::InvalidParameter(format!(
139 "warmup boost must be positive, got {}",
140 boost_f64
141 )));
142 }
143 self.warmup_boost = boost;
144 self.warmup_steps = steps;
145 Ok(self)
146 }
147
148 pub fn warmup_boost(&self) -> T {
150 self.warmup_boost
151 }
152
153 pub fn step<'a, P, G>(&mut self, params: P, grads: G) -> Result<Array1<T>>
182 where
183 P: Into<ArrayView1<'a, T>>,
184 G: Into<ArrayView1<'a, T>>,
185 T: 'a,
186 {
187 self.step_view(params.into(), grads.into())
188 }
189
190 pub fn step_view(&mut self, params: ArrayView1<T>, grads: ArrayView1<T>) -> Result<Array1<T>> {
194 let n = params.len();
195
196 if grads.len() != n {
197 return Err(OptimError::DimensionMismatch(format!(
198 "Expected gradient size {}, got {}",
199 n,
200 grads.len()
201 )));
202 }
203
204 let acc_grad = self
206 .accumulated_gradients
207 .get_or_insert_with(|| Array1::zeros(n));
208 let acc_update = self
209 .accumulated_updates
210 .get_or_insert_with(|| Array1::zeros(n));
211
212 let one = T::one();
215 let one_minus_rho = one - self.rho;
216
217 for i in 0..n {
218 let grad = grads[i];
219 acc_grad[i] = self.rho * acc_grad[i] + one_minus_rho * grad * grad;
220 }
221
222 let mut delta_params = Array1::zeros(n);
226
227 for i in 0..n {
228 let rms_grad = (acc_grad[i] + self.epsilon).sqrt();
229 let rms_update = (acc_update[i] + self.epsilon).sqrt();
230
231 delta_params[i] = -(rms_update / rms_grad) * grads[i];
233 }
234
235 for i in 0..n {
240 let delta = delta_params[i];
241 acc_update[i] = self.rho * acc_update[i] + one_minus_rho * delta * delta;
242 }
243
244 let boost = if self.step_count < self.warmup_steps {
246 self.warmup_boost
247 } else {
248 T::one()
249 };
250
251 let mut updated_params = params.to_owned();
253 for i in 0..n {
254 updated_params[i] = updated_params[i] + delta_params[i] * boost;
255 }
256
257 self.step_count += 1;
258
259 Ok(updated_params)
260 }
261
262 pub fn step_count(&self) -> usize {
264 self.step_count
265 }
266
267 pub fn reset(&mut self) {
271 self.accumulated_gradients = None;
272 self.accumulated_updates = None;
273 self.step_count = 0;
274 }
275
276 pub fn rms_gradients(&self) -> Option<Array1<T>> {
280 self.accumulated_gradients
281 .as_ref()
282 .map(|acc_grad| acc_grad.mapv(|x| (x + self.epsilon).sqrt()))
283 }
284
285 pub fn rms_updates(&self) -> Option<Array1<T>> {
289 self.accumulated_updates
290 .as_ref()
291 .map(|acc_update| acc_update.mapv(|x| (x + self.epsilon).sqrt()))
292 }
293}
294
295impl<T> Optimizer<T, Ix1> for AdaDelta<T>
296where
297 T: Float + ScalarOperand + Debug + Send + Sync,
298{
299 fn step(&mut self, params: &Array1<T>, gradients: &Array1<T>) -> Result<Array1<T>> {
300 self.step_view(params.view(), gradients.view())
301 }
302
303 fn get_learning_rate(&self) -> T {
306 T::one()
307 }
308
309 fn set_learning_rate(&mut self, _learning_rate: T) {}
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use approx::assert_relative_eq;
318 use scirs2_core::ndarray_ext::array;
319
320 #[test]
321 fn test_adadelta_creation() {
322 let optimizer = AdaDelta::<f32>::new(0.95, 1e-6)
323 .expect("AdaDelta::<f32>::new succeeds in test_adadelta_creation");
324 assert_eq!(optimizer.step_count(), 0);
325 }
326
327 #[test]
328 fn test_adadelta_invalid_rho() {
329 assert!(AdaDelta::<f32>::new(1.5, 1e-6).is_err());
330 assert!(AdaDelta::<f32>::new(-0.1, 1e-6).is_err());
331 }
332
333 #[test]
334 fn test_adadelta_invalid_epsilon() {
335 assert!(AdaDelta::<f32>::new(0.95, -1e-6).is_err());
336 }
337
338 #[test]
339 fn test_adadelta_single_step() {
340 let mut optimizer = AdaDelta::<f32>::new(0.9, 1e-6)
341 .expect("AdaDelta::<f32>::new succeeds in test_adadelta_single_step");
342 let params = array![1.0, 2.0, 3.0];
343 let grads = array![0.1, 0.2, 0.3];
344
345 let updated_params = optimizer
346 .step(params.view(), grads.view())
347 .expect("step succeeds in test_adadelta_single_step");
348
349 assert!(updated_params.len() == 3);
351 assert_eq!(optimizer.step_count(), 1);
352
353 for i in 0..3 {
355 assert_ne!(updated_params[i], params[i]);
356 }
357 }
358
359 #[test]
360 fn test_adadelta_multiple_steps() {
361 let mut optimizer = AdaDelta::<f32>::new(0.95, 1e-6)
362 .expect("AdaDelta::<f32>::new succeeds in test_adadelta_multiple_steps");
363 let mut params = array![1.0, 2.0, 3.0];
364
365 for _ in 0..10 {
366 let grads = array![0.1, 0.2, 0.3];
367 params = optimizer
368 .step(params.view(), grads.view())
369 .expect("step succeeds in test_adadelta_multiple_steps");
370 }
371
372 assert_eq!(optimizer.step_count(), 10);
373
374 assert!(params[0] < 1.0);
376 assert!(params[1] < 2.0);
377 assert!(params[2] < 3.0);
378 }
379
380 #[test]
381 fn test_adadelta_shape_mismatch() {
382 let mut optimizer = AdaDelta::<f32>::new(0.95, 1e-6)
383 .expect("AdaDelta::<f32>::new succeeds in test_adadelta_shape_mismatch");
384 let params = array![1.0, 2.0, 3.0];
385 let grads = array![0.1, 0.2]; assert!(optimizer.step(params.view(), grads.view()).is_err());
388 }
389
390 #[test]
391 fn test_adadelta_reset() {
392 let mut optimizer = AdaDelta::<f32>::new(0.95, 1e-6)
393 .expect("AdaDelta::<f32>::new succeeds in test_adadelta_reset");
394 let params = array![1.0, 2.0, 3.0];
395 let grads = array![0.1, 0.2, 0.3];
396
397 optimizer
398 .step(params.view(), grads.view())
399 .expect("step succeeds in test_adadelta_reset");
400 assert_eq!(optimizer.step_count(), 1);
401 assert!(optimizer.accumulated_gradients.is_some());
402
403 optimizer.reset();
404 assert_eq!(optimizer.step_count(), 0);
405 assert!(optimizer.accumulated_gradients.is_none());
406 assert!(optimizer.accumulated_updates.is_none());
407 }
408
409 #[test]
410 fn test_adadelta_convergence() {
411 let mut optimizer = AdaDelta::<f64>::new(0.99, 1e-6)
419 .expect("AdaDelta::<f64>::new succeeds in test_adadelta_convergence");
420 let mut params = array![10.0]; for _ in 0..3000 {
423 let grads = params.mapv(|x| 2.0 * x); params = optimizer
425 .step(params.view(), grads.view())
426 .expect("step succeeds in test_adadelta_convergence");
427 }
428
429 assert!(
430 params[0].abs() < 0.5,
431 "Failed to converge, got {}",
432 params[0]
433 );
434 }
435
436 #[test]
441 fn test_adadelta_first_step_matches_published_formula() {
442 let rho = 0.95f64;
443 let epsilon = 1e-6f64;
444 let mut optimizer = AdaDelta::<f64>::new(rho, epsilon).expect("valid config");
445
446 let params = array![1.0f64];
447 let grads = array![0.5f64];
448
449 let updated = optimizer
450 .step(params.view(), grads.view())
451 .expect("step failed");
452
453 let acc_grad = (1.0 - rho) * 0.5 * 0.5;
454 let expected_delta = -((0.0f64 + epsilon).sqrt() / (acc_grad + epsilon).sqrt()) * 0.5;
455
456 assert_relative_eq!(updated[0], 1.0 + expected_delta, epsilon = 1e-12);
457 assert_relative_eq!(optimizer.warmup_boost(), 1.0, epsilon = 1e-12);
458 }
459
460 #[test]
462 fn test_adadelta_warmup_boost_is_opt_in_and_uncontaminating() {
463 let rho = 0.95f64;
464 let epsilon = 1e-6f64;
465
466 let mut plain = AdaDelta::<f64>::new(rho, epsilon).expect("valid config");
467 let mut boosted = AdaDelta::<f64>::new(rho, epsilon)
468 .expect("valid config")
469 .with_warmup_boost(10.0, 1)
470 .expect("valid boost");
471
472 let params = array![1.0f64];
473 let grads = array![0.5f64];
474
475 let plain_out = plain.step(params.view(), grads.view()).expect("plain step");
476 let boosted_out = boosted
477 .step(params.view(), grads.view())
478 .expect("boosted step");
479
480 let plain_delta = plain_out[0] - 1.0;
481 let boosted_delta = boosted_out[0] - 1.0;
482
483 assert_relative_eq!(boosted_delta, plain_delta * 10.0, epsilon = 1e-12);
485
486 let plain_rms = plain.rms_updates().expect("rms after step");
488 let boosted_rms = boosted.rms_updates().expect("rms after step");
489 assert_relative_eq!(plain_rms[0], boosted_rms[0], epsilon = 1e-15);
490 }
491
492 #[test]
494 fn test_adadelta_optimizer_trait() {
495 let mut optimizer = AdaDelta::<f64>::new(0.95, 1e-6).expect("valid config");
496 let params = array![1.0f64, 2.0, 3.0];
497 let grads = array![0.1f64, 0.2, 0.3];
498
499 let updated =
500 Optimizer::<f64, scirs2_core::ndarray::Ix1>::step(&mut optimizer, ¶ms, &grads)
501 .expect("trait step failed");
502 assert_eq!(updated.len(), 3);
503
504 let again = optimizer.step(¶ms, &grads).expect("ref step failed");
506 assert_eq!(again.len(), 3);
507 }
508
509 #[test]
510 fn test_adadelta_rms_values() {
511 let mut optimizer = AdaDelta::<f32>::new(0.9, 1e-6)
512 .expect("AdaDelta::<f32>::new succeeds in test_adadelta_rms_values");
513
514 assert!(optimizer.rms_gradients().is_none());
516 assert!(optimizer.rms_updates().is_none());
517
518 let params = array![1.0, 2.0, 3.0];
519 let grads = array![0.1, 0.2, 0.3];
520
521 optimizer
522 .step(params.view(), grads.view())
523 .expect("step succeeds in test_adadelta_rms_values");
524
525 assert!(optimizer.rms_gradients().is_some());
527 assert!(optimizer.rms_updates().is_some());
528
529 let rms_grads = optimizer
530 .rms_gradients()
531 .expect("optimizer.rms_gradients succeeds in test_adadelta_rms_values");
532 assert_eq!(rms_grads.len(), 3);
533 }
534
535 #[test]
536 fn test_adadelta_f64() {
537 let mut optimizer = AdaDelta::<f64>::new(0.95, 1e-8)
538 .expect("AdaDelta::<f64>::new succeeds in test_adadelta_f64");
539 let params = array![1.0, 2.0, 3.0];
540 let grads = array![0.1, 0.2, 0.3];
541
542 let updated_params = optimizer
543 .step(params.view(), grads.view())
544 .expect("step succeeds in test_adadelta_f64");
545 assert_eq!(updated_params.len(), 3);
546 }
547}