Skip to main content

trueno/vector/ops/
normalization.rs

1//! Normalization operations for Vector<f32>
2//!
3//! This module provides normalization methods:
4//! - `zscore()` - Z-score normalization (standardization)
5//! - `minmax_normalize()` - Min-max normalization to [0, 1]
6//! - `layer_norm()` - Layer normalization with learnable parameters
7//! - `layer_norm_simple()` - Layer normalization without learnable parameters
8//! - `normalize()` - Normalize to unit length (L2 norm = 1)
9
10use crate::{Result, TruenoError, Vector};
11
12impl Vector<f32> {
13    /// Z-score normalization (standardization)
14    ///
15    /// Transforms the vector to have mean = 0 and standard deviation = 1.
16    /// Each element is transformed as: z\[i\] = (x\[i\] - μ) / σ
17    ///
18    /// This is a fundamental preprocessing step in machine learning and statistics,
19    /// ensuring features have comparable scales and are centered around zero.
20    ///
21    /// # Performance
22    ///
23    /// Uses optimized SIMD implementations via mean() and stddev(), then applies
24    /// element-wise operations (sub, scale) which also use SIMD.
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
30    /// use trueno::Vector;
31    ///
32    /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
33    /// let z = v.zscore()?;
34    ///
35    /// // Verify mean ≈ 0
36    /// let mean = z.mean()?;
37    /// assert!(mean.abs() < 1e-5);
38    ///
39    /// // Verify stddev ≈ 1
40    /// let std = z.stddev()?;
41    /// assert!((std - 1.0).abs() < 1e-5);
42    /// # Ok(())
43    /// # }
44    /// ```
45    ///
46    /// # Empty vectors
47    ///
48    /// Returns EmptyVector error for empty vectors (cannot compute mean/stddev).
49    ///
50    /// # Division by zero
51    ///
52    /// Returns DivisionByZero error if the vector has zero standard deviation
53    /// (i.e., all elements are identical/constant).
54    ///
55    /// ```
56    /// use trueno::{Vector, TruenoError};
57    ///
58    /// let v = Vector::from_slice(&[5.0, 5.0, 5.0]); // Constant
59    /// assert!(matches!(v.zscore(), Err(TruenoError::DivisionByZero)));
60    /// ```
61    pub fn zscore(&self) -> Result<Self> {
62        if self.as_slice().is_empty() {
63            return Err(TruenoError::EmptyVector);
64        }
65
66        let mean_val = self.mean()?;
67        let std_val = self.stddev()?;
68
69        // Check for zero standard deviation (constant vector)
70        if std_val.abs() < 1e-10 {
71            return Err(TruenoError::DivisionByZero);
72        }
73
74        // Transform: z[i] = (x[i] - μ) / σ
75        let inv_std = 1.0 / std_val;
76        let data: Vec<f32> = self.as_slice().iter().map(|&x| (x - mean_val) * inv_std).collect();
77
78        Ok(Vector::from_vec(data))
79    }
80
81    /// Min-max normalization (scaling to [0, 1] range)
82    ///
83    /// Transforms the vector so that the minimum value becomes 0 and the maximum
84    /// value becomes 1, with all other values scaled proportionally.
85    /// Formula: x'\[i\] = (x\[i\] - min) / (max - min)
86    ///
87    /// This is a fundamental preprocessing technique in machine learning, especially
88    /// for algorithms sensitive to feature magnitudes (e.g., neural networks, k-NN).
89    ///
90    /// # Performance
91    ///
92    /// Uses optimized SIMD implementations via min() and max() operations, then
93    /// applies element-wise transformation.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
99    /// use trueno::Vector;
100    ///
101    /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
102    /// let normalized = v.minmax_normalize()?;
103    ///
104    /// // Verify range [0, 1]
105    /// let min = normalized.min()?;
106    /// let max = normalized.max()?;
107    /// assert!((min - 0.0).abs() < 1e-5);
108    /// assert!((max - 1.0).abs() < 1e-5);
109    /// # Ok(())
110    /// # }
111    /// ```
112    ///
113    /// # Empty vectors
114    ///
115    /// Returns EmptyVector error for empty vectors (cannot compute min/max).
116    ///
117    /// # Division by zero
118    ///
119    /// Returns DivisionByZero error if the vector has all identical elements
120    /// (i.e., min = max, causing division by zero in the normalization formula).
121    ///
122    /// ```
123    /// use trueno::{Vector, TruenoError};
124    ///
125    /// let v = Vector::from_slice(&[5.0, 5.0, 5.0]); // Constant
126    /// assert!(matches!(v.minmax_normalize(), Err(TruenoError::DivisionByZero)));
127    /// ```
128    pub fn minmax_normalize(&self) -> Result<Self> {
129        if self.as_slice().is_empty() {
130            return Err(TruenoError::EmptyVector);
131        }
132
133        let min_val = self.min()?;
134        let max_val = self.max()?;
135        let range = max_val - min_val;
136
137        // Check for zero range (constant vector)
138        if range.abs() < 1e-10 {
139            return Err(TruenoError::DivisionByZero);
140        }
141
142        // Transform: x'[i] = (x[i] - min) / (max - min)
143        let inv_range = 1.0 / range;
144        let data: Vec<f32> = self.as_slice().iter().map(|&x| (x - min_val) * inv_range).collect();
145
146        Ok(Vector::from_vec(data))
147    }
148
149    /// Layer normalization with learnable parameters (Issue #61: ML primitives)
150    ///
151    /// Applies layer normalization: `y = gamma * (x - mean) / sqrt(variance + eps) + beta`
152    ///
153    /// This is a fundamental normalization technique in transformers and other
154    /// modern neural network architectures. Unlike batch normalization, layer norm
155    /// normalizes across the feature dimension, making it suitable for sequence models.
156    ///
157    /// # Arguments
158    ///
159    /// * `gamma` - Scale parameter (typically learned, initialized to 1.0)
160    /// * `beta` - Shift parameter (typically learned, initialized to 0.0)
161    /// * `eps` - Small constant for numerical stability (typically 1e-5 or 1e-6)
162    ///
163    /// # Returns
164    ///
165    /// Normalized vector with the same shape as input
166    ///
167    /// # Errors
168    ///
169    /// Returns `SizeMismatch` if gamma or beta have different lengths than self
170    /// Returns `EmptyVector` if input is empty
171    ///
172    /// # Example
173    ///
174    /// ```
175    /// use trueno::Vector;
176    ///
177    /// let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
178    /// let gamma = Vector::from_slice(&[1.0, 1.0, 1.0, 1.0]); // Scale = 1
179    /// let beta = Vector::from_slice(&[0.0, 0.0, 0.0, 0.0]);  // Shift = 0
180    ///
181    /// let y = x.layer_norm(&gamma, &beta, 1e-5).unwrap();
182    ///
183    /// // Output should be approximately standardized (mean ≈ 0, std ≈ 1)
184    /// let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
185    /// assert!(mean.abs() < 1e-5);
186    /// ```
187    ///
188    /// # Performance
189    ///
190    /// Single-pass computation using Welford's algorithm for numerical stability.
191    /// Time complexity: O(n), Space complexity: O(n).
192    pub fn layer_norm(&self, gamma: &Self, beta: &Self, eps: f32) -> Result<Self> {
193        if self.as_slice().is_empty() {
194            return Err(TruenoError::EmptyVector);
195        }
196
197        if self.len() != gamma.len() {
198            return Err(TruenoError::SizeMismatch { expected: self.len(), actual: gamma.len() });
199        }
200
201        if self.len() != beta.len() {
202            return Err(TruenoError::SizeMismatch { expected: self.len(), actual: beta.len() });
203        }
204
205        // Compute mean
206        let mean_val = self.mean()?;
207
208        // Compute variance: E[(x - mean)^2]
209        let variance: f32 = self
210            .as_slice()
211            .iter()
212            .map(|&x| {
213                let diff = x - mean_val;
214                diff * diff
215            })
216            .sum::<f32>()
217            / self.len().max(1) as f32;
218
219        // Compute inverse standard deviation for numerical stability
220        let inv_std = 1.0 / (variance + eps).sqrt();
221
222        // Apply normalization: y = gamma * (x - mean) * inv_std + beta
223        let data: Vec<f32> = self
224            .as_slice()
225            .iter()
226            .zip(gamma.as_slice().iter())
227            .zip(beta.as_slice().iter())
228            .map(|((&x, &g), &b)| g * (x - mean_val) * inv_std + b)
229            .collect();
230
231        Ok(Vector::from_vec(data))
232    }
233
234    /// Layer normalization without learnable parameters
235    ///
236    /// Simplified version that just standardizes the input: `y = (x - mean) / sqrt(variance + eps)`
237    ///
238    /// This is equivalent to calling `layer_norm` with gamma=1 and beta=0.
239    ///
240    /// # Arguments
241    ///
242    /// * `eps` - Small constant for numerical stability (typically 1e-5)
243    ///
244    /// # Example
245    ///
246    /// ```
247    /// use trueno::Vector;
248    ///
249    /// let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
250    /// let y = x.layer_norm_simple(1e-5).unwrap();
251    ///
252    /// // Output should be standardized
253    /// let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
254    /// assert!(mean.abs() < 1e-5);
255    /// ```
256    pub fn layer_norm_simple(&self, eps: f32) -> Result<Self> {
257        if self.as_slice().is_empty() {
258            return Err(TruenoError::EmptyVector);
259        }
260
261        let mean_val = self.mean()?;
262
263        // Compute variance
264        let variance: f32 = self
265            .as_slice()
266            .iter()
267            .map(|&x| {
268                let diff = x - mean_val;
269                diff * diff
270            })
271            .sum::<f32>()
272            / self.len().max(1) as f32;
273
274        let inv_std = 1.0 / (variance + eps).sqrt();
275
276        let data: Vec<f32> = self.as_slice().iter().map(|&x| (x - mean_val) * inv_std).collect();
277
278        Ok(Vector::from_vec(data))
279    }
280
281    /// Normalize the vector to unit length (L2 norm = 1)
282    ///
283    /// Returns a new vector in the same direction but with magnitude 1.
284    ///
285    /// # Errors
286    ///
287    /// Returns `TruenoError::DivisionByZero` if the vector has zero norm (cannot normalize zero vector).
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use trueno::Vector;
293    ///
294    /// let v = Vector::from_slice(&[3.0, 4.0]);
295    /// let unit = v.normalize().unwrap();
296    ///
297    /// // Result is [0.6, 0.8] (a unit vector)
298    /// assert!((unit.as_slice()[0] - 0.6).abs() < 1e-5);
299    /// assert!((unit.as_slice()[1] - 0.8).abs() < 1e-5);
300    ///
301    /// // Verify it's a unit vector (norm = 1)
302    /// assert!((unit.norm_l2().unwrap() - 1.0).abs() < 1e-5);
303    /// ```
304    ///
305    /// # Zero Vector Error
306    ///
307    /// ```
308    /// use trueno::{Vector, TruenoError};
309    ///
310    /// let v = Vector::from_slice(&[0.0, 0.0]);
311    /// assert!(matches!(v.normalize(), Err(TruenoError::DivisionByZero)));
312    /// ```
313    pub fn normalize(&self) -> Result<Vector<f32>> {
314        let norm = self.norm_l2()?;
315
316        // Check for zero or near-zero norm (cannot normalize zero vector)
317        if norm.abs() < 1e-10 {
318            return Err(TruenoError::DivisionByZero);
319        }
320
321        // Divide each element by the norm using scalar multiplication
322        // This avoids creating an intermediate vector
323        self.scale(1.0 / norm)
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_zscore_basic() {
333        let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
334        let z = v.zscore().unwrap();
335
336        // Mean should be ~0
337        let mean = z.mean().unwrap();
338        assert!(mean.abs() < 1e-5);
339
340        // Stddev should be ~1
341        let std = z.stddev().unwrap();
342        assert!((std - 1.0).abs() < 1e-5);
343    }
344
345    #[test]
346    fn test_zscore_empty() {
347        let v: Vector<f32> = Vector::from_slice(&[]);
348        assert!(matches!(v.zscore(), Err(TruenoError::EmptyVector)));
349    }
350
351    #[test]
352    fn test_zscore_constant() {
353        let v = Vector::from_slice(&[5.0, 5.0, 5.0]);
354        assert!(matches!(v.zscore(), Err(TruenoError::DivisionByZero)));
355    }
356
357    #[test]
358    fn test_minmax_normalize_basic() {
359        let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
360        let normalized = v.minmax_normalize().unwrap();
361
362        assert!((normalized.min().unwrap() - 0.0).abs() < 1e-5);
363        assert!((normalized.max().unwrap() - 1.0).abs() < 1e-5);
364    }
365
366    #[test]
367    fn test_minmax_normalize_empty() {
368        let v: Vector<f32> = Vector::from_slice(&[]);
369        assert!(matches!(v.minmax_normalize(), Err(TruenoError::EmptyVector)));
370    }
371
372    #[test]
373    fn test_minmax_normalize_constant() {
374        let v = Vector::from_slice(&[5.0, 5.0, 5.0]);
375        assert!(matches!(v.minmax_normalize(), Err(TruenoError::DivisionByZero)));
376    }
377
378    #[test]
379    fn test_layer_norm() {
380        let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
381        let gamma = Vector::from_slice(&[1.0, 1.0, 1.0, 1.0]);
382        let beta = Vector::from_slice(&[0.0, 0.0, 0.0, 0.0]);
383
384        let y = x.layer_norm(&gamma, &beta, 1e-5).unwrap();
385
386        // Mean should be ~0
387        let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
388        assert!(mean.abs() < 1e-5);
389    }
390
391    #[test]
392    fn test_layer_norm_size_mismatch() {
393        let x = Vector::from_slice(&[1.0, 2.0, 3.0]);
394        let gamma = Vector::from_slice(&[1.0, 1.0]); // Wrong size
395        let beta = Vector::from_slice(&[0.0, 0.0, 0.0]);
396
397        assert!(matches!(x.layer_norm(&gamma, &beta, 1e-5), Err(TruenoError::SizeMismatch { .. })));
398    }
399
400    #[test]
401    fn test_layer_norm_simple() {
402        let x = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
403        let y = x.layer_norm_simple(1e-5).unwrap();
404
405        let mean: f32 = y.as_slice().iter().sum::<f32>() / y.len() as f32;
406        assert!(mean.abs() < 1e-5);
407    }
408
409    #[test]
410    fn test_normalize_unit_vector() {
411        let v = Vector::from_slice(&[3.0, 4.0]);
412        let unit = v.normalize().unwrap();
413
414        assert!((unit.as_slice()[0] - 0.6).abs() < 1e-5);
415        assert!((unit.as_slice()[1] - 0.8).abs() < 1e-5);
416        assert!((unit.norm_l2().unwrap() - 1.0).abs() < 1e-5);
417    }
418
419    #[test]
420    fn test_normalize_zero_vector() {
421        let v = Vector::from_slice(&[0.0, 0.0]);
422        assert!(matches!(v.normalize(), Err(TruenoError::DivisionByZero)));
423    }
424}