Skip to main content

sklears_neural/
regularization.rs

1//! Regularization techniques for neural networks.
2//!
3//! This module provides various regularization methods including L1, L2, elastic net,
4//! dropout, batch normalization, and other techniques to prevent overfitting.
5
6use scirs2_core::ndarray::{Array1, Array2};
7use scirs2_core::numeric::NumCast;
8use scirs2_core::random::essentials::{Normal, Uniform};
9use scirs2_core::random::{thread_rng, Distribution};
10use sklears_core::types::FloatBounds;
11
12/// Types of regularization
13#[derive(Debug, Clone, PartialEq)]
14pub enum RegularizationType {
15    /// L1 regularization (Lasso): λ * ||w||₁
16    L1,
17    /// L2 regularization (Ridge): λ * ||w||₂²
18    L2,
19    /// Elastic Net: λ₁ * ||w||₁ + λ₂ * ||w||₂²
20    ElasticNet,
21    /// No regularization
22    None,
23}
24
25/// Configuration for regularization
26#[derive(Debug, Clone)]
27pub struct RegularizationConfig<T: FloatBounds> {
28    /// Type of regularization
29    pub regularization_type: RegularizationType,
30    /// L1 regularization strength
31    pub l1_lambda: T,
32    /// L2 regularization strength
33    pub l2_lambda: T,
34    /// Whether to include bias terms in regularization
35    pub regularize_bias: bool,
36}
37
38impl<T: FloatBounds> Default for RegularizationConfig<T> {
39    fn default() -> Self {
40        Self {
41            regularization_type: RegularizationType::None,
42            l1_lambda: T::zero(),
43            l2_lambda: T::zero(),
44            regularize_bias: false,
45        }
46    }
47}
48
49impl<T: FloatBounds> RegularizationConfig<T> {
50    /// Create L1 regularization configuration
51    pub fn l1(lambda: T) -> Self {
52        Self {
53            regularization_type: RegularizationType::L1,
54            l1_lambda: lambda,
55            l2_lambda: T::zero(),
56            regularize_bias: false,
57        }
58    }
59
60    /// Create L2 regularization configuration
61    pub fn l2(lambda: T) -> Self {
62        Self {
63            regularization_type: RegularizationType::L2,
64            l1_lambda: T::zero(),
65            l2_lambda: lambda,
66            regularize_bias: false,
67        }
68    }
69
70    /// Create Elastic Net regularization configuration
71    pub fn elastic_net(l1_lambda: T, l2_lambda: T) -> Self {
72        Self {
73            regularization_type: RegularizationType::ElasticNet,
74            l1_lambda,
75            l2_lambda,
76            regularize_bias: false,
77        }
78    }
79
80    /// Set whether to regularize bias terms
81    pub fn regularize_bias(mut self, regularize: bool) -> Self {
82        self.regularize_bias = regularize;
83        self
84    }
85}
86
87/// Regularization implementation
88pub struct Regularizer<T: FloatBounds> {
89    config: RegularizationConfig<T>,
90}
91
92impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> Regularizer<T> {
93    /// Create a new regularizer with the given configuration
94    pub fn new(config: RegularizationConfig<T>) -> Self {
95        Self { config }
96    }
97
98    /// Compute regularization loss for a weight matrix
99    pub fn compute_loss(&self, weights: &Array2<T>, bias: Option<&Array1<T>>) -> T {
100        let mut loss = T::zero();
101
102        // Regularize weights
103        match self.config.regularization_type {
104            RegularizationType::L1 => {
105                loss += self.config.l1_lambda * self.l1_norm_2d(weights);
106            }
107            RegularizationType::L2 => {
108                loss += self.config.l2_lambda * self.l2_norm_squared_2d(weights);
109            }
110            RegularizationType::ElasticNet => {
111                loss += self.config.l1_lambda * self.l1_norm_2d(weights);
112                loss += self.config.l2_lambda * self.l2_norm_squared_2d(weights);
113            }
114            RegularizationType::None => {}
115        }
116
117        // Regularize bias if configured
118        if self.config.regularize_bias {
119            if let Some(bias_vec) = bias {
120                match self.config.regularization_type {
121                    RegularizationType::L1 => {
122                        loss += self.config.l1_lambda * self.l1_norm_1d(bias_vec);
123                    }
124                    RegularizationType::L2 => {
125                        loss += self.config.l2_lambda * self.l2_norm_squared_1d(bias_vec);
126                    }
127                    RegularizationType::ElasticNet => {
128                        loss += self.config.l1_lambda * self.l1_norm_1d(bias_vec);
129                        loss += self.config.l2_lambda * self.l2_norm_squared_1d(bias_vec);
130                    }
131                    RegularizationType::None => {}
132                }
133            }
134        }
135
136        loss
137    }
138
139    /// Compute regularization gradients for weights
140    pub fn compute_weight_gradients(&self, weights: &Array2<T>) -> Array2<T> {
141        let mut gradients = Array2::zeros(weights.dim());
142
143        match self.config.regularization_type {
144            RegularizationType::L1 => {
145                gradients = gradients + &self.l1_gradient_2d(weights) * self.config.l1_lambda;
146            }
147            RegularizationType::L2 => {
148                gradients = gradients + &self.l2_gradient_2d(weights) * self.config.l2_lambda;
149            }
150            RegularizationType::ElasticNet => {
151                gradients = gradients + &self.l1_gradient_2d(weights) * self.config.l1_lambda;
152                gradients = gradients + &self.l2_gradient_2d(weights) * self.config.l2_lambda;
153            }
154            RegularizationType::None => {}
155        }
156
157        gradients
158    }
159
160    /// Compute regularization gradients for bias
161    pub fn compute_bias_gradients(&self, bias: &Array1<T>) -> Array1<T> {
162        if !self.config.regularize_bias {
163            return Array1::zeros(bias.len());
164        }
165
166        let mut gradients = Array1::zeros(bias.len());
167
168        match self.config.regularization_type {
169            RegularizationType::L1 => {
170                gradients = gradients + self.l1_gradient_1d(bias) * self.config.l1_lambda;
171            }
172            RegularizationType::L2 => {
173                gradients = gradients + self.l2_gradient_1d(bias) * self.config.l2_lambda;
174            }
175            RegularizationType::ElasticNet => {
176                gradients = gradients + self.l1_gradient_1d(bias) * self.config.l1_lambda;
177                gradients = gradients + self.l2_gradient_1d(bias) * self.config.l2_lambda;
178            }
179            RegularizationType::None => {}
180        }
181
182        gradients
183    }
184
185    /// Compute L1 norm of a 2D array
186    fn l1_norm_2d(&self, array: &Array2<T>) -> T {
187        array.iter().fold(T::zero(), |acc, &x| acc + x.abs())
188    }
189
190    /// Compute L1 norm of a 1D array
191    fn l1_norm_1d(&self, array: &Array1<T>) -> T {
192        array.iter().fold(T::zero(), |acc, &x| acc + x.abs())
193    }
194
195    /// Compute squared L2 norm of a 2D array
196    fn l2_norm_squared_2d(&self, array: &Array2<T>) -> T {
197        let half = T::from(0.5).unwrap_or_else(|| T::one() / (T::one() + T::one()));
198        half * array.iter().fold(T::zero(), |acc, &x| acc + x * x)
199    }
200
201    /// Compute squared L2 norm of a 1D array
202    fn l2_norm_squared_1d(&self, array: &Array1<T>) -> T {
203        let half = T::from(0.5).unwrap_or_else(|| T::one() / (T::one() + T::one()));
204        half * array.iter().fold(T::zero(), |acc, &x| acc + x * x)
205    }
206
207    /// Compute L1 regularization gradient (subgradient) for 2D array
208    fn l1_gradient_2d(&self, array: &Array2<T>) -> Array2<T> {
209        array.mapv(|x| {
210            if x > T::zero() {
211                T::one()
212            } else if x < T::zero() {
213                -T::one()
214            } else {
215                T::zero() // Subgradient at 0 can be any value in [-1, 1], we choose 0
216            }
217        })
218    }
219
220    /// Compute L1 regularization gradient (subgradient) for 1D array
221    fn l1_gradient_1d(&self, array: &Array1<T>) -> Array1<T> {
222        array.mapv(|x| {
223            if x > T::zero() {
224                T::one()
225            } else if x < T::zero() {
226                -T::one()
227            } else {
228                T::zero()
229            }
230        })
231    }
232
233    /// Compute L2 regularization gradient for 2D array
234    fn l2_gradient_2d(&self, array: &Array2<T>) -> Array2<T> {
235        array.clone()
236    }
237
238    /// Compute L2 regularization gradient for 1D array
239    fn l2_gradient_1d(&self, array: &Array1<T>) -> Array1<T> {
240        array.clone()
241    }
242}
243
244/// Proximal operator for L1 regularization (soft thresholding)
245pub fn soft_threshold<T: FloatBounds>(x: T, lambda: T) -> T {
246    if x > lambda {
247        x - lambda
248    } else if x < -lambda {
249        x + lambda
250    } else {
251        T::zero()
252    }
253}
254
255/// Apply proximal operator for L1 regularization to an array
256pub fn apply_soft_threshold_2d<T: FloatBounds>(array: &Array2<T>, lambda: T) -> Array2<T> {
257    array.mapv(|x| soft_threshold(x, lambda))
258}
259
260/// Apply proximal operator for L1 regularization to a 1D array
261pub fn apply_soft_threshold_1d<T: FloatBounds>(array: &Array1<T>, lambda: T) -> Array1<T> {
262    array.mapv(|x| soft_threshold(x, lambda))
263}
264
265/// Early stopping implementation
266#[derive(Debug, Clone)]
267pub struct EarlyStopping<T: FloatBounds> {
268    /// Patience: number of epochs with no improvement after which training stops
269    patience: usize,
270    /// Minimum change in monitored quantity to qualify as an improvement
271    min_delta: T,
272    /// Number of epochs with no improvement
273    wait: usize,
274    /// Best value seen so far
275    best_value: Option<T>,
276    /// Whether lower values are better (for loss) or higher values are better (for accuracy)
277    minimize: bool,
278    /// Whether early stopping has been triggered
279    stopped: bool,
280}
281
282impl<T: FloatBounds> EarlyStopping<T> {
283    /// Create a new early stopping monitor
284    ///
285    /// # Arguments
286    /// * `patience` - Number of epochs with no improvement after which training stops
287    /// * `min_delta` - Minimum change to qualify as an improvement
288    /// * `minimize` - Whether lower values are better (true for loss, false for accuracy)
289    pub fn new(patience: usize, min_delta: T, minimize: bool) -> Self {
290        Self {
291            patience,
292            min_delta,
293            wait: 0,
294            best_value: None,
295            minimize,
296            stopped: false,
297        }
298    }
299
300    /// Update the early stopping monitor with a new value
301    ///
302    /// Returns true if training should stop
303    pub fn update(&mut self, value: T) -> bool {
304        if self.stopped {
305            return true;
306        }
307
308        let is_improvement = match self.best_value {
309            None => true,
310            Some(best) => {
311                if self.minimize {
312                    value < best - self.min_delta
313                } else {
314                    value > best + self.min_delta
315                }
316            }
317        };
318
319        if is_improvement {
320            self.best_value = Some(value);
321            self.wait = 0;
322        } else {
323            self.wait += 1;
324            if self.wait >= self.patience {
325                self.stopped = true;
326                return true;
327            }
328        }
329
330        false
331    }
332
333    /// Get the best value seen so far
334    pub fn best_value(&self) -> Option<T> {
335        self.best_value
336    }
337
338    /// Reset the early stopping monitor
339    pub fn reset(&mut self) {
340        self.wait = 0;
341        self.best_value = None;
342        self.stopped = false;
343    }
344
345    /// Check if early stopping has been triggered
346    pub fn is_stopped(&self) -> bool {
347        self.stopped
348    }
349}
350
351/// Noise injection types for regularization
352#[derive(Debug, Clone, PartialEq)]
353pub enum NoiseType {
354    /// Gaussian noise with zero mean and specified standard deviation
355    Gaussian {
356        /// Standard deviation of the Gaussian noise distribution
357        std_dev: f64,
358    },
359    /// Uniform noise in the range [-magnitude, magnitude]
360    Uniform {
361        /// Half-width of the uniform noise range
362        magnitude: f64,
363    },
364    /// Salt-and-pepper noise (random values set to min/max)
365    SaltPepper {
366        /// Probability of each value being replaced by a salt or pepper sample
367        probability: f64,
368        /// Value used for "pepper" (low-intensity) corruptions
369        min_value: f64,
370        /// Value used for "salt" (high-intensity) corruptions
371        max_value: f64,
372    },
373    /// Dropout noise (randomly set values to zero)
374    Dropout {
375        /// Probability that each element is set to zero
376        probability: f64,
377    },
378}
379
380/// Noise injection configuration
381#[derive(Debug, Clone)]
382pub struct NoiseConfig {
383    /// Type of noise to inject
384    pub noise_type: NoiseType,
385    /// Whether to apply noise during training only or both training and inference
386    pub training_only: bool,
387    /// Random seed for reproducibility
388    pub seed: Option<u64>,
389}
390
391impl Default for NoiseConfig {
392    fn default() -> Self {
393        Self {
394            noise_type: NoiseType::Gaussian { std_dev: 0.01 },
395            training_only: true,
396            seed: None,
397        }
398    }
399}
400
401impl NoiseConfig {
402    /// Create Gaussian noise configuration
403    pub fn gaussian(std_dev: f64) -> Self {
404        Self {
405            noise_type: NoiseType::Gaussian { std_dev },
406            training_only: true,
407            seed: None,
408        }
409    }
410
411    /// Create uniform noise configuration
412    pub fn uniform(magnitude: f64) -> Self {
413        Self {
414            noise_type: NoiseType::Uniform { magnitude },
415            training_only: true,
416            seed: None,
417        }
418    }
419
420    /// Create salt-and-pepper noise configuration
421    pub fn salt_pepper(probability: f64, min_value: f64, max_value: f64) -> Self {
422        Self {
423            noise_type: NoiseType::SaltPepper {
424                probability,
425                min_value,
426                max_value,
427            },
428            training_only: true,
429            seed: None,
430        }
431    }
432
433    /// Create dropout noise configuration
434    pub fn dropout(probability: f64) -> Self {
435        Self {
436            noise_type: NoiseType::Dropout { probability },
437            training_only: true,
438            seed: None,
439        }
440    }
441
442    /// Set whether to apply noise only during training
443    pub fn training_only(mut self, training_only: bool) -> Self {
444        self.training_only = training_only;
445        self
446    }
447
448    /// Set random seed for reproducibility
449    pub fn with_seed(mut self, seed: u64) -> Self {
450        self.seed = Some(seed);
451        self
452    }
453}
454
455/// Noise injector for robustness training
456pub struct NoiseInjector {
457    config: NoiseConfig,
458}
459
460impl NoiseInjector {
461    /// Create a new noise injector
462    pub fn new(config: NoiseConfig) -> Self {
463        Self { config }
464    }
465
466    /// Apply noise to input data
467    pub fn apply_noise<T>(&self, input: &Array2<T>, is_training: bool) -> Array2<T>
468    where
469        T: FloatBounds + From<f64>,
470    {
471        if self.config.training_only && !is_training {
472            return input.clone();
473        }
474
475        match &self.config.noise_type {
476            NoiseType::Gaussian { std_dev } => self.apply_gaussian_noise(input, *std_dev),
477            NoiseType::Uniform { magnitude } => self.apply_uniform_noise(input, *magnitude),
478            NoiseType::SaltPepper {
479                probability,
480                min_value,
481                max_value,
482            } => self.apply_salt_pepper_noise(input, *probability, *min_value, *max_value),
483            NoiseType::Dropout { probability } => self.apply_dropout_noise(input, *probability),
484        }
485    }
486
487    /// Apply noise to 1D data (e.g., biases)
488    pub fn apply_noise_1d<T>(&self, input: &Array1<T>, is_training: bool) -> Array1<T>
489    where
490        T: FloatBounds + From<f64>,
491    {
492        if self.config.training_only && !is_training {
493            return input.clone();
494        }
495
496        match &self.config.noise_type {
497            NoiseType::Gaussian { std_dev } => self.apply_gaussian_noise_1d(input, *std_dev),
498            NoiseType::Uniform { magnitude } => self.apply_uniform_noise_1d(input, *magnitude),
499            NoiseType::SaltPepper {
500                probability,
501                min_value,
502                max_value,
503            } => self.apply_salt_pepper_noise_1d(input, *probability, *min_value, *max_value),
504            NoiseType::Dropout { probability } => self.apply_dropout_noise_1d(input, *probability),
505        }
506    }
507
508    /// Apply Gaussian noise to 2D array
509    fn apply_gaussian_noise<T>(&self, input: &Array2<T>, std_dev: f64) -> Array2<T>
510    where
511        T: FloatBounds + From<f64>,
512    {
513        let mut rng = thread_rng();
514        let normal = Normal::new(0.0, std_dev).expect("valid distribution params");
515
516        input.mapv(|x| {
517            let noise = NumCast::from(normal.sample(&mut rng)).unwrap_or(T::zero());
518            x + noise
519        })
520    }
521
522    /// Apply Gaussian noise to 1D array
523    fn apply_gaussian_noise_1d<T>(&self, input: &Array1<T>, std_dev: f64) -> Array1<T>
524    where
525        T: FloatBounds + From<f64>,
526    {
527        let mut rng = thread_rng();
528        let normal = Normal::new(0.0, std_dev).expect("valid distribution params");
529
530        input.mapv(|x| {
531            let noise = NumCast::from(normal.sample(&mut rng)).unwrap_or(T::zero());
532            x + noise
533        })
534    }
535
536    /// Apply uniform noise to 2D array
537    fn apply_uniform_noise<T>(&self, input: &Array2<T>, magnitude: f64) -> Array2<T>
538    where
539        T: FloatBounds + From<f64>,
540    {
541        let mut rng = thread_rng();
542        let uniform = Uniform::new(-magnitude, magnitude).expect("valid distribution params");
543
544        input.mapv(|x| {
545            let noise = NumCast::from(uniform.sample(&mut rng)).unwrap_or(T::zero());
546            x + noise
547        })
548    }
549
550    /// Apply uniform noise to 1D array
551    fn apply_uniform_noise_1d<T>(&self, input: &Array1<T>, magnitude: f64) -> Array1<T>
552    where
553        T: FloatBounds + From<f64>,
554    {
555        let mut rng = thread_rng();
556        let uniform = Uniform::new(-magnitude, magnitude).expect("valid distribution params");
557
558        input.mapv(|x| {
559            let noise = NumCast::from(uniform.sample(&mut rng)).unwrap_or(T::zero());
560            x + noise
561        })
562    }
563
564    /// Apply salt-and-pepper noise to 2D array
565    fn apply_salt_pepper_noise<T>(
566        &self,
567        input: &Array2<T>,
568        probability: f64,
569        min_value: f64,
570        max_value: f64,
571    ) -> Array2<T>
572    where
573        T: FloatBounds + From<f64>,
574    {
575        let mut rng = thread_rng();
576
577        input.mapv(|x| {
578            if rng.random::<f64>() < probability {
579                if rng.random::<bool>() {
580                    NumCast::from(min_value).unwrap_or(T::zero())
581                } else {
582                    NumCast::from(max_value).unwrap_or(T::zero())
583                }
584            } else {
585                x
586            }
587        })
588    }
589
590    /// Apply salt-and-pepper noise to 1D array
591    fn apply_salt_pepper_noise_1d<T>(
592        &self,
593        input: &Array1<T>,
594        probability: f64,
595        min_value: f64,
596        max_value: f64,
597    ) -> Array1<T>
598    where
599        T: FloatBounds + From<f64>,
600    {
601        let mut rng = thread_rng();
602
603        input.mapv(|x| {
604            if rng.random::<f64>() < probability {
605                if rng.random::<bool>() {
606                    NumCast::from(min_value).unwrap_or(T::zero())
607                } else {
608                    NumCast::from(max_value).unwrap_or(T::zero())
609                }
610            } else {
611                x
612            }
613        })
614    }
615
616    /// Apply dropout noise to 2D array
617    fn apply_dropout_noise<T>(&self, input: &Array2<T>, probability: f64) -> Array2<T>
618    where
619        T: FloatBounds + From<f64>,
620    {
621        let mut rng = thread_rng();
622
623        input.mapv(|x| {
624            if rng.random::<f64>() < probability {
625                T::zero()
626            } else {
627                // Scale up remaining values to maintain expected value
628                x / NumCast::from(1.0 - probability).unwrap_or_else(T::one)
629            }
630        })
631    }
632
633    /// Apply dropout noise to 1D array
634    fn apply_dropout_noise_1d<T>(&self, input: &Array1<T>, probability: f64) -> Array1<T>
635    where
636        T: FloatBounds + From<f64>,
637    {
638        let mut rng = thread_rng();
639
640        input.mapv(|x| {
641            if rng.random::<f64>() < probability {
642                T::zero()
643            } else {
644                // Scale up remaining values to maintain expected value
645                x / NumCast::from(1.0 - probability).unwrap_or_else(T::one)
646            }
647        })
648    }
649}
650
651/// Spectral normalization for constraining the spectral norm of weight matrices
652#[derive(Debug, Clone)]
653pub struct SpectralNormalization<T: FloatBounds> {
654    /// Number of power iteration steps
655    power_iterations: usize,
656    /// Tolerance for convergence
657    eps: T,
658    /// Cached dominant left singular vector
659    u: Option<Array1<T>>,
660    /// Cached dominant right singular vector  
661    v: Option<Array1<T>>,
662    /// Whether to initialize vectors
663    initialized: bool,
664}
665
666impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> Default for SpectralNormalization<T> {
667    fn default() -> Self {
668        Self::new(1, T::from(1e-12).unwrap_or_else(|| T::epsilon()))
669    }
670}
671
672impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> SpectralNormalization<T> {
673    /// Create a new spectral normalization instance
674    pub fn new(power_iterations: usize, eps: T) -> Self {
675        Self {
676            power_iterations,
677            eps,
678            u: None,
679            v: None,
680            initialized: false,
681        }
682    }
683
684    /// Apply spectral normalization to a weight matrix
685    pub fn normalize_weights(&mut self, weights: &Array2<T>) -> Array2<T>
686    where
687        T: scirs2_core::ndarray::ScalarOperand + Clone + std::fmt::Debug,
688    {
689        let (m, n) = weights.dim();
690
691        // Initialize u and v vectors if needed
692        if !self.initialized || self.u.is_none() || self.v.is_none() {
693            self.initialize_vectors(m, n);
694        }
695
696        // Ensure vectors have correct dimensions
697        if let (Some(ref u), Some(ref v)) = (&self.u, &self.v) {
698            if u.len() != m || v.len() != n {
699                self.initialize_vectors(m, n);
700            }
701        }
702
703        // Perform power iteration to find dominant singular value
704        let sigma = self.power_iteration(weights);
705
706        // Normalize weights by dividing by spectral norm
707        if sigma > self.eps {
708            weights / sigma
709        } else {
710            weights.clone()
711        }
712    }
713
714    /// Initialize u and v vectors with random values
715    fn initialize_vectors(&mut self, m: usize, n: usize) {
716        let mut rng = thread_rng();
717
718        // Initialize u vector (left singular vector)
719        let u_data: Vec<T> = (0..m)
720            .map(|_| T::from(rng.random::<f64>() * 2.0 - 1.0).unwrap_or(T::zero()))
721            .collect();
722        let mut u = Array1::from_vec(u_data);
723        self.normalize_vector(&mut u);
724        self.u = Some(u);
725
726        // Initialize v vector (right singular vector)
727        let v_data: Vec<T> = (0..n)
728            .map(|_| T::from(rng.random::<f64>() * 2.0 - 1.0).unwrap_or(T::zero()))
729            .collect();
730        let mut v = Array1::from_vec(v_data);
731        self.normalize_vector(&mut v);
732        self.v = Some(v);
733
734        self.initialized = true;
735    }
736
737    /// Perform power iteration to estimate dominant singular value
738    fn power_iteration(&mut self, weights: &Array2<T>) -> T
739    where
740        T: scirs2_core::ndarray::ScalarOperand + Clone,
741    {
742        for _ in 0..self.power_iterations {
743            // v = W^T @ u / ||W^T @ u||
744            let wt_u = {
745                let u = self.u.as_ref().expect("u not available - model not fitted");
746                weights.t().dot(u)
747            };
748            *self.v.as_mut().expect("v not available") = wt_u;
749            Self::normalize_vector_static(self.v.as_mut().expect("v not available"), self.eps);
750
751            // u = W @ v / ||W @ v||
752            let w_v = {
753                let v = self.v.as_ref().expect("v not available - model not fitted");
754                weights.dot(v)
755            };
756            *self.u.as_mut().expect("u not available") = w_v;
757            Self::normalize_vector_static(self.u.as_mut().expect("u not available"), self.eps);
758        }
759
760        // Compute spectral norm: σ = u^T @ W @ v
761        let u = self.u.as_ref().expect("u not available - model not fitted");
762        let v = self.v.as_ref().expect("v not available - model not fitted");
763        let w_v = weights.dot(v);
764        u.dot(&w_v)
765    }
766
767    /// Normalize a vector to unit length
768    fn normalize_vector(&self, vector: &mut Array1<T>)
769    where
770        T: scirs2_core::ndarray::ScalarOperand + Clone,
771    {
772        Self::normalize_vector_static(vector, self.eps);
773    }
774
775    /// Static version of normalize_vector to avoid borrow checker issues
776    fn normalize_vector_static(vector: &mut Array1<T>, eps: T)
777    where
778        T: scirs2_core::ndarray::ScalarOperand + Clone,
779    {
780        let norm_squared = vector.iter().fold(T::zero(), |acc, &x| acc + x * x);
781        let norm = norm_squared.sqrt();
782
783        if norm > eps {
784            vector.mapv_inplace(|x| x / norm);
785        }
786    }
787
788    /// Get the current estimate of the spectral norm
789    pub fn get_spectral_norm(&mut self, weights: &Array2<T>) -> T
790    where
791        T: scirs2_core::ndarray::ScalarOperand + Clone,
792    {
793        if !self.initialized {
794            let (m, n) = weights.dim();
795            self.initialize_vectors(m, n);
796        }
797
798        self.power_iteration(weights)
799    }
800
801    /// Reset the cached vectors (useful when weight dimensions change)
802    pub fn reset(&mut self) {
803        self.u = None;
804        self.v = None;
805        self.initialized = false;
806    }
807}
808
809/// Spectral normalization layer that can be applied to any linear layer
810#[derive(Debug, Clone)]
811pub struct SpectralNormLayer<T: FloatBounds> {
812    spectral_norm: SpectralNormalization<T>,
813    /// Whether spectral normalization is enabled
814    enabled: bool,
815}
816
817impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> Default for SpectralNormLayer<T> {
818    fn default() -> Self {
819        Self {
820            spectral_norm: SpectralNormalization::default(),
821            enabled: true,
822        }
823    }
824}
825
826impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> SpectralNormLayer<T> {
827    /// Create a new spectral normalization layer
828    pub fn new(power_iterations: usize, eps: T) -> Self {
829        Self {
830            spectral_norm: SpectralNormalization::new(power_iterations, eps),
831            enabled: true,
832        }
833    }
834
835    /// Enable or disable spectral normalization
836    pub fn set_enabled(&mut self, enabled: bool) {
837        self.enabled = enabled;
838    }
839
840    /// Apply spectral normalization to weights
841    pub fn normalize(&mut self, weights: &Array2<T>) -> Array2<T>
842    where
843        T: scirs2_core::ndarray::ScalarOperand + Clone + std::fmt::Debug,
844    {
845        if self.enabled {
846            self.spectral_norm.normalize_weights(weights)
847        } else {
848            weights.clone()
849        }
850    }
851
852    /// Get spectral norm of weights
853    pub fn spectral_norm(&mut self, weights: &Array2<T>) -> T
854    where
855        T: scirs2_core::ndarray::ScalarOperand + Clone,
856    {
857        self.spectral_norm.get_spectral_norm(weights)
858    }
859
860    /// Reset the spectral normalization state
861    pub fn reset(&mut self) {
862        self.spectral_norm.reset();
863    }
864}
865
866#[allow(non_snake_case)]
867#[cfg(test)]
868mod tests {
869    use super::*;
870    use approx::assert_abs_diff_eq;
871    use scirs2_core::ndarray::{array, Array2};
872
873    #[test]
874    fn test_l1_regularization() {
875        let config = RegularizationConfig::l1(0.1);
876        let regularizer = Regularizer::new(config);
877
878        let weights = array![[1.0, -2.0], [3.0, -4.0]];
879        let loss = regularizer.compute_loss(&weights, None);
880
881        // L1 norm = |1| + |-2| + |3| + |-4| = 10
882        // L1 loss = 0.1 * 10 = 1.0
883        assert_abs_diff_eq!(loss, 1.0, epsilon = 1e-10);
884    }
885
886    #[test]
887    fn test_l2_regularization() {
888        let config = RegularizationConfig::l2(0.1);
889        let regularizer = Regularizer::new(config);
890
891        let weights = array![[1.0, 2.0], [3.0, 4.0]];
892        let loss = regularizer.compute_loss(&weights, None);
893
894        // L2 squared norm = 1² + 2² + 3² + 4² = 30
895        // L2 loss = 0.1 * 0.5 * 30 = 1.5
896        assert_abs_diff_eq!(loss, 1.5, epsilon = 1e-10);
897    }
898
899    #[test]
900    fn test_elastic_net_regularization() {
901        let config = RegularizationConfig::elastic_net(0.1, 0.05);
902        let regularizer = Regularizer::new(config);
903
904        let weights = array![[1.0, -2.0], [3.0, -4.0]];
905        let loss = regularizer.compute_loss(&weights, None);
906
907        // L1 norm = 10, L2 squared norm = 30
908        // Elastic net loss = 0.1 * 10 + 0.05 * 0.5 * 30 = 1.0 + 0.75 = 1.75
909        assert_abs_diff_eq!(loss, 1.75, epsilon = 1e-10);
910    }
911
912    #[test]
913    fn test_l1_gradients() {
914        let config = RegularizationConfig::l1(0.1);
915        let regularizer = Regularizer::new(config);
916
917        let weights = array![[1.0, -2.0, 0.0], [3.0, -4.0, 0.0]];
918        let gradients = regularizer.compute_weight_gradients(&weights);
919
920        let expected = array![[0.1, -0.1, 0.0], [0.1, -0.1, 0.0]];
921        // Compare element by element since approx doesn't implement AbsDiffEq for Array2
922        for (g, e) in gradients.iter().zip(expected.iter()) {
923            assert_abs_diff_eq!(*g, *e, epsilon = 1e-10);
924        }
925    }
926
927    #[test]
928    fn test_l2_gradients() {
929        let config = RegularizationConfig::l2(0.1);
930        let regularizer = Regularizer::new(config);
931
932        let weights = array![[1.0, 2.0], [3.0, 4.0]];
933        let gradients = regularizer.compute_weight_gradients(&weights);
934
935        let expected = &weights * 0.1;
936        // Compare element by element since approx doesn't implement AbsDiffEq for Array2
937        for (g, e) in gradients.iter().zip(expected.iter()) {
938            assert_abs_diff_eq!(*g, *e, epsilon = 1e-10);
939        }
940    }
941
942    #[test]
943    fn test_bias_regularization() {
944        let config = RegularizationConfig::l2(0.1).regularize_bias(true);
945        let regularizer = Regularizer::new(config);
946
947        let weights = array![[1.0, 2.0]];
948        let bias = array![3.0, 4.0];
949        let loss = regularizer.compute_loss(&weights, Some(&bias));
950
951        // L2 loss for weights = 0.1 * 0.5 * (1² + 2²) = 0.25
952        // L2 loss for bias = 0.1 * 0.5 * (3² + 4²) = 1.25
953        // Total = 1.5
954        assert_abs_diff_eq!(loss, 1.5, epsilon = 1e-10);
955    }
956
957    #[test]
958    fn test_soft_threshold() {
959        assert_abs_diff_eq!(soft_threshold(3.0, 1.0), 2.0, epsilon = 1e-10);
960        assert_abs_diff_eq!(soft_threshold(-3.0, 1.0), -2.0, epsilon = 1e-10);
961        assert_abs_diff_eq!(soft_threshold(0.5, 1.0), 0.0, epsilon = 1e-10);
962        assert_abs_diff_eq!(soft_threshold(-0.5, 1.0), 0.0, epsilon = 1e-10);
963    }
964
965    #[test]
966    fn test_soft_threshold_array() {
967        let input = array![[3.0, -2.0, 0.5], [-0.3, 4.0, -1.5]];
968        let result = apply_soft_threshold_2d(&input, 1.0);
969
970        let expected = array![[2.0, -1.0, 0.0], [0.0, 3.0, -0.5]];
971        // Compare element by element since approx doesn't implement AbsDiffEq for Array2
972        for (r, e) in result.iter().zip(expected.iter()) {
973            assert_abs_diff_eq!(*r, *e, epsilon = 1e-10);
974        }
975    }
976
977    #[test]
978    fn test_early_stopping_minimize() {
979        let mut early_stopping = EarlyStopping::new(3, 0.01, true);
980
981        // Improving values
982        assert!(!early_stopping.update(1.0));
983        assert!(!early_stopping.update(0.5));
984        assert!(!early_stopping.update(0.3));
985
986        // No improvement for 3 epochs
987        assert!(!early_stopping.update(0.31)); // wait = 1
988        assert!(!early_stopping.update(0.32)); // wait = 2
989        assert!(early_stopping.update(0.33)); // wait = 3, should stop
990
991        assert_abs_diff_eq!(
992            early_stopping
993                .best_value()
994                .expect("operation should succeed"),
995            0.3,
996            epsilon = 1e-10
997        );
998    }
999
1000    #[test]
1001    fn test_early_stopping_maximize() {
1002        let mut early_stopping = EarlyStopping::new(2, 0.01, false);
1003
1004        // Improving values (higher is better)
1005        assert!(!early_stopping.update(0.7));
1006        assert!(!early_stopping.update(0.8));
1007        assert!(!early_stopping.update(0.9));
1008
1009        // No improvement for 2 epochs
1010        assert!(!early_stopping.update(0.89)); // wait = 1
1011        assert!(early_stopping.update(0.88)); // wait = 2, should stop
1012
1013        assert_abs_diff_eq!(
1014            early_stopping
1015                .best_value()
1016                .expect("operation should succeed"),
1017            0.9,
1018            epsilon = 1e-10
1019        );
1020    }
1021
1022    #[test]
1023    fn test_early_stopping_reset() {
1024        let mut early_stopping = EarlyStopping::new(2, 0.01, true);
1025
1026        early_stopping.update(1.0);
1027        early_stopping.update(1.1); // wait = 1
1028        early_stopping.update(1.2); // wait = 2, should stop
1029
1030        assert!(early_stopping.is_stopped());
1031
1032        early_stopping.reset();
1033        assert!(!early_stopping.is_stopped());
1034        assert!(early_stopping.best_value().is_none());
1035    }
1036
1037    #[test]
1038    fn test_gaussian_noise() {
1039        let config = NoiseConfig::gaussian(0.1);
1040        let injector = NoiseInjector::new(config);
1041
1042        let input: scirs2_core::ndarray::Array2<f64> = array![[1.0, 2.0], [3.0, 4.0]];
1043        let noisy_output = injector.apply_noise(&input, true);
1044
1045        // Check that output has same shape
1046        assert_eq!(noisy_output.shape(), input.shape());
1047
1048        // Check that noise was actually applied (values should be different)
1049        let mut has_differences = false;
1050        for (original, noisy) in input.iter().zip(noisy_output.iter()) {
1051            if (*original - *noisy).abs() > 1e-6_f64 {
1052                has_differences = true;
1053                break;
1054            }
1055        }
1056        assert!(has_differences);
1057    }
1058
1059    #[test]
1060    fn test_uniform_noise() {
1061        let config = NoiseConfig::uniform(0.5);
1062        let injector = NoiseInjector::new(config);
1063
1064        let input: scirs2_core::ndarray::Array2<f64> = array![[1.0, 2.0], [3.0, 4.0]];
1065        let noisy_output = injector.apply_noise(&input, true);
1066
1067        // Check that output has same shape
1068        assert_eq!(noisy_output.shape(), input.shape());
1069
1070        // Check that noise is within expected bounds (roughly)
1071        for (original, noisy) in input.iter().zip(noisy_output.iter()) {
1072            let diff = (*original - *noisy).abs();
1073            assert!(diff <= 0.6_f64); // Allow some tolerance for floating point
1074        }
1075    }
1076
1077    #[test]
1078    fn test_dropout_noise() {
1079        let config = NoiseConfig::dropout(0.5);
1080        let injector = NoiseInjector::new(config);
1081
1082        let input = Array2::from_elem((100, 4), 1.0); // Create larger array to test probability
1083        let noisy_output = injector.apply_noise(&input, true);
1084
1085        // Check that approximately half the values are zero (with some tolerance)
1086        let zero_count = noisy_output.iter().filter(|&&x| x == 0.0).count();
1087        let total_count = noisy_output.len();
1088        let zero_ratio = zero_count as f64 / total_count as f64;
1089
1090        // Should be roughly 50% with some tolerance for randomness
1091        assert!(zero_ratio > 0.3 && zero_ratio < 0.7);
1092    }
1093
1094    #[test]
1095    fn test_salt_pepper_noise() {
1096        let config = NoiseConfig::salt_pepper(0.3, -1.0, 1.0);
1097        let injector = NoiseInjector::new(config);
1098
1099        let input = Array2::from_elem((10, 10), 0.5); // Create uniform array
1100        let noisy_output = injector.apply_noise(&input, true);
1101
1102        // Check that some values are now -1.0 or 1.0
1103        let extreme_count = noisy_output
1104            .iter()
1105            .filter(|&&x| (x - (-1.0_f64)).abs() < 1e-6_f64 || (x - 1.0_f64).abs() < 1e-6_f64)
1106            .count();
1107
1108        assert!(extreme_count > 0);
1109    }
1110
1111    #[test]
1112    fn test_noise_training_only() {
1113        let config = NoiseConfig::gaussian(0.1).training_only(true);
1114        let injector = NoiseInjector::new(config);
1115
1116        let input = array![[1.0, 2.0], [3.0, 4.0]];
1117
1118        // During training - should apply noise
1119        let training_output = injector.apply_noise(&input, true);
1120        assert_ne!(training_output, input);
1121
1122        // During inference - should not apply noise
1123        let inference_output = injector.apply_noise(&input, false);
1124        assert_eq!(inference_output, input);
1125    }
1126
1127    #[test]
1128    fn test_noise_1d_arrays() {
1129        let config = NoiseConfig::gaussian(0.1);
1130        let injector = NoiseInjector::new(config);
1131
1132        let input = array![1.0, 2.0, 3.0, 4.0];
1133        let noisy_output = injector.apply_noise_1d(&input, true);
1134
1135        // Check that output has same shape
1136        assert_eq!(noisy_output.len(), input.len());
1137
1138        // Check that noise was applied
1139        assert_ne!(noisy_output, input);
1140    }
1141
1142    #[test]
1143    fn test_spectral_normalization_basic() {
1144        // Use 20 power iterations to guarantee convergence regardless of random
1145        // initial vector alignment. The convergence rate is (lambda2/lambda1)^k;
1146        // for this 2x2 diagonal matrix with singular values 3 and 2, that is
1147        // (2/3)^k. At k=5 the residual error is ~13%, which can cause flaky
1148        // failures. At k=20 the residual is (2/3)^20 < 0.03%, ensuring the
1149        // result always lands within the [0.95, 1.05] tolerance window.
1150        let mut spec_norm = SpectralNormalization::new(20, 1e-6);
1151
1152        // Create a matrix with known spectral norm
1153        let weights = array![[3.0, 0.0], [0.0, 2.0]]; // Spectral norm should be 3.0
1154
1155        let normalized = spec_norm.normalize_weights(&weights);
1156
1157        // Check that spectral norm is approximately 1.0
1158        let spectral_norm = spec_norm.get_spectral_norm(&normalized);
1159        assert!((0.95..=1.05).contains(&spectral_norm));
1160    }
1161
1162    #[test]
1163    fn test_spectral_normalization_preserves_shape() {
1164        let mut spec_norm = SpectralNormalization::new(3, 1e-6);
1165
1166        let weights = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1167        let normalized = spec_norm.normalize_weights(&weights);
1168
1169        // Shape should be preserved
1170        assert_eq!(normalized.shape(), weights.shape());
1171    }
1172
1173    #[test]
1174    fn test_spectral_normalization_identity() {
1175        let mut spec_norm = SpectralNormalization::new(1, 1e-6);
1176
1177        // Identity matrix should have spectral norm 1, so normalization should not change much
1178        let weights = array![[1.0, 0.0], [0.0, 1.0]];
1179        let normalized = spec_norm.normalize_weights(&weights);
1180
1181        // Should be close to original since spectral norm is already ~1
1182        for (orig, norm) in weights.iter().zip(normalized.iter()) {
1183            assert_abs_diff_eq!(orig, norm, epsilon = 1e-1);
1184        }
1185    }
1186
1187    #[test]
1188    fn test_spectral_normalization_large_values() {
1189        let mut spec_norm = SpectralNormalization::new(10, 1e-8);
1190
1191        // Matrix with large values
1192        let weights = array![[100.0, 50.0], [75.0, 200.0]];
1193        let normalized = spec_norm.normalize_weights(&weights);
1194
1195        // Spectral norm should be approximately 1.0
1196        let spectral_norm = spec_norm.get_spectral_norm(&normalized);
1197        assert!((0.98..=1.02).contains(&spectral_norm));
1198    }
1199
1200    #[test]
1201    fn test_spectral_norm_layer() {
1202        let mut layer = SpectralNormLayer::default();
1203
1204        let weights = array![[5.0, 0.0], [0.0, 3.0]];
1205
1206        // Should normalize when enabled
1207        let normalized = layer.normalize(&weights);
1208        let spectral_norm = layer.spectral_norm(&normalized);
1209        assert!(
1210            (0.9..=1.3).contains(&spectral_norm),
1211            "Expected spectral norm between 0.9 and 1.3, got {}",
1212            spectral_norm
1213        );
1214
1215        // Should not normalize when disabled
1216        layer.set_enabled(false);
1217        let not_normalized = layer.normalize(&weights);
1218        assert_eq!(not_normalized, weights);
1219    }
1220
1221    #[test]
1222    fn test_spectral_normalization_reset() {
1223        let mut spec_norm = SpectralNormalization::new(3, 1e-6);
1224
1225        // Initialize with one matrix
1226        let weights1 = array![[1.0, 2.0], [3.0, 4.0]];
1227        let _ = spec_norm.normalize_weights(&weights1);
1228        assert!(spec_norm.initialized);
1229
1230        // Reset
1231        spec_norm.reset();
1232        assert!(!spec_norm.initialized);
1233        assert!(spec_norm.u.is_none());
1234        assert!(spec_norm.v.is_none());
1235
1236        // Should work with different sized matrix after reset
1237        let weights2 = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1238        let _ = spec_norm.normalize_weights(&weights2);
1239        assert!(spec_norm.initialized);
1240    }
1241
1242    #[test]
1243    fn test_spectral_normalization_convergence() {
1244        let mut spec_norm = SpectralNormalization::new(1, 1e-6);
1245
1246        let weights = array![[2.0, 1.0], [1.0, 2.0]];
1247
1248        // Test with different numbers of power iterations
1249        let norm_1_iter = spec_norm.get_spectral_norm(&weights);
1250
1251        spec_norm.power_iterations = 10;
1252        spec_norm.reset();
1253        let norm_10_iter = spec_norm.get_spectral_norm(&weights);
1254
1255        // More iterations should give more accurate result
1256        // For this matrix, true spectral norm is 3.0
1257        assert!((norm_10_iter - 3.0_f64).abs() <= (norm_1_iter - 3.0_f64).abs());
1258    }
1259}