Skip to main content

burn_optim/lr_scheduler/
linear.rs

1use burn_core as burn;
2
3use super::{LrScheduler, LrSchedulerRecord, String};
4use crate::LearningRate;
5use crate::RecordState;
6use crate::lr_scheduler::module_lr_scheduler::ModuleLrScheduler;
7use burn::config::Config;
8
9/// The configuration for creating a [linear learning rate scheduler](LinearLrScheduler).
10///
11/// This scheduler returns the learning rate `initial_lr` at the first step, then changes it by a
12/// constant amount on each iteration until reaching a final learning rate `final_lr`. The
13/// `num_iters` parameter controls how many iterations are needed to go from `initial_lr` to
14/// `final_lr`.
15#[derive(Config, Debug)]
16pub struct LinearLrSchedulerConfig {
17    // The initial learning rate.
18    initial_lr: LearningRate,
19    // The final learning rate.
20    final_lr: LearningRate,
21    // The number of iterations before reaching the final learning rate.
22    num_iters: usize,
23}
24
25impl LinearLrSchedulerConfig {
26    /// Initializes a [linear learning rate scheduler](LinearLrScheduler).
27    pub(crate) fn build(&self) -> Result<LinearLrScheduler, String> {
28        if self.initial_lr <= 0. || self.initial_lr > 1. {
29            return Err("Initial learning rate must be greater than 0 and at most 1".into());
30        }
31        if self.final_lr < 0. || self.final_lr > 1. {
32            return Err("Final learning rate must be at least 0 and at most 1".into());
33        }
34        if self.num_iters == 0 {
35            return Err("Number of iterations must be at least 1".into());
36        }
37
38        Ok(LinearLrScheduler {
39            final_lr: self.final_lr,
40            step_size: (self.final_lr - self.initial_lr) / self.num_iters as f64,
41            remaining_iters: self.num_iters + 1,
42        })
43    }
44
45    /// Initializes a [module learning rate scheduler](ModuleLrScheduler).
46    ///
47    /// # Errors
48    ///
49    /// An error will be returned if any of the following conditions is true:
50    ///
51    /// * `initial_lr` is out of range (0.0, 1.0]
52    /// * `final_lr` is out of range [0.0, 1.0]
53    /// * `num_iters` is 0
54    pub fn init(&self) -> Result<ModuleLrScheduler, String> {
55        self.build().map(|s| s.into())
56    }
57}
58
59/// A linear learning rate scheduler.
60///
61/// See [LinearLrSchedulerConfig] for more information.
62#[derive(Clone, Copy, Debug)]
63pub struct LinearLrScheduler {
64    // The final learning rate after the linear changing process stops.
65    final_lr: LearningRate,
66    // The amount that the learning rate changes by on each iteration.
67    step_size: f64,
68    // The number of iterations left before reaching the final learning rate.
69    remaining_iters: usize,
70}
71
72impl LrScheduler for LinearLrScheduler {
73    fn step(&mut self) -> LearningRate {
74        self.remaining_iters -= (self.remaining_iters != 0) as usize;
75        self.final_lr - self.step_size * self.remaining_iters as f64
76    }
77
78    fn to_record(&self) -> LrSchedulerRecord {
79        LrSchedulerRecord::from_state(&LinearLrSchedulerState {
80            remaining_iters: self.remaining_iters,
81        })
82    }
83
84    fn load_record(&mut self, record: LrSchedulerRecord) {
85        if let Some(state) = record.into_state::<LinearLrSchedulerState>() {
86            self.remaining_iters = state.remaining_iters;
87        }
88    }
89}
90
91/// The serializable state of a [linear scheduler](LinearLrScheduler).
92#[derive(RecordState, Clone, Debug)]
93pub struct LinearLrSchedulerState {
94    remaining_iters: usize,
95}
96
97#[cfg(test)]
98mod tests {
99    use super::super::test_utils;
100    use super::*;
101
102    #[test]
103    fn config_initial_lr_too_low() {
104        let r = LinearLrSchedulerConfig::new(0., 0.5, 100).build();
105        assert!(r.is_err(), "Should return an error");
106        assert_eq!(
107            r.unwrap_err(),
108            "Initial learning rate must be greater than 0 and at most 1",
109            "Error messages should match",
110        );
111    }
112
113    #[test]
114    fn config_initial_lr_too_high() {
115        let r = LinearLrSchedulerConfig::new(1.5, 0.5, 100).build();
116        assert!(r.is_err(), "Should return an error");
117        assert_eq!(
118            r.unwrap_err(),
119            "Initial learning rate must be greater than 0 and at most 1",
120            "Error messages should match",
121        );
122    }
123
124    #[test]
125    fn config_final_lr_too_low() {
126        let r = LinearLrSchedulerConfig::new(0.5, -0.5, 100).build();
127        assert!(r.is_err(), "Should return an error");
128        assert_eq!(
129            r.unwrap_err(),
130            "Final learning rate must be at least 0 and at most 1",
131            "Error messages should match",
132        );
133    }
134
135    #[test]
136    fn config_final_lr_too_high() {
137        let r = LinearLrSchedulerConfig::new(0.5, 1.5, 100).build();
138        assert!(r.is_err(), "Should return an error");
139        assert_eq!(
140            r.unwrap_err(),
141            "Final learning rate must be at least 0 and at most 1",
142            "Error messages should match",
143        );
144    }
145
146    #[test]
147    fn config_num_iters_too_low() {
148        let r = LinearLrSchedulerConfig::new(0.9, 0.1, 0).build();
149        assert!(r.is_err(), "Should return an error");
150        assert_eq!(
151            r.unwrap_err(),
152            "Number of iterations must be at least 1",
153            "Error messages should match",
154        );
155    }
156
157    #[test]
158    fn test_lr_decreasing() {
159        let scheduler = LinearLrSchedulerConfig::new(0.9, 0.5, 4).build().unwrap();
160        let expected_lrs = [0.9, 0.8, 0.7, 0.6, 0.5, 0.5];
161        test_utils::check_lr_sequence(scheduler, expected_lrs);
162    }
163
164    #[test]
165    fn test_lr_increasing() {
166        let scheduler = LinearLrSchedulerConfig::new(0.01, 0.04, 3).build().unwrap();
167        let expected_lrs = [0.01, 0.02, 0.03, 0.04, 0.04];
168        test_utils::check_lr_sequence(scheduler, expected_lrs);
169    }
170
171    #[test]
172    fn test_lr_unchanging() {
173        let scheduler = LinearLrSchedulerConfig::new(0.3, 0.3, 2).build().unwrap();
174        let expected_lrs = [0.3, 0.3, 0.3, 0.3];
175        test_utils::check_lr_sequence(scheduler, expected_lrs);
176    }
177
178    #[test]
179    fn test_save_and_load() {
180        const NUM_ITERS: usize = 6;
181        let scheduler = LinearLrSchedulerConfig::new(1.0, 0.01, NUM_ITERS)
182            .build()
183            .unwrap();
184        test_utils::check_save_load(scheduler, NUM_ITERS / 3 * 2);
185    }
186}