1#![allow(clippy::excessive_precision)]
2
3use burn_core as burn;
4
5use super::GradientsParams;
6use crate::{LearningRate, OptimizerRecord};
7use crate::{RecordState, StateSink, StateSource};
8use burn::config::Config;
9use burn::module::{AutodiffModule, Module, ModuleMapper, ModuleVisitor, Param};
10use burn::store::RecordError;
11use burn::tensor::{Bytes, Device, Tensor, TensorData};
12use serde::{Deserialize, Serialize};
13
14use alloc::vec;
15use alloc::vec::Vec;
16#[cfg(not(feature = "std"))]
17#[allow(unused_imports)]
18use num_traits::Float as _;
19
20fn cubic_interpolate(
25 x1: f64,
26 f1: f64,
27 g1: f64,
28 x2: f64,
29 f2: f64,
30 g2: f64,
31 bounds: Option<(f64, f64)>,
32) -> f64 {
33 let (min_bound, max_bound) = bounds.unwrap_or(if x1 <= x2 { (x1, x2) } else { (x2, x1) });
35 let d1 = g1 + g2 - 3.0 * (f1 - f2) / (x1 - x2);
43 let d2_square = d1 * d1 - g1 * g2;
44
45 if d2_square >= 0.0 {
46 let d2 = d2_square.sqrt();
47 let min_pos = if x1 <= x2 {
48 x2 - (x2 - x1) * ((g2 + d2 - d1) / (g2 - g1 + 2.0 * d2))
49 } else {
50 x1 - (x1 - x2) * ((g1 + d2 - d1) / (g1 - g2 + 2.0 * d2))
51 };
52 min_pos.max(min_bound).min(max_bound)
53 } else {
54 (min_bound + max_bound) / 2.0
55 }
56}
57struct LineSearchSample {
59 t: f64,
61 f: f64,
63 g: Tensor<1>,
65 gtd: f64,
67}
68
69#[allow(clippy::too_many_arguments)]
70fn strong_wolfe<F>(
71 obj_func: &mut F,
73 x: &Tensor<1>,
74 mut t: f64,
76 d: &Tensor<1>,
77 f: f64,
78 g: Tensor<1>,
79 gtd: f64,
80 c1: f64,
81 c2: f64,
82 tolerance_change: f64,
83 max_ls: usize,
84) -> (f64, Tensor<1>, f64, usize)
85where
86 F: FnMut(&Tensor<1>, f64, &Tensor<1>) -> (f64, Tensor<1>),
87{
88 let d_norm: f64 = d.clone().abs().max().into_scalar();
89
90 let (mut f_new, mut g_new) = obj_func(x, t, d);
92 let mut ls_func_evals = 1;
93 let mut gtd_new = g_new.clone().dot(d.clone()).into_scalar();
94
95 let (mut t_prev, mut f_prev, mut g_prev, mut gtd_prev) = (0.0, f, g.clone(), gtd);
97 let mut done = false;
98 let mut ls_iter = 0;
99
100 let mut bracket: Option<[LineSearchSample; 2]> = None;
102 let mut wolfe_bracket: Option<LineSearchSample> = None;
104 while ls_iter < max_ls {
105 if f_new > (f + c1 * t * gtd) || (ls_iter > 1 && f_new >= f_prev) {
110 bracket = Some([
111 LineSearchSample {
112 t: t_prev,
113 f: f_prev,
114 g: g_prev,
115 gtd: gtd_prev,
116 },
117 LineSearchSample {
118 t,
119 f: f_new,
120 g: g_new.clone(),
121 gtd: gtd_new,
122 },
123 ]);
124 break;
125 }
126
127 if gtd_new.abs() <= -c2 * gtd {
130 wolfe_bracket = Some(LineSearchSample {
131 t,
132 f: f_new,
133 g: g_new.clone(),
134 gtd: gtd_new,
135 });
136 done = true;
137 break;
138 }
139
140 if gtd_new >= 0.0 {
142 bracket = Some([
143 LineSearchSample {
144 t: t_prev,
145 f: f_prev,
146 g: g_prev,
147 gtd: gtd_prev,
148 },
149 LineSearchSample {
150 t,
151 f: f_new,
152 g: g_new.clone(),
153 gtd: gtd_new,
154 },
155 ]);
156 break;
157 }
158
159 let min_step = t + 0.01 * (t - t_prev);
161 let max_step = t * 10.0;
162 let t_next = cubic_interpolate(
163 t_prev,
164 f_prev,
165 gtd_prev,
166 t,
167 f_new,
168 gtd_new,
169 Some((min_step, max_step)),
170 );
171 t_prev = t;
172 f_prev = f_new;
173 g_prev = g_new;
174 gtd_prev = gtd_new;
175
176 t = t_next;
178 (f_new, g_new) = obj_func(x, t, d);
179 ls_func_evals += 1;
180 gtd_new = g_new.clone().dot(d.clone()).into_scalar();
181 ls_iter += 1;
182 }
183 if let Some(sample) = wolfe_bracket {
184 return (sample.f, sample.g, sample.t, ls_func_evals);
185 }
186
187 let mut bracket = bracket.unwrap_or_else(|| {
188 [
189 LineSearchSample {
190 t: 0.0,
191 f,
192 g: g.clone(),
193 gtd,
194 },
195 LineSearchSample {
196 t,
197 f: f_new,
198 g: g_new.clone(),
199 gtd: gtd_new,
200 },
201 ]
202 });
203
204 let mut insuf_progress = false;
206
207 let (mut low_idx, mut high_idx) = if bracket[0].f <= bracket[1].f {
209 (0, 1)
210 } else {
211 (1, 0)
212 };
213
214 while !done && ls_iter < max_ls {
215 let diff = (bracket[1].t - bracket[0].t).abs();
216 if diff * d_norm < tolerance_change {
218 break;
219 }
220
221 t = cubic_interpolate(
223 bracket[0].t,
224 bracket[0].f,
225 bracket[0].gtd,
226 bracket[1].t,
227 bracket[1].f,
228 bracket[1].gtd,
229 None,
230 );
231
232 let b_min = bracket[0].t.min(bracket[1].t);
233 let b_max = bracket[0].t.max(bracket[1].t);
234 let eps = 0.1 * (b_max - b_min);
235
236 if (b_max - t).min(t - b_min) < eps {
237 if insuf_progress || t >= b_max || t <= b_min {
239 t = if (t - b_max).abs() < (t - b_min).abs() {
240 b_max - eps
241 } else {
242 b_min + eps
243 };
244 insuf_progress = false;
245 } else {
246 insuf_progress = true;
247 }
248 } else {
249 insuf_progress = false;
250 }
251
252 (f_new, g_new) = obj_func(x, t, d);
254
255 ls_func_evals += 1;
256 gtd_new = g_new.clone().dot(d.clone()).into_scalar();
257 ls_iter += 1;
258
259 let armijo_holds = f_new <= (f + c1 * t * gtd) && f_new < bracket[low_idx].f;
260
261 if !armijo_holds {
262 bracket[high_idx] = LineSearchSample {
263 t,
264 f: f_new,
265 g: g_new,
266 gtd: gtd_new,
267 };
268 } else {
269 if gtd_new.abs() <= -c2 * gtd {
270 return (f_new, g_new, t, ls_func_evals);
271 }
272
273 if gtd_new * (bracket[high_idx].t - bracket[low_idx].t) >= 0.0 {
274 bracket[high_idx] = LineSearchSample {
275 t: bracket[low_idx].t,
276 f: bracket[low_idx].f,
277 g: bracket[low_idx].g.clone(),
278 gtd: bracket[low_idx].gtd,
279 };
280 }
281 bracket[low_idx] = LineSearchSample {
282 t,
283 f: f_new,
284 g: g_new,
285 gtd: gtd_new,
286 };
287 }
288
289 if bracket[0].f <= bracket[1].f {
290 low_idx = 0;
291 high_idx = 1;
292 } else {
293 low_idx = 1;
294 high_idx = 0;
295 }
296 }
297 (
299 bracket[low_idx].f,
300 bracket[low_idx].g.clone(),
301 bracket[low_idx].t,
302 ls_func_evals,
303 )
304}
305
306#[derive(Clone, Default, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
308pub enum LineSearchFn {
309 #[default]
311 None,
312 StrongWolfe,
316}
317
318#[derive(Config, Debug)]
320pub struct LBFGSConfig {
321 #[config(default = 20)]
323 pub max_iter: usize,
324 #[config(default = 100)]
326 pub history_size: usize,
327 #[config(default = 1e-7)]
329 pub tolerance_grad: f64,
330 #[config(default = 1e-9)]
332 pub tolerance_change: f64,
333 #[config(default = "None")]
335 pub max_eval: Option<usize>,
336 #[config(default = "LineSearchFn::None")]
338 pub line_search_fn: LineSearchFn,
339}
340
341impl LBFGSConfig {
342 pub fn init(&self) -> LBFGS {
348 let max_eval = self.max_eval.unwrap_or(self.max_iter * 5 / 4);
350 LBFGS {
351 config: LBFGSConfig {
352 max_iter: self.max_iter,
353 history_size: self.history_size,
354 tolerance_grad: self.tolerance_grad,
355 tolerance_change: self.tolerance_change,
356 max_eval: Some(max_eval),
357 line_search_fn: self.line_search_fn,
358 },
359 state: Default::default(),
360 }
361 }
362}
363
364struct FlattenGradsVisitorInner<'a> {
366 grads: &'a GradientsParams,
367 tensors: &'a mut Vec<Tensor<1>>,
368}
369
370impl ModuleVisitor for FlattenGradsVisitorInner<'_> {
371 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
372 if let Some(g) = self.grads.get::<D>(param.id) {
373 let numel = g.shape().num_elements();
374 self.tensors.push(g.reshape([numel]));
375 }
376 }
377}
378
379fn flatten_params_inner<M: Module>(module: &M) -> Tensor<1> {
381 let mut tensors = Vec::new();
382 let mut visitor = FlattenParamsVisitorInner {
383 tensors: &mut tensors,
384 };
385 module.visit(&mut visitor);
386 if tensors.is_empty() {
387 return Tensor::empty([0], &module.devices()[0].clone().inner());
388 }
389 Tensor::cat(tensors, 0)
390}
391
392struct FlattenParamsVisitorInner<'a> {
393 tensors: &'a mut Vec<Tensor<1>>,
394}
395
396impl ModuleVisitor for FlattenParamsVisitorInner<'_> {
397 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
398 let t = param.val().inner();
399 let numel = t.shape().num_elements();
400 self.tensors.push(t.reshape([numel]));
401 }
402}
403
404fn flatten_grads_inner<M: Module>(module: &M, grads: &GradientsParams) -> Tensor<1> {
406 let mut tensors = Vec::new();
407 let mut visitor = FlattenGradsVisitorInner {
408 grads,
409 tensors: &mut tensors,
410 };
411 module.visit(&mut visitor);
412 if tensors.is_empty() {
413 return Tensor::empty([0], &module.devices()[0].clone().inner());
414 }
415 Tensor::cat(tensors, 0)
416}
417
418struct ParamsFromFlatMapperInner<'a> {
420 flat: &'a Tensor<1>,
421 offset: &'a mut usize,
422}
423
424impl ParamsFromFlatMapperInner<'_> {
425 fn take_slice(&mut self, numel: usize) -> Tensor<1> {
426 let start = *self.offset;
427 *self.offset += numel;
428 self.flat.clone().slice(start..*self.offset)
429 }
430}
431
432impl ModuleMapper for ParamsFromFlatMapperInner<'_> {
433 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
434 let (id, tensor, mapper) = param.consume();
435 let numel = tensor.shape().num_elements();
436 let slice_1d = self.take_slice(numel);
437 let new_inner = slice_1d.reshape(tensor.shape());
438 let new_tensor = Tensor::from_inner(new_inner).require_grad();
439 Param::from_mapped_value(id, new_tensor, mapper)
440 }
441}
442
443fn set_params_from_flat_inner<M: Module>(module: M, flat: Tensor<1>) -> M {
445 let mut offset = 0;
446 let mut mapper = ParamsFromFlatMapperInner {
447 flat: &flat,
448 offset: &mut offset,
449 };
450 module.map(&mut mapper)
451}
452
453#[derive(Clone, RecordState)]
455pub struct LBFGSState {
456 pub history_s: Vec<Tensor<1>>,
458 pub history_y: Vec<Tensor<1>>,
460 pub d: Option<Tensor<1>>,
462 pub t: Option<f64>,
464 pub prev_flat_grad: Option<Tensor<1>>,
466 pub prev_loss: Option<f64>,
468 pub g_iter: usize,
470}
471
472impl LBFGSState {
473 fn current_device(&self) -> Option<Device> {
475 self.prev_flat_grad
476 .as_ref()
477 .or(self.d.as_ref())
478 .or(self.history_s.first())
479 .map(|t| t.device())
480 }
481
482 pub fn to_device(self, device: &Device) -> Self {
484 Self {
485 history_s: self
486 .history_s
487 .into_iter()
488 .map(|t| t.to_device(device))
489 .collect(),
490 history_y: self
491 .history_y
492 .into_iter()
493 .map(|t| t.to_device(device))
494 .collect(),
495 d: self.d.map(|t| t.to_device(device)),
496 t: self.t,
497 prev_flat_grad: self.prev_flat_grad.map(|t| t.to_device(device)),
498 prev_loss: self.prev_loss,
499 g_iter: self.g_iter,
500 }
501 }
502}
503impl Default for LBFGSState {
504 fn default() -> Self {
505 Self {
506 history_s: Vec::new(),
507 history_y: Vec::new(),
508 d: None,
509 t: Some(1.0),
510 prev_flat_grad: None,
511 prev_loss: None,
512 g_iter: 0,
513 }
514 }
515}
516
517#[derive(Clone)]
527pub struct LBFGS {
528 config: LBFGSConfig,
529 state: LBFGSState,
530}
531
532impl LBFGS {
533 pub fn to_record(&self) -> OptimizerRecord {
538 let mut sink = StateSink::default();
539 RecordState::state_flatten(&self.state, "", &mut sink);
540
541 let tensors = sink
542 .tensors
543 .into_iter()
544 .map(|(name, data)| {
545 burn_pack::Tensor::new(name, data.dtype, data.shape, None, data.bytes)
546 })
547 .collect();
548 let scalars = sink.scalars.into_iter().collect();
549
550 OptimizerRecord {
551 tensors,
552 scalars,
553 paths: Default::default(),
554 }
555 }
556
557 pub fn load_record(mut self, record: OptimizerRecord) -> Self {
562 let device = Device::default();
563 let mut source = StateSource::new(record.scalars);
564 for tensor in record.tensors {
565 let data = TensorData::from_bytes(tensor.bytes, tensor.shape, tensor.dtype);
566 source.insert_tensor(tensor.name, data);
567 }
568 if let Some(state) = LBFGSState::state_unflatten("", &mut source, &device) {
569 self.state = state;
570 }
571 self
572 }
573
574 pub fn into_bytes(&self) -> Result<Bytes, RecordError> {
576 self.to_record().into_bytes()
577 }
578
579 pub fn from_bytes(self, bytes: Bytes) -> Result<Self, RecordError> {
581 Ok(self.load_record(OptimizerRecord::from_bytes(bytes)?))
582 }
583
584 #[cfg(feature = "std")]
586 pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), RecordError> {
587 self.to_record().save(path)
588 }
589
590 #[cfg(feature = "std")]
592 pub fn load<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self, RecordError> {
593 Ok(self.load_record(OptimizerRecord::load(path)?))
594 }
595
596 pub fn step<M, F>(&mut self, lr: LearningRate, mut module: M, mut closure: F) -> (M, f64)
598 where
599 M: AutodiffModule + Clone,
600 F: FnMut(M) -> (f64, GradientsParams),
601 {
602 let (mut loss, grads) = closure(module.clone());
604 let mut current_evals = 1;
605
606 let mut flat_grad = flatten_grads_inner::<M>(&module, &grads);
607 let mut x_flat = flatten_params_inner::<M>(&module);
608
609 let device = flat_grad.device();
612 if self.state.current_device().is_some_and(|d| d != device) {
613 self.state = core::mem::take(&mut self.state).to_device(&device);
614 }
615
616 let opt_cond =
617 flat_grad.clone().abs().max().into_scalar::<f64>() <= self.config.tolerance_grad;
618 if opt_cond {
620 return (module, loss);
621 }
622
623 let mut d = self
625 .state
626 .d
627 .take()
628 .unwrap_or_else(|| flat_grad.clone().neg());
629 let mut t = self.state.t.unwrap_or(lr);
630 let mut prev_flat_grad = self.state.prev_flat_grad.take();
631
632 let mut n_iter = 0;
633
634 while n_iter < self.config.max_iter {
636 n_iter += 1;
638 self.state.g_iter += 1;
639
640 if self.state.g_iter == 1 {
642 d = flat_grad.clone().neg();
643 self.state.history_s.clear();
644 self.state.history_y.clear();
645 } else {
646 if let Some(pg) = prev_flat_grad.as_ref() {
648 let y = flat_grad.clone().sub(pg.clone());
649 let s = d.clone().mul_scalar(t);
650
651 let ys: f64 = y.clone().dot(s.clone()).into_scalar();
652
653 if ys > 1e-10 {
654 if self.state.history_s.len() >= self.config.history_size {
656 self.state.history_s.remove(0);
658 self.state.history_y.remove(0);
659 }
660 self.state.history_s.push(s);
661 self.state.history_y.push(y);
662 }
663 }
664
665 let num_old = self.state.history_s.len();
668 let mut q = flat_grad.clone().neg();
669 let mut alphas: Vec<Tensor<1>> =
670 vec![Tensor::zeros([1], &flat_grad.device().inner()); num_old];
671
672 if num_old > 0 {
673 for i in (0..num_old).rev() {
676 let s = &self.state.history_s[i];
677 let y = &self.state.history_y[i];
678 let rho = y.clone().dot(s.clone()).powf_scalar(-1.0);
679 let alpha = rho.clone().mul(s.clone().dot(q.clone()));
680 alphas[i] = alpha.clone();
681 q = q.sub(y.clone().mul(alpha));
682 }
683
684 let last_s = &self.state.history_s[num_old - 1];
685 let last_y = &self.state.history_y[num_old - 1];
686 let ys = last_y.clone().dot(last_s.clone());
687 let yy = last_y.clone().dot(last_y.clone());
688 let h_diag = ys.div(yy);
689
690 let mut r = q.mul(h_diag);
691
692 for ((s, y), alpha) in self
693 .state
694 .history_s
695 .iter()
696 .zip(self.state.history_y.iter())
697 .zip(alphas)
698 .take(num_old)
699 {
700 let rho = y.clone().dot(s.clone()).powf_scalar(-1.0);
701
702 let beta = rho.mul(y.clone().dot(r.clone()));
703
704 r = r.add(s.clone().mul(alpha.sub(beta)));
705 }
706 d = r;
707 } else {
708 d = q;
709 }
710 }
711
712 prev_flat_grad = Some(flat_grad.clone());
713 let prev_loss_iter = loss;
714
715 if self.state.g_iter == 1 {
717 let grad_l1: f64 = flat_grad.clone().abs().sum().into_scalar();
718 t = (1.0f64 / grad_l1).min(1.0) * lr;
719 } else {
720 t = lr;
721 }
722
723 let gtd = flat_grad.clone().dot(d.clone()).into_scalar();
725
726 if gtd > -self.config.tolerance_change {
727 break;
728 }
729
730 let ls_func_evals;
731
732 if let LineSearchFn::StrongWolfe = self.config.line_search_fn {
733 let mut obj_func = |current_x: &Tensor<1>, step: f64, dir: &Tensor<1>| {
735 let update = dir.clone().mul_scalar(step);
736 let new_x = current_x.clone().add(update);
737 let tmp_module = set_params_from_flat_inner::<M>(module.clone(), new_x);
738 let (l, g) = closure(tmp_module);
739 (l, flatten_grads_inner::<M>(&module, &g))
740 };
741
742 let (ls_f, ls_g, ls_t, evals) = strong_wolfe(
743 &mut obj_func,
744 &x_flat,
745 t,
746 &d,
747 loss,
748 flat_grad.clone(),
749 gtd,
750 1e-4,
751 0.9,
752 self.config.tolerance_change,
753 self.config.max_eval.unwrap() - current_evals,
754 );
755
756 loss = ls_f;
757 flat_grad = ls_g;
758 t = ls_t;
759 ls_func_evals = evals;
760
761 x_flat = x_flat.add(d.clone().mul_scalar(t));
762 module = set_params_from_flat_inner::<M>(module, x_flat.clone());
763 } else {
764 let step_vec = d.clone().mul_scalar(t);
766 x_flat = x_flat.add(step_vec);
767 module = set_params_from_flat_inner::<M>(module, x_flat.clone());
768 let (new_loss, new_grads) = closure(module.clone());
772 loss = new_loss;
773 flat_grad = flatten_grads_inner::<M>(&module, &new_grads);
774 ls_func_evals = 1;
775 }
776
777 current_evals += ls_func_evals;
779
780 if current_evals >= self.config.max_eval.unwrap() {
783 break;
784 }
785
786 if flat_grad.clone().abs().max().into_scalar::<f64>() <= self.config.tolerance_grad {
787 break;
788 }
789
790 if d.clone().mul_scalar(t).abs().max().into_scalar::<f64>()
791 <= self.config.tolerance_change
792 {
793 break;
794 }
795
796 if (loss - prev_loss_iter).abs() < self.config.tolerance_change {
797 break;
798 }
799 }
800 self.state.d = Some(d);
801 self.state.t = Some(t);
802 self.state.prev_flat_grad = prev_flat_grad;
803 self.state.prev_loss = Some(loss);
804 (module, loss)
805 }
806 pub fn to_device(self, device: &Device) -> Self {
808 Self {
809 config: self.config,
810 state: self.state.to_device(device),
812 }
813 }
814}
815
816#[cfg(test)]
817mod tests {
818
819 use super::*;
820 use crate::GradientsParams;
821 use burn::module::Param;
822 use burn::tensor::{Tensor, TensorData};
823 use burn_nn::Linear;
824
825 fn given_linear_layer(weight: TensorData, bias: TensorData, device: &Device) -> Linear {
826 Linear {
827 weight: Param::from_data(weight, device),
828 bias: Some(Param::from_data(bias, device)),
829 }
830 }
831 #[test]
832 fn test_cubic_interpolate() {
833 let tolerance = 1e-8;
834
835 let (x1, f1, g1, x2, f2, g2) = (-1.0, 1.0, -2.0, 1.0, 1.0, 2.0);
837 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
838 assert!(
839 (result - 0.00000).abs() < tolerance,
840 "Basic: Result {} should be close to 0.0",
841 result
842 );
843
844 let (x1, f1, g1, x2, f2, g2) = (0.0, 0.25, -1.0, 1.0, 0.25, 1.0);
846 let bounds = Some((0.6, 1.0));
847 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, bounds);
848 assert!(
849 (result - 0.6000000000).abs() < tolerance,
850 "Bound: Result {} should be clamped to 0.6",
851 result
852 );
853
854 let (x1, f1, g1, x2, f2, g2) = (0.0, 0.0, 10.0, 1.0, 5.0, 10.0);
856 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, Some((0.0, 1.0)));
857 assert!(
858 (result - 0.5000000).abs() < tolerance,
859 "Fallback: Result {} should be midpoint 0.5",
860 result
861 );
862
863 let (x1, f1, g1, x2, f2, g2) = (0.0, 1.0, -5.0, 1.0, 0.5, 1.0);
865 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
866 assert!(
867 (result - 0.4606553370833684).abs() < tolerance,
868 "Asymmetric: Result {} should be 0.4606553370833684",
869 result
870 );
871
872 let (x1, f1, g1, x2, f2, g2) = (
874 1.231232145,
875 -0.12567458754,
876 9.1231243007,
877 8.239105015,
878 -100.9012398021,
879 123201321.0293982,
880 );
881 let result_1 = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
882 let result_2 = cubic_interpolate(x1, f1, g1, x2, f2, g2, Some((-4.4, 4.4)));
883 assert!(
884 (result_1 - 5.9031480234724434).abs() < tolerance,
885 "not good value 1: Result {} should be 5.9031480234724434",
886 result
887 );
888 assert!(
889 (result_2 - 4.4000000000000004).abs() < tolerance,
890 "not good value 2: Result {} should be 4.4000000000000004",
891 result
892 );
893 }
894 #[test]
895 fn test_strong_wolfe_direct_comparison() {
896 let device = Device::default().autodiff();
897 let tol = 1e-6;
898
899 {
900 let x = Tensor::<1>::from_floats([2.1321912957_f64], &device);
901 let d = Tensor::<1>::from_floats([0.91312321_f64], &device);
902 let t_initial = 1.213132_f64;
903 fn func(x_base: &Tensor<1>, t_val: f64, d_vec: &Tensor<1>) -> (f64, Tensor<1>) {
904 let curr_x = x_base.clone().add(d_vec.clone().mul_scalar(t_val));
905 let x2 = curr_x.clone().mul(curr_x.clone());
906 let x3 = x2.clone().mul(curr_x.clone());
907 let x4 = x2.clone().mul(x2.clone());
908
909 let f_elements = x4 - x2.mul_scalar(2.0) + curr_x.clone();
911
912 let f_val = f_elements.sum().into_scalar();
913
914 let g = x3.mul_scalar(4.0) - curr_x.clone().mul_scalar(4.0)
916 + Tensor::ones_like(&curr_x);
917
918 (f_val, g)
919 }
920 let (f_init, g_init) = func(&x, 0.0, &d);
921 let gtd_init = g_init.clone().dot(d.clone()).into_scalar::<f64>();
922 println!("Initial State: f={},gtd = {}", f_init, gtd_init);
923 assert!((f_init - 13.7080059052).abs() < tol);
924 assert!((gtd_init - 28.5305728912).abs() < tol);
925 let mut obj_func = |xb: &Tensor<1>, tv: f64, dv: &Tensor<1>| func(xb, tv, dv);
926
927 let (f_final, _g_final, t_final, evals) = strong_wolfe(
928 &mut obj_func,
929 &x,
930 t_initial,
931 &d,
932 f_init,
933 g_init,
934 gtd_init,
935 1e-4, 0.9, 1e-9, 10, );
940 let g_f = _g_final.into_scalar::<f64>();
941 println!(
942 "f_final:{:?},_g_final:{:?},t_final:{:?},evals:{:?}",
943 f_final, g_f, t_final, evals
944 );
945 assert!((f_final - 13.708005905151367).abs() < tol);
946 assert!((g_f - 31.2450428009).abs() < tol);
947 assert!((t_final - 0.0).abs() < tol);
948 assert!((evals == 11));
949 }
950 }
951 #[test]
952 fn test_lbfgs_strong_wolfe_comparison() {
953 let device = Device::default().autodiff();
954 let tol = 1e-5;
955 let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
956 let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
957 let weight = TensorData::from([[0.5f64]]);
958 let bias = TensorData::from([0.1f64]);
959 let module = given_linear_layer(weight, bias, &device);
960
961 let mut optimizer = LBFGSConfig::new()
962 .with_line_search_fn(LineSearchFn::StrongWolfe)
963 .init();
964 let mut closure = |mod_in: Linear| {
965 let output = mod_in.forward(x_data.clone());
966 let loss = burn_nn::loss::MseLoss::new().forward(
967 output,
968 y_true.clone(),
969 burn_nn::loss::Reduction::Sum,
970 );
971
972 let grads = loss.backward();
973 let grads_params = GradientsParams::from_grads(grads, &mod_in);
974
975 (loss.into_scalar::<f64>(), grads_params)
976 };
977 let initial_loss = closure(module.clone()).0;
978 assert!((initial_loss - 50.1300048828).abs() < tol);
979 let (updated_module, final_loss) = optimizer.step(0.001, module, &mut closure);
980 assert!((final_loss - 0.0234732367).abs() < tol);
981 let optimized_data: f64 = updated_module.weight.val().into_scalar();
982 let optimized_bias: f64 = updated_module.bias.as_ref().unwrap().val().into_scalar();
983 assert!((optimized_data - 2.0570652485).abs() < tol);
984 assert!((optimized_bias - 0.8106800914).abs() < tol);
985 }
986
987 #[test]
990 fn test_lbfgs_burnpack_round_trip() {
991 let device = Device::default().autodiff();
992 let tol = 1e-6;
993 let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
994 let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
995 let module = given_linear_layer(
996 TensorData::from([[0.5f64]]),
997 TensorData::from([0.1f64]),
998 &device,
999 );
1000
1001 let make_closure = || {
1002 let x = x_data.clone();
1003 let y = y_true.clone();
1004 move |mod_in: Linear| {
1005 let output = mod_in.forward(x.clone());
1006 let loss = burn_nn::loss::MseLoss::new().forward(
1007 output,
1008 y.clone(),
1009 burn_nn::loss::Reduction::Sum,
1010 );
1011 let grads = loss.backward();
1012 let grads_params = GradientsParams::from_grads(grads, &mod_in);
1013 (loss.into_scalar::<f64>(), grads_params)
1014 }
1015 };
1016
1017 let mut optimizer = LBFGSConfig::new()
1018 .with_line_search_fn(LineSearchFn::StrongWolfe)
1019 .init();
1020 let (module, _) = optimizer.step(0.001, module, &mut make_closure());
1021
1022 let bytes = optimizer.into_bytes().unwrap();
1024 let mut reloaded = LBFGSConfig::new()
1025 .with_line_search_fn(LineSearchFn::StrongWolfe)
1026 .init()
1027 .from_bytes(bytes)
1028 .unwrap();
1029
1030 let (_, loss_original) = optimizer.step(0.001, module.clone(), &mut make_closure());
1032 let (_, loss_reloaded) = reloaded.step(0.001, module, &mut make_closure());
1033 assert!(
1034 (loss_original - loss_reloaded).abs() < tol,
1035 "losses differ after burnpack round-trip: {loss_original} vs {loss_reloaded}"
1036 );
1037 }
1038
1039 #[test]
1040 fn test_lbfgs_no_strong_wolfe_comparison() {
1041 let device = Device::default().autodiff();
1042 let tol = 1e-5;
1043 let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
1044 let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
1045 let weight = TensorData::from([[0.5f64]]);
1046 let bias = TensorData::from([0.1f64]);
1047 let module = given_linear_layer(weight, bias, &device);
1048
1049 let mut optimizer = LBFGSConfig::new()
1050 .with_line_search_fn(LineSearchFn::None)
1051 .init();
1052 let mut closure = |mod_in: Linear| {
1053 let output = mod_in.forward(x_data.clone());
1054 let loss = burn_nn::loss::MseLoss::new().forward(
1055 output,
1056 y_true.clone(),
1057 burn_nn::loss::Reduction::Sum,
1058 );
1059
1060 let grads = loss.backward();
1061 let grads_params = GradientsParams::from_grads(grads, &mod_in);
1062
1063 (loss.into_scalar::<f64>(), grads_params)
1064 };
1065 let initial_loss = closure(module.clone()).0;
1066 assert!((initial_loss - 50.1300048828).abs() < tol);
1067 let (updated_module, final_loss) = optimizer.step(0.001, module, &mut closure);
1068 assert!((final_loss - 48.2181930542).abs() < tol);
1069 let optimized_data: f64 = updated_module.weight.val().into_scalar();
1070 let optimized_bias: f64 = updated_module.bias.as_ref().unwrap().val().into_scalar();
1071
1072 assert!((optimized_data - 0.5302446192).abs() < tol);
1073 assert!((optimized_bias - 0.1142520783).abs() < tol);
1074 }
1075}