1use crate::error::{OptimError, Result};
11use crate::optimizers::Optimizer;
12use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
13use scirs2_core::numeric::Float;
14use std::fmt::Debug;
15
16#[derive(Debug, Clone)]
54pub struct LARS<A: Float> {
55 learning_rate: A,
56 momentum: A,
57 weight_decay: A,
58 trust_coefficient: A,
59 eps: A,
60 exclude_bias_and_norm: bool,
61 velocity: Option<Vec<Vec<A>>>,
63}
64
65impl<A: Float + ScalarOperand + Debug + Send + Sync> LARS<A> {
66 pub fn new(learning_rate: A) -> Self {
68 Self {
69 learning_rate,
70 momentum: A::from(0.9).expect("LARS: default momentum (0.9) must fit in A"),
71 weight_decay: A::from(0.0001)
72 .expect("LARS: default weight_decay (0.0001) must fit in A"),
73 trust_coefficient: A::from(0.001)
74 .expect("LARS: default trust_coefficient (0.001) must fit in A"),
75 eps: A::from(1e-8).expect("LARS: default eps (1e-8) must fit in A"),
76 exclude_bias_and_norm: true,
77 velocity: None,
78 }
79 }
80
81 pub fn with_momentum(mut self, momentum: A) -> Self {
83 self.momentum = momentum;
84 self
85 }
86
87 pub fn with_weight_decay(mut self, weight_decay: A) -> Self {
89 self.weight_decay = weight_decay;
90 self
91 }
92
93 pub fn with_trust_coefficient(mut self, trust_coefficient: A) -> Self {
95 self.trust_coefficient = trust_coefficient;
96 self
97 }
98
99 pub fn with_eps(mut self, eps: A) -> Self {
101 self.eps = eps;
102 self
103 }
104
105 pub fn with_exclude_bias_and_norm(mut self, exclude_bias_and_norm: bool) -> Self {
107 self.exclude_bias_and_norm = exclude_bias_and_norm;
108 self
109 }
110
111 pub fn reset(&mut self) {
113 self.velocity = None;
114 }
115
116 fn ensure_state(&mut self, index: usize, len: usize) {
118 let velocity = self.velocity.get_or_insert_with(Vec::new);
119 while velocity.len() <= index {
120 velocity.push(vec![A::zero(); len]);
121 }
122 if velocity[index].len() != len {
123 velocity[index] = vec![A::zero(); len];
124 }
125 }
126
127 pub fn step_indexed<D: Dimension>(
132 &mut self,
133 index: usize,
134 params: &Array<A, D>,
135 gradients: &Array<A, D>,
136 ) -> Result<Array<A, D>> {
137 if params.shape() != gradients.shape() {
138 return Err(OptimError::DimensionMismatch(format!(
139 "Incompatible shapes: parameters have shape {:?}, gradients have shape {:?}",
140 params.shape(),
141 gradients.shape()
142 )));
143 }
144
145 let is_bias_or_norm = params.ndim() <= 1;
147 let n_params = gradients.len();
148 self.ensure_state(index, n_params);
149
150 let weight_norm = params.mapv(|x| x * x).sum().sqrt();
152 let grad_norm = gradients.mapv(|x| x * x).sum().sqrt();
153
154 let should_apply_lars = !(self.exclude_bias_and_norm && is_bias_or_norm);
158
159 let local_lr = if should_apply_lars && weight_norm > A::zero() && grad_norm > A::zero() {
161 self.trust_coefficient * weight_norm
162 / (grad_norm + self.weight_decay * weight_norm + self.eps)
163 } else {
164 A::one()
165 };
166
167 let scaled_lr = self.learning_rate * local_lr;
168 let momentum = self.momentum;
169 let weight_decay = self.weight_decay;
170 let use_weight_decay = weight_decay > A::zero();
171
172 let velocity = self
173 .velocity
174 .as_mut()
175 .ok_or_else(|| OptimError::InvalidConfig("LARS state not initialized".to_string()))?;
176 let buffer = velocity.get_mut(index).ok_or_else(|| {
177 OptimError::InvalidConfig(format!("LARS has no velocity buffer for index {}", index))
178 })?;
179
180 let mut updated_params = params.clone();
181 for (slot, (p, g)) in buffer
182 .iter_mut()
183 .zip(updated_params.iter_mut().zip(gradients.iter()))
184 {
185 let grad = if use_weight_decay {
186 *g + weight_decay * *p
187 } else {
188 *g
189 };
190 *slot = momentum * *slot + grad * scaled_lr;
191 *p = *p - *slot;
192 }
193
194 Ok(updated_params)
195 }
196}
197
198impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync> Optimizer<A, D>
199 for LARS<A>
200{
201 fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
202 self.step_indexed(0, params, gradients)
203 }
204
205 fn step_list(
206 &mut self,
207 params_list: &[&Array<A, D>],
208 gradients_list: &[&Array<A, D>],
209 ) -> Result<Vec<Array<A, D>>> {
210 if params_list.len() != gradients_list.len() {
211 return Err(OptimError::InvalidConfig(format!(
212 "Number of parameter arrays ({}) does not match number of gradient arrays ({})",
213 params_list.len(),
214 gradients_list.len()
215 )));
216 }
217
218 let mut results = Vec::with_capacity(params_list.len());
219 for (index, (params, grads)) in params_list.iter().zip(gradients_list.iter()).enumerate() {
220 results.push(self.step_indexed(index, params, grads)?);
221 }
222 Ok(results)
223 }
224
225 fn set_learning_rate(&mut self, learning_rate: A) {
226 self.learning_rate = learning_rate;
227 }
228
229 fn get_learning_rate(&self) -> A {
230 self.learning_rate
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use approx::assert_abs_diff_eq;
238 use scirs2_core::ndarray::Array1;
239
240 #[test]
241 fn test_lars_creation() {
242 let optimizer = LARS::new(0.01);
243 assert_abs_diff_eq!(optimizer.learning_rate, 0.01);
244 assert_abs_diff_eq!(optimizer.momentum, 0.9);
245 assert_abs_diff_eq!(optimizer.weight_decay, 0.0001);
246 assert_abs_diff_eq!(optimizer.trust_coefficient, 0.001);
247 assert_abs_diff_eq!(optimizer.eps, 1e-8);
248 assert!(optimizer.exclude_bias_and_norm);
249 }
250
251 #[test]
252 fn test_lars_builder() {
253 let optimizer = LARS::new(0.01)
254 .with_momentum(0.95)
255 .with_weight_decay(0.0005)
256 .with_trust_coefficient(0.01)
257 .with_eps(1e-6)
258 .with_exclude_bias_and_norm(false);
259
260 assert_abs_diff_eq!(optimizer.momentum, 0.95);
261 assert_abs_diff_eq!(optimizer.weight_decay, 0.0005);
262 assert_abs_diff_eq!(optimizer.trust_coefficient, 0.01);
263 assert_abs_diff_eq!(optimizer.eps, 1e-6);
264 assert!(!optimizer.exclude_bias_and_norm);
265 }
266
267 #[test]
268 fn test_lars_update() {
269 let mut optimizer = LARS::new(0.1)
272 .with_momentum(0.9)
273 .with_weight_decay(0.0)
274 .with_trust_coefficient(1.0)
275 .with_exclude_bias_and_norm(false);
276
277 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
278 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
279
280 let updated_params = optimizer
282 .step(¶ms, &gradients)
283 .expect("optimizer.step succeeds in test_lars_update");
284
285 let weight_norm = params.mapv(|x| x * x).sum().sqrt();
290 let grad_norm = gradients.mapv(|x| x * x).sum().sqrt();
291 let scale = weight_norm / grad_norm;
292
293 assert_abs_diff_eq!(updated_params[0], 1.0 - 0.1 * scale * 0.1, epsilon = 1e-5);
294 assert_abs_diff_eq!(updated_params[1], 2.0 - 0.1 * scale * 0.2, epsilon = 1e-5);
295 assert_abs_diff_eq!(updated_params[2], 3.0 - 0.1 * scale * 0.3, epsilon = 1e-5);
296
297 let updated_params2 = optimizer
299 .step(&updated_params, &gradients)
300 .expect("step succeeds in test_lars_update");
301
302 assert!(updated_params2[0] < updated_params[0]);
305 assert!(updated_params2[1] < updated_params[1]);
306 assert!(updated_params2[2] < updated_params[2]);
307 }
308
309 #[test]
310 fn test_lars_weight_decay() {
311 let mut optimizer = LARS::new(0.01)
312 .with_momentum(0.0) .with_weight_decay(0.1)
314 .with_trust_coefficient(1.0)
315 .with_exclude_bias_and_norm(false);
316
317 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
318 let gradients = Array1::from_vec(vec![0.1, 0.2, 0.3]);
319
320 let updated_params = optimizer
321 .step(¶ms, &gradients)
322 .expect("optimizer.step succeeds in test_lars_weight_decay");
323
324 let weight_norm = params.mapv(|x| x * x).sum().sqrt();
329 let grad_norm = gradients.mapv(|x| x * x).sum().sqrt();
330 let expected_scale = weight_norm / (grad_norm + 0.1 * weight_norm);
331
332 let expected_p0 = 1.0 - 0.01 * expected_scale * (0.1 + 0.1 * 1.0);
334 let expected_p1 = 2.0 - 0.01 * expected_scale * (0.2 + 0.1 * 2.0);
335 let expected_p2 = 3.0 - 0.01 * expected_scale * (0.3 + 0.1 * 3.0);
336
337 assert_abs_diff_eq!(updated_params[0], expected_p0, epsilon = 1e-5);
338 assert_abs_diff_eq!(updated_params[1], expected_p1, epsilon = 1e-5);
339 assert_abs_diff_eq!(updated_params[2], expected_p2, epsilon = 1e-5);
340 }
341
342 #[test]
343 fn test_zero_gradients() {
344 let mut optimizer = LARS::new(0.01);
345 let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
346 let zero_gradients = Array1::zeros(3);
347
348 let updated_params = optimizer
349 .step(¶ms, &zero_gradients)
350 .expect("step succeeds in test_zero_gradients");
351
352 assert_abs_diff_eq!(updated_params[0], params[0], epsilon = 1e-3);
355 assert_abs_diff_eq!(updated_params[1], params[1], epsilon = 1e-3);
356 assert_abs_diff_eq!(updated_params[2], params[2], epsilon = 1e-3);
357 }
358
359 #[test]
360 fn test_exclude_bias_and_norm() {
361 let mut optimizer_excluded = LARS::new(0.01)
362 .with_momentum(0.0)
363 .with_weight_decay(0.0)
364 .with_exclude_bias_and_norm(true);
365
366 let mut optimizer_included = LARS::new(0.01)
367 .with_momentum(0.0)
368 .with_weight_decay(0.0)
369 .with_exclude_bias_and_norm(false);
370
371 let bias_params = Array1::from_vec(vec![0.1, 0.2]);
373 let bias_grads = Array1::from_vec(vec![0.01, 0.02]);
374
375 let updated_excluded = optimizer_excluded
376 .step(&bias_params, &bias_grads)
377 .expect("step succeeds in test_exclude_bias_and_norm");
378 let updated_included = optimizer_included
379 .step(&bias_params, &bias_grads)
380 .expect("step succeeds in test_exclude_bias_and_norm");
381
382 assert_abs_diff_eq!(updated_excluded[0], 0.1 - 0.01 * 0.01, epsilon = 1e-4);
384
385 let weight_norm = (0.1f64.powi(2) + 0.2f64.powi(2)).sqrt();
387 let grad_norm = (0.01f64.powi(2) + 0.02f64.powi(2)).sqrt();
388 let expected_factor = 0.001 * weight_norm / grad_norm; assert_abs_diff_eq!(
391 updated_included[0],
392 0.1 - 0.01 * expected_factor * 0.01,
393 epsilon = 1e-5
394 );
395 }
396
397 #[test]
404 fn test_exclude_bias_and_norm_is_decided_by_rank() {
405 use scirs2_core::ndarray::Array2;
406
407 let mut bias_opt = LARS::new(0.01)
409 .with_momentum(0.0)
410 .with_weight_decay(0.0)
411 .with_trust_coefficient(1.0)
412 .with_exclude_bias_and_norm(true);
413
414 let bias = Array1::from_vec(vec![1.0f64, 2.0, 3.0]);
415 let bias_grads = Array1::from_vec(vec![0.1f64, 0.2, 0.3]);
416 let updated_bias = bias_opt.step(&bias, &bias_grads).expect("bias step");
417
418 assert_abs_diff_eq!(updated_bias[0], 1.0 - 0.01 * 0.1, epsilon = 1e-12);
420 assert_abs_diff_eq!(updated_bias[2], 3.0 - 0.01 * 0.3, epsilon = 1e-12);
421
422 let mut weight_opt = LARS::new(0.01)
425 .with_momentum(0.0)
426 .with_weight_decay(0.0)
427 .with_trust_coefficient(1.0)
428 .with_exclude_bias_and_norm(true);
429
430 let weights =
431 Array2::from_shape_vec((3, 1), vec![1.0f64, 2.0, 3.0]).expect("valid 3x1 matrix");
432 let weight_grads =
433 Array2::from_shape_vec((3, 1), vec![0.1f64, 0.2, 0.3]).expect("valid 3x1 matrix");
434 let updated_weights = weight_opt
435 .step(&weights, &weight_grads)
436 .expect("weight step");
437
438 let weight_norm = weights.mapv(|x: f64| x * x).sum().sqrt();
439 let grad_norm = weight_grads.mapv(|x: f64| x * x).sum().sqrt();
440 let scale = weight_norm / (grad_norm + 1e-8);
441 assert_abs_diff_eq!(
442 updated_weights[[0, 0]],
443 1.0 - 0.01 * scale * 0.1,
444 epsilon = 1e-8
445 );
446
447 assert!((updated_bias[0] - updated_weights[[0, 0]]).abs() > 1e-6);
449 }
450}