Skip to main content

trustformers_optim/
gradient_processing.rs

1//! # Gradient Processing Enhancements
2//!
3//! This module provides advanced gradient processing techniques that can improve
4//! training stability, convergence speed, and final model performance.
5//!
6//! ## Available Techniques
7//!
8//! - **Gradient Centralization**: Removes the mean of gradients to improve convergence
9//! - **Gradient Standardization**: Normalizes gradients to unit variance
10//! - **Adaptive Gradient Clipping**: Dynamically adjusts clipping based on gradient history
11//! - **Gradient Noise Injection**: Adds controlled noise to escape local minima
12//! - **Gradient Smoothing**: Applies exponential moving average to gradients
13//! - **Hessian-based Preconditioning**: Uses second-order information to precondition gradients
14
15use anyhow::{anyhow, Result};
16use scirs2_core::random::thread_rng;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use trustformers_core::tensor::Tensor;
20
21/// Configuration for gradient processing techniques.
22#[derive(Debug, Clone, Serialize, Deserialize, Default)]
23pub struct GradientProcessingConfig {
24    /// Enable gradient centralization
25    pub enable_centralization: bool,
26    /// Enable gradient standardization
27    pub enable_standardization: bool,
28    /// Enable adaptive gradient clipping
29    pub enable_adaptive_clipping: bool,
30    /// Enable gradient noise injection
31    pub enable_noise_injection: bool,
32    /// Enable gradient smoothing
33    pub enable_smoothing: bool,
34    /// Enable Hessian-based preconditioning
35    pub enable_hessian_preconditioning: bool,
36    /// Adaptive clipping parameters
37    pub adaptive_clipping: AdaptiveClippingConfig,
38    /// Noise injection parameters
39    pub noise_injection: NoiseInjectionConfig,
40    /// Smoothing parameters
41    pub smoothing: SmoothingConfig,
42    /// Hessian preconditioning parameters
43    pub hessian_preconditioning: HessianPreconditioningConfig,
44}
45
46/// Configuration for adaptive gradient clipping.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct AdaptiveClippingConfig {
49    /// Initial clipping threshold
50    pub initial_clip_norm: f32,
51    /// Minimum clipping threshold
52    pub min_clip_norm: f32,
53    /// Maximum clipping threshold
54    pub max_clip_norm: f32,
55    /// Adaptation rate
56    pub adaptation_rate: f32,
57    /// Target gradient norm percentile
58    pub target_percentile: f32,
59    /// History window size for computing statistics
60    pub history_window: usize,
61}
62
63impl Default for AdaptiveClippingConfig {
64    fn default() -> Self {
65        Self {
66            initial_clip_norm: 1.0,
67            min_clip_norm: 0.1,
68            max_clip_norm: 10.0,
69            adaptation_rate: 0.01,
70            target_percentile: 0.9,
71            history_window: 100,
72        }
73    }
74}
75
76/// Configuration for gradient noise injection.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct NoiseInjectionConfig {
79    /// Initial noise scale
80    pub initial_noise_scale: f32,
81    /// Noise decay rate per step
82    pub decay_rate: f32,
83    /// Minimum noise scale
84    pub min_noise_scale: f32,
85    /// Noise type
86    pub noise_type: NoiseType,
87}
88
89impl Default for NoiseInjectionConfig {
90    fn default() -> Self {
91        Self {
92            initial_noise_scale: 0.1,
93            decay_rate: 0.999,
94            min_noise_scale: 1e-6,
95            noise_type: NoiseType::Gaussian,
96        }
97    }
98}
99
100/// Configuration for gradient smoothing.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SmoothingConfig {
103    /// Exponential moving average decay rate
104    pub decay: f32,
105    /// Whether to debias the moving average
106    pub debias: bool,
107}
108
109impl Default for SmoothingConfig {
110    fn default() -> Self {
111        Self {
112            decay: 0.9,
113            debias: true,
114        }
115    }
116}
117
118/// Configuration for Hessian-based preconditioning.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct HessianPreconditioningConfig {
121    /// Type of Hessian approximation to use
122    pub approximation_type: HessianApproximationType,
123    /// Damping factor for numerical stability
124    pub damping: f32,
125    /// Update frequency for Hessian approximation (every N steps)
126    pub update_frequency: usize,
127    /// History window for maintaining Hessian approximation
128    pub history_window: usize,
129    /// Minimum eigenvalue threshold for conditioning
130    pub min_eigenvalue: f32,
131    /// Maximum condition number allowed
132    pub max_condition_number: f32,
133}
134
135impl Default for HessianPreconditioningConfig {
136    fn default() -> Self {
137        Self {
138            approximation_type: HessianApproximationType::Diagonal,
139            damping: 1e-4,
140            update_frequency: 10,
141            history_window: 20,
142            min_eigenvalue: 1e-8,
143            max_condition_number: 1e6,
144        }
145    }
146}
147
148/// Types of noise for gradient noise injection.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub enum NoiseType {
151    Gaussian,
152    Uniform,
153    Laplace,
154}
155
156/// Types of Hessian approximation methods.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub enum HessianApproximationType {
159    /// Use only the diagonal of the Hessian (most efficient)
160    Diagonal,
161    /// Use Gauss-Newton approximation (J^T J)
162    GaussNewton,
163    /// Use Fisher Information Matrix approximation
164    FisherInformation,
165    /// Use quasi-Newton L-BFGS-style approximation
166    QuasiNewton,
167}
168
169/// Gradient processor that applies various enhancement techniques.
170#[derive(Debug)]
171pub struct GradientProcessor {
172    config: GradientProcessingConfig,
173    current_step: usize,
174
175    // Adaptive clipping state
176    gradient_norm_history: Vec<f32>,
177    current_clip_norm: f32,
178
179    // Noise injection state
180    current_noise_scale: f32,
181
182    // Smoothing state
183    smoothed_gradients: HashMap<usize, Tensor>,
184    smoothing_bias_correction: f32,
185
186    // Hessian preconditioning state
187    hessian_diagonal: HashMap<usize, Tensor>,
188    hessian_inverse: HashMap<usize, Tensor>,
189    last_hessian_update: usize,
190    gradient_history: Vec<Vec<Tensor>>,
191}
192
193impl GradientProcessor {
194    /// Create a new gradient processor with the given configuration.
195    pub fn new(config: GradientProcessingConfig) -> Self {
196        Self {
197            current_clip_norm: config.adaptive_clipping.initial_clip_norm,
198            current_noise_scale: config.noise_injection.initial_noise_scale,
199            config,
200            current_step: 0,
201            gradient_norm_history: Vec::new(),
202            smoothed_gradients: HashMap::new(),
203            smoothing_bias_correction: 1.0,
204            hessian_diagonal: HashMap::new(),
205            hessian_inverse: HashMap::new(),
206            last_hessian_update: 0,
207            gradient_history: Vec::new(),
208        }
209    }
210
211    /// Create a gradient processor with default configuration.
212    pub fn with_defaults() -> Self {
213        Self::new(GradientProcessingConfig::default())
214    }
215
216    /// Process gradients with enabled techniques.
217    pub fn process_gradients(&mut self, gradients: &mut [Tensor]) -> Result<()> {
218        self.current_step += 1;
219
220        // Apply gradient centralization
221        if self.config.enable_centralization {
222            self.apply_centralization(gradients)?;
223        }
224
225        // Apply gradient standardization
226        if self.config.enable_standardization {
227            self.apply_standardization(gradients)?;
228        }
229
230        // Apply gradient smoothing
231        if self.config.enable_smoothing {
232            self.apply_smoothing(gradients)?;
233        }
234
235        // Apply Hessian-based preconditioning
236        if self.config.enable_hessian_preconditioning {
237            self.apply_hessian_preconditioning(gradients)?;
238        }
239
240        // Apply adaptive gradient clipping
241        if self.config.enable_adaptive_clipping {
242            self.apply_adaptive_clipping(gradients)?;
243        }
244
245        // Apply gradient noise injection
246        if self.config.enable_noise_injection {
247            self.apply_noise_injection(gradients)?;
248        }
249
250        Ok(())
251    }
252
253    /// Apply gradient centralization (remove mean).
254    fn apply_centralization(&self, gradients: &mut [Tensor]) -> Result<()> {
255        for gradient in gradients.iter_mut() {
256            // Compute mean across all dimensions
257            let mean = gradient.mean()?;
258            *gradient = gradient.sub(&mean)?;
259        }
260        Ok(())
261    }
262
263    /// Apply gradient standardization (normalize to unit variance).
264    fn apply_standardization(&self, gradients: &mut [Tensor]) -> Result<()> {
265        for gradient in gradients.iter_mut() {
266            // Compute standard deviation manually
267            let mean = gradient.mean()?;
268            let centered = gradient.sub(&mean)?;
269            let squared = centered.mul(&centered)?;
270            let variance = squared.mean()?;
271            let std_dev = variance.sqrt()?;
272
273            // Add small epsilon to prevent division by zero
274            let epsilon = Tensor::scalar(1e-8)?;
275            let std_dev_safe = std_dev.add(&epsilon)?;
276
277            // Normalize
278            *gradient = gradient.div(&std_dev_safe)?;
279        }
280        Ok(())
281    }
282
283    /// Apply adaptive gradient clipping.
284    fn apply_adaptive_clipping(&mut self, gradients: &mut [Tensor]) -> Result<()> {
285        // Compute total gradient norm
286        let mut total_norm_sq = 0.0;
287        for gradient in gradients.iter() {
288            let norm_sq = gradient.norm_squared()?.to_scalar()?;
289            total_norm_sq += norm_sq;
290        }
291        let total_norm = total_norm_sq.sqrt();
292
293        // Update gradient norm history
294        self.gradient_norm_history.push(total_norm);
295        if self.gradient_norm_history.len() > self.config.adaptive_clipping.history_window {
296            self.gradient_norm_history.remove(0);
297        }
298
299        // Update adaptive clipping threshold
300        if self.gradient_norm_history.len() >= 10 {
301            // Compute target percentile of gradient norms
302            let mut sorted_norms = self.gradient_norm_history.clone();
303            sorted_norms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
304            let percentile_idx = (sorted_norms.len() as f32
305                * self.config.adaptive_clipping.target_percentile)
306                as usize;
307            let target_norm = sorted_norms[percentile_idx.min(sorted_norms.len() - 1)];
308
309            // Adapt clipping threshold towards target
310            let adaptation = self.config.adaptive_clipping.adaptation_rate
311                * (target_norm - self.current_clip_norm);
312            self.current_clip_norm += adaptation;
313
314            // Clamp to bounds
315            self.current_clip_norm = self
316                .current_clip_norm
317                .max(self.config.adaptive_clipping.min_clip_norm)
318                .min(self.config.adaptive_clipping.max_clip_norm);
319        }
320
321        // Apply clipping if needed
322        if total_norm > self.current_clip_norm {
323            let clip_factor = self.current_clip_norm / total_norm;
324            for gradient in gradients.iter_mut() {
325                *gradient = gradient.mul_scalar(clip_factor)?;
326            }
327        }
328
329        Ok(())
330    }
331
332    /// Apply gradient noise injection.
333    fn apply_noise_injection(&mut self, gradients: &mut [Tensor]) -> Result<()> {
334        // Decay noise scale
335        self.current_noise_scale *= self.config.noise_injection.decay_rate;
336        self.current_noise_scale =
337            self.current_noise_scale.max(self.config.noise_injection.min_noise_scale);
338
339        let scale = self.current_noise_scale;
340        for gradient in gradients.iter_mut() {
341            let shape = gradient.shape();
342            let numel: usize = shape.iter().product();
343
344            let noise = match self.config.noise_injection.noise_type {
345                NoiseType::Gaussian => {
346                    // `randn` is already N(0, 1); scaling it gives N(0, scale²).
347                    Tensor::randn(&shape)?.mul_scalar(scale)?
348                },
349                NoiseType::Uniform => {
350                    // U(−b, b) has variance b²/3, so b = scale·√3 matches N(0, scale²).
351                    let bound = scale * 3.0_f32.sqrt();
352                    let mut rng = thread_rng();
353                    let values: Vec<f32> =
354                        (0..numel).map(|_| rng.random_range(-bound..=bound)).collect();
355                    Tensor::from_vec(values, &shape)?
356                },
357                NoiseType::Laplace => {
358                    // Inverse-CDF sampling: for u ~ U(−½, ½),
359                    // x = −b·sgn(u)·ln(1 − 2|u|) is Laplace(0, b) with variance 2b²,
360                    // so b = scale/√2 matches N(0, scale²).
361                    let diversity = scale / 2.0_f32.sqrt();
362                    let mut rng = thread_rng();
363                    let values: Vec<f32> = (0..numel)
364                        .map(|_| {
365                            let u: f32 = rng.random_range(-0.5_f32..0.5_f32);
366                            let magnitude = (1.0 - 2.0 * u.abs()).max(f32::MIN_POSITIVE);
367                            -diversity * u.signum() * magnitude.ln()
368                        })
369                        .collect();
370                    Tensor::from_vec(values, &shape)?
371                },
372            };
373
374            *gradient = gradient.add(&noise)?;
375        }
376
377        Ok(())
378    }
379
380    /// Apply gradient smoothing with exponential moving average.
381    fn apply_smoothing(&mut self, gradients: &mut [Tensor]) -> Result<()> {
382        let decay = self.config.smoothing.decay;
383
384        for (i, gradient) in gradients.iter_mut().enumerate() {
385            if let Some(smoothed) = self.smoothed_gradients.get(&i) {
386                // Update smoothed gradient: smoothed = decay * smoothed + (1 - decay) * gradient
387                let new_smoothed =
388                    smoothed.mul_scalar(decay)?.add(&gradient.mul_scalar(1.0 - decay)?)?;
389                self.smoothed_gradients.insert(i, new_smoothed.clone());
390
391                // Apply bias correction if enabled
392                if self.config.smoothing.debias {
393                    self.smoothing_bias_correction *= decay;
394                    let bias_corrected =
395                        new_smoothed.div_scalar(1.0 - self.smoothing_bias_correction)?;
396                    *gradient = bias_corrected;
397                } else {
398                    *gradient = new_smoothed;
399                }
400            } else {
401                // First time seeing this gradient
402                self.smoothed_gradients.insert(i, gradient.clone());
403            }
404        }
405
406        Ok(())
407    }
408
409    /// Apply Hessian-based preconditioning to gradients.
410    fn apply_hessian_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
411        // Store gradient history for Hessian approximation
412        self.gradient_history.push(gradients.to_vec());
413        if self.gradient_history.len() > self.config.hessian_preconditioning.history_window {
414            self.gradient_history.remove(0);
415        }
416
417        // Update Hessian approximation if needed
418        if self.current_step - self.last_hessian_update
419            >= self.config.hessian_preconditioning.update_frequency
420        {
421            self.update_hessian_approximation(gradients)?;
422            self.last_hessian_update = self.current_step;
423        }
424
425        // Apply preconditioning based on approximation type
426        match self.config.hessian_preconditioning.approximation_type {
427            HessianApproximationType::Diagonal => {
428                self.apply_diagonal_preconditioning(gradients)?;
429            },
430            HessianApproximationType::GaussNewton => {
431                self.apply_gauss_newton_preconditioning(gradients)?;
432            },
433            HessianApproximationType::FisherInformation => {
434                self.apply_fisher_information_preconditioning(gradients)?;
435            },
436            HessianApproximationType::QuasiNewton => {
437                self.apply_quasi_newton_preconditioning(gradients)?;
438            },
439        }
440
441        Ok(())
442    }
443
444    /// Update Hessian approximation based on gradient history.
445    fn update_hessian_approximation(&mut self, gradients: &[Tensor]) -> Result<()> {
446        match self.config.hessian_preconditioning.approximation_type {
447            HessianApproximationType::Diagonal => {
448                self.update_diagonal_hessian(gradients)?;
449            },
450            HessianApproximationType::GaussNewton => {
451                self.update_gauss_newton_hessian(gradients)?;
452            },
453            HessianApproximationType::FisherInformation => {
454                self.update_fisher_information_hessian(gradients)?;
455            },
456            HessianApproximationType::QuasiNewton => {
457                self.update_quasi_newton_hessian(gradients)?;
458            },
459        }
460        Ok(())
461    }
462
463    /// Update diagonal Hessian approximation using gradient variance.
464    fn update_diagonal_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
465        for (i, gradient) in gradients.iter().enumerate() {
466            // Approximate diagonal Hessian using gradient variance over history
467            if self.gradient_history.len() > 1 {
468                let mut variance = Tensor::zeros(&gradient.shape())?;
469                let mut mean = Tensor::zeros(&gradient.shape())?;
470
471                // Compute mean
472                for grad_vec in &self.gradient_history {
473                    if let Some(hist_grad) = grad_vec.get(i) {
474                        mean = mean.add(hist_grad)?;
475                    }
476                }
477                mean = mean.div_scalar(self.gradient_history.len() as f32)?;
478
479                // Compute variance (approximation of diagonal Hessian)
480                for grad_vec in &self.gradient_history {
481                    if let Some(hist_grad) = grad_vec.get(i) {
482                        let diff = hist_grad.sub(&mean)?;
483                        variance = variance.add(&diff.mul(&diff)?)?;
484                    }
485                }
486                variance = variance.div_scalar(self.gradient_history.len() as f32)?;
487
488                // Add damping for numerical stability
489                let damping = Tensor::ones(&gradient.shape())?
490                    .mul_scalar(self.config.hessian_preconditioning.damping)?;
491                variance = variance.add(&damping)?;
492
493                self.hessian_diagonal.insert(i, variance);
494            }
495        }
496        Ok(())
497    }
498
499    /// Update Gauss-Newton Hessian approximation (simplified).
500    fn update_gauss_newton_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
501        // Simplified Gauss-Newton approximation using gradient outer product
502        for (i, gradient) in gradients.iter().enumerate() {
503            // Approximate with gradient outer product (simplified)
504            let outer_product = gradient.mul(gradient)?;
505
506            // Add damping
507            let damping = Tensor::ones(&gradient.shape())?
508                .mul_scalar(self.config.hessian_preconditioning.damping)?;
509            let hessian_approx = outer_product.add(&damping)?;
510
511            self.hessian_diagonal.insert(i, hessian_approx);
512        }
513        Ok(())
514    }
515
516    /// Update Fisher Information Matrix approximation.
517    fn update_fisher_information_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
518        // Fisher Information Matrix approximation (similar to Gauss-Newton for this context)
519        for (i, gradient) in gradients.iter().enumerate() {
520            // Approximate Fisher Information using gradient squared
521            let fisher_approx = gradient.mul(gradient)?;
522
523            // Add damping
524            let damping = Tensor::ones(&gradient.shape())?
525                .mul_scalar(self.config.hessian_preconditioning.damping)?;
526            let hessian_approx = fisher_approx.add(&damping)?;
527
528            self.hessian_diagonal.insert(i, hessian_approx);
529        }
530        Ok(())
531    }
532
533    /// Update quasi-Newton Hessian approximation using L-BFGS-style update.
534    fn update_quasi_newton_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
535        // Simplified quasi-Newton approximation using gradient differences
536        if self.gradient_history.len() > 1 {
537            for (i, gradient) in gradients.iter().enumerate() {
538                // Get previous gradient
539                if let Some(prev_grad_vec) =
540                    self.gradient_history.get(self.gradient_history.len() - 2)
541                {
542                    if let Some(prev_grad) = prev_grad_vec.get(i) {
543                        // Compute gradient difference
544                        let grad_diff = gradient.sub(prev_grad)?;
545
546                        // Approximate Hessian using gradient difference magnitude
547                        let hessian_approx = grad_diff.abs()?;
548
549                        // Add damping
550                        let damping = Tensor::ones(&gradient.shape())?
551                            .mul_scalar(self.config.hessian_preconditioning.damping)?;
552                        let final_hessian = hessian_approx.add(&damping)?;
553
554                        self.hessian_diagonal.insert(i, final_hessian);
555                    }
556                }
557            }
558        }
559        Ok(())
560    }
561
562    /// Apply diagonal preconditioning to gradients.
563    fn apply_diagonal_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
564        for (i, gradient) in gradients.iter_mut().enumerate() {
565            if let Some(hessian_diag) = self.hessian_diagonal.get(&i) {
566                // Compute preconditioned gradient: H^{-1} * g
567                // For diagonal H, this is element-wise division
568                let min_val = Tensor::scalar(self.config.hessian_preconditioning.min_eigenvalue)?;
569                let clamped_hessian = hessian_diag.max(&min_val)?;
570
571                *gradient = gradient.div(&clamped_hessian)?;
572            }
573        }
574        Ok(())
575    }
576
577    /// Apply Gauss-Newton preconditioning to gradients.
578    fn apply_gauss_newton_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
579        // For simplicity, use diagonal approximation
580        self.apply_diagonal_preconditioning(gradients)
581    }
582
583    /// Apply Fisher Information preconditioning to gradients.
584    fn apply_fisher_information_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
585        // For simplicity, use diagonal approximation
586        self.apply_diagonal_preconditioning(gradients)
587    }
588
589    /// Apply quasi-Newton preconditioning to gradients.
590    fn apply_quasi_newton_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
591        // For simplicity, use diagonal approximation
592        self.apply_diagonal_preconditioning(gradients)
593    }
594
595    /// Get current adaptive clipping threshold.
596    pub fn get_current_clip_norm(&self) -> f32 {
597        self.current_clip_norm
598    }
599
600    /// Get current noise scale.
601    pub fn get_current_noise_scale(&self) -> f32 {
602        self.current_noise_scale
603    }
604
605    /// Get gradient norm statistics.
606    pub fn get_gradient_norm_stats(&self) -> Option<(f32, f32, f32)> {
607        if self.gradient_norm_history.is_empty() {
608            return None;
609        }
610
611        let sum: f32 = self.gradient_norm_history.iter().sum();
612        let mean = sum / self.gradient_norm_history.len() as f32;
613
614        let variance = self.gradient_norm_history.iter().map(|x| (x - mean).powi(2)).sum::<f32>()
615            / self.gradient_norm_history.len() as f32;
616        let std_dev = variance.sqrt();
617
618        let max_norm = self.gradient_norm_history.iter().fold(0.0f32, |acc, &x| acc.max(x));
619
620        Some((mean, std_dev, max_norm))
621    }
622
623    /// Reset internal state.
624    pub fn reset(&mut self) {
625        self.current_step = 0;
626        self.gradient_norm_history.clear();
627        self.smoothed_gradients.clear();
628        self.current_clip_norm = self.config.adaptive_clipping.initial_clip_norm;
629        self.current_noise_scale = self.config.noise_injection.initial_noise_scale;
630        self.smoothing_bias_correction = 1.0;
631        self.hessian_diagonal.clear();
632        self.hessian_inverse.clear();
633        self.last_hessian_update = 0;
634        self.gradient_history.clear();
635    }
636
637    /// Update configuration.
638    pub fn set_config(&mut self, config: GradientProcessingConfig) {
639        self.config = config;
640        self.reset();
641    }
642
643    /// Get current configuration.
644    pub fn get_config(&self) -> &GradientProcessingConfig {
645        &self.config
646    }
647}
648
649/// Wrapper for optimizers that automatically applies gradient processing.
650pub struct GradientProcessedOptimizer<T> {
651    base_optimizer: T,
652    gradient_processor: GradientProcessor,
653}
654
655impl<T> GradientProcessedOptimizer<T> {
656    /// Create a new gradient-processed optimizer.
657    pub fn new(base_optimizer: T, config: GradientProcessingConfig) -> Self {
658        Self {
659            base_optimizer,
660            gradient_processor: GradientProcessor::new(config),
661        }
662    }
663
664    /// Create with default gradient processing configuration.
665    pub fn with_default_processing(base_optimizer: T) -> Self {
666        Self::new(base_optimizer, GradientProcessingConfig::default())
667    }
668
669    /// Get reference to the gradient processor.
670    pub fn gradient_processor(&self) -> &GradientProcessor {
671        &self.gradient_processor
672    }
673
674    /// Get mutable reference to the gradient processor.
675    pub fn gradient_processor_mut(&mut self) -> &mut GradientProcessor {
676        &mut self.gradient_processor
677    }
678
679    /// Get reference to the base optimizer.
680    pub fn base_optimizer(&self) -> &T {
681        &self.base_optimizer
682    }
683
684    /// Get mutable reference to the base optimizer.
685    pub fn base_optimizer_mut(&mut self) -> &mut T {
686        &mut self.base_optimizer
687    }
688}
689
690impl<T: crate::optimizer::OptimizerState> crate::optimizer::OptimizerState
691    for GradientProcessedOptimizer<T>
692{
693    fn zero_grad(&mut self) -> Result<()> {
694        self.base_optimizer.zero_grad()
695    }
696
697    fn step(&mut self, parameters: &mut [Tensor]) -> Result<()> {
698        // Extract gradients from parameters
699        let mut gradients = Vec::new();
700        for param in parameters.iter() {
701            if let Ok(grad) = param.grad() {
702                gradients.push(grad);
703            } else {
704                return Err(anyhow!("Parameter missing gradient"));
705            }
706        }
707
708        // Process gradients
709        self.gradient_processor.process_gradients(&mut gradients)?;
710
711        // Update parameter gradients with processed versions
712        for (param, processed_grad) in parameters.iter_mut().zip(gradients.iter()) {
713            param.set_grad(processed_grad.clone())?;
714        }
715
716        // Perform optimization step
717        self.base_optimizer.step(parameters)
718    }
719
720    fn get_lr(&self) -> f32 {
721        self.base_optimizer.get_lr()
722    }
723
724    fn set_lr(&mut self, lr: f32) {
725        self.base_optimizer.set_lr(lr);
726    }
727
728    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
729        // For simplicity, we'll only save the base optimizer state
730        // In a full implementation, we'd also save gradient processor state
731        self.base_optimizer.state_dict()
732    }
733
734    fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
735        self.base_optimizer.load_state_dict(state)
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742
743    #[test]
744    fn test_gradient_processing_config_default() {
745        let config = GradientProcessingConfig::default();
746        assert!(!config.enable_centralization);
747        assert!(!config.enable_standardization);
748        assert!(!config.enable_adaptive_clipping);
749        assert!(!config.enable_noise_injection);
750        assert!(!config.enable_smoothing);
751    }
752
753    #[test]
754    fn test_adaptive_clipping_config_default() {
755        let config = AdaptiveClippingConfig::default();
756        assert_eq!(config.initial_clip_norm, 1.0);
757        assert_eq!(config.min_clip_norm, 0.1);
758        assert_eq!(config.max_clip_norm, 10.0);
759        assert_eq!(config.adaptation_rate, 0.01);
760        assert_eq!(config.target_percentile, 0.9);
761        assert_eq!(config.history_window, 100);
762    }
763
764    #[test]
765    fn test_gradient_processor_creation() {
766        let processor = GradientProcessor::with_defaults();
767        assert_eq!(processor.current_step, 0);
768        assert_eq!(processor.gradient_norm_history.len(), 0);
769    }
770
771    #[test]
772    fn test_gradient_norm_stats_empty() {
773        let processor = GradientProcessor::with_defaults();
774        assert!(processor.get_gradient_norm_stats().is_none());
775    }
776
777    #[test]
778    fn test_gradient_processor_reset() {
779        let mut processor = GradientProcessor::with_defaults();
780        processor.current_step = 10;
781        processor.gradient_norm_history.push(1.0);
782
783        processor.reset();
784
785        assert_eq!(processor.current_step, 0);
786        assert_eq!(processor.gradient_norm_history.len(), 0);
787        assert_eq!(processor.hessian_diagonal.len(), 0);
788        assert_eq!(processor.gradient_history.len(), 0);
789    }
790
791    #[test]
792    fn test_hessian_preconditioning_config_default() {
793        let config = HessianPreconditioningConfig::default();
794        assert!(matches!(
795            config.approximation_type,
796            HessianApproximationType::Diagonal
797        ));
798        assert_eq!(config.damping, 1e-4);
799        assert_eq!(config.update_frequency, 10);
800        assert_eq!(config.history_window, 20);
801        assert_eq!(config.min_eigenvalue, 1e-8);
802        assert_eq!(config.max_condition_number, 1e6);
803    }
804
805    #[test]
806    fn test_hessian_preconditioning_enabled() {
807        let config = GradientProcessingConfig {
808            enable_hessian_preconditioning: true,
809            ..GradientProcessingConfig::default()
810        };
811
812        let processor = GradientProcessor::new(config);
813        assert!(processor.config.enable_hessian_preconditioning);
814    }
815
816    #[test]
817    fn test_hessian_approximation_types() {
818        let mut config = GradientProcessingConfig {
819            enable_hessian_preconditioning: true,
820            ..GradientProcessingConfig::default()
821        };
822
823        // Test different approximation types
824        config.hessian_preconditioning.approximation_type = HessianApproximationType::Diagonal;
825        let processor = GradientProcessor::new(config.clone());
826        assert!(matches!(
827            processor.config.hessian_preconditioning.approximation_type,
828            HessianApproximationType::Diagonal
829        ));
830
831        config.hessian_preconditioning.approximation_type = HessianApproximationType::GaussNewton;
832        let processor = GradientProcessor::new(config.clone());
833        assert!(matches!(
834            processor.config.hessian_preconditioning.approximation_type,
835            HessianApproximationType::GaussNewton
836        ));
837
838        config.hessian_preconditioning.approximation_type =
839            HessianApproximationType::FisherInformation;
840        let processor = GradientProcessor::new(config.clone());
841        assert!(matches!(
842            processor.config.hessian_preconditioning.approximation_type,
843            HessianApproximationType::FisherInformation
844        ));
845
846        config.hessian_preconditioning.approximation_type = HessianApproximationType::QuasiNewton;
847        let processor = GradientProcessor::new(config.clone());
848        assert!(matches!(
849            processor.config.hessian_preconditioning.approximation_type,
850            HessianApproximationType::QuasiNewton
851        ));
852    }
853
854    fn noise_processor(scale: f32, noise_type: NoiseType) -> GradientProcessor {
855        let config = GradientProcessingConfig {
856            enable_noise_injection: true,
857            noise_injection: NoiseInjectionConfig {
858                initial_noise_scale: scale,
859                decay_rate: 1.0,
860                min_noise_scale: 0.0,
861                noise_type,
862            },
863            ..GradientProcessingConfig::default()
864        };
865        GradientProcessor::new(config)
866    }
867
868    fn injected_noise(processor: &mut GradientProcessor, numel: usize) -> Vec<f32> {
869        let mut gradients =
870            vec![Tensor::from_vec(vec![0.0_f32; numel], &[numel]).expect("zero gradient")];
871        processor.apply_noise_injection(&mut gradients).expect("noise injection");
872        gradients[0].data_f32().expect("data")
873    }
874
875    fn sample_std(values: &[f32]) -> f32 {
876        let mean = values.iter().sum::<f32>() / values.len() as f32;
877        (values.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / values.len() as f32).sqrt()
878    }
879
880    /// Regression: the scaled tensor used to be dropped, so the injected noise was
881    /// always unit-variance regardless of `initial_noise_scale`/`decay_rate`.
882    #[test]
883    fn gaussian_noise_tracks_the_configured_scale() {
884        let mut processor = noise_processor(4.0, NoiseType::Gaussian);
885        let noise = injected_noise(&mut processor, 20_000);
886        let std = sample_std(&noise);
887        assert!(
888            (std - 4.0).abs() < 0.4,
889            "empirical std {std} must track the configured scale 4.0"
890        );
891    }
892
893    /// Two different scales must produce two different noise magnitudes.
894    #[test]
895    fn noise_scale_decay_reaches_the_output() {
896        let mut processor = noise_processor(1.0, NoiseType::Gaussian);
897        processor.config.noise_injection.decay_rate = 0.1;
898        let first = sample_std(&injected_noise(&mut processor, 20_000));
899        let second = sample_std(&injected_noise(&mut processor, 20_000));
900        assert!(
901            second < first * 0.3,
902            "decayed noise must shrink: {first} -> {second}"
903        );
904    }
905
906    /// Regression: `NoiseType::Uniform` used to call `Tensor::randn`, so it was
907    /// indistinguishable from Gaussian. Uniform noise is strictly bounded.
908    #[test]
909    fn uniform_noise_is_bounded() {
910        let scale = 1.0_f32;
911        let mut processor = noise_processor(scale, NoiseType::Uniform);
912        let noise = injected_noise(&mut processor, 20_000);
913        let bound = scale * 3.0_f32.sqrt();
914        assert!(
915            noise.iter().all(|v| v.abs() <= bound + 1e-4),
916            "uniform noise must stay inside ±{bound}"
917        );
918        let std = sample_std(&noise);
919        assert!(
920            (std - scale).abs() < 0.1,
921            "empirical std {std} must match {scale}"
922        );
923    }
924
925    /// Regression: `NoiseType::Laplace` used to be Gaussian too. A Laplace sample is
926    /// unbounded and much more heavy-tailed than a uniform one.
927    #[test]
928    fn laplace_noise_is_heavy_tailed() {
929        let scale = 1.0_f32;
930        let mut processor = noise_processor(scale, NoiseType::Laplace);
931        let noise = injected_noise(&mut processor, 20_000);
932        let max = noise.iter().fold(0.0_f32, |acc, v| acc.max(v.abs()));
933        assert!(
934            max > scale * 3.0_f32.sqrt(),
935            "Laplace noise must exceed the uniform bound, got max {max}"
936        );
937        let std = sample_std(&noise);
938        assert!(
939            (std - scale).abs() < 0.15,
940            "empirical std {std} must match {scale}"
941        );
942    }
943}