Skip to main content

torsh_optim/
natural_gradient.rs

1//! Natural Gradient optimizer
2
3use crate::{optimizer::BaseOptimizer, Optimizer, OptimizerResult, OptimizerState, ParamGroup};
4use parking_lot::RwLock;
5use std::collections::HashMap;
6use std::ops::Add;
7use std::sync::Arc;
8use torsh_core::error::Result;
9use torsh_tensor::{creation::zeros_like, Tensor};
10
11/// Natural Gradient optimizer
12///
13/// Natural gradients use the Fisher Information Matrix to precondition gradients,
14/// providing better-conditioned updates especially for neural networks and probabilistic models.
15///
16/// Reference: "Natural Gradient Works Efficiently in Learning" (Amari, 1998)
17pub struct NaturalGradient {
18    base: BaseOptimizer,
19    lr: f32,
20    momentum: f32,
21    damping: f32,
22    fisher_update_freq: usize,
23    use_empirical_fisher: bool,
24    fisher_ema_decay: f32,
25    step_count: usize,
26}
27
28impl NaturalGradient {
29    /// Create a new Natural Gradient optimizer
30    pub fn new(
31        params: Vec<Arc<RwLock<Tensor>>>,
32        lr: Option<f32>,
33        momentum: Option<f32>,
34        damping: Option<f32>,
35        fisher_update_freq: Option<usize>,
36        use_empirical_fisher: Option<bool>,
37        fisher_ema_decay: Option<f32>,
38    ) -> Self {
39        let lr = lr.unwrap_or(0.03);
40        let momentum = momentum.unwrap_or(0.9);
41        let damping = damping.unwrap_or(0.001);
42        let fisher_update_freq = fisher_update_freq.unwrap_or(1);
43        let use_empirical_fisher = use_empirical_fisher.unwrap_or(true);
44        let fisher_ema_decay = fisher_ema_decay.unwrap_or(0.95);
45
46        let mut defaults = HashMap::new();
47        defaults.insert("lr".to_string(), lr);
48        defaults.insert("momentum".to_string(), momentum);
49        defaults.insert("damping".to_string(), damping);
50
51        let param_group = ParamGroup::new(params, lr);
52
53        let base = BaseOptimizer {
54            param_groups: vec![param_group],
55            state: HashMap::new(),
56            optimizer_type: "NaturalGradient".to_string(),
57            defaults,
58        };
59
60        Self {
61            base,
62            lr,
63            momentum,
64            damping,
65            fisher_update_freq,
66            use_empirical_fisher,
67            fisher_ema_decay,
68            step_count: 0,
69        }
70    }
71
72    /// Builder pattern for Natural Gradient optimizer
73    pub fn builder() -> NaturalGradientBuilder {
74        NaturalGradientBuilder::default()
75    }
76
77    /// Compute Fisher Information Matrix (simplified approximation)
78    fn compute_fisher_matrix(&self, param: &Tensor, grad: &Tensor) -> Result<Tensor> {
79        // For neural networks, we use a diagonal approximation of the Fisher matrix
80        // F_ii ≈ E[g_i^2] where g_i is the gradient of log-likelihood w.r.t. parameter i
81
82        if self.use_empirical_fisher {
83            // Empirical Fisher: use squared gradients
84            Ok(grad.pow(2.0)?)
85        } else {
86            // True Fisher would require second-order information
87            // For now, we approximate with squared gradients plus some regularization
88            let reg = param.mul_scalar(1e-6)?;
89            let fisher_approx = grad.pow(2.0)?;
90            Ok(fisher_approx.add(&reg)?)
91        }
92    }
93
94    /// Apply natural gradient update
95    fn apply_natural_gradient_update(
96        &self,
97        param: &mut Tensor,
98        grad: &Tensor,
99        fisher: &Tensor,
100    ) -> Result<()> {
101        // Natural gradient update: θ_{t+1} = θ_t - η * F^{-1} * ∇L
102        // We approximate F^{-1} with element-wise division for diagonal Fisher matrix
103
104        let damped_fisher = fisher.add_scalar(self.damping)?;
105        let natural_grad = grad.div(&damped_fisher)?;
106        let update = natural_grad.mul_scalar(self.lr)?;
107
108        crate::param_update::sub_assign(&mut *param, &update)?;
109        Ok(())
110    }
111}
112
113impl Optimizer for NaturalGradient {
114    fn step(&mut self) -> OptimizerResult<()> {
115        self.step_count += 1;
116        let should_update_fisher = self.step_count % self.fisher_update_freq == 0;
117
118        // Extract values to avoid borrowing conflicts
119        let lr = self.lr;
120        let momentum = self.momentum;
121        let damping = self.damping;
122        let fisher_ema_decay = self.fisher_ema_decay;
123
124        for group in &mut self.base.param_groups {
125            for param_arc in &group.params {
126                let param = param_arc.write();
127
128                // Check if parameter has gradients
129                if !param.has_grad() {
130                    continue;
131                }
132
133                let grad = param
134                    .grad()
135                    .expect("gradient should exist after has_grad check");
136                let param_id = format!("{:p}", param_arc.as_ref());
137
138                // Extract data early to avoid borrow conflicts
139                let _param_data = param.clone();
140                let grad_data = grad.clone();
141
142                // Get or initialize optimizer state
143                let needs_init = !self.base.state.contains_key(&param_id);
144                let state = self
145                    .base
146                    .state
147                    .entry(param_id.clone())
148                    .or_insert_with(HashMap::new);
149
150                if needs_init {
151                    state.insert("momentum_buffer".to_string(), zeros_like(&param)?);
152                    state.insert("fisher_matrix".to_string(), zeros_like(&param)?);
153                    state.insert("step".to_string(), zeros_like(&param)?);
154                }
155
156                let mut momentum_buffer = state
157                    .get("momentum_buffer")
158                    .expect("momentum_buffer state should exist")
159                    .clone();
160                let mut fisher_matrix = state
161                    .get("fisher_matrix")
162                    .expect("fisher_matrix state should exist")
163                    .clone();
164                let mut step_tensor = state.get("step").expect("step state should exist").clone();
165
166                // Increment step count
167                step_tensor.add_scalar_(1.0)?;
168
169                // Update Fisher matrix if needed
170                let mut new_fisher_opt = None;
171                if should_update_fisher {
172                    // Temporarily drop the mutable borrow
173                    drop(param);
174
175                    // Compute Fisher matrix inline: F = g ⊗ g (outer product approximation)
176                    let new_fisher = grad_data.mul_op(&grad_data)?;
177                    new_fisher_opt = Some(new_fisher);
178                }
179
180                let fisher_was_updated = new_fisher_opt.is_some();
181                if let Some(new_fisher) = new_fisher_opt.clone() {
182                    if step_tensor.to_vec()?[0] == 1.0 {
183                        // First step: initialize Fisher matrix
184                        fisher_matrix = new_fisher;
185                    } else {
186                        // Update Fisher matrix with exponential moving average
187                        fisher_matrix = fisher_matrix
188                            .mul_scalar(fisher_ema_decay)?
189                            .add(&new_fisher.mul_scalar(1.0 - fisher_ema_decay)?)?;
190                    }
191                }
192
193                // Compute natural gradient
194                let damped_fisher = fisher_matrix.add_scalar(damping)?;
195                let natural_grad = grad_data.div(&damped_fisher)?;
196
197                // Re-acquire parameter lock if it was dropped
198                let mut param = if fisher_was_updated {
199                    param_arc.write()
200                } else {
201                    // Re-acquire the lock since we dropped it earlier
202                    param_arc.write()
203                };
204
205                // Apply momentum
206                if momentum != 0.0 {
207                    momentum_buffer = momentum_buffer.mul_scalar(momentum)?.add(&natural_grad)?;
208                    let update = momentum_buffer.mul_scalar(lr)?;
209                    crate::param_update::sub_assign(&mut param, &update)?;
210                } else {
211                    let update = natural_grad.mul_scalar(lr)?;
212                    crate::param_update::sub_assign(&mut param, &update)?;
213                }
214
215                // Update state
216                state.insert("momentum_buffer".to_string(), momentum_buffer);
217                state.insert("fisher_matrix".to_string(), fisher_matrix);
218                state.insert("step".to_string(), step_tensor);
219            }
220        }
221
222        Ok(())
223    }
224
225    fn zero_grad(&mut self) {
226        self.base.zero_grad();
227    }
228
229    fn get_lr(&self) -> Vec<f32> {
230        self.base.get_lr()
231    }
232
233    fn set_lr(&mut self, lr: f32) {
234        self.lr = lr;
235        self.base.set_lr(lr);
236    }
237
238    fn set_lrs(&mut self, lrs: &[f32]) {
239        if let Some(&lr) = lrs.first() {
240            self.lr = lr;
241        }
242        self.base.set_lrs(lrs);
243    }
244
245    fn add_param_group(&mut self, params: Vec<Arc<RwLock<Tensor>>>, options: HashMap<String, f32>) {
246        self.base.add_param_group(params, options);
247    }
248
249    fn parameters(&self) -> Vec<Arc<RwLock<Tensor>>> {
250        self.base.parameters()
251    }
252
253    fn state_dict(&self) -> OptimizerResult<OptimizerState> {
254        self.base.state_dict()
255    }
256
257    fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
258        self.base.load_state_dict(state)
259    }
260}
261
262/// Builder for Natural Gradient optimizer
263pub struct NaturalGradientBuilder {
264    params: Vec<Arc<RwLock<Tensor>>>,
265    lr: f32,
266    momentum: f32,
267    damping: f32,
268    fisher_update_freq: usize,
269    use_empirical_fisher: bool,
270    fisher_ema_decay: f32,
271}
272
273impl Default for NaturalGradientBuilder {
274    fn default() -> Self {
275        Self {
276            params: Vec::new(),
277            lr: 0.03,
278            momentum: 0.9,
279            damping: 0.001,
280            fisher_update_freq: 1,
281            use_empirical_fisher: true,
282            fisher_ema_decay: 0.95,
283        }
284    }
285}
286
287impl NaturalGradientBuilder {
288    pub fn params(mut self, params: Vec<Arc<RwLock<Tensor>>>) -> Self {
289        self.params = params;
290        self
291    }
292
293    pub fn lr(mut self, lr: f32) -> Self {
294        self.lr = lr;
295        self
296    }
297
298    pub fn momentum(mut self, momentum: f32) -> Self {
299        self.momentum = momentum;
300        self
301    }
302
303    pub fn damping(mut self, damping: f32) -> Self {
304        self.damping = damping;
305        self
306    }
307
308    pub fn fisher_update_freq(mut self, freq: usize) -> Self {
309        self.fisher_update_freq = freq;
310        self
311    }
312
313    pub fn use_empirical_fisher(mut self, use_empirical: bool) -> Self {
314        self.use_empirical_fisher = use_empirical;
315        self
316    }
317
318    pub fn fisher_ema_decay(mut self, decay: f32) -> Self {
319        self.fisher_ema_decay = decay;
320        self
321    }
322
323    pub fn build(self) -> NaturalGradient {
324        NaturalGradient::new(
325            self.params,
326            Some(self.lr),
327            Some(self.momentum),
328            Some(self.damping),
329            Some(self.fisher_update_freq),
330            Some(self.use_empirical_fisher),
331            Some(self.fisher_ema_decay),
332        )
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use approx::assert_relative_eq;
340    use torsh_tensor::creation::randn;
341
342    #[test]
343    fn test_natural_gradient_creation() {
344        let param = Arc::new(RwLock::new(randn::<f32>(&[10, 10]).unwrap()));
345        let params = vec![param];
346
347        let optimizer = NaturalGradient::new(
348            params,
349            Some(0.01),
350            Some(0.9),
351            Some(0.001),
352            Some(1),
353            Some(true),
354            Some(0.95),
355        );
356
357        assert_eq!(optimizer.lr, 0.01);
358        assert_eq!(optimizer.momentum, 0.9);
359        assert_eq!(optimizer.damping, 0.001);
360    }
361
362    #[test]
363    fn test_natural_gradient_builder() {
364        let param = Arc::new(RwLock::new(randn::<f32>(&[5, 5]).unwrap()));
365        let params = vec![param];
366
367        let optimizer = NaturalGradient::builder()
368            .params(params)
369            .lr(0.02)
370            .momentum(0.95)
371            .damping(0.01)
372            .fisher_update_freq(5)
373            .use_empirical_fisher(false)
374            .fisher_ema_decay(0.9)
375            .build();
376
377        assert_eq!(optimizer.lr, 0.02);
378        assert_eq!(optimizer.momentum, 0.95);
379        assert_eq!(optimizer.damping, 0.01);
380        assert_eq!(optimizer.fisher_update_freq, 5);
381        assert!(!optimizer.use_empirical_fisher);
382        assert_eq!(optimizer.fisher_ema_decay, 0.9);
383    }
384
385    #[test]
386    fn test_natural_gradient_step() -> OptimizerResult<()> {
387        let param = Arc::new(RwLock::new(randn::<f32>(&[3, 3]).unwrap()));
388        let initial_param = param.read().clone();
389
390        // Set up gradient
391        {
392            let mut p = param.write();
393            let grad = randn::<f32>(&[3, 3]).unwrap();
394            p.set_grad(Some(grad));
395        }
396
397        let params = vec![param.clone()];
398        let mut optimizer = NaturalGradient::new(params, Some(0.01), None, None, None, None, None);
399
400        // Perform optimization step
401        optimizer.step().unwrap();
402
403        // Check that parameter changed
404        let final_param = param.read().clone();
405        let diff = initial_param.sub(&final_param).unwrap();
406        let norm = diff.norm()?.to_vec()?[0];
407        assert!(
408            norm > 0.0,
409            "Parameter should change after optimization step"
410        );
411        Ok(())
412    }
413
414    #[test]
415    #[allow(dead_code)]
416    fn test_fisher_matrix_computation() -> OptimizerResult<()> {
417        let param = randn::<f32>(&[2, 2]).unwrap();
418        let grad = randn::<f32>(&[2, 2]).unwrap();
419
420        let optimizer =
421            NaturalGradient::new(vec![], Some(0.01), None, None, None, Some(true), None);
422
423        let fisher = optimizer.compute_fisher_matrix(&param, &grad).unwrap();
424
425        // Fisher matrix should be positive (squared gradients)
426        let fisher_vec = fisher.to_vec()?;
427        for &val in &fisher_vec {
428            assert!(val >= 0.0, "Fisher matrix elements should be non-negative");
429        }
430
431        Ok(())
432    }
433}