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}