1use 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
11pub 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 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 pub fn builder() -> NaturalGradientBuilder {
74 NaturalGradientBuilder::default()
75 }
76
77 fn compute_fisher_matrix(&self, param: &Tensor, grad: &Tensor) -> Result<Tensor> {
79 if self.use_empirical_fisher {
83 Ok(grad.pow(2.0)?)
85 } else {
86 let reg = param.mul_scalar(1e-6)?;
89 let fisher_approx = grad.pow(2.0)?;
90 Ok(fisher_approx.add(®)?)
91 }
92 }
93
94 fn apply_natural_gradient_update(
96 &self,
97 param: &mut Tensor,
98 grad: &Tensor,
99 fisher: &Tensor,
100 ) -> Result<()> {
101 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 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 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 let _param_data = param.clone();
140 let grad_data = grad.clone();
141
142 let needs_init = !self.base.state.contains_key(¶m_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(¶m)?);
152 state.insert("fisher_matrix".to_string(), zeros_like(¶m)?);
153 state.insert("step".to_string(), zeros_like(¶m)?);
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 step_tensor.add_scalar_(1.0)?;
168
169 let mut new_fisher_opt = None;
171 if should_update_fisher {
172 drop(param);
174
175 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 fisher_matrix = new_fisher;
185 } else {
186 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 let damped_fisher = fisher_matrix.add_scalar(damping)?;
195 let natural_grad = grad_data.div(&damped_fisher)?;
196
197 let mut param = if fisher_was_updated {
199 param_arc.write()
200 } else {
201 param_arc.write()
203 };
204
205 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 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
262pub 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 {
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 optimizer.step().unwrap();
402
403 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(¶m, &grad).unwrap();
424
425 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}