1use crate::prelude::*;
6use burn::module::AutodiffModule;
7use burn::optim::{adaptor::OptimizerAdaptor, LrDecayState, SimpleOptimizer};
8use burn::record::Record;
9use burn::tensor::backend::AutodiffBackend;
10use burn::optim::LearningRate;
11use std::marker::PhantomData;
12pub mod hessian_optimizer;
13pub mod many_steps;
14pub mod multiple;
15pub use many_steps::LessSimpleOptimizer;
16
17#[derive(Debug)]
18pub struct ManifoldRGDConfig<M: Manifold<B>, B: Backend> {
19 _manifold: PhantomData<M>,
20 _backend: PhantomData<B>,
21}
22
23impl<M, B> Default for ManifoldRGDConfig<M, B>
24where
25 M: Manifold<B>,
26 B: Backend,
27{
28 fn default() -> Self {
29 Self {
30 _manifold: PhantomData,
31 _backend: PhantomData,
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
37pub struct ManifoldRGD<M: Manifold<B>, B: Backend> {
38 _manifold: PhantomData<M>,
39 _backend: PhantomData<B>,
40}
41
42impl<M, B> Default for ManifoldRGD<M, B>
43where
44 M: Manifold<B>,
45 B: Backend,
46{
47 fn default() -> Self {
48 Self {
49 _manifold: PhantomData,
50 _backend: PhantomData,
51 }
52 }
53}
54
55#[derive(Record, Clone)]
56pub struct ManifoldRGDState<B: Backend, const D: usize> {
57 lr_decay: LrDecayState<B, D>,
58}
59
60impl<M, B> SimpleOptimizer<B> for ManifoldRGD<M, B>
61where
62 M: Manifold<B>,
63 B: Backend,
64{
65 type State<const D: usize> = ManifoldRGDState<B, D>;
66
67 fn step<const D: usize>(
68 &self,
69 lr: LearningRate,
70 tensor: Tensor<B, D>,
71 grad: Tensor<B, D>,
72 state: Option<Self::State<D>>,
73 ) -> (Tensor<B, D>, Option<Self::State<D>>) {
74 let direction = M::project(tensor.clone(), -grad);
75 let result = M::retract(tensor, direction * lr);
76 (result, state)
77 }
78
79 fn to_device<const D: usize>(
80 state: Self::State<D>,
81 device: &<B as Backend>::Device,
82 ) -> Self::State<D> {
83 const DECAY_STATE_TO_DEVICE: bool = false;
84 if DECAY_STATE_TO_DEVICE {
85 ManifoldRGDState {
86 lr_decay: state.lr_decay.to_device(device),
87 }
88 } else {
89 state
90 }
91 }
92}
93
94impl<M, B> ManifoldRGDConfig<M, B>
95where
96 M: Manifold<B>,
97 B: Backend,
98{
99 #[must_use]
100 pub fn init<Back: AutodiffBackend, Mod: AutodiffModule<Back>>(
101 &self,
102 ) -> OptimizerAdaptor<ManifoldRGD<M, Back::InnerBackend>, Mod, Back>
103 where
104 M: Manifold<Back::InnerBackend>,
105 {
106 let optim = ManifoldRGD::<M, Back::InnerBackend>::default();
107
108 OptimizerAdaptor::from(optim)
109 }
110}
111
112#[derive(Debug, Clone)]
131pub struct RiemannianAdamConfig<M: Manifold<B>, B: Backend> {
132 pub lr: f64,
133 pub beta1: f64,
134 pub beta2: f64,
135 pub eps: f64,
136 pub weight_decay: f64,
137 pub amsgrad: bool,
138 pub stabilize: Option<usize>,
139 _manifold: PhantomData<M>,
140 _backend: PhantomData<B>,
141}
142
143impl<M, B> Default for RiemannianAdamConfig<M, B>
144where
145 M: Manifold<B>,
146 B: Backend,
147{
148 fn default() -> Self {
149 Self {
150 lr: 1e-3,
151 beta1: 0.9,
152 beta2: 0.999,
153 eps: 1e-8,
154 weight_decay: 0.0,
155 amsgrad: false,
156 stabilize: None,
157 _manifold: PhantomData,
158 _backend: PhantomData,
159 }
160 }
161}
162
163impl<M, B> RiemannianAdamConfig<M, B>
164where
165 M: Manifold<B>,
166 B: Backend,
167{
168 #[must_use]
169 pub fn new() -> Self {
170 Self::default()
171 }
172
173 #[must_use]
174 pub fn with_lr(mut self, lr: f64) -> Self {
175 self.lr = lr;
176 self
177 }
178
179 #[must_use]
180 pub fn with_beta1(mut self, beta1: f64) -> Self {
181 self.beta1 = beta1;
182 self
183 }
184
185 #[must_use]
186 pub fn with_beta2(mut self, beta2: f64) -> Self {
187 self.beta2 = beta2;
188 self
189 }
190
191 #[must_use]
192 pub fn with_eps(mut self, eps: f64) -> Self {
193 self.eps = eps;
194 self
195 }
196
197 #[must_use]
198 pub fn with_weight_decay(mut self, weight_decay: f64) -> Self {
199 self.weight_decay = weight_decay;
200 self
201 }
202
203 #[must_use]
204 pub fn with_amsgrad(mut self, amsgrad: bool) -> Self {
205 self.amsgrad = amsgrad;
206 self
207 }
208
209 #[must_use]
210 pub fn with_stabilize(mut self, stabilize: Option<usize>) -> Self {
211 self.stabilize = stabilize;
212 self
213 }
214}
215
216#[derive(Debug, Clone)]
218pub struct RiemannianAdam<M: Manifold<B>, B: Backend> {
219 config: RiemannianAdamConfig<M, B>,
220}
221
222impl<M, B> RiemannianAdam<M, B>
223where
224 M: Manifold<B>,
225 B: Backend,
226{
227 #[must_use]
228 pub fn new(config: RiemannianAdamConfig<M, B>) -> Self {
229 Self { config }
230 }
231}
232
233#[derive(Record, Clone)]
235pub struct RiemannianAdamState<B: Backend, const D: usize> {
236 pub step: usize,
237 pub exp_avg: Tensor<B, D>,
238 pub exp_avg_sq: Tensor<B, D>,
239 pub max_exp_avg_sq: Option<Tensor<B, D>>,
240 lr_decay: LrDecayState<B, D>,
241}
242
243impl<M, B> SimpleOptimizer<B> for RiemannianAdam<M, B>
244where
245 M: Manifold<B>,
246 B: Backend,
247{
248 type State<const D: usize> = RiemannianAdamState<B, D>;
249
250 fn step<const D: usize>(
251 &self,
252 _lr: LearningRate,
253 tensor: Tensor<B, D>,
254 grad: Tensor<B, D>,
255 state: Option<Self::State<D>>,
256 ) -> (Tensor<B, D>, Option<Self::State<D>>) {
257 let learning_rate = self.config.lr;
258
259 let grad = if self.config.weight_decay > 0.0 {
261 grad + tensor.clone() * self.config.weight_decay
262 } else {
263 grad
264 };
265
266 let rgrad = M::egrad2rgrad(tensor.clone(), grad);
268
269 let mut state = match state {
270 Some(mut state) => {
271 state.step += 1;
272 state
273 }
274 None => RiemannianAdamState {
275 step: 1,
276 exp_avg: Tensor::zeros_like(&tensor),
277 exp_avg_sq: Tensor::zeros_like(&tensor),
278 max_exp_avg_sq: if self.config.amsgrad {
279 Some(Tensor::zeros_like(&tensor))
280 } else {
281 None
282 },
283 lr_decay: LrDecayState::new(0, tensor.clone()),
284 },
285 };
286
287 state.exp_avg =
289 state.exp_avg.clone() * self.config.beta1 + rgrad.clone() * (1.0 - self.config.beta1);
290
291 let inner_product = M::inner::<D>(tensor.clone(), rgrad.clone(), rgrad.clone());
292 state.exp_avg_sq = state.exp_avg_sq.clone() * self.config.beta2
293 + inner_product * (1.0 - self.config.beta2);
294
295 let denom = if self.config.amsgrad {
297 let max_exp_avg_sq = state.max_exp_avg_sq.as_ref().expect(
298 "On an initial None state, having config.amsgrad be True makes this maximum field set to 0. \
299 If there was an input state then it will be present because of earlier steps");
300 let new_max = Tensor::max_pair(max_exp_avg_sq.clone(), state.exp_avg_sq.clone());
301 state.max_exp_avg_sq = Some(new_max.clone());
302 new_max.sqrt() + self.config.eps
303 } else {
304 state.exp_avg_sq.clone().sqrt() + self.config.eps
305 };
306
307 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
309 let bias_correction1 = 1.0 - self.config.beta1.powi(state.step as i32);
310 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
311 let bias_correction2 = 1.0 - self.config.beta2.powi(state.step as i32);
312 let step_size = learning_rate * bias_correction2.sqrt() / bias_correction1;
313
314 let direction = state.exp_avg.clone() / denom;
316
317 let new_point = M::expmap(tensor.clone(), direction.clone() * (-step_size));
319 let new_point = M::proj(new_point);
320
321 let exp_avg_new = M::parallel_transport(tensor, new_point.clone(), state.exp_avg);
323 state.exp_avg = exp_avg_new;
324
325 (new_point, Some(state))
326 }
327
328 fn to_device<const D: usize>(
329 mut state: Self::State<D>,
330 device: &<B as Backend>::Device,
331 ) -> Self::State<D> {
332 state.exp_avg = state.exp_avg.to_device(device);
333 state.exp_avg_sq = state.exp_avg_sq.to_device(device);
334 if let Some(ref max_exp_avg_sq) = state.max_exp_avg_sq {
335 state.max_exp_avg_sq = Some(max_exp_avg_sq.clone().to_device(device));
336 }
337 state.lr_decay = LrDecayState::to_device(state.lr_decay, device);
338 state
339 }
340}
341
342impl<M, B> RiemannianAdamConfig<M, B>
343where
344 M: Manifold<B>,
345 B: Backend,
346{
347 #[must_use]
348 pub fn init<Back: AutodiffBackend, Mod: AutodiffModule<Back>>(
349 &self,
350 ) -> OptimizerAdaptor<RiemannianAdam<M, Back::InnerBackend>, Mod, Back>
351 where
352 M: Manifold<Back::InnerBackend>,
353 {
354 let optim = RiemannianAdam::<M, Back::InnerBackend>::new(RiemannianAdamConfig {
355 lr: self.lr,
356 beta1: self.beta1,
357 beta2: self.beta2,
358 eps: self.eps,
359 weight_decay: self.weight_decay,
360 amsgrad: self.amsgrad,
361 stabilize: self.stabilize,
362 _manifold: PhantomData,
363 _backend: PhantomData,
364 });
365
366 OptimizerAdaptor::from(optim)
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use burn::backend::NdArray;
374 use burn::optim::SimpleOptimizer;
375
376 type TestBackend = NdArray;
377
378 #[test]
379 fn test_riemannian_adam_basic() {
380 let config = RiemannianAdamConfig::<Euclidean, TestBackend>::new()
381 .with_lr(0.1)
382 .with_beta1(0.9)
383 .with_beta2(0.999);
384
385 let optimizer = RiemannianAdam::new(config);
386
387 let tensor = Tensor::<TestBackend, 1>::zeros([3], &Default::default());
389 let grad = Tensor::<TestBackend, 1>::ones([3], &Default::default());
390
391 let (new_tensor, state) = optimizer.step(1.0, tensor.clone(), grad, None);
393
394 let scalar_value = new_tensor.slice([0; 1]).into_scalar();
396 assert!(
397 scalar_value < 0.0,
398 "Should move in negative gradient direction"
399 );
400 assert!(state.is_some(), "State should be initialized");
401 }
402
403 #[test]
404 fn test_riemannian_adam_convergence() {
405 let config = RiemannianAdamConfig::<Euclidean, TestBackend>::new().with_lr(0.1);
406
407 let optimizer = RiemannianAdam::new(config);
408
409 let target = Tensor::<TestBackend, 1>::from_floats([1.0, -0.5, 2.0], &Default::default());
411 let mut x = Tensor::<TestBackend, 1>::zeros([3], &Default::default());
412 let mut state = None;
413
414 let initial_loss = (x.clone() - target.clone()).powf_scalar(2.0).sum();
415
416 for _ in 0..50 {
418 let grad = (x.clone() - target.clone()) * 2.0;
419 let (new_x, new_state) = optimizer.step(1.0, x, grad, state);
420 x = new_x;
421 state = new_state;
422 }
423
424 let final_loss = (x.clone() - target.clone()).powf_scalar(2.0).sum();
425
426 assert!(
428 final_loss.into_scalar() < initial_loss.into_scalar(),
429 "Loss should decrease"
430 );
431 }
432
433 #[test]
434 fn test_riemannian_adam_amsgrad() {
435 let config = RiemannianAdamConfig::<Euclidean, TestBackend>::new()
436 .with_lr(0.1)
437 .with_amsgrad(true);
438
439 let optimizer = RiemannianAdam::new(config);
440
441 let tensor = Tensor::<TestBackend, 1>::zeros([2], &Default::default());
442 let grad = Tensor::<TestBackend, 1>::ones([2], &Default::default());
443
444 let (_, state) = optimizer.step(1.0, tensor, grad, None);
445
446 assert!(state.is_some());
448 let state =
449 state.expect("RiemannianAdam optimizer always gives back an initialized state on step");
450 assert!(
451 state.max_exp_avg_sq.is_some(),
452 "AMSGrad should initialize max_exp_avg_sq. See the explanation around the compute denominator part of step"
453 );
454 }
455
456 #[test]
457 fn test_riemannian_adam_weight_decay() {
458 let config = RiemannianAdamConfig::<Euclidean, TestBackend>::new()
459 .with_lr(0.1)
460 .with_weight_decay(0.1);
461
462 let optimizer = RiemannianAdam::new(config);
463
464 let tensor = Tensor::<TestBackend, 1>::ones([2], &Default::default());
465 let grad = Tensor::<TestBackend, 1>::zeros([2], &Default::default());
466
467 let (new_tensor, _) = optimizer.step(1.0, tensor.clone(), grad, None);
468
469 let original_norm = tensor.powf_scalar(2.0).sum().sqrt();
471 let new_norm = new_tensor.powf_scalar(2.0).sum().sqrt();
472
473 assert!(
474 new_norm.into_scalar() < original_norm.into_scalar(),
475 "Weight decay should reduce tensor magnitude"
476 );
477 }
478
479 #[test]
480 fn test_riemannian_adam_state_persistence() {
481 let config = RiemannianAdamConfig::<Euclidean, TestBackend>::new().with_lr(0.1);
482
483 let optimizer = RiemannianAdam::new(config);
484
485 let tensor = Tensor::<TestBackend, 1>::zeros([2], &Default::default());
486 let grad = Tensor::<TestBackend, 1>::ones([2], &Default::default());
487
488 let (tensor1, state1) = optimizer.step(1.0, tensor, grad.clone(), None);
490 assert!(state1.is_some());
491 let state1 = state1.expect(
492 "RiemannianAdam optimizer always gives back an initialized state on step even with None initial state");
493 assert_eq!(state1.step, 1);
494
495 let (_, state2) = optimizer.step(1.0, tensor1, grad, Some(state1));
497 assert!(state2.is_some());
498 let state2 = state2.expect(
499 "There was an input state so RiemannianAdam optimizer's step modifies that and returns it"
500 );
501 assert_eq!(state2.step, 2);
502 }
503}