Skip to main content

optirs_core/regularizers/
spectral_norm.rs

1// Spectral normalization regularization
2//
3// Spectral normalization is a weight normalization technique that controls the
4// Lipschitz constant of the neural network by normalizing the spectral norm
5// (largest singular value) of weight matrices.
6
7use scirs2_core::ndarray::{Array, Array2, Array4, Dimension, ScalarOperand};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use scirs2_core::Random;
10use std::cell::RefCell;
11use std::fmt::Debug;
12
13use crate::error::{OptimError, Result};
14use crate::regularizers::Regularizer;
15
16/// Spectral normalization regularizer
17///
18/// Normalizes weight matrices by their spectral norm to ensure the Lipschitz
19/// constant is bounded. This helps with training stability and generalization.
20///
21/// # Example
22///
23/// ```no_run
24/// use scirs2_core::ndarray::array;
25/// use optirs_core::regularizers::SpectralNorm;
26///
27/// let spec_norm = SpectralNorm::new(1);
28/// let weights = array![[1.0, 2.0], [3.0, 4.0]];
29///
30/// // Normalize weights by spectral norm
31/// let normalized_weights = spec_norm.normalize(&weights).expect("normalize failed");
32/// ```
33#[derive(Debug)]
34pub struct SpectralNorm<A: Float> {
35    /// Number of power iterations for SVD approximation
36    n_power_iterations: usize,
37    /// Epsilon for numerical stability
38    eps: A,
39    /// Cached left singular vector
40    u: RefCell<Option<Array<A, scirs2_core::ndarray::Ix1>>>,
41    /// Cached right singular vector
42    v: RefCell<Option<Array<A, scirs2_core::ndarray::Ix1>>>,
43    /// Random number generator
44    rng: RefCell<Random<scirs2_core::random::rngs::StdRng>>,
45}
46
47impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> SpectralNorm<A> {
48    /// Create a new spectral normalization regularizer
49    ///
50    /// # Arguments
51    ///
52    /// * `n_power_iterations` - Number of power iterations for SVD approximation
53    pub fn new(n_poweriterations: usize) -> Self {
54        Self {
55            n_power_iterations: n_poweriterations,
56            eps: A::from_f64(1e-12).unwrap_or_else(|| A::epsilon()),
57            u: RefCell::new(None),
58            v: RefCell::new(None),
59            rng: RefCell::new(Random::seed(42)),
60        }
61    }
62
63    /// Compute the spectral norm (largest singular value) using power iteration
64    fn compute_spectral_norm(&self, weights: &Array2<A>) -> Result<A> {
65        let (m, n) = (weights.nrows(), weights.ncols());
66
67        // Initialize u and v if not already done
68        {
69            let u_ref = self.u.borrow();
70            let needs_init = u_ref.is_none() || u_ref.as_ref().is_none_or(|arr| arr.len() != m);
71            drop(u_ref);
72            if needs_init {
73                let mut rng = self.rng.borrow_mut();
74                let new_u = Array::from_shape_fn((m,), |_| {
75                    let val: f64 = rng.gen_range(0.0..1.0);
76                    A::from_f64(val).unwrap_or_else(|| A::one())
77                });
78                *self.u.borrow_mut() = Some(new_u);
79            }
80        }
81
82        {
83            let v_ref = self.v.borrow();
84            let needs_init = v_ref.is_none() || v_ref.as_ref().is_none_or(|arr| arr.len() != n);
85            drop(v_ref);
86            if needs_init {
87                let mut rng = self.rng.borrow_mut();
88                let new_v = Array::from_shape_fn((n,), |_| {
89                    let val: f64 = rng.gen_range(0.0..1.0);
90                    A::from_f64(val).unwrap_or_else(|| A::one())
91                });
92                *self.v.borrow_mut() = Some(new_v);
93            }
94        }
95
96        let mut u = self
97            .u
98            .borrow()
99            .as_ref()
100            .ok_or_else(|| {
101                OptimError::InvalidParameter("Left singular vector not initialized".to_string())
102            })?
103            .clone();
104        let mut v = self
105            .v
106            .borrow()
107            .as_ref()
108            .ok_or_else(|| {
109                OptimError::InvalidParameter("Right singular vector not initialized".to_string())
110            })?
111            .clone();
112
113        // Power iteration
114        for _ in 0..self.n_power_iterations {
115            // v = W^T u / ||W^T u||
116            let wt_u = weights.t().dot(&u);
117            let v_norm = (wt_u.dot(&wt_u) + self.eps).sqrt();
118            v = wt_u / v_norm;
119
120            // u = W v / ||W v||
121            let w_v = weights.dot(&v);
122            let u_norm = (w_v.dot(&w_v) + self.eps).sqrt();
123            u = w_v / u_norm;
124        }
125
126        // Update cached vectors
127        *self.u.borrow_mut() = Some(u.clone());
128        *self.v.borrow_mut() = Some(v.clone());
129
130        // Compute spectral norm as u^T W v
131        let w_v = weights.dot(&v);
132        let spectral_norm = u.dot(&w_v);
133
134        Ok(spectral_norm)
135    }
136
137    /// Normalize weights by spectral norm
138    pub fn normalize(&self, weights: &Array2<A>) -> Result<Array2<A>> {
139        let spectral_norm = self.compute_spectral_norm(weights)?;
140
141        if spectral_norm > self.eps {
142            Ok(weights / spectral_norm)
143        } else {
144            Ok(weights.clone())
145        }
146    }
147
148    /// Apply spectral normalization to 4D convolutional weights
149    pub fn normalize_conv4d(&self, weights: &Array4<A>) -> Result<Array4<A>> {
150        // Reshape to 2D for spectral norm computation
151        let shape = weights.shape();
152        let out_channels = shape[0];
153        let in_channels = shape[1];
154        let kernel_h = shape[2];
155        let kernel_w = shape[3];
156
157        let weights_2d = weights
158            .to_shape((out_channels, in_channels * kernel_h * kernel_w))
159            .map_err(|e| OptimError::InvalidConfig(format!("Cannot reshape weights: {}", e)))?;
160        let weights_2d_owned = weights_2d.to_owned();
161        let normalized_2d = self.normalize(&weights_2d_owned)?;
162
163        // Reshape back to 4D
164        let normalized_4d = normalized_2d
165            .to_shape((out_channels, in_channels, kernel_h, kernel_w))
166            .map_err(|e| {
167                OptimError::InvalidConfig(format!("Cannot reshape normalized weights: {}", e))
168            })?;
169        Ok(normalized_4d.to_owned())
170    }
171}
172
173// Implement Regularizer trait
174impl<A: Float + Debug + ScalarOperand + FromPrimitive, D: Dimension + Send + Sync> Regularizer<A, D>
175    for SpectralNorm<A>
176{
177    fn apply(&self, _params: &Array<A, D>, _gradients: &mut Array<A, D>) -> Result<A> {
178        // For spectral normalization, we don't modify gradients directly
179        // Instead, the normalization is typically applied during the forward pass
180        Ok(A::zero())
181    }
182
183    fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
184        // Spectral normalization doesn't add a penalty term
185        Ok(A::zero())
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use approx::assert_relative_eq;
193    use scirs2_core::ndarray::array;
194
195    #[test]
196    fn test_spectral_norm_creation() {
197        let sn = SpectralNorm::<f64>::new(5);
198        assert_eq!(sn.n_power_iterations, 5);
199    }
200
201    #[test]
202    fn test_spectral_norm_2d() {
203        let sn = SpectralNorm::new(10);
204
205        // Create a simple matrix with known singular values
206        // For a 2x2 matrix [[1, 0], [0, 2]], the singular values are 1 and 2
207        let weights = array![[1.0, 0.0], [0.0, 2.0]];
208
209        let spectral_norm = sn
210            .compute_spectral_norm(&weights)
211            .expect("test: compute_spectral_norm failed");
212
213        // The spectral norm should be close to 2.0 (largest singular value)
214        assert_relative_eq!(spectral_norm, 2.0, epsilon = 0.1);
215    }
216
217    #[test]
218    fn test_normalize_2d() {
219        let sn = SpectralNorm::new(10);
220
221        let weights = array![[1.0, 2.0], [3.0, 4.0]];
222        let normalized = sn.normalize(&weights).expect("test: normalize failed");
223
224        // After normalization, the spectral norm should be close to 1
225        let spec_norm = sn
226            .compute_spectral_norm(&normalized)
227            .expect("test: compute_spectral_norm failed");
228        assert_relative_eq!(spec_norm, 1.0, epsilon = 0.1);
229    }
230
231    #[test]
232    fn test_conv4d_normalization() {
233        let sn = SpectralNorm::new(5);
234
235        // Create a 4D tensor (out_channels, in_channels, height, width)
236        let weights = Array::from_shape_fn((2, 3, 3, 3), |(o, i, h, w)| {
237            (o * 27 + i * 9 + h * 3 + w) as f64
238        });
239
240        let normalized = sn
241            .normalize_conv4d(&weights)
242            .expect("test: normalize_conv4d failed");
243
244        // Check that the shape is preserved
245        assert_eq!(normalized.shape(), weights.shape());
246    }
247
248    #[test]
249    fn test_invalid_conv4d() {
250        let sn = SpectralNorm::<f64>::new(5);
251
252        // Create a 4D tensor (which is valid)
253        let weights = Array::zeros((2, 3, 4, 4));
254
255        // Should succeed for 4D tensors
256        assert!(sn.normalize_conv4d(&weights).is_ok());
257    }
258
259    #[test]
260    fn test_regularizer_trait() {
261        let sn = SpectralNorm::new(5);
262        let params = array![[1.0, 2.0], [3.0, 4.0]];
263        let mut gradient = array![[0.1, 0.2], [0.3, 0.4]];
264
265        // Spectral norm doesn't modify gradients or add penalties
266        let penalty = sn.penalty(&params).expect("test: penalty failed");
267        assert_eq!(penalty, 0.0);
268
269        let apply_result = sn
270            .apply(&params, &mut gradient)
271            .expect("test: apply failed");
272        assert_eq!(apply_result, 0.0);
273
274        // Gradients should be unchanged
275        assert_eq!(gradient, array![[0.1, 0.2], [0.3, 0.4]]);
276    }
277}