Skip to main content

trustformers_optim/second_order/
lbfgs.rs

1use anyhow::Result;
2use std::collections::{HashMap, VecDeque};
3use trustformers_core::tensor::Tensor;
4
5/// Limited-memory Broyden-Fletcher-Goldfarb-Shanno (L-BFGS) optimizer.
6///
7/// L-BFGS is a quasi-Newton method that approximates the second-order derivative
8/// information using only first-order gradients. It maintains a limited history
9/// of gradient and parameter updates to approximate the inverse Hessian matrix.
10#[derive(Debug)]
11pub struct LBFGS {
12    pub learning_rate: f32,
13    pub history_size: usize,
14    pub line_search_fn: Option<LineSearchMethod>,
15    pub max_iter: usize,
16    pub tolerance_grad: f32,
17    pub tolerance_change: f32,
18
19    // Internal state
20    pub step: usize,
21    pub s_history: VecDeque<HashMap<String, Vec<f32>>>, // parameter differences
22    pub y_history: VecDeque<HashMap<String, Vec<f32>>>, // gradient differences
23    pub rho_history: VecDeque<f32>,                     // 1 / (y^T s)
24    pub prev_params: HashMap<String, Vec<f32>>,
25    pub prev_grads: HashMap<String, Vec<f32>>,
26}
27
28#[derive(Debug, Clone)]
29pub enum LineSearchMethod {
30    None,
31    StrongWolfe,
32    Backtracking,
33}
34
35impl Default for LBFGS {
36    fn default() -> Self {
37        Self {
38            learning_rate: 1.0,
39            history_size: 10,
40            line_search_fn: Some(LineSearchMethod::StrongWolfe),
41            max_iter: 20,
42            tolerance_grad: 1e-7,
43            tolerance_change: 1e-9,
44            step: 0,
45            s_history: VecDeque::new(),
46            y_history: VecDeque::new(),
47            rho_history: VecDeque::new(),
48            prev_params: HashMap::new(),
49            prev_grads: HashMap::new(),
50        }
51    }
52}
53
54impl LBFGS {
55    pub fn new(learning_rate: f32) -> Self {
56        Self {
57            learning_rate,
58            ..Default::default()
59        }
60    }
61
62    pub fn with_config(
63        learning_rate: f32,
64        history_size: usize,
65        line_search_fn: Option<LineSearchMethod>,
66        max_iter: usize,
67    ) -> Self {
68        Self {
69            learning_rate,
70            history_size,
71            line_search_fn,
72            max_iter,
73            ..Default::default()
74        }
75    }
76
77    pub fn step(
78        &mut self,
79        parameters: &mut HashMap<String, Tensor>,
80        gradients: &HashMap<String, Tensor>,
81    ) -> Result<()> {
82        // First step - store current state
83        if self.step == 0 {
84            for (name, param) in parameters.iter() {
85                self.prev_params.insert(name.clone(), param.data()?);
86            }
87            for (name, grad) in gradients.iter() {
88                self.prev_grads.insert(name.clone(), grad.data()?);
89            }
90
91            // Simple gradient descent for first step
92            for (name, param) in parameters.iter_mut() {
93                let grad = gradients
94                    .get(name)
95                    .ok_or_else(|| anyhow::anyhow!("Missing gradient for parameter: {}", name))?;
96                let mut param_data = param.data()?;
97                let grad_data = grad.data()?;
98
99                for i in 0..param_data.len() {
100                    param_data[i] -= self.learning_rate * grad_data[i];
101                }
102
103                *param = Tensor::new(param_data)?;
104            }
105
106            self.step += 1;
107            return Ok(());
108        }
109
110        // Subsequent steps - use L-BFGS
111        let mut s_k = HashMap::new();
112        let mut y_k = HashMap::new();
113
114        // Compute parameter and gradient differences
115        for (name, param) in parameters.iter() {
116            let param_data = param.data()?;
117            let prev_param = self
118                .prev_params
119                .get(name)
120                .ok_or_else(|| anyhow::anyhow!("prev_params must exist for name"))?;
121
122            let s: Vec<f32> =
123                param_data.iter().zip(prev_param.iter()).map(|(p, prev_p)| p - prev_p).collect();
124            s_k.insert(name.clone(), s);
125        }
126
127        for (name, grad) in gradients.iter() {
128            let grad_data = grad.data()?;
129            let prev_grad = self
130                .prev_grads
131                .get(name)
132                .ok_or_else(|| anyhow::anyhow!("prev_grads must exist for name"))?;
133
134            let y: Vec<f32> =
135                grad_data.iter().zip(prev_grad.iter()).map(|(g, prev_g)| g - prev_g).collect();
136            y_k.insert(name.clone(), y);
137        }
138
139        // Compute rho = 1 / (y^T s)
140        let mut rho = 0.0;
141        for name in parameters.keys() {
142            let s = s_k.get(name).ok_or_else(|| anyhow::anyhow!("s_k must exist for name"))?;
143            let y = y_k.get(name).ok_or_else(|| anyhow::anyhow!("y_k must exist for name"))?;
144
145            rho += s.iter().zip(y.iter()).map(|(s_i, y_i)| s_i * y_i).sum::<f32>();
146        }
147
148        if rho.abs() < 1e-10 {
149            // Skip this update if rho is too small
150            self.step += 1;
151            return Ok(());
152        }
153
154        rho = 1.0 / rho;
155
156        // Store in history
157        self.s_history.push_back(s_k);
158        self.y_history.push_back(y_k);
159        self.rho_history.push_back(rho);
160
161        // Maintain history size
162        if self.s_history.len() > self.history_size {
163            self.s_history.pop_front();
164            self.y_history.pop_front();
165            self.rho_history.pop_front();
166        }
167
168        // Compute search direction using two-loop recursion
169        let search_direction = self.compute_search_direction(gradients)?;
170
171        // Apply update
172        for (name, param) in parameters.iter_mut() {
173            let direction = search_direction
174                .get(name)
175                .ok_or_else(|| anyhow::anyhow!("search_direction must exist for name"))?;
176            let mut param_data = param.data()?;
177
178            for i in 0..param_data.len() {
179                param_data[i] -= self.learning_rate * direction[i];
180            }
181
182            *param = Tensor::new(param_data)?;
183        }
184
185        // Update stored state
186        for (name, param) in parameters.iter() {
187            self.prev_params.insert(name.clone(), param.data()?);
188        }
189        for (name, grad) in gradients.iter() {
190            self.prev_grads.insert(name.clone(), grad.data()?);
191        }
192
193        self.step += 1;
194        Ok(())
195    }
196
197    fn compute_search_direction(
198        &self,
199        gradients: &HashMap<String, Tensor>,
200    ) -> Result<HashMap<String, Vec<f32>>> {
201        let mut q: HashMap<String, Vec<f32>> = HashMap::new();
202
203        // Initialize q with current gradients
204        for (name, grad) in gradients.iter() {
205            q.insert(name.clone(), grad.data()?);
206        }
207
208        let history_len = self.s_history.len();
209        let mut alpha = vec![0.0; history_len];
210
211        // First loop (backward)
212        for i in (0..history_len).rev() {
213            let rho_i = self.rho_history[i];
214            let s_i = &self.s_history[i];
215
216            let mut alpha_i = 0.0;
217            for name in gradients.keys() {
218                let s_i_param =
219                    s_i.get(name).ok_or_else(|| anyhow::anyhow!("s_i must exist for name"))?;
220                let q_param =
221                    q.get(name).ok_or_else(|| anyhow::anyhow!("q must exist for name"))?;
222
223                alpha_i +=
224                    s_i_param.iter().zip(q_param.iter()).map(|(s, q_val)| s * q_val).sum::<f32>();
225            }
226            alpha_i *= rho_i;
227            alpha[i] = alpha_i;
228
229            // Update q
230            for name in gradients.keys() {
231                let y_i_param = self.y_history[i]
232                    .get(name)
233                    .ok_or_else(|| anyhow::anyhow!("y_history must have all gradient keys"))?;
234                let q_param =
235                    q.get_mut(name).ok_or_else(|| anyhow::anyhow!("q must exist for name"))?;
236
237                for j in 0..q_param.len() {
238                    q_param[j] -= alpha_i * y_i_param[j];
239                }
240            }
241        }
242
243        // Scale by initial Hessian approximation (H_0 = I / gamma)
244        if !self.s_history.is_empty() {
245            let recent_idx = self.s_history.len() - 1;
246            let recent_s = &self.s_history[recent_idx];
247            let recent_y = &self.y_history[recent_idx];
248
249            let mut s_dot_y = 0.0;
250            let mut y_dot_y = 0.0;
251
252            for name in gradients.keys() {
253                let s_param = recent_s
254                    .get(name)
255                    .ok_or_else(|| anyhow::anyhow!("recent_s must exist for name"))?;
256                let y_param = recent_y
257                    .get(name)
258                    .ok_or_else(|| anyhow::anyhow!("recent_y must exist for name"))?;
259
260                s_dot_y += s_param.iter().zip(y_param.iter()).map(|(s, y)| s * y).sum::<f32>();
261                y_dot_y += y_param.iter().map(|y| y * y).sum::<f32>();
262            }
263
264            if y_dot_y > 1e-10 {
265                let gamma = s_dot_y / y_dot_y;
266                for (_, q_param) in q.iter_mut() {
267                    for val in q_param.iter_mut() {
268                        *val *= gamma;
269                    }
270                }
271            }
272        }
273
274        // Second loop (forward)
275        for i in 0..history_len {
276            let rho_i = self.rho_history[i];
277            let y_i = &self.y_history[i];
278
279            let mut beta = 0.0;
280            for name in gradients.keys() {
281                let y_i_param =
282                    y_i.get(name).ok_or_else(|| anyhow::anyhow!("y_i must exist for name"))?;
283                let q_param =
284                    q.get(name).ok_or_else(|| anyhow::anyhow!("q must exist for name"))?;
285
286                beta +=
287                    y_i_param.iter().zip(q_param.iter()).map(|(y, q_val)| y * q_val).sum::<f32>();
288            }
289            beta *= rho_i;
290
291            let correction = alpha[i] - beta;
292
293            // Update q
294            for name in gradients.keys() {
295                let s_i_param = self.s_history[i]
296                    .get(name)
297                    .ok_or_else(|| anyhow::anyhow!("s_history must have all gradient keys"))?;
298                let q_param =
299                    q.get_mut(name).ok_or_else(|| anyhow::anyhow!("q must exist for name"))?;
300
301                for j in 0..q_param.len() {
302                    q_param[j] += correction * s_i_param[j];
303                }
304            }
305        }
306
307        Ok(q)
308    }
309
310    pub fn reset(&mut self) {
311        self.step = 0;
312        self.s_history.clear();
313        self.y_history.clear();
314        self.rho_history.clear();
315        self.prev_params.clear();
316        self.prev_grads.clear();
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_lbfgs_creation() {
326        let optimizer = LBFGS::new(0.01);
327        assert_eq!(optimizer.learning_rate, 0.01);
328        assert_eq!(optimizer.history_size, 10);
329        assert_eq!(optimizer.step, 0);
330    }
331
332    #[test]
333    fn test_lbfgs_with_config() {
334        let optimizer = LBFGS::with_config(0.1, 5, None, 10);
335        assert_eq!(optimizer.learning_rate, 0.1);
336        assert_eq!(optimizer.history_size, 5);
337        assert_eq!(optimizer.max_iter, 10);
338    }
339
340    #[test]
341    fn test_lbfgs_reset() {
342        let mut optimizer = LBFGS::new(0.01);
343        optimizer.step = 5;
344        optimizer.reset();
345        assert_eq!(optimizer.step, 0);
346        assert!(optimizer.s_history.is_empty());
347        assert!(optimizer.y_history.is_empty());
348        assert!(optimizer.rho_history.is_empty());
349    }
350
351    #[test]
352    fn test_lbfgs_first_step() -> Result<(), Box<dyn std::error::Error>> {
353        let mut optimizer = LBFGS::new(0.01);
354        let mut parameters = HashMap::new();
355        let mut gradients = HashMap::new();
356
357        let param_data = vec![1.0, 2.0, 3.0];
358        let grad_data = vec![0.1, 0.2, 0.3];
359
360        parameters.insert(
361            "param1".to_string(),
362            Tensor::new(param_data.clone()).expect("Failed to create tensor"),
363        );
364        gradients.insert(
365            "param1".to_string(),
366            Tensor::new(grad_data.clone()).expect("Failed to create tensor"),
367        );
368
369        optimizer.step(&mut parameters, &gradients).expect("Step failed");
370
371        assert_eq!(optimizer.step, 1);
372
373        let updated_data = parameters.get("param1").expect("Key not found").data()?;
374        for i in 0..updated_data.len() {
375            let expected = param_data[i] - 0.01 * grad_data[i];
376            assert!((updated_data[i] - expected).abs() < 1e-6);
377        }
378        Ok(())
379    }
380}