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