1use anyhow::anyhow;
6use tch::Tensor;
7
8pub trait Optimizer: Send + Sync {
10 fn optimize(&self, function: &dyn Fn(&Tensor) -> Tensor, x0: &Tensor) -> anyhow::Result<Tensor>;
12}
13
14pub struct BFGS {
16 max_steps: usize,
18 gtol: Option<f64>,
20 ftol: Option<f64>,
22 linesearch_atol: f64,
24}
25
26pub struct CG {
28 max_steps: usize,
30 gtol: Option<f64>,
32 ftol: Option<f64>,
34 linesearch_atol: f64,
36}
37
38fn differentiate(function: &dyn Fn(&Tensor) -> Tensor, x: &Tensor) -> Tensor {
39 let x_with_grad = x.detach().copy().set_requires_grad(true);
40 let y = function(&x_with_grad);
41
42 tch::Tensor::run_backward(&[y], &[x_with_grad], false, false)[0].copy()
43}
44
45const P0: f64 = 0.000001f64;
46
47const PHI2: f64 = 2.618033988749894848207f64;
48const RPHI: f64 = 0.618033988749894848207f64;
49
50fn choose_step(
51 x0: &Tensor,
52 direction: &Tensor,
53 function: &dyn Fn(&Tensor) -> Tensor,
54 atol: f64,
55) -> Tensor {
56 let (mut x1, mut x2, mut x3, mut x4): (Tensor, Tensor, Tensor, Tensor);
57 let (fx1, mut fx3, mut fx4): (Tensor, Tensor, Tensor);
58
59 let kind = x0.kind();
60
61 fx1 = function(&x0);
62
63 x1 = Tensor::from(0.).to_kind(kind);
64 x2 = Tensor::from(P0).to_kind(kind);
65 while function(&(x0 + direction * &x2)).double_value(&[]) <= fx1.double_value(&[]) {
66 x2 = &x1 + (&x2 - &x1) * PHI2;
67 }
68
69 x3 = x2.copy() - (x2.copy() - &x1) * RPHI;
70 x4 = x1.copy() + (x2.copy() - &x1) * RPHI;
71 fx3 = function(&(x0 + direction * &x3));
72 fx4 = function(&(x0 + direction * &x4));
73 while (x1.copy() - &x2).abs().double_value(&[]) > atol {
74 if fx3.double_value(&[]) < fx4.double_value(&[]) {
75 x2 = x4.copy();
76
77 fx4 = fx3.copy();
78 x3 = x2.copy() - (x2.copy() - &x1) * RPHI;
79 x4 = x1.copy() + (x2.copy() - &x1) * RPHI;
80 fx3 = function(&(x0 + direction * &x3));
81 } else {
82 x1 = x3.copy();
83
84 fx3 = fx4.copy();
85 x3 = x2.copy() - (x2.copy() - &x1) * RPHI;
86 x4 = x1.copy() + (x2.copy() - &x1) * RPHI;
87 fx4 = function(&(x0 + direction * &x4));
88 }
89 }
90
91 direction * ((&x1 + &x2) / 2.)
92}
93
94impl CG {
95 pub fn new(
96 max_steps: usize,
97 gtol: Option<f64>,
98 ftol: Option<f64>,
99 linesearch_atol: Option<f64>,
100 ) -> Self {
101 Self {
102 max_steps,
103 gtol,
104 ftol,
105 linesearch_atol: if let Some(linesearch_atol) = linesearch_atol {
106 linesearch_atol
107 } else {
108 P0
109 },
110 }
111 }
112}
113
114impl Optimizer for CG {
115 fn optimize(&self, function: &dyn Fn(&Tensor) -> Tensor, x0: &Tensor) -> anyhow::Result<Tensor> {
116 if x0.size().len() != 1 {
118 return Err(anyhow!("`x0` must have rank 1"));
119 }
120
121 let iters_to_reset = x0.size().len();
122 let mut prev_grad = Tensor::zeros_like(&x0);
123 let mut prev_direction = Tensor::zeros_like(&x0);
124 let mut prev_y: Option<Tensor> = None;
125 let mut x = x0.copy();
126
127 for step_num in 0..self.max_steps {
128 let grad = differentiate(function, &x);
129
130 if let Some(gtol) = self.gtol {
132 if grad.norm().double_value(&[]) < gtol {
133 return Ok(x);
134 }
135 } else {
136 if grad.norm().double_value(&[]) == 0. {
140 return Ok(x);
141 }
142 }
143
144 let direction = match step_num % iters_to_reset {
146 0 => -&grad,
147 _ => {
148 let beta = grad.squeeze().dot(&(&grad - &prev_grad).squeeze())
149 / prev_grad.squeeze().dot(&prev_grad.squeeze());
150
151 -&grad + beta * &prev_direction
152 }
153 };
154
155 let step = choose_step(&x, &direction, &function, self.linesearch_atol);
157
158 x = x + step;
160
161 let y = function(&x);
163 if let (Some(prev_y), Some(ftol)) = (prev_y, self.ftol) {
164 if (&prev_y - &y).double_value(&[]) < ftol {
165 return Ok(x);
166 }
167 }
168 prev_y = Some(y);
169
170 prev_grad = grad;
172 prev_direction = direction;
173 }
174
175 Ok(x)
176 }
177}
178
179impl BFGS {
180 pub fn new(
181 max_steps: usize,
182 gtol: Option<f64>,
183 ftol: Option<f64>,
184 linesearch_atol: Option<f64>,
185 ) -> Self {
186 Self {
187 max_steps,
188 gtol,
189 ftol,
190 linesearch_atol: if let Some(linesearch_atol) = linesearch_atol {
191 linesearch_atol
192 } else {
193 P0
194 },
195 }
196 }
197}
198
199impl Optimizer for BFGS {
200 fn optimize(&self, function: &dyn Fn(&Tensor) -> Tensor, x0: &Tensor) -> anyhow::Result<Tensor> {
201 if x0.size().len() != 1 {
203 return Err(anyhow!("`x0` must have rank 1"));
204 }
205
206 let kind = x0.kind();
208 let device = x0.device();
209
210 let identity = Tensor::eye(x0.size()[0], (kind, device));
211 let mut x = x0.copy();
212 let mut appr_inv_h = identity.copy();
213 let mut curr_grad = differentiate(function, &x);
214 let mut curr_y = function(&x);
215
216 if curr_y.size() != Vec::<i64>::new() {
218 return Err(anyhow!("Output of function `function` must be scalar"));
219 }
220
221 for _ in 0..self.max_steps {
222 if let Some(gtol) = self.gtol {
224 if curr_grad.norm().double_value(&[]) < gtol {
225 return Ok(x);
226 }
227 } else {
228 if curr_grad.norm().double_value(&[]) == 0. {
232 return Ok(x);
233 }
234 }
235
236 let direction = (-appr_inv_h.mm(&curr_grad.unsqueeze(1))).squeeze();
238
239 let step = choose_step(&x, &direction, function, self.linesearch_atol);
241
242 x = x + &step;
244
245 let y = function(&x);
247 if let Some(ftol) = self.ftol {
248 if (curr_y.double_value(&[]) - y.double_value(&[])) < ftol {
249 return Ok(x);
250 }
251 }
252 curr_y = y;
253
254 let grad = differentiate(function, &x);
255 let gdiff = &grad - &curr_grad;
256
257 let gamma = {
260 let delta = 0.0001;
261
262 let sty = step.dot(&gdiff).double_value(&[]);
263 let step_norm_sq = step.dot(&step).double_value(&[]);
264
265 let theta = if sty >= delta * step_norm_sq {
266 1.
267 } else {
268 let numerator = (1. - delta) * step_norm_sq;
269 let denominator = step_norm_sq - sty;
270
271 if denominator.abs() < 1e-10 {
272 1.
273 } else {
274 (numerator / denominator).min(1.)
275 }
276 };
277
278 let projection_factor = if step_norm_sq < 1e-10 {
279 0.
280 } else {
281 sty / step_norm_sq
282 };
283 let gdiff_prime = &gdiff * theta + &step * ((1. - theta) * projection_factor);
284 let sty_prime = step.dot(&gdiff_prime).double_value(&[]);
285
286 if sty_prime.abs() < 1e-10 {
287 1. / (delta * step_norm_sq + 1e-10)
288 } else {
289 1. / sty_prime
290 }
291 };
292
293 appr_inv_h = (&identity - gamma * step.reshape([-1, 1]).mm(&gdiff.reshape([1, -1])))
295 .mm(&appr_inv_h)
296 .mm(&(&identity - gamma * gdiff.reshape([-1, 1]).mm(&step.reshape([1, -1]))))
297 + gamma * step.reshape([-1, 1]).mm(&step.reshape([1, -1]));
298
299 curr_grad = grad;
300 }
301
302 Ok(x)
303 }
304}