Skip to main content

burn_optim/optim/
adam.rs

1use burn_core as burn;
2
3use crate::RecordState;
4
5use burn::config::Config;
6use burn::tensor::Device;
7use burn::tensor::Tensor;
8
9use super::{
10    Optimizer,
11    decay::{WeightDecay, WeightDecayConfig},
12    module_optimizer::ModuleOptimizer,
13};
14use crate::{LearningRate, grad_clipping::GradientClippingConfig};
15
16#[cfg(not(feature = "std"))]
17#[allow(unused_imports)]
18use num_traits::Float as _;
19
20/// Adam configuration.
21#[derive(Config, Debug)]
22pub struct AdamConfig {
23    /// Parameter for Adam.
24    #[config(default = 0.9)]
25    beta_1: f32,
26    /// Parameter for Adam.
27    #[config(default = 0.999)]
28    beta_2: f32,
29    /// A value required for numerical stability.
30    #[config(default = 1e-5)]
31    epsilon: f32,
32    /// Whether to use AMSGrad algorithm
33    #[config(default = false)]
34    amsgrad: bool,
35    /// [Weight decay](WeightDecayConfig) config.
36    weight_decay: Option<WeightDecayConfig>,
37    /// [Gradient Clipping](GradientClippingConfig) config.
38    grad_clipping: Option<GradientClippingConfig>,
39}
40
41/// Adam optimizer.
42///
43/// See:
44/// - [Adam: A Method for Stochastic Optimization](https://arxiv.org/pdf/1412.6980.pdf).
45/// - [On the Convergence of Adam and Beyond](https://openreview.net/forum?id=ryQu7f-RZ)
46#[derive(Clone)]
47pub struct Adam {
48    momentum: AdaptiveMomentum,
49    weight_decay: Option<WeightDecay>,
50}
51
52/// Adam state.
53#[derive(RecordState, Clone, new)]
54pub struct AdamState<const D: usize> {
55    /// The current adaptive momentum.
56    pub momentum: AdaptiveMomentumState<D>,
57}
58
59impl Optimizer for Adam {
60    type State<const D: usize> = AdamState<D>;
61
62    fn step<const D: usize>(
63        &self,
64        lr: LearningRate,
65        tensor: Tensor<D>,
66        mut grad: Tensor<D>,
67        state: Option<Self::State<D>>,
68    ) -> (Tensor<D>, Option<Self::State<D>>) {
69        let mut state_momentum = None;
70
71        if let Some(state) = state {
72            state_momentum = Some(state.momentum);
73        }
74
75        if let Some(weight_decay) = &self.weight_decay {
76            grad = weight_decay.transform(grad, tensor.clone());
77        }
78
79        let (grad, state_momentum) = self.momentum.transform(grad, state_momentum);
80
81        let state = AdamState::new(state_momentum);
82        let delta = grad.mul_scalar(lr);
83
84        (tensor - delta, Some(state))
85    }
86
87    fn to_device<const D: usize>(mut state: Self::State<D>, device: &Device) -> Self::State<D> {
88        state.momentum = state.momentum.to_device(device);
89        state
90    }
91}
92
93impl AdamConfig {
94    /// Build an [`Adam`] from the config.
95    ///
96    /// The bare optimizer, which
97    /// [`ModuleOptimizer::with_group`](crate::ModuleOptimizer::with_group) takes to
98    /// optimize one parameter group. [`init`](Self::init) is the whole-module
99    /// counterpart, and the only one that applies the configured gradient clipping.
100    pub fn build(&self) -> Adam {
101        Adam {
102            momentum: AdaptiveMomentum {
103                beta_1: self.beta_1,
104                beta_2: self.beta_2,
105                epsilon: self.epsilon,
106                amsgrad: self.amsgrad,
107            },
108            weight_decay: self.weight_decay.as_ref().map(WeightDecay::new),
109        }
110    }
111
112    /// Initialize Adam optimizer.
113    ///
114    /// # Returns
115    ///
116    /// Returns an optimizer that can be used to optimize a module.
117    pub fn init(&self) -> ModuleOptimizer {
118        let mut optim = ModuleOptimizer::from(self.build());
119        if let Some(config) = &self.grad_clipping {
120            optim = optim.with_grad_clipping(config.init());
121        }
122        optim
123    }
124}
125
126/// Adaptive momentum state.
127#[derive(RecordState, new, Clone)]
128pub struct AdaptiveMomentumState<const D: usize> {
129    /// The number of iterations aggregated.
130    pub time: usize,
131    /// The first order momentum.
132    pub moment_1: Tensor<D>,
133    /// The second order momentum.
134    pub moment_2: Tensor<D>,
135    /// Max of second  order momentum (for AMSGrad)
136    #[new(default)]
137    pub max_moment_2: Option<Tensor<D>>,
138}
139
140#[derive(Clone)]
141struct AdaptiveMomentum {
142    beta_1: f32,
143    beta_2: f32,
144    epsilon: f32,
145    amsgrad: bool,
146}
147
148impl AdaptiveMomentum {
149    pub fn transform<const D: usize>(
150        &self,
151        grad: Tensor<D>,
152        momentum_state: Option<AdaptiveMomentumState<D>>,
153    ) -> (Tensor<D>, AdaptiveMomentumState<D>) {
154        let state = if let Some(mut state) = momentum_state {
155            let factor = 1.0 - self.beta_1;
156            state.moment_1 = state
157                .moment_1
158                .mul_scalar(self.beta_1)
159                .add(grad.clone().mul_scalar(factor));
160
161            let factor = 1.0 - self.beta_2;
162            state.moment_2 = state
163                .moment_2
164                .mul_scalar(self.beta_2)
165                .add(grad.square().mul_scalar(factor));
166            if self.amsgrad {
167                let max_v = state
168                    .max_moment_2
169                    .take()
170                    .unwrap_or_else(|| state.moment_2.clone());
171
172                let new_max = max_v.max_pair(state.moment_2.clone());
173                state.max_moment_2 = Some(new_max);
174            }
175
176            state.time += 1;
177
178            state
179        } else {
180            let factor = 1.0 - self.beta_1;
181            let moment_1 = grad.clone().mul_scalar(factor);
182
183            let factor = 1.0 - self.beta_2;
184            let moment_2 = grad.square().mul_scalar(factor);
185            let max_moment_2 = self.amsgrad.then(|| moment_2.clone());
186            AdaptiveMomentumState {
187                time: 1,
188                moment_1,
189                moment_2,
190                max_moment_2,
191            }
192        };
193
194        let time = state.time as i32;
195        let bias_correction2_sqrt = (1.0 - self.beta_2.powi(time)).sqrt();
196        let combined_factor = bias_correction2_sqrt / (1.0 - self.beta_1.powi(time));
197
198        let v_to_use = if self.amsgrad {
199            state.max_moment_2.as_ref().unwrap_or(&state.moment_2)
200        } else {
201            &state.moment_2
202        };
203
204        let grad = state.moment_1.clone().mul_scalar(combined_factor).div(
205            v_to_use
206                .clone()
207                .sqrt()
208                .add_scalar(self.epsilon * bias_correction2_sqrt),
209        );
210        (grad, state)
211    }
212}
213
214impl<const D: usize> AdaptiveMomentumState<D> {
215    /// Move state to device.
216    ///
217    /// # Arguments
218    ///
219    /// * `device` - Device to move state to.
220    ///
221    /// # Returns
222    ///
223    /// Returns state moved to device.
224    pub fn to_device(mut self, device: &Device) -> Self {
225        self.moment_1 = self.moment_1.to_device(device);
226        self.moment_2 = self.moment_2.to_device(device);
227        self.max_moment_2 = self.max_moment_2.map(|tensor| tensor.to_device(device));
228        self
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use crate::optim::test_utils::assert_optimizer_resume;
235    use burn::tensor::Tolerance;
236
237    use super::*;
238    use crate::GradientsParams;
239    use burn::module::Param;
240    use burn::tensor::{Tensor, TensorData};
241    use burn_nn::{Linear, LinearConfig};
242
243    const LEARNING_RATE: LearningRate = 0.01;
244
245    #[test]
246    fn test_adam_optimizer_save_load_state() {
247        let device = Device::default().autodiff();
248        let linear = LinearConfig::new(6, 6).init(&device);
249        assert_optimizer_resume(|| AdamConfig::new().init(), linear, LEARNING_RATE);
250    }
251
252    /// A burnpack round-trip must restore the full state — the moment tensors, amsgrad's optional
253    /// `max_moment_2`, and the `time` step counter — so that a subsequent step produces identical
254    /// parameters whether taken on the original or the reloaded optimizer.
255    #[test]
256    fn test_adam_state_survives_burnpack_round_trip() {
257        let device = Device::default().autodiff();
258        let linear = LinearConfig::new(6, 6).init(&device);
259        assert_optimizer_resume(
260            || AdamConfig::new().with_amsgrad(true).init(),
261            linear,
262            LEARNING_RATE,
263        );
264    }
265
266    #[test]
267    fn test_adam_optimizer_with_amsgrad_50_steps() {
268        let device = Device::default().autodiff();
269        let mut linear = given_linear_layer(
270            TensorData::from([
271                [-0.3206, 0.1374, 0.4043, 0.3200, 0.0859, 0.0671],
272                [0.0777, -0.0185, -0.3667, 0.2550, 0.1955, -0.2922],
273                [-0.0190, 0.0346, -0.2962, 0.2484, -0.2780, 0.3130],
274                [-0.2980, -0.2214, -0.3715, -0.2981, -0.0761, 0.1626],
275                [0.3300, -0.2182, 0.3717, -0.1729, 0.3796, -0.0304],
276                [-0.0159, -0.0120, 0.1258, 0.1921, 0.0293, 0.3833],
277            ]),
278            TensorData::from([-0.3905, 0.0884, -0.0970, 0.1176, 0.1366, 0.0130]),
279            &device,
280        );
281
282        let mut optimizer = AdamConfig::new()
283            .with_epsilon(1e-8)
284            .with_beta_1(0.9)
285            .with_beta_2(0.999)
286            .with_amsgrad(true)
287            .with_weight_decay(Some(WeightDecayConfig::new(0.5)))
288            .init();
289
290        for i in 1..=50 {
291            let x = Tensor::<2>::ones([2, 6], &device)
292                .mul_scalar(i as f32 * 0.1)
293                .require_grad();
294
295            let grads = linear.forward(x).backward();
296            let grads = GradientsParams::from_grads(grads, &linear);
297            linear = optimizer.step(LEARNING_RATE, linear, grads);
298        }
299
300        let state_updated = linear;
301        let weight_updated = state_updated.weight.to_data();
302        let bias_updated = state_updated.bias.unwrap().to_data();
303
304        let weights_expected = TensorData::from([
305            [
306                -0.9125810265541077,
307                -0.45855265855789185,
308                -0.1915993094444275,
309                -0.2759990692138672,
310                -0.5099529027938843,
311                -0.5287043452262878,
312            ],
313            [
314                -0.5181325674057007,
315                -0.6139854788780212,
316                -0.9574727416038513,
317                -0.34102925658226013,
318                -0.400514155626297,
319                -0.8847861886024475,
320            ],
321            [
322                -0.614483118057251,
323                -0.5611032247543335,
324                -0.8887064456939697,
325                -0.34762972593307495,
326                -0.8708556890487671,
327                -0.2830044627189636,
328            ],
329            [
330                -0.8904699683189392,
331                -0.8151527643203735,
332                -0.9621278643608093,
333                -0.8905676603317261,
334                -0.671261191368103,
335                -0.4333854615688324,
336            ],
337            [
338                -0.26599061489105225,
339                -0.8119961023330688,
340                -0.22424538433551788,
341                -0.7672406435012817,
342                -0.2163349837064743,
343                -0.6258266568183899,
344            ],
345            [
346                -0.611397922039032,
347                -0.6075160503387451,
348                -0.4701341986656189,
349                -0.4039117991924286,
350                -0.5663845539093018,
351                -0.21262989938259125,
352            ],
353        ]);
354        let bias_expected = TensorData::from([
355            -0.8817203044891357,
356            -0.4038999378681183,
357            -0.5889149308204651,
358            -0.37475723028182983,
359            -0.3557940721511841,
360            -0.47914788126945496,
361        ]);
362
363        let tolerance = Tolerance::absolute(1e-5);
364        weight_updated.assert_approx_eq::<f32>(&weights_expected, tolerance);
365        bias_updated.assert_approx_eq::<f32>(&bias_expected, tolerance);
366    }
367    #[test]
368    fn test_adam_optimizer_with_numbers() {
369        let device = Device::default().autodiff();
370        let linear = given_linear_layer(
371            TensorData::from([
372                [-0.3206, 0.1374, 0.4043, 0.3200, 0.0859, 0.0671],
373                [0.0777, -0.0185, -0.3667, 0.2550, 0.1955, -0.2922],
374                [-0.0190, 0.0346, -0.2962, 0.2484, -0.2780, 0.3130],
375                [-0.2980, -0.2214, -0.3715, -0.2981, -0.0761, 0.1626],
376                [0.3300, -0.2182, 0.3717, -0.1729, 0.3796, -0.0304],
377                [-0.0159, -0.0120, 0.1258, 0.1921, 0.0293, 0.3833],
378            ]),
379            TensorData::from([-0.3905, 0.0884, -0.0970, 0.1176, 0.1366, 0.0130]),
380            &device,
381        );
382        let x_1 = Tensor::<2>::from_floats(
383            [
384                [0.6294, 0.0940, 0.8176, 0.8824, 0.5228, 0.4310],
385                [0.7152, 0.9559, 0.7893, 0.5684, 0.5939, 0.8883],
386            ],
387            &device,
388        )
389        .require_grad();
390        let x_2 = Tensor::<2>::from_floats(
391            [
392                [0.8491, 0.2108, 0.8939, 0.4433, 0.5527, 0.2528],
393                [0.3270, 0.0412, 0.5538, 0.9605, 0.3195, 0.9085],
394            ],
395            &device,
396        )
397        .require_grad();
398
399        let mut optimizer = AdamConfig::new()
400            .with_epsilon(1e-8)
401            .with_beta_1(0.9)
402            .with_beta_2(0.999)
403            .with_weight_decay(Some(WeightDecayConfig::new(0.5)))
404            .init();
405
406        let grads = linear.forward(x_1).backward();
407        let grads = GradientsParams::from_grads(grads, &linear);
408        let linear = optimizer.step(LEARNING_RATE, linear, grads);
409
410        let grads = linear.forward(x_2).backward();
411        let grads = GradientsParams::from_grads(grads, &linear);
412        let linear = optimizer.step(LEARNING_RATE, linear, grads);
413
414        let state_updated = linear;
415        let weights_expected = TensorData::from([
416            [-0.340528, 0.118929, 0.384336, 0.300010, 0.066034, 0.047154],
417            [
418                0.057757, -0.036690, -0.386649, 0.235010, 0.175624, -0.312133,
419            ],
420            [
421                -0.038940, 0.016306, -0.316151, 0.228410, -0.297819, 0.293047,
422            ],
423            [
424                -0.317929, -0.239100, -0.391449, -0.318087, -0.095948, 0.142651,
425            ],
426            [
427                0.310050, -0.235909, 0.351736, -0.192888, 0.359710, -0.050343,
428            ],
429            [-0.035840, -0.030203, 0.105840, 0.172110, 0.009440, 0.363346],
430        ]);
431        let bias_expected = TensorData::from([
432            -0.410499, 0.068401, -0.116999, 0.097601, 0.116601, -0.006999,
433        ]);
434
435        let (weight_updated, bias_updated) = (
436            state_updated.weight.to_data(),
437            state_updated.bias.unwrap().to_data(),
438        );
439
440        let tolerance = Tolerance::absolute(1e-2);
441        bias_updated.assert_approx_eq::<f32>(&bias_expected, tolerance);
442        weight_updated.assert_approx_eq::<f32>(&weights_expected, tolerance);
443    }
444
445    #[test]
446    fn test_adam_optimizer_no_nan() {
447        let device = Device::default().autodiff();
448        let linear = given_linear_layer(
449            TensorData::from([
450                [-0.3206, 0.1374, 0.4043, 0.3200, 0.0859, 0.0671],
451                [0.0777, -0.0185, -0.3667, 0.2550, 0.1955, -0.2922],
452                [-0.0190, 0.0346, -0.2962, 0.2484, -0.2780, 0.3130],
453                [-0.2980, -0.2214, -0.3715, -0.2981, -0.0761, 0.1626],
454                [0.3300, -0.2182, 0.3717, -0.1729, 0.3796, -0.0304],
455                [-0.0159, -0.0120, 0.1258, 0.1921, 0.0293, 0.3833],
456            ]),
457            TensorData::from([-0.3905, 0.0884, -0.0970, 0.1176, 0.1366, 0.0130]),
458            &device,
459        );
460
461        let x = Tensor::<2>::from_floats(
462            [
463                [0.8491, 0.2108, 0.8939, 0.4433, 0.5527, 0.2528],
464                [0.3270, 0.0412, 0.5538, 0.9605, 0.3195, 0.9085],
465            ],
466            &device,
467        )
468        .require_grad();
469
470        let mut optimizer = AdamConfig::new()
471            .with_epsilon(1e-8)
472            .with_beta_1(0.9)
473            .with_beta_2(0.999)
474            .with_weight_decay(Some(WeightDecayConfig::new(0.5)))
475            .init();
476
477        let grads = linear.forward(x.clone()).backward();
478        let grads = GradientsParams::from_grads(grads, &linear);
479        let linear = optimizer.step(LEARNING_RATE, linear, grads);
480
481        let grads = linear.forward(x).backward();
482        let grads = GradientsParams::from_grads(grads, &linear);
483        let linear = optimizer.step(LEARNING_RATE, linear, grads);
484
485        let state_updated = linear;
486        assert!(!state_updated.weight.to_data().as_slice::<f32>().unwrap()[0].is_nan());
487    }
488
489    fn given_linear_layer(weight: TensorData, bias: TensorData, device: &Device) -> Linear {
490        Linear {
491            weight: Param::from_data(weight, device),
492            bias: Some(Param::from_data(bias, device)),
493        }
494    }
495}