burn_optim/lr_scheduler/
cosine.rs1use 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#[derive(Config, Debug)]
21pub struct CosineAnnealingLrSchedulerConfig {
22 initial_lr: LearningRate,
24 #[config(default = 0.0)]
26 min_lr: LearningRate,
27 num_iters: usize,
29}
30
31impl CosineAnnealingLrSchedulerConfig {
32 pub(crate) fn build(&self) -> Result<CosineAnnealingLrScheduler, String> {
34 if self.initial_lr <= 0. || self.initial_lr > 1. {
35 return Err("Initial learning rate must be greater than 0 and at most 1".into());
36 }
37 if self.min_lr < 0.0 || self.min_lr > self.initial_lr {
38 return Err(
39 "Minimum learning rate must be at least 0 and at most equal to the initial \
40 learning rate"
41 .into(),
42 );
43 }
44 if self.num_iters == 0 {
45 return Err("Number of iterations must be at least 1".into());
46 }
47
48 Ok(CosineAnnealingLrScheduler {
49 min_lr: self.min_lr,
50 max_lr: self.initial_lr,
51 num_iters: self.num_iters,
52 current_iter: usize::MAX,
53 })
54 }
55
56 pub fn init(&self) -> Result<ModuleLrScheduler, String> {
66 self.build().map(|s| s.into())
67 }
68}
69
70#[derive(Clone, Copy, Debug)]
80pub struct CosineAnnealingLrScheduler {
81 min_lr: LearningRate,
82 max_lr: LearningRate,
83 num_iters: usize,
84 current_iter: usize,
85}
86
87impl LrScheduler for CosineAnnealingLrScheduler {
88 fn step(&mut self) -> LearningRate {
89 self.current_iter = self.current_iter.wrapping_add(1);
93 self.min_lr
94 + 0.5
95 * (self.max_lr - self.min_lr)
96 * (1.0
97 + (self.current_iter as f64 / self.num_iters as f64 * std::f64::consts::PI)
98 .cos())
99 }
100
101 fn to_record(&self) -> LrSchedulerRecord {
102 LrSchedulerRecord::from_state(&CosineAnnealingLrSchedulerState {
103 current_iter: self.current_iter,
104 })
105 }
106
107 fn load_record(&mut self, record: LrSchedulerRecord) {
108 if let Some(state) = record.into_state::<CosineAnnealingLrSchedulerState>() {
109 self.current_iter = state.current_iter;
110 }
111 }
112}
113
114#[derive(RecordState, Clone, Debug)]
116pub struct CosineAnnealingLrSchedulerState {
117 current_iter: usize,
118}
119
120#[cfg(test)]
121mod tests {
122 use super::super::test_utils;
123 use super::*;
124
125 #[test]
126 fn config_initial_lr_too_low() {
127 let r = CosineAnnealingLrSchedulerConfig::new(0., 10).build();
128 assert!(r.is_err(), "Should return an error");
129 assert_eq!(
130 r.unwrap_err(),
131 "Initial learning rate must be greater than 0 and at most 1",
132 "Error messages should match",
133 );
134 }
135
136 #[test]
137 fn config_initial_lr_too_high() {
138 let r = CosineAnnealingLrSchedulerConfig::new(1.5, 10).build();
139 assert!(r.is_err(), "Should return an error");
140 assert_eq!(
141 r.unwrap_err(),
142 "Initial learning rate must be greater than 0 and at most 1",
143 "Error messages should match",
144 );
145 }
146
147 #[test]
148 fn config_min_lr_too_low() {
149 let r = CosineAnnealingLrSchedulerConfig::new(0.5, 10)
150 .with_min_lr(-0.1)
151 .build();
152 assert!(r.is_err(), "Should return an error");
153 assert_eq!(
154 r.unwrap_err(),
155 "Minimum learning rate must be at least 0 and at most equal to the initial learning \
156 rate",
157 "Error messages should match",
158 );
159 }
160
161 #[test]
162 fn config_min_lr_too_high() {
163 let r = CosineAnnealingLrSchedulerConfig::new(0.5, 10)
164 .with_min_lr(0.6)
165 .build();
166 assert!(r.is_err(), "Should return an error");
167 assert_eq!(
168 r.unwrap_err(),
169 "Minimum learning rate must be at least 0 and at most equal to the initial learning \
170 rate",
171 "Error messages should match",
172 );
173 }
174
175 #[test]
176 fn config_num_iters_too_low() {
177 let r = CosineAnnealingLrSchedulerConfig::new(0.5, 0).build();
178 assert!(r.is_err(), "Should return an error");
179 assert_eq!(
180 r.unwrap_err(),
181 "Number of iterations must be at least 1",
182 "Error messages should match",
183 );
184 }
185
186 #[test]
187 fn test_lr_change() {
188 const INITIAL_LR: LearningRate = 0.5;
189 const MIN_LR: LearningRate = 0.1;
190
191 let scheduler = CosineAnnealingLrSchedulerConfig::new(INITIAL_LR, 2)
192 .with_min_lr(MIN_LR)
193 .build()
194 .unwrap();
195 let expected_lrs = [
196 INITIAL_LR, (INITIAL_LR + MIN_LR) * 0.5, MIN_LR, (INITIAL_LR + MIN_LR) * 0.5, INITIAL_LR, ];
202 test_utils::check_lr_sequence(scheduler, expected_lrs);
203 }
204
205 #[test]
206 fn test_save_and_load() {
207 const NUM_ITERS: usize = 9;
208 let scheduler = CosineAnnealingLrSchedulerConfig::new(1.0, NUM_ITERS)
209 .build()
210 .unwrap();
211 test_utils::check_save_load(scheduler, NUM_ITERS / 3 * 2);
212 }
213}