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#[derive(Config, Debug)]
20pub struct AdanConfig {
21 #[config(default = 0.98)]
23 beta_1: f32,
24 #[config(default = 0.92)]
26 beta_2: f32,
27 #[config(default = 0.99)]
29 beta_3: f32,
30 #[config(default = 1e-8)]
32 epsilon: f32,
33 #[config(default = 0.0)]
35 weight_decay: f32,
36 #[config(default = false)]
38 no_prox: bool,
39 grad_clipping: Option<GradientClippingConfig>,
41}
42
43#[derive(Clone)]
50pub struct Adan {
51 momentum: AdaptiveNesterovMomentum,
52 weight_decay: f32,
53 no_prox: bool,
54}
55
56#[derive(RecordState, Clone, new)]
58pub struct AdanState<const D: usize> {
59 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 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 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#[derive(RecordState, Clone, new)]
133pub struct AdaptiveNesterovMomentumState<const D: usize> {
134 pub time: usize,
136 pub exp_avg: Tensor<D>,
138 pub exp_avg_sq: Tensor<D>,
140 pub exp_avg_diff: Tensor<D>,
142 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}