1pub mod kfac;
7pub mod newton_cg;
8
9use crate::error::{OptimError, Result};
10use scirs2_core::ndarray::{Array, Array1, Array2, Dimension, ScalarOperand};
11use scirs2_core::numeric::Float;
12use std::collections::VecDeque;
13use std::fmt::Debug;
14
15pub use self::kfac::{KFACConfig, KFACLayerState, KFACStats, LayerInfo, LayerType, KFAC};
16pub use self::newton_cg::NewtonCG;
17
18pub trait SecondOrderOptimizer<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension> {
20 fn step_second_order(
22 &mut self,
23 params: &Array<A, D>,
24 gradients: &Array<A, D>,
25 hessian_info: &HessianInfo<A, D>,
26 ) -> Result<Array<A, D>>;
27
28 fn reset(&mut self);
30}
31
32#[derive(Debug, Clone)]
34pub enum HessianInfo<A: Float, D: Dimension> {
35 Full(Array2<A>),
37 Diagonal(Array<A, D>),
39 QuasiNewton {
41 s_history: VecDeque<Array<A, D>>,
43 y_history: VecDeque<Array<A, D>>,
45 },
46 GaussNewton(Array2<A>),
48}
49
50pub mod hessian_approximation {
52 use super::*;
53
54 pub fn diagonal_finite_difference<A, F>(
56 params: &Array1<A>,
57 gradient_fn: F,
58 epsilon: A,
59 ) -> Result<Array1<A>>
60 where
61 A: Float + ScalarOperand + Debug + Copy,
62 F: Fn(&Array1<A>) -> Result<Array1<A>>,
63 {
64 let mut hessian_diag = Array1::zeros(params.len());
65 let _original_grad = gradient_fn(params)?;
66
67 for i in 0..params.len() {
68 let mut param_plus = params.clone();
69 let mut param_minus = params.clone();
70
71 param_plus[i] = params[i] + epsilon;
73 let grad_plus = gradient_fn(¶m_plus)?;
74
75 param_minus[i] = params[i] - epsilon;
77 let grad_minus = gradient_fn(¶m_minus)?;
78
79 let two = A::from(2.0).ok_or_else(|| {
81 OptimError::InvalidConfig(
82 "diagonal_finite_difference: integer literal 2.0 must fit in A".to_string(),
83 )
84 })?;
85 let second_deriv = (grad_plus[i] - grad_minus[i]) / (two * epsilon);
86 hessian_diag[i] = second_deriv;
87 }
88
89 Ok(hessian_diag)
90 }
91
92 fn curvature_threshold<A: Float>() -> A {
100 A::from(1e-8).unwrap_or_else(A::epsilon)
101 }
102
103 fn euclidean_norm<A, D>(v: &Array<A, D>) -> A
105 where
106 A: Float,
107 D: Dimension,
108 {
109 v.iter().fold(A::zero(), |acc, &x| acc + x * x).sqrt()
110 }
111
112 fn dot_product<A, D>(a: &Array<A, D>, b: &Array<A, D>) -> A
114 where
115 A: Float,
116 D: Dimension,
117 {
118 a.iter()
119 .zip(b.iter())
120 .fold(A::zero(), |acc, (&x, &y)| acc + x * y)
121 }
122
123 pub fn is_curvature_pair_acceptable<A, D>(
126 param_diff: &Array<A, D>,
127 grad_diff: &Array<A, D>,
128 ) -> bool
129 where
130 A: Float,
131 D: Dimension,
132 {
133 if param_diff.len() != grad_diff.len() {
134 return false;
135 }
136 let ys = dot_product(param_diff, grad_diff);
137 if !ys.is_finite() || ys <= A::zero() {
138 return false;
139 }
140 let threshold =
141 curvature_threshold::<A>() * euclidean_norm(param_diff) * euclidean_norm(grad_diff);
142 ys > threshold
143 }
144
145 pub fn update_lbfgs_approximation<A, D>(
155 s_history: &mut VecDeque<Array<A, D>>,
156 y_history: &mut VecDeque<Array<A, D>>,
157 param_diff: Array<A, D>,
158 grad_diff: Array<A, D>,
159 max_history: usize,
160 ) -> bool
161 where
162 A: Float + ScalarOperand + Debug,
163 D: Dimension,
164 {
165 if !is_curvature_pair_acceptable(¶m_diff, &grad_diff) {
166 return false;
167 }
168
169 s_history.push_back(param_diff);
171 y_history.push_back(grad_diff);
172
173 while s_history.len() > max_history {
175 s_history.pop_front();
176 y_history.pop_front();
177 }
178 true
179 }
180
181 pub fn initial_hessian_scaling<A, D>(
187 s_history: &VecDeque<Array<A, D>>,
188 y_history: &VecDeque<Array<A, D>>,
189 ) -> Option<A>
190 where
191 A: Float,
192 D: Dimension,
193 {
194 let m = s_history.len().min(y_history.len());
195 for i in (0..m).rev() {
196 let s_i = &s_history[i];
197 let y_i = &y_history[i];
198 if !is_curvature_pair_acceptable(s_i, y_i) {
199 continue;
200 }
201 let yy = dot_product(y_i, y_i);
202 if yy <= A::zero() || !yy.is_finite() {
203 continue;
204 }
205 let gamma = dot_product(s_i, y_i) / yy;
206 if gamma.is_finite() && gamma > A::zero() {
207 return Some(gamma);
208 }
209 }
210 None
211 }
212
213 pub fn lbfgs_two_loop_recursion<A, D>(
230 gradient: &Array<A, D>,
231 s_history: &VecDeque<Array<A, D>>,
232 y_history: &VecDeque<Array<A, D>>,
233 initial_hessian_scale: A,
234 ) -> Result<Array<A, D>>
235 where
236 A: Float + ScalarOperand + Debug,
237 D: Dimension,
238 {
239 if s_history.len() != y_history.len() {
240 return Err(OptimError::InvalidConfig(
241 "History sizes don't match in L-BFGS".to_string(),
242 ));
243 }
244
245 let m = s_history.len();
246 if m == 0 {
247 return Ok(gradient * initial_hessian_scale);
249 }
250
251 let mut rhos: Vec<Option<A>> = Vec::with_capacity(m);
254 for i in 0..m {
255 let s_i = &s_history[i];
256 let y_i = &y_history[i];
257 if s_i.len() != gradient.len() || y_i.len() != gradient.len() {
258 return Err(OptimError::DimensionMismatch(format!(
259 "L-BFGS history entry {} has length {}/{}, expected {}",
260 i,
261 s_i.len(),
262 y_i.len(),
263 gradient.len()
264 )));
265 }
266 if is_curvature_pair_acceptable(s_i, y_i) {
267 rhos.push(Some(A::one() / dot_product(y_i, s_i)));
268 } else {
269 rhos.push(None);
270 }
271 }
272
273 let scale = initial_hessian_scaling(s_history, y_history).unwrap_or(initial_hessian_scale);
276
277 let mut q = gradient.clone();
278 let mut alphas = vec![A::zero(); m];
279
280 for i in (0..m).rev() {
282 let rho_i = match rhos[i] {
283 Some(rho) => rho,
284 None => continue,
285 };
286 let s_i = &s_history[i];
287 let y_i = &y_history[i];
288
289 let alpha_i = rho_i * dot_product(s_i, &q);
291 alphas[i] = alpha_i;
292
293 for (q_val, &y_val) in q.iter_mut().zip(y_i.iter()) {
295 *q_val = *q_val - alpha_i * y_val;
296 }
297 }
298
299 q.mapv_inplace(|x| x * scale);
301
302 for i in 0..m {
304 let rho_i = match rhos[i] {
305 Some(rho) => rho,
306 None => continue,
307 };
308 let s_i = &s_history[i];
309 let y_i = &y_history[i];
310
311 let beta = rho_i * dot_product(y_i, &q);
313
314 let coeff = alphas[i] - beta;
316 for (q_val, &s_val) in q.iter_mut().zip(s_i.iter()) {
317 *q_val = *q_val + coeff * s_val;
318 }
319 }
320
321 Ok(q)
322 }
323
324 pub fn gauss_newton_approximation<A>(jacobian: &Array2<A>) -> Result<Array2<A>>
326 where
327 A: Float + ScalarOperand + Debug,
328 {
329 let j_transpose = jacobian.t();
331 let hessian_approx = j_transpose.dot(jacobian);
332 Ok(hessian_approx)
333 }
334}
335
336#[derive(Debug, Clone)]
345pub struct Newton<A: Float> {
346 learning_rate: A,
347 regularization: A, min_curvature: A, }
350
351impl<A: Float + ScalarOperand + Debug + Send + Sync + Send + Sync> Newton<A> {
352 fn default_min_curvature() -> A {
354 A::from(1e-8).unwrap_or_else(A::epsilon)
355 }
356
357 pub fn new(learning_rate: A) -> Self {
359 Self {
360 learning_rate,
361 regularization: A::from(1e-6).unwrap_or_else(A::epsilon),
362 min_curvature: Self::default_min_curvature(),
363 }
364 }
365
366 pub fn with_regularization(mut self, regularization: A) -> Self {
368 self.regularization = regularization;
369 self
370 }
371
372 pub fn with_min_curvature(mut self, min_curvature: A) -> Self {
377 if min_curvature > A::zero() {
378 self.min_curvature = min_curvature;
379 }
380 self
381 }
382
383 pub fn min_curvature(&self) -> A {
385 self.min_curvature
386 }
387}
388
389impl<A: Float + ScalarOperand + Debug + Send + Sync + Send + Sync>
390 SecondOrderOptimizer<A, scirs2_core::ndarray::Ix1> for Newton<A>
391{
392 fn step_second_order(
393 &mut self,
394 params: &Array1<A>,
395 gradients: &Array1<A>,
396 hessian_info: &HessianInfo<A, scirs2_core::ndarray::Ix1>,
397 ) -> Result<Array1<A>> {
398 match hessian_info {
399 HessianInfo::Diagonal(hessian_diag) => {
400 if params.len() != hessian_diag.len() || params.len() != gradients.len() {
401 return Err(OptimError::DimensionMismatch(
402 "Parameter, gradient, and Hessian dimensions must match".to_string(),
403 ));
404 }
405
406 let mut update = Array1::zeros(params.len());
407 for i in 0..params.len() {
408 let h_ii = hessian_diag[i] + self.regularization;
418 let denom = h_ii.abs().max(self.min_curvature);
419 update[i] = gradients[i] / denom;
420 }
421
422 Ok(params - &(update * self.learning_rate))
423 }
424 HessianInfo::QuasiNewton {
425 s_history,
426 y_history,
427 } => {
428 let search_direction = hessian_approximation::lbfgs_two_loop_recursion(
430 gradients,
431 s_history,
432 y_history,
433 A::one(), )?;
435
436 Ok(params - &(search_direction * self.learning_rate))
437 }
438 _ => Err(OptimError::InvalidConfig(
439 "Unsupported Hessian information type for Newton method".to_string(),
440 )),
441 }
442 }
443
444 fn reset(&mut self) {
445 }
447}
448
449#[derive(Debug)]
451pub struct LBFGS<A: Float, D: Dimension> {
452 learning_rate: A,
453 max_history: usize,
454 s_history: VecDeque<Array<A, D>>,
455 y_history: VecDeque<Array<A, D>>,
456 previous_params: Option<Array<A, D>>,
457 previous_grad: Option<Array<A, D>>,
458}
459
460impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync> LBFGS<A, D> {
461 pub fn new(learning_rate: A) -> Self {
463 Self {
464 learning_rate,
465 max_history: 10,
466 s_history: VecDeque::new(),
467 y_history: VecDeque::new(),
468 previous_params: None,
469 previous_grad: None,
470 }
471 }
472
473 pub fn with_max_history(mut self, max_history: usize) -> Self {
475 self.max_history = max_history;
476 self
477 }
478
479 pub fn step(&mut self, params: &Array<A, D>, gradients: &Array<A, D>) -> Result<Array<A, D>> {
481 if let (Some(prev_params), Some(prev_grad)) = (&self.previous_params, &self.previous_grad) {
483 let s = params - prev_params; let y = gradients - prev_grad; let _accepted = hessian_approximation::update_lbfgs_approximation(
489 &mut self.s_history,
490 &mut self.y_history,
491 s,
492 y,
493 self.max_history,
494 );
495 }
496
497 let search_direction = if self.s_history.is_empty() {
499 gradients.clone()
501 } else {
502 hessian_approximation::lbfgs_two_loop_recursion(
503 gradients,
504 &self.s_history,
505 &self.y_history,
506 A::one(),
507 )?
508 };
509
510 let new_params = params - &(search_direction * self.learning_rate);
512
513 self.previous_params = Some(params.clone());
515 self.previous_grad = Some(gradients.clone());
516
517 Ok(new_params)
518 }
519}
520
521impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync>
522 SecondOrderOptimizer<A, D> for LBFGS<A, D>
523{
524 fn step_second_order(
525 &mut self,
526 params: &Array<A, D>,
527 gradients: &Array<A, D>,
528 _hessian_info: &HessianInfo<A, D>, ) -> Result<Array<A, D>> {
530 self.step(params, gradients)
531 }
532
533 fn reset(&mut self) {
534 self.s_history.clear();
535 self.y_history.clear();
536 self.previous_params = None;
537 self.previous_grad = None;
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use approx::assert_relative_eq;
545 use scirs2_core::ndarray::Array1;
546
547 #[test]
548 fn test_diagonal_hessian_approximation() {
549 let params = Array1::from_vec(vec![1.0]);
551
552 let gradient_fn =
554 |x: &Array1<f64>| -> Result<Array1<f64>> { Ok(Array1::from_vec(vec![2.0 * x[0]])) };
555
556 let hessian_diag =
557 hessian_approximation::diagonal_finite_difference(¶ms, gradient_fn, 1e-5)
558 .expect("hessian_approximation::diagonal_finite_difference succeeds in test_diagonal_hessian_approximation");
559
560 assert_relative_eq!(hessian_diag[0], 2.0, epsilon = 1e-1);
562 }
563
564 #[test]
565 fn test_lbfgs_two_loop_recursion() {
566 let gradient = Array1::from_vec(vec![1.0, 2.0, 3.0]);
567 let mut s_history = VecDeque::new();
568 let mut y_history = VecDeque::new();
569
570 s_history.push_back(Array1::from_vec(vec![0.1, 0.1, 0.1]));
572 y_history.push_back(Array1::from_vec(vec![0.2, 0.3, 0.4]));
573
574 let result =
575 hessian_approximation::lbfgs_two_loop_recursion(&gradient, &s_history, &y_history, 1.0)
576 .expect("hessian_approximation::lbfgs_two_loop_recursion succeeds in test_lbfgs_two_loop_recursion");
577
578 assert_ne!(result, gradient);
580 assert_eq!(result.len(), gradient.len());
581 }
582
583 #[test]
584 fn test_newton_method() {
585 let mut optimizer = Newton::new(0.1);
586 let params = Array1::from_vec(vec![1.0, 2.0]);
587 let gradients = Array1::from_vec(vec![0.1, 0.2]);
588 let hessian_diag = Array1::from_vec(vec![2.0, 4.0]);
589
590 let hessian_info = HessianInfo::Diagonal(hessian_diag);
591 let new_params = optimizer
592 .step_second_order(¶ms, &gradients, &hessian_info)
593 .expect("step_second_order succeeds in test_newton_method");
594
595 assert!(new_params[0] < params[0]);
597 assert!(new_params[1] < params[1]);
598 }
599
600 #[test]
601 fn test_lbfgs_optimizer() {
602 let mut optimizer = LBFGS::new(0.01).with_max_history(5);
603 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
604 let gradients1 = Array1::from_vec(vec![0.1, 0.2, 0.3]);
605 let gradients2 = Array1::from_vec(vec![0.05, 0.15, 0.25]);
606
607 params = optimizer
609 .step(¶ms, &gradients1)
610 .expect("optimizer.step succeeds in test_lbfgs_optimizer");
611
612 let new_params = optimizer
614 .step(¶ms, &gradients2)
615 .expect("optimizer.step succeeds in test_lbfgs_optimizer");
616
617 assert_ne!(new_params, params);
619 assert_eq!(optimizer.s_history.len(), 1);
620 assert_eq!(optimizer.y_history.len(), 1);
621 }
622
623 #[test]
624 fn test_gauss_newton_approximation() {
625 let jacobian = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
626 .expect("Array2::from_shape_vec succeeds in test_gauss_newton_approximation");
627 let hessian_approx =
628 hessian_approximation::gauss_newton_approximation(&jacobian).expect("hessian_approximation::gauss_newton_approximation succeeds in test_gauss_newton_approximation");
629
630 assert_eq!(hessian_approx.dim(), (2, 2));
632
633 assert!(hessian_approx[(0, 0)] >= 0.0);
635 assert!(hessian_approx[(1, 1)] >= 0.0);
636 }
637}