1use anyhow::{anyhow, Result};
16use scirs2_core::random::thread_rng;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use trustformers_core::tensor::Tensor;
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default)]
23pub struct GradientProcessingConfig {
24 pub enable_centralization: bool,
26 pub enable_standardization: bool,
28 pub enable_adaptive_clipping: bool,
30 pub enable_noise_injection: bool,
32 pub enable_smoothing: bool,
34 pub enable_hessian_preconditioning: bool,
36 pub adaptive_clipping: AdaptiveClippingConfig,
38 pub noise_injection: NoiseInjectionConfig,
40 pub smoothing: SmoothingConfig,
42 pub hessian_preconditioning: HessianPreconditioningConfig,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct AdaptiveClippingConfig {
49 pub initial_clip_norm: f32,
51 pub min_clip_norm: f32,
53 pub max_clip_norm: f32,
55 pub adaptation_rate: f32,
57 pub target_percentile: f32,
59 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#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct NoiseInjectionConfig {
79 pub initial_noise_scale: f32,
81 pub decay_rate: f32,
83 pub min_noise_scale: f32,
85 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#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SmoothingConfig {
103 pub decay: f32,
105 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#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct HessianPreconditioningConfig {
121 pub approximation_type: HessianApproximationType,
123 pub damping: f32,
125 pub update_frequency: usize,
127 pub history_window: usize,
129 pub min_eigenvalue: f32,
131 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#[derive(Debug, Clone, Serialize, Deserialize)]
150pub enum NoiseType {
151 Gaussian,
152 Uniform,
153 Laplace,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub enum HessianApproximationType {
159 Diagonal,
161 GaussNewton,
163 FisherInformation,
165 QuasiNewton,
167}
168
169#[derive(Debug)]
171pub struct GradientProcessor {
172 config: GradientProcessingConfig,
173 current_step: usize,
174
175 gradient_norm_history: Vec<f32>,
177 current_clip_norm: f32,
178
179 current_noise_scale: f32,
181
182 smoothed_gradients: HashMap<usize, Tensor>,
184 smoothing_bias_correction: f32,
185
186 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 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 pub fn with_defaults() -> Self {
213 Self::new(GradientProcessingConfig::default())
214 }
215
216 pub fn process_gradients(&mut self, gradients: &mut [Tensor]) -> Result<()> {
218 self.current_step += 1;
219
220 if self.config.enable_centralization {
222 self.apply_centralization(gradients)?;
223 }
224
225 if self.config.enable_standardization {
227 self.apply_standardization(gradients)?;
228 }
229
230 if self.config.enable_smoothing {
232 self.apply_smoothing(gradients)?;
233 }
234
235 if self.config.enable_hessian_preconditioning {
237 self.apply_hessian_preconditioning(gradients)?;
238 }
239
240 if self.config.enable_adaptive_clipping {
242 self.apply_adaptive_clipping(gradients)?;
243 }
244
245 if self.config.enable_noise_injection {
247 self.apply_noise_injection(gradients)?;
248 }
249
250 Ok(())
251 }
252
253 fn apply_centralization(&self, gradients: &mut [Tensor]) -> Result<()> {
255 for gradient in gradients.iter_mut() {
256 let mean = gradient.mean()?;
258 *gradient = gradient.sub(&mean)?;
259 }
260 Ok(())
261 }
262
263 fn apply_standardization(&self, gradients: &mut [Tensor]) -> Result<()> {
265 for gradient in gradients.iter_mut() {
266 let mean = gradient.mean()?;
268 let centered = gradient.sub(&mean)?;
269 let squared = centered.mul(¢ered)?;
270 let variance = squared.mean()?;
271 let std_dev = variance.sqrt()?;
272
273 let epsilon = Tensor::scalar(1e-8)?;
275 let std_dev_safe = std_dev.add(&epsilon)?;
276
277 *gradient = gradient.div(&std_dev_safe)?;
279 }
280 Ok(())
281 }
282
283 fn apply_adaptive_clipping(&mut self, gradients: &mut [Tensor]) -> Result<()> {
285 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 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 if self.gradient_norm_history.len() >= 10 {
301 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 let adaptation = self.config.adaptive_clipping.adaptation_rate
311 * (target_norm - self.current_clip_norm);
312 self.current_clip_norm += adaptation;
313
314 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 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 fn apply_noise_injection(&mut self, gradients: &mut [Tensor]) -> Result<()> {
334 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 Tensor::randn(&shape)?.mul_scalar(scale)?
348 },
349 NoiseType::Uniform => {
350 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 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 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 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 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 self.smoothed_gradients.insert(i, gradient.clone());
403 }
404 }
405
406 Ok(())
407 }
408
409 fn apply_hessian_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
411 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 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 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 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 fn update_diagonal_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
465 for (i, gradient) in gradients.iter().enumerate() {
466 if self.gradient_history.len() > 1 {
468 let mut variance = Tensor::zeros(&gradient.shape())?;
469 let mut mean = Tensor::zeros(&gradient.shape())?;
470
471 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 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 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 fn update_gauss_newton_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
501 for (i, gradient) in gradients.iter().enumerate() {
503 let outer_product = gradient.mul(gradient)?;
505
506 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 fn update_fisher_information_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
518 for (i, gradient) in gradients.iter().enumerate() {
520 let fisher_approx = gradient.mul(gradient)?;
522
523 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 fn update_quasi_newton_hessian(&mut self, gradients: &[Tensor]) -> Result<()> {
535 if self.gradient_history.len() > 1 {
537 for (i, gradient) in gradients.iter().enumerate() {
538 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 let grad_diff = gradient.sub(prev_grad)?;
545
546 let hessian_approx = grad_diff.abs()?;
548
549 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 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 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 fn apply_gauss_newton_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
579 self.apply_diagonal_preconditioning(gradients)
581 }
582
583 fn apply_fisher_information_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
585 self.apply_diagonal_preconditioning(gradients)
587 }
588
589 fn apply_quasi_newton_preconditioning(&mut self, gradients: &mut [Tensor]) -> Result<()> {
591 self.apply_diagonal_preconditioning(gradients)
593 }
594
595 pub fn get_current_clip_norm(&self) -> f32 {
597 self.current_clip_norm
598 }
599
600 pub fn get_current_noise_scale(&self) -> f32 {
602 self.current_noise_scale
603 }
604
605 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 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 pub fn set_config(&mut self, config: GradientProcessingConfig) {
639 self.config = config;
640 self.reset();
641 }
642
643 pub fn get_config(&self) -> &GradientProcessingConfig {
645 &self.config
646 }
647}
648
649pub struct GradientProcessedOptimizer<T> {
651 base_optimizer: T,
652 gradient_processor: GradientProcessor,
653}
654
655impl<T> GradientProcessedOptimizer<T> {
656 pub fn new(base_optimizer: T, config: GradientProcessingConfig) -> Self {
658 Self {
659 base_optimizer,
660 gradient_processor: GradientProcessor::new(config),
661 }
662 }
663
664 pub fn with_default_processing(base_optimizer: T) -> Self {
666 Self::new(base_optimizer, GradientProcessingConfig::default())
667 }
668
669 pub fn gradient_processor(&self) -> &GradientProcessor {
671 &self.gradient_processor
672 }
673
674 pub fn gradient_processor_mut(&mut self) -> &mut GradientProcessor {
676 &mut self.gradient_processor
677 }
678
679 pub fn base_optimizer(&self) -> &T {
681 &self.base_optimizer
682 }
683
684 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 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 self.gradient_processor.process_gradients(&mut gradients)?;
710
711 for (param, processed_grad) in parameters.iter_mut().zip(gradients.iter()) {
713 param.set_grad(processed_grad.clone())?;
714 }
715
716 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 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 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 #[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 #[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 #[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 #[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}