Skip to main content

burn_optim/optim/
rmsprop.rs

1use burn_core as burn;
2
3use crate::RecordState;
4
5use super::{
6    Optimizer,
7    decay::{WeightDecay, WeightDecayConfig},
8    module_optimizer::ModuleOptimizer,
9};
10use crate::{LearningRate, grad_clipping::GradientClippingConfig};
11
12use burn::config::Config;
13use burn::tensor::{Device, Tensor};
14
15/// Configuration to create the [RmsProp](RmsProp) optimizer.
16#[derive(Config, Debug)]
17pub struct RmsPropConfig {
18    /// Smoothing constant.
19    #[config(default = 0.99)]
20    alpha: f32,
21    /// momentum for RmsProp.
22    #[config(default = 0.9)]
23    momentum: f32,
24    /// A value required for numerical stability.
25    #[config(default = 1e-5)]
26    epsilon: f32,
27    /// if True, compute the centered RmsProp, the gradient is normalized by an estimation of its variance
28    #[config(default = false)]
29    centered: bool,
30    /// [Weight decay](WeightDecayConfig) config.
31    weight_decay: Option<WeightDecayConfig>,
32    /// [Gradient Clipping](GradientClippingConfig) config.
33    grad_clipping: Option<GradientClippingConfig>,
34}
35
36impl RmsPropConfig {
37    /// Build a [`RmsProp`] from the config.
38    pub(crate) fn build(&self) -> RmsProp {
39        let weight_decay = self.weight_decay.as_ref().map(WeightDecay::new);
40        RmsProp {
41            alpha: self.alpha,
42            centered: self.centered,
43            weight_decay,
44            momentum: RmsPropMomentum {
45                momentum: self.momentum,
46                epsilon: self.epsilon,
47            },
48        }
49    }
50
51    /// Initialize RmsProp optimizer.
52    ///
53    /// # Returns
54    ///
55    /// Returns an optimizer that can be used to optimize a module.
56    pub fn init(&self) -> ModuleOptimizer {
57        let mut optim = ModuleOptimizer::from(self.build());
58        if let Some(config) = &self.grad_clipping {
59            optim = optim.with_grad_clipping(config.init());
60        }
61
62        optim
63    }
64}
65
66/// Optimizer that implements stochastic gradient descent with momentum.
67/// The optimizer can be configured with [RmsPropConfig](RmsPropConfig).
68#[derive(Clone)]
69pub struct RmsProp {
70    alpha: f32,
71    // epsilon: f32,
72    centered: bool,
73    // momentum: Option<Momentum>,
74    momentum: RmsPropMomentum,
75    weight_decay: Option<WeightDecay>,
76}
77
78impl Optimizer for RmsProp {
79    type State<const D: usize> = RmsPropState<D>;
80
81    fn step<const D: usize>(
82        &self,
83        lr: LearningRate,
84        tensor: Tensor<D>,
85        mut grad: Tensor<D>,
86        state: Option<Self::State<D>>,
87    ) -> (Tensor<D>, Option<Self::State<D>>) {
88        // fetch state for params
89        let mut state_square_avg = None;
90        let mut state_centered = None;
91        let mut state_momentum = None;
92        if let Some(state) = state {
93            state_square_avg = Some(state.square_avg);
94            state_centered = Some(state.centered);
95            state_momentum = state.momentum;
96        }
97
98        // weight_decay transform
99        if let Some(weight_decay) = &self.weight_decay {
100            grad = weight_decay.transform(grad, tensor.clone());
101        }
102
103        // square_avg transform
104        let (grad, state_square_avg) =
105            SquareAvgState::transform(self.alpha, grad, state_square_avg);
106
107        // centered transform
108        let (grad, state_square_avg, state_centered) = CenteredState::transform(
109            self.alpha,
110            self.centered,
111            grad,
112            state_square_avg,
113            state_centered,
114        );
115
116        // momentum transform
117        let (grad, state_centered, state_momentum) =
118            self.momentum
119                .transform(grad, state_centered, state_momentum);
120
121        // transition state
122        let state = RmsPropState::new(state_square_avg, state_centered, state_momentum);
123
124        // tensor param transform
125        let delta = grad.mul_scalar(lr);
126        (tensor - delta, Some(state))
127    }
128
129    fn to_device<const D: usize>(mut state: Self::State<D>, device: &Device) -> Self::State<D> {
130        state.square_avg = state.square_avg.to_device(device);
131        state.centered = state.centered.to_device(device);
132        state.momentum = state.momentum.map(|momentum| momentum.to_device(device));
133        state
134    }
135}
136
137/// State of [RmsProp](RmsProp)
138#[derive(RecordState, Clone, new)]
139pub struct RmsPropState<const D: usize> {
140    /// Current squared average state.
141    pub square_avg: SquareAvgState<D>,
142    /// Current centered state
143    pub centered: CenteredState<D>,
144    /// Current gradient momentum, if any.
145    pub momentum: Option<RmsPropMomentumState<D>>,
146}
147
148/// [SquareAvgState](SquareAvgState) is to store and pass optimizer step params.
149#[derive(RecordState, Clone, new)]
150pub struct SquareAvgState<const D: usize> {
151    /// Current squared average.
152    pub square_avg: Tensor<D>,
153}
154
155impl<const D: usize> SquareAvgState<D> {
156    /// transform [SquareAvgState] to the next step
157    fn transform(alpha: f32, grad: Tensor<D>, state: Option<Self>) -> (Tensor<D>, Self) {
158        match state {
159            Some(state) => {
160                let square_avg = state
161                    .square_avg
162                    .mul_scalar(alpha)
163                    .add(grad.clone().square().mul_scalar(1. - alpha));
164                (grad, Self { square_avg })
165            }
166            _ => {
167                let square_avg = grad.clone().square().mul_scalar(1. - alpha);
168                (grad, Self { square_avg })
169            }
170        }
171    }
172
173    /// Moves the state to a device.
174    ///
175    /// # Arguments
176    ///
177    /// * `device` - Device to move the state to.
178    ///
179    /// # Returns
180    ///
181    /// * `self` - Moved state.
182    pub fn to_device(mut self, device: &Device) -> Self {
183        self.square_avg = self.square_avg.to_device(device);
184        self
185    }
186}
187
188/// [CenteredState](CenteredState) is to store and pass optimizer step params.
189#[derive(RecordState, Clone, new)]
190pub struct CenteredState<const D: usize> {
191    /// The averaged gradient to calculate the centered gradient, if available.
192    pub grad_avg: Option<Tensor<D>>,
193    /// The current average value.
194    pub avg: Tensor<D>,
195}
196
197impl<const D: usize> CenteredState<D> {
198    /// transform [CenteredState] to the next step
199    fn transform(
200        alpha: f32,
201        centered: bool,
202        grad: Tensor<D>,
203        square_avg_state: SquareAvgState<D>,
204        centered_state: Option<Self>,
205    ) -> (Tensor<D>, SquareAvgState<D>, Self) {
206        if centered {
207            let grad_avg_constant = grad.clone().mul_scalar(1. - alpha);
208            let grad_avg = match centered_state {
209                Some(state) => state
210                    .grad_avg
211                    .map_or(grad_avg_constant.clone(), move |grad_avg| {
212                        grad_avg.mul_scalar(alpha).add(grad_avg_constant)
213                    }),
214                _ => grad_avg_constant,
215            };
216            let avg = square_avg_state
217                .square_avg
218                .clone()
219                .sub(grad_avg.clone().square());
220
221            (
222                grad,
223                square_avg_state,
224                Self {
225                    grad_avg: Some(grad_avg),
226                    avg,
227                },
228            )
229        } else {
230            (
231                grad,
232                square_avg_state.clone(),
233                Self {
234                    grad_avg: None,
235                    avg: square_avg_state.square_avg,
236                },
237            )
238        }
239    }
240
241    /// Moves the state to a device.
242    ///
243    /// # Arguments
244    ///
245    /// * `device` - Device to move the state to.
246    ///
247    /// # Returns
248    ///
249    /// * `self` - Moved state.
250    pub fn to_device(mut self, device: &Device) -> Self {
251        self.grad_avg = self.grad_avg.map(|grad_avg| grad_avg.to_device(device));
252        self.avg = self.avg.to_device(device);
253        self
254    }
255}
256
257/// [RmsPropMomentum](RmsPropMomentum) is to store config status for optimizer.
258/// (, which is stored in [optimizer](RmsProp) itself and not passed in during `step()` calculation)
259#[derive(Clone)]
260pub struct RmsPropMomentum {
261    momentum: f32,
262    epsilon: f32,
263}
264
265impl RmsPropMomentum {
266    /// transform [grad](Tensor) and [RmsPropMomentumState] to the next step
267    fn transform<const D: usize>(
268        &self,
269        grad: Tensor<D>,
270        centered_state: CenteredState<D>,
271        momentum_state: Option<RmsPropMomentumState<D>>,
272    ) -> (Tensor<D>, CenteredState<D>, Option<RmsPropMomentumState<D>>) {
273        let grad = grad.div(centered_state.avg.clone().sqrt().add_scalar(self.epsilon));
274
275        if self.momentum > 0. {
276            let buf = match momentum_state {
277                Some(state) => state.buf.mul_scalar(self.momentum).add(grad),
278                _ => grad,
279            };
280            (
281                buf.clone(),
282                centered_state,
283                Some(RmsPropMomentumState { buf }),
284            )
285        } else {
286            (grad, centered_state, None)
287        }
288    }
289}
290
291/// [RmsPropMomentumState](RmsPropMomentumState) is to store and pass optimizer step params.
292#[derive(RecordState, Clone, new)]
293pub struct RmsPropMomentumState<const D: usize> {
294    buf: Tensor<D>,
295}
296
297impl<const D: usize> RmsPropMomentumState<D> {
298    /// Moves the state to a device.
299    ///
300    /// # Arguments
301    ///
302    /// * `device` - Device to move the state to.
303    ///
304    /// # Returns
305    ///
306    /// * `self` - Moved state.
307    pub fn to_device(mut self, device: &Device) -> Self {
308        self.buf = self.buf.to_device(device);
309        self
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use burn::tensor::Tolerance;
316
317    use super::*;
318    use crate::optim::GradientsParams;
319    use burn::module::Param;
320    use burn::tensor::{Distribution, Tensor, TensorData};
321    use burn_nn::{Linear, LinearConfig};
322
323    type FT = f32;
324
325    const LEARNING_RATE: LearningRate = 0.01;
326
327    #[test]
328    fn test_rmsprop_optimizer_save_load_state() {
329        let device = Device::default().autodiff();
330        let linear = LinearConfig::new(6, 6).init(&device);
331        let x = Tensor::<2>::random([2, 6], Distribution::Default, &device);
332        let mut optimizer = create_rmsprop();
333        let grads = linear.forward(x).backward();
334        let grads = GradientsParams::from_grads(grads, &linear);
335        let _linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
336
337        let bytes = optimizer.into_bytes().unwrap();
338        assert!(!bytes.is_empty());
339
340        #[cfg(feature = "std")]
341        optimizer
342            .save(std::env::temp_dir().as_path().join("test_optim_rmsprop"))
343            .unwrap();
344
345        let state_optim_before = optimizer.to_record();
346        let optimizer = create_rmsprop().from_bytes(bytes).unwrap();
347        let state_optim_after = optimizer.to_record();
348
349        assert_eq!(state_optim_before.len(), state_optim_after.len());
350    }
351
352    /// used for test differences and debug
353    #[test]
354    fn test_rmsprop_optimizer_with_numbers_basic() {
355        let device = Device::default().autodiff();
356        let linear = given_linear_layer(
357            TensorData::from([
358                [1., 1., 1., 1., 1., 1.],
359                [1., 1., 1., 1., 1., 1.],
360                [1., 1., 1., 1., 1., 1.],
361                [1., 1., 1., 1., 1., 1.],
362                [1., 1., 1., 1., 1., 1.],
363                [1., 1., 1., 1., 1., 1.],
364            ]),
365            TensorData::from([0.5, 0.5, 0.5, 0.5, 0.5, 0.5]),
366            &device,
367        );
368        let x_1 = Tensor::<2>::from_floats(
369            [
370                [0.6294, 0.0940, 0.8176, 0.8824, 0.5228, 0.4310],
371                [0.7152, 0.9559, 0.7893, 0.5684, 0.5939, 0.8883],
372            ],
373            &device,
374        )
375        .require_grad();
376        let x_2 = Tensor::<2>::from_floats(
377            [
378                [0.8491, 0.2108, 0.8939, 0.4433, 0.5527, 0.2528],
379                [0.3270, 0.0412, 0.5538, 0.9605, 0.3195, 0.9085],
380            ],
381            &device,
382        )
383        .require_grad();
384
385        let mut optimizer = RmsPropConfig::new()
386            .with_alpha(0.99)
387            .with_epsilon(1e-8)
388            .with_weight_decay(WeightDecayConfig::new(0.05).into())
389            .with_momentum(0.9)
390            .with_centered(false)
391            .init();
392
393        // println!("linear is {:?}", linear);
394        let grads = linear.forward(x_1).backward();
395        let grads = GradientsParams::from_grads(grads, &linear);
396        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
397
398        // println!("linear is {:?}", linear);
399        let grads = linear.forward(x_2).backward();
400        let grads = GradientsParams::from_grads(grads, &linear);
401        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
402
403        // println!("linear is {:?}", linear);
404        let state_updated = linear;
405
406        let (weight_updated, bias_updated) = (
407            state_updated.weight.to_data(),
408            state_updated.bias.unwrap().to_data(),
409        );
410
411        // println!("\nweight_updated\n{:?}", weight_updated);
412        // println!("\nbias_updated\n{:?}", bias_updated);
413
414        let weights_expected = TensorData::from([
415            [0.743937, 0.743937, 0.743937, 0.743937, 0.743937, 0.743937],
416            [0.783809, 0.783809, 0.783809, 0.783809, 0.783809, 0.783809],
417            [0.742881, 0.742881, 0.742881, 0.742881, 0.742881, 0.742881],
418            [0.740366, 0.740366, 0.740366, 0.740366, 0.740366, 0.740366],
419            [0.748005, 0.748005, 0.748005, 0.748005, 0.748005, 0.748005],
420            [0.743710, 0.743710, 0.743710, 0.743710, 0.743710, 0.743710],
421        ]);
422        let bias_expected =
423            TensorData::from([0.239199, 0.239199, 0.239199, 0.239199, 0.239199, 0.239199]);
424
425        let tolerance = Tolerance::absolute(1e-6);
426        bias_updated.assert_approx_eq::<FT>(&bias_expected, tolerance);
427        weight_updated.assert_approx_eq::<FT>(&weights_expected, tolerance);
428    }
429
430    #[test]
431    fn test_rmsprop_optimizer_with_numbers() {
432        let device = Device::default().autodiff();
433        let linear = given_linear_layer(
434            TensorData::from([
435                [-0.3206, 0.1374, 0.4043, 0.3200, 0.0859, 0.0671],
436                [0.0777, -0.0185, -0.3667, 0.2550, 0.1955, -0.2922],
437                [-0.0190, 0.0346, -0.2962, 0.2484, -0.2780, 0.3130],
438                [-0.2980, -0.2214, -0.3715, -0.2981, -0.0761, 0.1626],
439                [0.3300, -0.2182, 0.3717, -0.1729, 0.3796, -0.0304],
440                [-0.0159, -0.0120, 0.1258, 0.1921, 0.0293, 0.3833],
441            ]),
442            TensorData::from([-0.3905, 0.0884, -0.0970, 0.1176, 0.1366, 0.0130]),
443            &device,
444        );
445        let x_1 = Tensor::<2>::from_floats(
446            [
447                [0.6294, 0.0940, 0.8176, 0.8824, 0.5228, 0.4310],
448                [0.7152, 0.9559, 0.7893, 0.5684, 0.5939, 0.8883],
449            ],
450            &device,
451        )
452        .require_grad();
453        let x_2 = Tensor::<2>::from_floats(
454            [
455                [0.8491, 0.2108, 0.8939, 0.4433, 0.5527, 0.2528],
456                [0.3270, 0.0412, 0.5538, 0.9605, 0.3195, 0.9085],
457            ],
458            &device,
459        )
460        .require_grad();
461
462        let mut optimizer = RmsPropConfig::new()
463            .with_alpha(0.99)
464            .with_epsilon(1e-8)
465            .with_weight_decay(WeightDecayConfig::new(0.05).into())
466            .with_momentum(0.9)
467            .with_centered(false)
468            .init();
469
470        let grads = linear.forward(x_1).backward();
471        let grads = GradientsParams::from_grads(grads, &linear);
472        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
473
474        let grads = linear.forward(x_2).backward();
475        let grads = GradientsParams::from_grads(grads, &linear);
476        let linear = optimizer.step(LEARNING_RATE.into(), linear, grads);
477
478        let state_updated = linear;
479        let weights_expected = TensorData::from([
480            [
481                -0.576399, -0.118494, 0.148353, 0.064070, -0.169983, -0.188779,
482            ],
483            [
484                -0.135571, -0.231448, -0.578445, 0.041143, -0.018162, -0.504207,
485            ],
486            [
487                -0.275990, -0.222397, -0.553153, -0.008625, -0.534956, 0.055967,
488            ],
489            [
490                -0.557575, -0.480979, -0.631072, -0.557675, -0.335686, -0.096997,
491            ],
492            [
493                0.078313, -0.469618, 0.119993, -0.424341, 0.127890, -0.281912,
494            ],
495            [
496                -0.271996, -0.268097, -0.130324, -0.064037, -0.226805, 0.127126,
497            ],
498        ]);
499        let bias_expected = TensorData::from([
500            -0.651299, -0.172400, -0.357800, -0.143200, -0.124200, -0.247800,
501        ]);
502
503        let (weight_updated, bias_updated) = (
504            state_updated.weight.to_data(),
505            state_updated.bias.unwrap().to_data(),
506        );
507
508        // println!("\nweight_updated\n{:?}", weight_updated);
509        // println!("\nbias_updated\n{:?}", bias_updated);
510
511        let tolerance = Tolerance::absolute(1e-6);
512        bias_updated.assert_approx_eq::<FT>(&bias_expected, tolerance);
513        weight_updated.assert_approx_eq::<FT>(&weights_expected, tolerance);
514    }
515
516    fn given_linear_layer(weight: TensorData, bias: TensorData, device: &Device) -> Linear {
517        Linear {
518            weight: Param::from_data(weight, device),
519            bias: Some(Param::from_data(bias, device)),
520        }
521    }
522
523    fn create_rmsprop() -> ModuleOptimizer {
524        RmsPropConfig {
525            alpha: 0.99,
526            epsilon: 1e-9,
527            centered: false,
528            weight_decay: Some(WeightDecayConfig { penalty: 0.05 }),
529            momentum: 0.9,
530            grad_clipping: None,
531        }
532        .init()
533    }
534}