1use crate::error::{OptimError, Result};
7use crate::optimizers::Optimizer;
8use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
9use scirs2_core::numeric::Float;
10use std::fmt::Debug;
11use std::marker::PhantomData;
12
13pub struct Lookahead<A, O, D>
54where
55 A: Float + ScalarOperand + Debug,
56 O: Optimizer<A, D> + Clone,
57 D: Dimension,
58{
59 inner_optimizer: O,
61 alpha: A,
63 k: usize,
65 current_step: usize,
67 slow_weights: Option<Array<A, D>>,
69 fast_weights: Option<Array<A, D>>,
71 use_slow_weights: bool,
73 _phantom: PhantomData<D>,
75}
76
77impl<A, O, D> Lookahead<A, O, D>
78where
79 A: Float + ScalarOperand + Debug,
80 O: Optimizer<A, D> + Clone,
81 D: Dimension,
82{
83 pub fn new(inner_optimizer: O) -> Self {
85 Self {
86 inner_optimizer,
87 alpha: A::from(0.5).expect("Lookahead: default alpha (0.5) must fit in A"),
89 k: 5, current_step: 0,
91 slow_weights: None,
92 fast_weights: None,
93 use_slow_weights: false,
94 _phantom: PhantomData,
95 }
96 }
97
98 pub fn with_config(inner_optimizer: O, alpha: A, k: usize) -> Self {
100 Self {
101 inner_optimizer,
102 alpha,
103 k,
104 current_step: 0,
105 slow_weights: None,
106 fast_weights: None,
107 use_slow_weights: false,
108 _phantom: PhantomData,
109 }
110 }
111
112 pub fn with_alpha(mut self, alpha: A) -> Self {
114 self.alpha = alpha;
115 self
116 }
117
118 pub fn with_k(mut self, k: usize) -> Self {
120 self.k = k;
121 self
122 }
123
124 pub fn inner_optimizer(&self) -> &O {
126 &self.inner_optimizer
127 }
128
129 pub fn inner_optimizer_mut(&mut self) -> &mut O {
131 &mut self.inner_optimizer
132 }
133
134 pub fn alpha(&self) -> A {
136 self.alpha
137 }
138
139 pub fn k(&self) -> usize {
141 self.k
142 }
143
144 pub fn use_slow_weights_for_eval(&mut self) {
147 self.use_slow_weights = true;
148 }
149
150 pub fn use_fast_weights_for_train(&mut self) {
153 self.use_slow_weights = false;
154 }
155
156 pub fn reset(&mut self) {
158 self.current_step = 0;
159 self.slow_weights = None;
160 self.fast_weights = None;
161 }
162}
163
164impl<A, O, D> Clone for Lookahead<A, O, D>
165where
166 A: Float + ScalarOperand + Debug,
167 O: Optimizer<A, D> + Clone,
168 D: Dimension,
169{
170 fn clone(&self) -> Self {
171 Self {
172 inner_optimizer: self.inner_optimizer.clone(),
173 alpha: self.alpha,
174 k: self.k,
175 current_step: self.current_step,
176 slow_weights: self.slow_weights.clone(),
177 fast_weights: self.fast_weights.clone(),
178 use_slow_weights: self.use_slow_weights,
179 _phantom: PhantomData,
180 }
181 }
182}
183
184impl<A, O, D> Debug for Lookahead<A, O, D>
185where
186 A: Float + ScalarOperand + Debug,
187 O: Optimizer<A, D> + Clone + Debug,
188 D: Dimension,
189{
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 f.debug_struct("Lookahead")
192 .field("inner_optimizer", &self.inner_optimizer)
193 .field("alpha", &self.alpha)
194 .field("k", &self.k)
195 .field("current_step", &self.current_step)
196 .field("use_slow_weights", &self.use_slow_weights)
197 .finish()
198 }
199}
200
201impl<A, O, D> Optimizer<A, D> for Lookahead<A, O, D>
202where
203 A: Float + ScalarOperand + Debug + Send + Sync,
204 O: Optimizer<A, D> + Clone + Send + Sync,
205 D: Dimension,
206{
207 fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
208 if self.slow_weights.is_none() {
210 self.slow_weights = Some(params.clone());
211 self.fast_weights = Some(params.clone());
212 }
213
214 let fast_weights = match &mut self.fast_weights {
216 Some(w) => w,
217 None => {
218 return Err(OptimError::OptimizationError(
219 "Fast weights not initialized".to_string(),
220 ))
221 }
222 };
223
224 let slow_weights = match &mut self.slow_weights {
225 Some(w) => w,
226 None => {
227 return Err(OptimError::OptimizationError(
228 "Slow weights not initialized".to_string(),
229 ))
230 }
231 };
232
233 *fast_weights = self.inner_optimizer.step(fast_weights, gradients)?;
235
236 self.current_step += 1;
238
239 if self.current_step >= self.k {
241 let diff = &*fast_weights - &*slow_weights;
244
245 *slow_weights = &*slow_weights + &(diff * self.alpha);
247
248 *fast_weights = slow_weights.clone();
250
251 self.current_step = 0;
253 }
254
255 if self.use_slow_weights {
257 Ok(slow_weights.clone())
258 } else {
259 Ok(fast_weights.clone())
260 }
261 }
262
263 fn set_learning_rate(&mut self, learning_rate: A) {
264 self.inner_optimizer.set_learning_rate(learning_rate);
265 }
266
267 fn get_learning_rate(&self) -> A {
268 self.inner_optimizer.get_learning_rate()
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::optimizers::sgd::SGD;
276 use approx::assert_abs_diff_eq;
277 use scirs2_core::ndarray::Array1;
278
279 #[test]
280 fn test_lookahead_creation() {
281 let sgd = SGD::new(0.01);
282 let optimizer: Lookahead<f64, SGD<f64>, scirs2_core::ndarray::Ix1> = Lookahead::new(sgd);
283
284 assert_abs_diff_eq!(optimizer.alpha(), 0.5);
285 assert_eq!(optimizer.k(), 5);
286 assert_abs_diff_eq!(optimizer.get_learning_rate(), 0.01);
287 }
288
289 #[test]
290 fn test_lookahead_with_config() {
291 let sgd = SGD::new(0.01);
292 let optimizer: Lookahead<f64, SGD<f64>, scirs2_core::ndarray::Ix1> =
293 Lookahead::with_config(sgd, 0.8, 10);
294
295 assert_abs_diff_eq!(optimizer.alpha(), 0.8);
296 assert_eq!(optimizer.k(), 10);
297 }
298
299 #[test]
300 fn test_lookahead_step() {
301 let mut sgd = SGD::new(0.1);
302 sgd.set_momentum(0.0);
303 let mut optimizer: Lookahead<f64, SGD<f64>, scirs2_core::ndarray::Ix1> =
304 Lookahead::with_config(sgd, 0.5, 2);
305
306 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
307 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
308
309 let updated_params = optimizer
311 .step(¶ms, &gradients)
312 .expect("optimizer.step succeeds in test_lookahead_step");
313
314 assert_abs_diff_eq!(updated_params[0], 0.99, epsilon = 1e-6);
317 assert_abs_diff_eq!(updated_params[1], 1.98, epsilon = 1e-6);
318 assert_abs_diff_eq!(updated_params[2], 2.97, epsilon = 1e-6);
319
320 let updated_params2 = optimizer
322 .step(&updated_params, &gradients)
323 .expect("step succeeds in test_lookahead_step");
324
325 assert_abs_diff_eq!(updated_params2[0], 0.99, epsilon = 1e-6);
334 assert_abs_diff_eq!(updated_params2[1], 1.98, epsilon = 1e-6);
335 assert_abs_diff_eq!(updated_params2[2], 2.97, epsilon = 1e-6);
336
337 let updated_params3 = optimizer
339 .step(&updated_params2, &gradients)
340 .expect("step succeeds in test_lookahead_step");
341
342 assert_abs_diff_eq!(updated_params3[0], 0.98, epsilon = 1e-6);
344 assert_abs_diff_eq!(updated_params3[1], 1.96, epsilon = 1e-6);
345 assert_abs_diff_eq!(updated_params3[2], 2.94, epsilon = 1e-6);
346 }
347
348 #[test]
349 fn test_slow_weights_for_eval() {
350 let mut sgd = SGD::new(0.1);
351 sgd.set_momentum(0.0);
352 let mut optimizer: Lookahead<f64, SGD<f64>, scirs2_core::ndarray::Ix1> =
353 Lookahead::with_config(sgd, 0.5, 2);
354
355 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
356 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
357
358 let updated_params = optimizer
360 .step(¶ms, &gradients)
361 .expect("optimizer.step succeeds in test_slow_weights_for_eval");
362
363 optimizer.use_slow_weights_for_eval();
365
366 let eval_params = optimizer
368 .step(&updated_params, &gradients)
369 .expect("step succeeds in test_slow_weights_for_eval");
370
371 assert_abs_diff_eq!(eval_params[0], 0.99, epsilon = 1e-6);
375 assert_abs_diff_eq!(eval_params[1], 1.98, epsilon = 1e-6);
376 assert_abs_diff_eq!(eval_params[2], 2.97, epsilon = 1e-6);
377
378 optimizer.use_fast_weights_for_train();
380
381 let train_params = optimizer
383 .step(&eval_params, &gradients)
384 .expect("step succeeds in test_slow_weights_for_eval");
385 assert!(train_params[0] < 1.0);
386 }
387
388 #[test]
389 fn test_reset() {
390 let sgd = SGD::new(0.1);
391 let mut optimizer: Lookahead<f64, SGD<f64>, scirs2_core::ndarray::Ix1> =
392 Lookahead::new(sgd);
393
394 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
395 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
396
397 let _ = optimizer
399 .step(¶ms, &gradients)
400 .expect("optimizer.step succeeds in test_reset");
401
402 optimizer.reset();
404
405 let updated_params = optimizer
407 .step(¶ms, &gradients)
408 .expect("optimizer.step succeeds in test_reset");
409 assert_abs_diff_eq!(updated_params[0], 0.99, epsilon = 1e-6);
411 }
412}