Skip to main content

burn_optim/optim/
adan.rs

1use burn_core as burn;
2
3use crate::RecordState;
4use burn::config::Config;
5use burn::tensor::Device;
6use burn::tensor::Tensor;
7
8use super::{Optimizer, module_optimizer::ModuleOptimizer};
9use crate::{LearningRate, grad_clipping::GradientClippingConfig};
10
11#[cfg(not(feature = "std"))]
12#[allow(unused_imports)]
13use num_traits::Float as _;
14
15/// [`Adan`] Configuration.
16///
17/// See:
18/// - [Adan: Adaptive Nesterov Momentum Algorithm for Faster Optimizing Deep Models](https://arxiv.org/abs/2208.06677).
19#[derive(Config, Debug)]
20pub struct AdanConfig {
21    /// Parameter for the first moment.
22    #[config(default = 0.98)]
23    beta_1: f32,
24    /// Parameter for the gradient-difference momentum.
25    #[config(default = 0.92)]
26    beta_2: f32,
27    /// Parameter for the second moment.
28    #[config(default = 0.99)]
29    beta_3: f32,
30    /// A value required for numerical stability.
31    #[config(default = 1e-8)]
32    epsilon: f32,
33    /// Weight decay factor.
34    #[config(default = 0.0)]
35    weight_decay: f32,
36    /// Disable proximal weight decay and use the decoupled update instead.
37    #[config(default = false)]
38    no_prox: bool,
39    /// [Gradient Clipping](GradientClippingConfig) config.
40    grad_clipping: Option<GradientClippingConfig>,
41}
42
43/// Adan optimizer.
44///
45/// See:
46/// - [Adan: Adaptive Nesterov Momentum Algorithm for Faster Optimizing Deep Models](https://arxiv.org/abs/2208.06677).
47///
48/// Configured by [`AdanConfig`].
49#[derive(Clone)]
50pub struct Adan {
51    momentum: AdaptiveNesterovMomentum,
52    weight_decay: f32,
53    no_prox: bool,
54}
55
56/// Adan state.
57#[derive(RecordState, Clone, new)]
58pub struct AdanState<const D: usize> {
59    /// The current adaptive Nesterov momentum state.
60    pub momentum: AdaptiveNesterovMomentumState<D>,
61}
62
63impl Optimizer for Adan {
64    type State<const D: usize> = AdanState<D>;
65
66    fn step<const D: usize>(
67        &self,
68        lr: LearningRate,
69        tensor: Tensor<D>,
70        grad: Tensor<D>,
71        state: Option<Self::State<D>>,
72    ) -> (Tensor<D>, Option<Self::State<D>>) {
73        let (raw_delta, momentum_state) = self.momentum.transform(grad, state.map(|s| s.momentum));
74
75        let decay_rate = lr * (self.weight_decay as f64);
76        let delta = raw_delta.mul_scalar(lr);
77
78        let tensor_updated = if self.no_prox {
79            if decay_rate == 0.0 {
80                tensor - delta
81            } else {
82                tensor.mul_scalar(1.0 - decay_rate) - delta
83            }
84        } else {
85            let updated = tensor - delta;
86            if decay_rate == 0.0 {
87                updated
88            } else {
89                updated.div_scalar(1.0 + decay_rate)
90            }
91        };
92
93        (tensor_updated, Some(AdanState::new(momentum_state)))
94    }
95
96    fn to_device<const D: usize>(mut state: Self::State<D>, device: &Device) -> Self::State<D> {
97        state.momentum = state.momentum.to_device(device);
98        state
99    }
100}
101
102impl AdanConfig {
103    /// Build an [`Adan`] from the config.
104    pub(crate) fn build(&self) -> Adan {
105        Adan {
106            momentum: AdaptiveNesterovMomentum {
107                beta_1: self.beta_1,
108                beta_2: self.beta_2,
109                beta_3: self.beta_3,
110                epsilon: self.epsilon,
111            },
112            weight_decay: self.weight_decay,
113            no_prox: self.no_prox,
114        }
115    }
116
117    /// Initialize Adan optimizer.
118    ///
119    /// # Returns
120    ///
121    /// Returns an optimizer that can be used to optimize a module.
122    pub fn init(&self) -> ModuleOptimizer {
123        let mut optim = ModuleOptimizer::from(self.build());
124        if let Some(config) = &self.grad_clipping {
125            optim = optim.with_grad_clipping(config.init());
126        }
127        optim
128    }
129}
130
131/// Adaptive Nesterov momentum state.
132#[derive(RecordState, Clone, new)]
133pub struct AdaptiveNesterovMomentumState<const D: usize> {
134    /// The number of iterations aggregated.
135    pub time: usize,
136    /// The first order momentum.
137    pub exp_avg: Tensor<D>,
138    /// The gradient-difference weighted second order momentum.
139    pub exp_avg_sq: Tensor<D>,
140    /// The gradient-difference momentum.
141    pub exp_avg_diff: Tensor<D>,
142    /// The negated previous gradient.
143    pub neg_pre_grad: Tensor<D>,
144}
145
146#[derive(Clone)]
147struct AdaptiveNesterovMomentum {
148    beta_1: f32,
149    beta_2: f32,
150    beta_3: f32,
151    epsilon: f32,
152}
153
154impl AdaptiveNesterovMomentum {
155    pub fn transform<const D: usize>(
156        &self,
157        grad: Tensor<D>,
158        state: Option<AdaptiveNesterovMomentumState<D>>,
159    ) -> (Tensor<D>, AdaptiveNesterovMomentumState<D>) {
160        let state = if let Some(mut state) = state {
161            let grad_diff = state.neg_pre_grad.clone().add(grad.clone());
162            let grad_diff_sq = grad_diff
163                .clone()
164                .mul_scalar(self.beta_2)
165                .add(grad.clone())
166                .square();
167
168            state.exp_avg = state
169                .exp_avg
170                .mul_scalar(self.beta_1)
171                .add(grad.clone().mul_scalar(1.0 - self.beta_1));
172            state.exp_avg_diff = state
173                .exp_avg_diff
174                .mul_scalar(self.beta_2)
175                .add(grad_diff.mul_scalar(1.0 - self.beta_2));
176            state.exp_avg_sq = state
177                .exp_avg_sq
178                .mul_scalar(self.beta_3)
179                .add(grad_diff_sq.mul_scalar(1.0 - self.beta_3));
180            state.neg_pre_grad = grad.mul_scalar(-1.0);
181            state.time += 1;
182            state
183        } else {
184            AdaptiveNesterovMomentumState::new(
185                1,
186                grad.clone().mul_scalar(1.0 - self.beta_1),
187                grad.clone().square().mul_scalar(1.0 - self.beta_3),
188                grad.zeros_like(),
189                grad.clone().mul_scalar(-1.0),
190            )
191        };
192
193        let time = state.time as i32;
194        let denom = state
195            .exp_avg_sq
196            .clone()
197            .sqrt()
198            .div_scalar((1.0 - self.beta_3.powi(time)).sqrt())
199            .add_scalar(self.epsilon);
200        let update = state
201            .exp_avg
202            .clone()
203            .div_scalar(1.0 - self.beta_1.powi(time))
204            .div(denom.clone())
205            .add(
206                state
207                    .exp_avg_diff
208                    .clone()
209                    .mul_scalar(self.beta_2)
210                    .div_scalar(1.0 - self.beta_2.powi(time))
211                    .div(denom),
212            );
213
214        (update, state)
215    }
216}
217
218impl<const D: usize> AdaptiveNesterovMomentumState<D> {
219    #[allow(clippy::wrong_self_convention)]
220    fn to_device(mut self, device: &Device) -> Self {
221        self.exp_avg = self.exp_avg.to_device(device);
222        self.exp_avg_sq = self.exp_avg_sq.to_device(device);
223        self.exp_avg_diff = self.exp_avg_diff.to_device(device);
224        self.neg_pre_grad = self.neg_pre_grad.to_device(device);
225        self
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::GradientsParams;
233    use burn::module::Param;
234    use burn::tensor::Tolerance;
235    use burn::tensor::{Distribution, Tensor, TensorData};
236    use burn_nn::{Linear, LinearConfig};
237
238    type FT = f32;
239
240    const LEARNING_RATE: LearningRate = 0.01;
241
242    #[test]
243    fn test_adan_optimizer_save_load_state() {
244        let device = Device::default().autodiff();
245        let linear = LinearConfig::new(6, 6).init(&device);
246        let x = Tensor::<2>::random([2, 6], Distribution::Default, &device);
247        let mut optimizer = create_adan();
248        let grads = linear.forward(x).backward();
249        let grads = GradientsParams::from_grads(grads, &linear);
250        let _linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
251
252        let bytes = optimizer.into_bytes().unwrap();
253        assert!(!bytes.is_empty());
254
255        #[cfg(feature = "std")]
256        optimizer
257            .save(std::env::temp_dir().as_path().join("test_optim_adan"))
258            .unwrap();
259
260        let state_optim_before = optimizer.to_record();
261        let optimizer = create_adan().from_bytes(bytes).unwrap();
262        let state_optim_after = optimizer.to_record();
263
264        assert_eq!(state_optim_before.len(), state_optim_after.len());
265    }
266
267    #[test]
268    fn test_adan_optimizer_with_numbers() {
269        let device = Device::default().autodiff();
270        let linear = given_linear_layer(
271            TensorData::from([
272                [-0.3206, 0.1374, 0.4043, 0.3200, 0.0859, 0.0671],
273                [0.0777, -0.0185, -0.3667, 0.2550, 0.1955, -0.2922],
274                [-0.0190, 0.0346, -0.2962, 0.2484, -0.2780, 0.3130],
275                [-0.2980, -0.2214, -0.3715, -0.2981, -0.0761, 0.1626],
276                [0.3300, -0.2182, 0.3717, -0.1729, 0.3796, -0.0304],
277                [-0.0159, -0.0120, 0.1258, 0.1921, 0.0293, 0.3833],
278            ]),
279            TensorData::from([-0.3905, 0.0884, -0.0970, 0.1176, 0.1366, 0.0130]),
280            &device,
281        );
282        let x_1 = Tensor::<2>::from_floats(
283            [
284                [0.6294, 0.0940, 0.8176, 0.8824, 0.5228, 0.4310],
285                [0.7152, 0.9559, 0.7893, 0.5684, 0.5939, 0.8883],
286            ],
287            &device,
288        )
289        .require_grad();
290        let x_2 = Tensor::<2>::from_floats(
291            [
292                [0.8491, 0.2108, 0.8939, 0.4433, 0.5527, 0.2528],
293                [0.3270, 0.0412, 0.5538, 0.9605, 0.3195, 0.9085],
294            ],
295            &device,
296        )
297        .require_grad();
298
299        let mut optimizer = AdanConfig::new()
300            .with_beta_1(0.98)
301            .with_beta_2(0.92)
302            .with_beta_3(0.99)
303            .with_epsilon(1e-8)
304            .with_weight_decay(0.02)
305            .init();
306
307        let grads = linear.forward(x_1).backward();
308        let grads = GradientsParams::from_grads(grads, &linear);
309        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
310
311        let grads = linear.forward(x_2).backward();
312        let grads = GradientsParams::from_grads(grads, &linear);
313        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
314
315        let state_updated = linear;
316        let weights_expected = TensorData::from([
317            [
318                -0.34034607,
319                0.11747075,
320                0.38426402,
321                0.29999772,
322                0.06599136,
323                0.04719888,
324            ],
325            [
326                0.0644293,
327                -0.031732224,
328                -0.37979296,
329                0.24165839,
330                0.18218218,
331                -0.30532277,
332            ],
333            [
334                -0.038910445,
335                0.01466812,
336                -0.31599957,
337                0.2283826,
338                -0.29780683,
339                0.2929568,
340            ],
341            [
342                -0.3178632,
343                -0.24129382,
344                -0.39133376,
345                -0.31796312,
346                -0.09605193,
347                0.14255258,
348            ],
349            [
350                0.31026322,
351                -0.23771758,
352                0.3519465,
353                -0.19243571,
354                0.35984334,
355                -0.049992695,
356            ],
357            [
358                -0.03577819,
359                -0.031879753,
360                0.10586514,
361                0.17213862,
362                0.009403733,
363                0.36326218,
364            ],
365        ]);
366        let bias_expected = TensorData::from([
367            -0.4103378,
368            0.06837065,
369            -0.116955206,
370            0.097558975,
371            0.11655137,
372            -0.006999196,
373        ]);
374
375        let (weight_updated, bias_updated) = (
376            state_updated.weight.to_data(),
377            state_updated.bias.unwrap().to_data(),
378        );
379
380        let tolerance = Tolerance::absolute(1e-5);
381        bias_updated.assert_approx_eq::<FT>(&bias_expected, tolerance);
382        weight_updated.assert_approx_eq::<FT>(&weights_expected, tolerance);
383    }
384
385    #[test]
386    fn test_adan_optimizer_no_nan() {
387        let device = Device::default().autodiff();
388        let linear = given_linear_layer(
389            TensorData::from([
390                [-0.3206, 0.1374, 0.4043, 0.3200, 0.0859, 0.0671],
391                [0.0777, -0.0185, -0.3667, 0.2550, 0.1955, -0.2922],
392                [-0.0190, 0.0346, -0.2962, 0.2484, -0.2780, 0.3130],
393                [-0.2980, -0.2214, -0.3715, -0.2981, -0.0761, 0.1626],
394                [0.3300, -0.2182, 0.3717, -0.1729, 0.3796, -0.0304],
395                [-0.0159, -0.0120, 0.1258, 0.1921, 0.0293, 0.3833],
396            ]),
397            TensorData::from([-0.3905, 0.0884, -0.0970, 0.1176, 0.1366, 0.0130]),
398            &device,
399        );
400
401        let x = Tensor::<2>::from_floats(
402            [
403                [0.8491, 0.2108, 0.8939, 0.4433, 0.5527, 0.2528],
404                [0.3270, 0.0412, 0.5538, 0.9605, 0.3195, 0.9085],
405            ],
406            &device,
407        )
408        .require_grad();
409
410        let mut optimizer = AdanConfig::new()
411            .with_epsilon(1e-8)
412            .with_weight_decay(0.02)
413            .init();
414
415        let grads = linear.forward(x.clone()).backward();
416        let grads = GradientsParams::from_grads(grads, &linear);
417        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
418
419        let grads = linear.forward(x).backward();
420        let grads = GradientsParams::from_grads(grads, &linear);
421        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
422
423        let state_updated = linear;
424        assert!(!state_updated.weight.to_data().as_slice::<f32>().unwrap()[0].is_nan());
425    }
426
427    fn given_linear_layer(weight: TensorData, bias: TensorData, device: &Device) -> Linear {
428        Linear {
429            weight: Param::from_data(weight, device),
430            bias: Some(Param::from_data(bias, device)),
431        }
432    }
433
434    fn create_adan() -> ModuleOptimizer {
435        let config = AdanConfig::new();
436        Adan {
437            momentum: AdaptiveNesterovMomentum {
438                beta_1: config.beta_1,
439                beta_2: config.beta_2,
440                beta_3: config.beta_3,
441                epsilon: config.epsilon,
442            },
443            weight_decay: config.weight_decay,
444            no_prox: config.no_prox,
445        }
446        .into()
447    }
448}