1use anyhow::anyhow;
6use std::fmt;
7use tch::Tensor;
8
9pub trait Optimizer: Send + Sync + fmt::Display {
11 fn optimize(&self, function: &dyn Fn(&Tensor) -> Tensor, x0: &Tensor)
13 -> anyhow::Result<Tensor>;
14}
15
16pub struct BFGS {
18 max_steps: usize,
20 gtol: Option<f64>,
22 ftol: Option<f64>,
24 linesearch_atol: f64,
26}
27
28pub struct CG {
30 max_steps: usize,
32 gtol: Option<f64>,
34 ftol: Option<f64>,
36 linesearch_atol: f64,
38}
39
40fn differentiate(function: &dyn Fn(&Tensor) -> Tensor, x: &Tensor) -> Tensor {
41 let x_with_grad = x.detach().copy().set_requires_grad(true);
42 let y = function(&x_with_grad);
43
44 tch::Tensor::run_backward(&[y], &[x_with_grad], false, false)[0].copy()
45}
46
47const P0: f64 = 0.000001f64;
48
49const PHI2: f64 = 2.618033988749894848207f64;
50const RPHI: f64 = 0.618033988749894848207f64;
51
52fn choose_step(
53 x0: &Tensor,
54 direction: &Tensor,
55 function: &dyn Fn(&Tensor) -> Tensor,
56 atol: f64,
57) -> Tensor {
58 let (mut x1, mut x2, mut x3, mut x4): (Tensor, Tensor, Tensor, Tensor);
59 let (fx1, mut fx3, mut fx4): (Tensor, Tensor, Tensor);
60
61 let kind = x0.kind();
62
63 fx1 = function(&x0);
64
65 x1 = Tensor::from(0.).to_kind(kind);
66 x2 = Tensor::from(P0).to_kind(kind);
67 while function(&(x0 + direction * &x2)).double_value(&[]) <= fx1.double_value(&[]) {
68 x2 = &x1 + (&x2 - &x1) * PHI2;
69 }
70
71 x3 = x2.copy() - (x2.copy() - &x1) * RPHI;
72 x4 = x1.copy() + (x2.copy() - &x1) * RPHI;
73 fx3 = function(&(x0 + direction * &x3));
74 fx4 = function(&(x0 + direction * &x4));
75 while (x1.copy() - &x2).abs().double_value(&[]) > atol {
76 if fx3.double_value(&[]) < fx4.double_value(&[]) {
77 x2 = x4.copy();
78
79 fx4 = fx3.copy();
80 x3 = x2.copy() - (x2.copy() - &x1) * RPHI;
81 x4 = x1.copy() + (x2.copy() - &x1) * RPHI;
82 fx3 = function(&(x0 + direction * &x3));
83 } else {
84 x1 = x3.copy();
85
86 fx3 = fx4.copy();
87 x3 = x2.copy() - (x2.copy() - &x1) * RPHI;
88 x4 = x1.copy() + (x2.copy() - &x1) * RPHI;
89 fx4 = function(&(x0 + direction * &x4));
90 }
91 }
92
93 direction * ((&x1 + &x2) / 2.)
94}
95
96impl CG {
97 pub fn new(
98 max_steps: usize,
99 gtol: Option<f64>,
100 ftol: Option<f64>,
101 linesearch_atol: Option<f64>,
102 ) -> Self {
103 Self {
104 max_steps,
105 gtol,
106 ftol,
107 linesearch_atol: if let Some(linesearch_atol) = linesearch_atol {
108 linesearch_atol
109 } else {
110 P0
111 },
112 }
113 }
114}
115
116impl Optimizer for CG {
117 fn optimize(
118 &self,
119 function: &dyn Fn(&Tensor) -> Tensor,
120 x0: &Tensor,
121 ) -> anyhow::Result<Tensor> {
122 if x0.size().len() != 1 {
124 return Err(anyhow!("`x0` must have rank 1"));
125 }
126
127 let iters_to_reset = x0.size().len();
128 let mut prev_grad = Tensor::zeros_like(&x0);
129 let mut prev_direction = Tensor::zeros_like(&x0);
130 let mut prev_y: Option<Tensor> = None;
131 let mut x = x0.copy();
132
133 for step_num in 0..self.max_steps {
134 let grad = differentiate(function, &x);
135
136 if let Some(gtol) = self.gtol {
138 if grad.norm().double_value(&[]) < gtol {
139 return Ok(x);
140 }
141 } else {
142 if grad.norm().double_value(&[]) == 0. {
146 return Ok(x);
147 }
148 }
149
150 let direction = match step_num % iters_to_reset {
152 0 => -&grad,
153 _ => {
154 let beta = grad.squeeze().dot(&(&grad - &prev_grad).squeeze())
155 / prev_grad.squeeze().dot(&prev_grad.squeeze());
156
157 -&grad + beta * &prev_direction
158 }
159 };
160
161 let step = choose_step(&x, &direction, &function, self.linesearch_atol);
163
164 x = x + step;
166
167 let y = function(&x);
169 if let (Some(prev_y), Some(ftol)) = (prev_y, self.ftol) {
170 if (&prev_y - &y).double_value(&[]) < ftol {
171 return Ok(x);
172 }
173 }
174 prev_y = Some(y);
175
176 prev_grad = grad;
178 prev_direction = direction;
179 }
180
181 Ok(x)
182 }
183}
184
185impl fmt::Display for CG {
186 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
187 let mut string = String::from("CG(");
188
189 string = string + "max_steps=" + self.max_steps.to_string().as_str();
190 if let Some(gtol) = self.gtol {
191 string = string + ", gtol=" + gtol.to_string().as_str();
192 }
193 if let Some(ftol) = self.ftol {
194 string = string + ", ftol=" + ftol.to_string().as_str();
195 }
196
197 string = string + ", linesearch_atol=" + self.linesearch_atol.to_string().as_str() + ")";
198
199 write!(f, "{}", string)
200 }
201}
202
203impl BFGS {
204 pub fn new(
205 max_steps: usize,
206 gtol: Option<f64>,
207 ftol: Option<f64>,
208 linesearch_atol: Option<f64>,
209 ) -> Self {
210 Self {
211 max_steps,
212 gtol,
213 ftol,
214 linesearch_atol: if let Some(linesearch_atol) = linesearch_atol {
215 linesearch_atol
216 } else {
217 P0
218 },
219 }
220 }
221}
222
223impl Optimizer for BFGS {
224 fn optimize(
225 &self,
226 function: &dyn Fn(&Tensor) -> Tensor,
227 x0: &Tensor,
228 ) -> anyhow::Result<Tensor> {
229 if x0.size().len() != 1 {
231 return Err(anyhow!("`x0` must have rank 1"));
232 }
233
234 let kind = x0.kind();
236 let device = x0.device();
237
238 let x0_length = x0.size()[0];
239 let identity = match Tensor::f_eye(x0_length, (kind, device)) {
240 Ok(matrix) => matrix,
241 Err(tch::TchError::Torch(_)) => {
245 return Err(anyhow!(
246 "Could not allocate {}x{} matrix. Maybe try less resourcefull algorithm.",
247 x0_length,
248 x0_length
249 ));
250 }
251 e => e.unwrap(),
252 };
253 let mut x = x0.copy();
254 let mut appr_inv_h = identity.copy();
255 let mut curr_grad = differentiate(function, &x);
256 let mut curr_y = function(&x);
257
258 if curr_y.size() != Vec::<i64>::new() {
260 return Err(anyhow!("Output of function `function` must be scalar"));
261 }
262
263 for _ in 0..self.max_steps {
264 if let Some(gtol) = self.gtol {
266 if curr_grad.norm().double_value(&[]) < gtol {
267 return Ok(x);
268 }
269 } else {
270 if curr_grad.norm().double_value(&[]) == 0. {
274 return Ok(x);
275 }
276 }
277
278 let direction = (-appr_inv_h.mm(&curr_grad.unsqueeze(1))).squeeze();
280
281 let step = choose_step(&x, &direction, function, self.linesearch_atol);
283
284 x = x + &step;
286
287 let y = function(&x);
289 if let Some(ftol) = self.ftol {
290 if (curr_y.double_value(&[]) - y.double_value(&[])) < ftol {
291 return Ok(x);
292 }
293 }
294 curr_y = y;
295
296 let grad = differentiate(function, &x);
297 let gdiff = &grad - &curr_grad;
298
299 let gamma = {
302 let delta = 0.0001;
303
304 let sty = step.dot(&gdiff).double_value(&[]);
305 let step_norm_sq = step.dot(&step).double_value(&[]);
306
307 let theta = if sty >= delta * step_norm_sq {
308 1.
309 } else {
310 let numerator = (1. - delta) * step_norm_sq;
311 let denominator = step_norm_sq - sty;
312
313 if denominator.abs() < 1e-10 {
314 1.
315 } else {
316 (numerator / denominator).min(1.)
317 }
318 };
319
320 let projection_factor = if step_norm_sq < 1e-10 {
321 0.
322 } else {
323 sty / step_norm_sq
324 };
325 let gdiff_prime = &gdiff * theta + &step * ((1. - theta) * projection_factor);
326 let sty_prime = step.dot(&gdiff_prime).double_value(&[]);
327
328 if sty_prime.abs() < 1e-10 {
329 1. / (delta * step_norm_sq + 1e-10)
330 } else {
331 1. / sty_prime
332 }
333 };
334
335 appr_inv_h = (&identity - gamma * step.reshape([-1, 1]).mm(&gdiff.reshape([1, -1])))
337 .mm(&appr_inv_h)
338 .mm(&(&identity - gamma * gdiff.reshape([-1, 1]).mm(&step.reshape([1, -1]))))
339 + gamma * step.reshape([-1, 1]).mm(&step.reshape([1, -1]));
340
341 curr_grad = grad;
342 }
343
344 Ok(x)
345 }
346}
347
348impl fmt::Display for BFGS {
349 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
350 let mut string = String::from("BFGS(");
351
352 string = string + "max_steps=" + self.max_steps.to_string().as_str();
353 if let Some(gtol) = self.gtol {
354 string = string + ", gtol=" + gtol.to_string().as_str();
355 }
356 if let Some(ftol) = self.ftol {
357 string = string + ", ftol=" + ftol.to_string().as_str();
358 }
359
360 string = string + ", linesearch_atol=" + self.linesearch_atol.to_string().as_str() + ")";
361
362 write!(f, "{}", string)
363 }
364}