/home/noah/src/trueno/src/vector/ops/activations.rs
Line | Count | Source |
1 | | //! Activation functions for Vector<f32> |
2 | | //! |
3 | | //! This module provides neural network activation functions optimized with |
4 | | //! multi-backend SIMD support (Scalar, SSE2, AVX2, AVX-512, NEON, WASM SIMD). |
5 | | //! |
6 | | //! ## Activation Functions |
7 | | //! |
8 | | //! - [`softmax`](crate::Vector::softmax): Softmax normalization for classification |
9 | | //! - [`log_softmax`](crate::Vector::log_softmax): Numerically stable log-softmax |
10 | | //! - [`relu`](crate::Vector::relu): Rectified Linear Unit |
11 | | //! - [`sigmoid`](crate::Vector::sigmoid): Logistic sigmoid |
12 | | //! - [`leaky_relu`](crate::Vector::leaky_relu): Leaky ReLU with configurable slope |
13 | | //! - [`elu`](crate::Vector::elu): Exponential Linear Unit |
14 | | //! - [`gelu`](crate::Vector::gelu): Gaussian Error Linear Unit |
15 | | //! - [`swish`](crate::Vector::swish): Self-gated activation (SiLU) |
16 | | //! - [`hardswish`](crate::Vector::hardswish): Efficient hardware-friendly swish |
17 | | //! - [`mish`](crate::Vector::mish): Self-regularizing activation |
18 | | //! - [`selu`](crate::Vector::selu): Scaled Exponential Linear Unit |
19 | | |
20 | | use crate::backends::scalar::ScalarBackend; |
21 | | use crate::backends::VectorBackend; |
22 | | use crate::vector::Vector; |
23 | | use crate::{Backend, Result, TruenoError}; |
24 | | |
25 | | /// Backend dispatch macro for unary operations - centralizes platform-specific SIMD dispatch |
26 | | /// to eliminate code duplication across activation functions. |
27 | | /// |
28 | | /// # Safety |
29 | | /// The macro wraps unsafe backend calls internally, so callers don't need unsafe blocks. |
30 | | macro_rules! dispatch_unary_op { |
31 | | ($backend:expr, $op:ident, $input:expr, $output:expr) => {{ |
32 | | #[cfg(target_arch = "x86_64")] |
33 | | use crate::backends::{avx2::Avx2Backend, sse2::Sse2Backend}; |
34 | | // SAFETY: CPU features verified at runtime before backend selection |
35 | | unsafe { |
36 | | match $backend { |
37 | | Backend::Scalar => ScalarBackend::$op($input, $output), |
38 | | #[cfg(target_arch = "x86_64")] |
39 | | Backend::SSE2 | Backend::AVX => Sse2Backend::$op($input, $output), |
40 | | #[cfg(target_arch = "x86_64")] |
41 | | Backend::AVX2 | Backend::AVX512 => Avx2Backend::$op($input, $output), |
42 | | #[cfg(not(target_arch = "x86_64"))] |
43 | | Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512 => { |
44 | | ScalarBackend::$op($input, $output) |
45 | | } |
46 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
47 | | Backend::NEON => { |
48 | | use crate::backends::neon::NeonBackend; |
49 | | NeonBackend::$op($input, $output) |
50 | | } |
51 | | #[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))] |
52 | | Backend::NEON => ScalarBackend::$op($input, $output), |
53 | | #[cfg(target_arch = "wasm32")] |
54 | | Backend::WasmSIMD => { |
55 | | use crate::backends::wasm::WasmBackend; |
56 | | WasmBackend::$op($input, $output) |
57 | | } |
58 | | #[cfg(not(target_arch = "wasm32"))] |
59 | | Backend::WasmSIMD => ScalarBackend::$op($input, $output), |
60 | | Backend::GPU | Backend::Auto => ScalarBackend::$op($input, $output), |
61 | | } |
62 | | } |
63 | | }}; |
64 | | } |
65 | | |
66 | | impl Vector<f32> { |
67 | | /// Softmax activation function |
68 | | /// |
69 | | /// Converts a vector of real values into a probability distribution. |
70 | | /// Formula: softmax(x)\[i\] = exp(x\[i\] - max(x)) / sum(exp(x\[j\] - max(x))) |
71 | | /// |
72 | | /// Uses the numerically stable version with max subtraction to prevent overflow. |
73 | | /// The output is a probability distribution: all values in [0, 1] and sum to 1. |
74 | | /// |
75 | | /// This is the standard activation function for multi-class classification in neural networks. |
76 | | /// |
77 | | /// # Examples |
78 | | /// |
79 | | /// ``` |
80 | | /// use trueno::Vector; |
81 | | /// |
82 | | /// let logits = Vector::from_slice(&[1.0, 2.0, 3.0]); |
83 | | /// let probs = logits.softmax()?; |
84 | | /// |
85 | | /// // Verify sum ≈ 1 |
86 | | /// let sum: f32 = probs.as_slice().iter().sum(); |
87 | | /// assert!((sum - 1.0).abs() < 1e-5); |
88 | | /// |
89 | | /// // Verify all values in [0, 1] |
90 | | /// for &p in probs.as_slice() { |
91 | | /// assert!(p >= 0.0 && p <= 1.0); |
92 | | /// } |
93 | | /// # Ok::<(), trueno::TruenoError>(()) |
94 | | /// ``` |
95 | | /// |
96 | | /// # Empty vectors |
97 | | /// |
98 | | /// Returns EmptyVector error for empty vectors (cannot compute softmax). |
99 | 10.4k | pub fn softmax(&self) -> Result<Self> { |
100 | 10.4k | if self.data.is_empty() { |
101 | 0 | return Err(TruenoError::EmptyVector); |
102 | 10.4k | } |
103 | | |
104 | | // OpComplexity::Medium - GPU threshold: >10K elements (multi-pass overhead) |
105 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
106 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 4-368x slower, see docs/performance-analysis.md |
107 | | |
108 | | // Try GPU first for large vectors |
109 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
110 | | { |
111 | 10.4k | if self.data.len() >= GPU_THRESHOLD { |
112 | | use crate::backends::gpu::GpuDevice; |
113 | 0 | if GpuDevice::is_available() { |
114 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
115 | 0 | let mut result = vec![0.0; self.data.len()]; |
116 | 0 | if gpu.softmax(&self.data, &mut result).is_ok() { |
117 | 0 | return Ok(Vector::from_vec(result)); |
118 | 0 | } |
119 | 0 | } |
120 | 10.4k | } |
121 | | } |
122 | | |
123 | | // Scalar fallback: Multi-pass softmax for numerical stability |
124 | | // Find max for numerical stability (prevents overflow in exp) |
125 | 10.4k | let max_val = self.max()?0 ; |
126 | | |
127 | | // Compute exp(x - max) for each element |
128 | 227k | let exp_vals10.4k : Vec<f32>10.4k = self.data.iter()10.4k .map10.4k (|&x| (x - max_val).exp()).collect10.4k (); |
129 | | |
130 | | // Compute sum of exponentials |
131 | 10.4k | let sum_exp: f32 = exp_vals.iter().sum(); |
132 | | |
133 | | // Normalize by sum |
134 | 227k | let data10.4k : Vec<f32>10.4k = exp_vals.iter()10.4k .map10.4k (|&e| e / sum_exp).collect10.4k (); |
135 | | |
136 | 10.4k | Ok(Vector::from_vec(data)) |
137 | 10.4k | } |
138 | | |
139 | | /// Log-softmax activation function |
140 | | /// |
141 | | /// Computes the logarithm of the softmax function in a numerically stable way. |
142 | | /// Formula: log_softmax(x)\[i\] = x\[i\] - max(x) - log(sum(exp(x\[j\] - max(x)))) |
143 | | /// |
144 | | /// This is more numerically stable than computing log(softmax(x)) and is commonly |
145 | | /// used in neural networks for computing cross-entropy loss. |
146 | | /// |
147 | | /// # Examples |
148 | | /// |
149 | | /// ``` |
150 | | /// use trueno::Vector; |
151 | | /// |
152 | | /// let logits = Vector::from_slice(&[1.0, 2.0, 3.0]); |
153 | | /// let log_probs = logits.log_softmax()?; |
154 | | /// |
155 | | /// // Verify exp(log_softmax) = softmax |
156 | | /// let probs_from_log: Vec<f32> = log_probs.as_slice().iter().map(|&x| x.exp()).collect(); |
157 | | /// let sum: f32 = probs_from_log.iter().sum(); |
158 | | /// assert!((sum - 1.0).abs() < 1e-5); |
159 | | /// # Ok::<(), trueno::TruenoError>(()) |
160 | | /// ``` |
161 | | /// |
162 | | /// # Empty vectors |
163 | | /// |
164 | | /// Returns EmptyVector error for empty vectors. |
165 | 0 | pub fn log_softmax(&self) -> Result<Self> { |
166 | 0 | if self.data.is_empty() { |
167 | 0 | return Err(TruenoError::EmptyVector); |
168 | 0 | } |
169 | | |
170 | | // OpComplexity::Medium - GPU threshold: >10K elements (multi-pass overhead) |
171 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
172 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 4-368x slower, see docs/performance-analysis.md |
173 | | |
174 | | // Try GPU first for large vectors |
175 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
176 | | { |
177 | 0 | if self.data.len() >= GPU_THRESHOLD { |
178 | | use crate::backends::gpu::GpuDevice; |
179 | 0 | if GpuDevice::is_available() { |
180 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
181 | 0 | let mut result = vec![0.0; self.data.len()]; |
182 | 0 | if gpu.log_softmax(&self.data, &mut result).is_ok() { |
183 | 0 | return Ok(Vector::from_vec(result)); |
184 | 0 | } |
185 | 0 | } |
186 | 0 | } |
187 | | } |
188 | | |
189 | | // Scalar fallback: Multi-pass log_softmax for numerical stability |
190 | | // Find max for numerical stability |
191 | 0 | let max_val = self.max()?; |
192 | | |
193 | | // Compute exp(x - max) for each element |
194 | 0 | let exp_vals: Vec<f32> = self.data.iter().map(|&x| (x - max_val).exp()).collect(); |
195 | | |
196 | | // Compute log of sum of exponentials |
197 | 0 | let sum_exp: f32 = exp_vals.iter().sum(); |
198 | 0 | let log_sum_exp = sum_exp.ln(); |
199 | | |
200 | | // log_softmax(x)[i] = x[i] - max - log_sum_exp |
201 | 0 | let data: Vec<f32> = self |
202 | 0 | .data |
203 | 0 | .iter() |
204 | 0 | .map(|&x| x - max_val - log_sum_exp) |
205 | 0 | .collect(); |
206 | | |
207 | 0 | Ok(Vector::from_vec(data)) |
208 | 0 | } |
209 | | |
210 | | /// ReLU (Rectified Linear Unit) activation function |
211 | | /// |
212 | | /// Computes the element-wise ReLU: max(0, x). |
213 | | /// ReLU is one of the most widely used activation functions in neural networks. |
214 | | /// |
215 | | /// # Formula |
216 | | /// |
217 | | /// ```text |
218 | | /// relu(x)[i] = max(0, x\[i\]) |
219 | | /// = x\[i\] if x\[i\] > 0 |
220 | | /// = 0 otherwise |
221 | | /// ``` |
222 | | /// |
223 | | /// # Properties |
224 | | /// |
225 | | /// - **Non-linearity**: Introduces non-linearity while preserving linearity for positive values |
226 | | /// - **Sparsity**: Produces exactly zero for negative inputs (sparse activations) |
227 | | /// - **Gradient**: Derivative is 1 for positive inputs, 0 for negative (solves vanishing gradient) |
228 | | /// - **Computational efficiency**: Simple max operation, no exponentials |
229 | | /// |
230 | | /// # Applications |
231 | | /// |
232 | | /// - **Deep neural networks**: Default activation for hidden layers |
233 | | /// - **Convolutional networks**: Standard activation in CNNs |
234 | | /// - **Feature learning**: Encourages sparse representations |
235 | | /// |
236 | | /// # Performance |
237 | | /// |
238 | | /// This operation is memory-bound. SIMD provides modest speedups since |
239 | | /// the computation (comparison and selection) is simpler than memory access. |
240 | | /// |
241 | | /// # Errors |
242 | | /// |
243 | | /// Returns `EmptyVector` if the input vector is empty. |
244 | | /// |
245 | | /// # Examples |
246 | | /// |
247 | | /// ``` |
248 | | /// use trueno::Vector; |
249 | | /// |
250 | | /// let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
251 | | /// let result = v.relu()?; |
252 | | /// assert_eq!(result.as_slice(), &[0.0, 0.0, 0.0, 1.0, 2.0]); |
253 | | /// # Ok::<(), trueno::TruenoError>(()) |
254 | | /// ``` |
255 | 0 | pub fn relu(&self) -> Result<Self> { |
256 | 0 | if self.data.is_empty() { |
257 | 0 | return Err(TruenoError::EmptyVector); |
258 | 0 | } |
259 | | |
260 | | // OpComplexity::Low - GPU threshold: >100K elements |
261 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
262 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 2-800x slower, see docs/performance-analysis.md |
263 | | |
264 | | // Try GPU first for large vectors |
265 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
266 | | { |
267 | 0 | if self.data.len() >= GPU_THRESHOLD { |
268 | | use crate::backends::gpu::GpuDevice; |
269 | 0 | if GpuDevice::is_available() { |
270 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
271 | 0 | let mut result = vec![0.0; self.data.len()]; |
272 | 0 | if gpu.relu(&self.data, &mut result).is_ok() { |
273 | 0 | return Ok(Vector::from_vec(result)); |
274 | 0 | } |
275 | 0 | } |
276 | 0 | } |
277 | | } |
278 | | |
279 | 0 | let mut result = vec![0.0; self.len()]; |
280 | | |
281 | | // Use parallel processing for very large arrays (reduces TLB pressure and improves cache utilization) |
282 | | #[cfg(feature = "parallel")] |
283 | | { |
284 | | const PARALLEL_THRESHOLD: usize = 500_000; // Increased to avoid overhead at smaller sizes |
285 | | const CHUNK_SIZE: usize = 65536; // 64K elements = 256KB, cache-friendly |
286 | | |
287 | | if self.len() >= PARALLEL_THRESHOLD { |
288 | | use rayon::prelude::*; |
289 | | |
290 | | self.data |
291 | | .par_chunks(CHUNK_SIZE) |
292 | | .zip(result.par_chunks_mut(CHUNK_SIZE)) |
293 | | .for_each(|(chunk_in, chunk_out)| { |
294 | | dispatch_unary_op!(self.backend, relu, chunk_in, chunk_out); |
295 | | }); |
296 | | |
297 | | return Ok(Vector::from_vec(result)); // Use from_vec to avoid extra copy |
298 | | } |
299 | | } |
300 | | |
301 | | // Sequential processing for small arrays or when parallel feature disabled |
302 | 0 | dispatch_unary_op!(self.backend, relu, &self.data, &mut result); |
303 | | |
304 | 0 | Ok(Vector::from_vec(result)) // Use from_vec to avoid extra copy |
305 | 0 | } |
306 | | |
307 | | /// Sigmoid (logistic) activation function |
308 | | /// |
309 | | /// Computes the element-wise sigmoid: σ(x) = 1 / (1 + e^(-x)). |
310 | | /// Sigmoid is a classic activation function that squashes inputs to the range (0, 1). |
311 | | /// |
312 | | /// # Formula |
313 | | /// |
314 | | /// ```text |
315 | | /// sigmoid(x)[i] = 1 / (1 + exp(-x\[i\])) |
316 | | /// = exp(x\[i\]) / (1 + exp(x\[i\])) |
317 | | /// ``` |
318 | | /// |
319 | | /// # Properties |
320 | | /// |
321 | | /// - **Bounded output**: Maps all inputs to (0, 1) range |
322 | | /// - **Smooth**: Infinitely differentiable (C^∞) |
323 | | /// - **Symmetric**: σ(-x) = 1 - σ(x) |
324 | | /// - **Derivative**: σ'(x) = σ(x) * (1 - σ(x)) |
325 | | /// - **Interpretable**: Output can be interpreted as probability |
326 | | /// |
327 | | /// # Applications |
328 | | /// |
329 | | /// - **Binary classification**: Final layer for binary output (0 or 1) |
330 | | /// - **Logistic regression**: Traditional ML algorithm |
331 | | /// - **Gating mechanisms**: LSTM/GRU gates (input, forget, output) |
332 | | /// - **Attention mechanisms**: Soft attention weights |
333 | | /// |
334 | | /// # Numerical Considerations |
335 | | /// |
336 | | /// For very large negative inputs (x < -50), exp(-x) overflows to infinity. |
337 | | /// However, sigmoid(x) approaches 0, so we return 0 for numerical stability. |
338 | | /// For very large positive inputs (x > 50), exp(-x) underflows to 0, |
339 | | /// and sigmoid(x) approaches 1. |
340 | | /// |
341 | | /// # Performance |
342 | | /// |
343 | | /// This operation is compute-bound due to the exp() operation. SIMD provides |
344 | | /// modest speedups, but the exponential is the bottleneck. |
345 | | /// |
346 | | /// # Errors |
347 | | /// |
348 | | /// Returns `EmptyVector` if the input vector is empty. |
349 | | /// |
350 | | /// # Examples |
351 | | /// |
352 | | /// ``` |
353 | | /// use trueno::Vector; |
354 | | /// |
355 | | /// let v = Vector::from_slice(&[-2.0, 0.0, 2.0]); |
356 | | /// let result = v.sigmoid()?; |
357 | | /// |
358 | | /// // sigmoid(-2) ≈ 0.119, sigmoid(0) = 0.5, sigmoid(2) ≈ 0.881 |
359 | | /// assert!((result.as_slice()[0] - 0.119).abs() < 0.001); |
360 | | /// assert!((result.as_slice()[1] - 0.5).abs() < 0.001); |
361 | | /// assert!((result.as_slice()[2] - 0.881).abs() < 0.001); |
362 | | /// # Ok::<(), trueno::TruenoError>(()) |
363 | | /// ``` |
364 | 0 | pub fn sigmoid(&self) -> Result<Self> { |
365 | 0 | if self.data.is_empty() { |
366 | 0 | return Err(TruenoError::EmptyVector); |
367 | 0 | } |
368 | | |
369 | | // OpComplexity::Low - GPU threshold: >100K elements |
370 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
371 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 2-800x slower, see docs/performance-analysis.md |
372 | | |
373 | | // Try GPU first for large vectors |
374 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
375 | | { |
376 | 0 | if self.data.len() >= GPU_THRESHOLD { |
377 | | use crate::backends::gpu::GpuDevice; |
378 | 0 | if GpuDevice::is_available() { |
379 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
380 | 0 | let mut result = vec![0.0; self.data.len()]; |
381 | 0 | if gpu.sigmoid(&self.data, &mut result).is_ok() { |
382 | 0 | return Ok(Vector::from_vec(result)); |
383 | 0 | } |
384 | 0 | } |
385 | 0 | } |
386 | | } |
387 | | |
388 | 0 | let mut result = vec![0.0; self.len()]; |
389 | | |
390 | | // Dispatch to appropriate backend |
391 | 0 | dispatch_unary_op!(self.backend, sigmoid, &self.data, &mut result); |
392 | | |
393 | 0 | Ok(Vector::from_vec(result)) |
394 | 0 | } |
395 | | |
396 | | /// Leaky ReLU activation function |
397 | | /// |
398 | | /// Computes the element-wise Leaky ReLU with a configurable negative slope. |
399 | | /// Leaky ReLU addresses the "dying ReLU" problem by allowing small negative values. |
400 | | /// |
401 | | /// # Formula |
402 | | /// |
403 | | /// ```text |
404 | | /// leaky_relu(x, α)[i] = max(αx\[i\], x\[i\]) |
405 | | /// = x\[i\] if x\[i\] > 0 |
406 | | /// = αx\[i\] if x\[i\] ≤ 0 |
407 | | /// ``` |
408 | | /// |
409 | | /// # Parameters |
410 | | /// |
411 | | /// - `negative_slope`: The slope for negative values (typically 0.01) |
412 | | /// - Must be in range [0.0, 1.0) |
413 | | /// - Common values: 0.01 (default), 0.1, 0.2 |
414 | | /// - α = 0 reduces to standard ReLU |
415 | | /// - α = 1 reduces to identity function |
416 | | /// |
417 | | /// # Properties |
418 | | /// |
419 | | /// - **Fixes dying ReLU**: Neurons can't completely die (always has gradient) |
420 | | /// - **Non-zero gradient**: Gradient is α for negative inputs (not zero) |
421 | | /// - **Unbounded positive**: No saturation for positive values |
422 | | /// - **Parameterized**: Negative slope can be tuned or learned (PReLU) |
423 | | /// |
424 | | /// # Applications |
425 | | /// |
426 | | /// - **Deep networks**: Prevents dying neurons in very deep networks |
427 | | /// - **GANs**: Often used in generator and discriminator networks |
428 | | /// - **Better gradient flow**: Helps with vanishing gradient problem |
429 | | /// - **Empirical improvements**: Often outperforms ReLU in practice |
430 | | /// |
431 | | /// # Performance |
432 | | /// |
433 | | /// This operation is memory-bound (simple multiplication and comparison). |
434 | | /// SIMD provides modest speedups. |
435 | | /// |
436 | | /// # Errors |
437 | | /// |
438 | | /// Returns `EmptyVector` if the input vector is empty. |
439 | | /// Returns `InvalidInput` if negative_slope is not in [0.0, 1.0). |
440 | | /// |
441 | | /// # Examples |
442 | | /// |
443 | | /// ``` |
444 | | /// use trueno::Vector; |
445 | | /// |
446 | | /// let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
447 | | /// let result = v.leaky_relu(0.01)?; |
448 | | /// |
449 | | /// // Negative values multiplied by 0.01, positive unchanged |
450 | | /// assert_eq!(result.as_slice(), &[-0.02, -0.01, 0.0, 1.0, 2.0]); |
451 | | /// # Ok::<(), trueno::TruenoError>(()) |
452 | | /// ``` |
453 | 0 | pub fn leaky_relu(&self, negative_slope: f32) -> Result<Self> { |
454 | 0 | if self.data.is_empty() { |
455 | 0 | return Err(TruenoError::EmptyVector); |
456 | 0 | } |
457 | | |
458 | | // Validate negative_slope parameter |
459 | 0 | if !(0.0..1.0).contains(&negative_slope) { |
460 | 0 | return Err(TruenoError::InvalidInput(format!( |
461 | 0 | "negative_slope must be in [0.0, 1.0), got {}", |
462 | 0 | negative_slope |
463 | 0 | ))); |
464 | 0 | } |
465 | | |
466 | | // OpComplexity::Low - GPU threshold: >100K elements |
467 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
468 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 2-800x slower, see docs/performance-analysis.md |
469 | | |
470 | | // Try GPU first for large vectors |
471 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
472 | | { |
473 | 0 | if self.data.len() >= GPU_THRESHOLD { |
474 | | use crate::backends::gpu::GpuDevice; |
475 | 0 | if GpuDevice::is_available() { |
476 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
477 | 0 | let mut result = vec![0.0; self.data.len()]; |
478 | 0 | if gpu |
479 | 0 | .leaky_relu(&self.data, &mut result, negative_slope) |
480 | 0 | .is_ok() |
481 | | { |
482 | 0 | return Ok(Vector::from_vec(result)); |
483 | 0 | } |
484 | 0 | } |
485 | 0 | } |
486 | | } |
487 | | |
488 | | // Scalar fallback: leaky_relu(x, α) = x if x > 0, αx otherwise |
489 | 0 | let data: Vec<f32> = self |
490 | 0 | .data |
491 | 0 | .iter() |
492 | 0 | .map(|&x| if x > 0.0 { x } else { negative_slope * x }) |
493 | 0 | .collect(); |
494 | | |
495 | 0 | Ok(Vector::from_vec(data)) |
496 | 0 | } |
497 | | |
498 | | /// ELU (Exponential Linear Unit) activation function |
499 | | /// |
500 | | /// Computes the element-wise ELU with a configurable alpha parameter. |
501 | | /// ELU pushes mean activations closer to zero, improving learning. |
502 | | /// |
503 | | /// # Formula |
504 | | /// |
505 | | /// ```text |
506 | | /// elu(x, α)[i] = x\[i\] if x\[i\] > 0 |
507 | | /// = α(e^x\[i\] - 1) if x\[i\] ≤ 0 |
508 | | /// ``` |
509 | | /// |
510 | | /// # Parameters |
511 | | /// |
512 | | /// - `alpha`: Controls the saturation value for negative inputs (typically 1.0) |
513 | | /// - Must be > 0 |
514 | | /// - Common value: 1.0 (original ELU paper) |
515 | | /// - Larger α → slower saturation for negative inputs |
516 | | /// |
517 | | /// # Properties |
518 | | /// |
519 | | /// - **Smooth**: Unlike ReLU/Leaky ReLU, has smooth gradients everywhere |
520 | | /// - **Negative values**: Allows negative outputs (pushes mean closer to zero) |
521 | | /// - **Bounded below**: Saturates to -α for very negative inputs |
522 | | /// - **Unbounded above**: No saturation for positive values |
523 | | /// - **Non-zero gradient**: Has gradient everywhere (no dead neurons) |
524 | | /// |
525 | | /// # Applications |
526 | | /// |
527 | | /// - **Deep networks**: Better gradient flow than ReLU |
528 | | /// - **Mean activation near zero**: Reduces internal covariate shift |
529 | | /// - **Noise robustness**: Smooth activation helps with noisy gradients |
530 | | /// - **Empirical improvements**: Often outperforms ReLU and Leaky ReLU |
531 | | /// |
532 | | /// # Performance |
533 | | /// |
534 | | /// This operation is compute-bound due to exp() for negative values. |
535 | | /// More expensive than ReLU/Leaky ReLU but provides better properties. |
536 | | /// |
537 | | /// # Errors |
538 | | /// |
539 | | /// Returns `EmptyVector` if the input vector is empty. |
540 | | /// Returns `InvalidInput` if alpha <= 0. |
541 | | /// |
542 | | /// # Examples |
543 | | /// |
544 | | /// ``` |
545 | | /// use trueno::Vector; |
546 | | /// |
547 | | /// let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
548 | | /// let result = v.elu(1.0)?; |
549 | | /// |
550 | | /// // Negative values: α(e^x - 1), positive unchanged |
551 | | /// // elu(-2, 1) ≈ -0.865, elu(-1, 1) ≈ -0.632 |
552 | | /// assert!((result.as_slice()[0] - (-0.865)).abs() < 0.01); |
553 | | /// assert!((result.as_slice()[1] - (-0.632)).abs() < 0.01); |
554 | | /// assert_eq!(result.as_slice()[2], 0.0); |
555 | | /// assert_eq!(result.as_slice()[3], 1.0); |
556 | | /// assert_eq!(result.as_slice()[4], 2.0); |
557 | | /// # Ok::<(), trueno::TruenoError>(()) |
558 | | /// ``` |
559 | 0 | pub fn elu(&self, alpha: f32) -> Result<Self> { |
560 | 0 | if self.data.is_empty() { |
561 | 0 | return Err(TruenoError::EmptyVector); |
562 | 0 | } |
563 | | |
564 | | // Validate alpha parameter |
565 | 0 | if alpha <= 0.0 { |
566 | 0 | return Err(TruenoError::InvalidInput(format!( |
567 | 0 | "alpha must be > 0, got {}", |
568 | 0 | alpha |
569 | 0 | ))); |
570 | 0 | } |
571 | | |
572 | | // OpComplexity::Low - GPU threshold: >100K elements |
573 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
574 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 2-800x slower, see docs/performance-analysis.md |
575 | | |
576 | | // Try GPU first for large vectors |
577 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
578 | | { |
579 | 0 | if self.data.len() >= GPU_THRESHOLD { |
580 | | use crate::backends::gpu::GpuDevice; |
581 | 0 | if GpuDevice::is_available() { |
582 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
583 | 0 | let mut result = vec![0.0; self.data.len()]; |
584 | 0 | if gpu.elu(&self.data, &mut result, alpha).is_ok() { |
585 | 0 | return Ok(Vector::from_vec(result)); |
586 | 0 | } |
587 | 0 | } |
588 | 0 | } |
589 | | } |
590 | | |
591 | | // Scalar fallback: elu(x, α) = x if x > 0, α(e^x - 1) otherwise |
592 | 0 | let data: Vec<f32> = self |
593 | 0 | .data |
594 | 0 | .iter() |
595 | 0 | .map(|&x| if x > 0.0 { x } else { alpha * (x.exp() - 1.0) }) |
596 | 0 | .collect(); |
597 | | |
598 | 0 | Ok(Vector::from_vec(data)) |
599 | 0 | } |
600 | | |
601 | | /// GELU (Gaussian Error Linear Unit) activation function |
602 | | /// |
603 | | /// Computes the element-wise GELU activation using the tanh approximation. |
604 | | /// GELU is the activation function used in transformers (BERT, GPT, etc.). |
605 | | /// |
606 | | /// # Formula |
607 | | /// |
608 | | /// ```text |
609 | | /// gelu(x) ≈ 0.5 * x * (1 + tanh(√(2/π) * (x + 0.044715 * x³))) |
610 | | /// ``` |
611 | | /// |
612 | | /// This is the tanh approximation which is faster than the exact form |
613 | | /// involving the error function (erf). |
614 | | /// |
615 | | /// # Properties |
616 | | /// |
617 | | /// - **Smooth**: Infinitely differentiable everywhere |
618 | | /// - **Non-monotonic**: Unlike ReLU variants, has slight non-monotonicity near zero |
619 | | /// - **Stochastic regularizer**: Can be viewed as adaptive dropout |
620 | | /// - **Zero-centered**: Mean activation close to zero |
621 | | /// - **Bounded below**: Approaches 0 as x → -∞ |
622 | | /// - **Unbounded above**: Linear growth for large positive x |
623 | | /// |
624 | | /// # Applications |
625 | | /// |
626 | | /// - **Transformers**: BERT, GPT-2, GPT-3, GPT-4 (default activation) |
627 | | /// - **Vision transformers**: ViT, DINO, MAE |
628 | | /// - **Modern architectures**: State-of-the-art NLP and vision models |
629 | | /// - **Better than ReLU**: Empirically outperforms ReLU in many tasks |
630 | | /// |
631 | | /// # Performance |
632 | | /// |
633 | | /// This operation is compute-intensive (tanh, x³ calculations). |
634 | | /// More expensive than ReLU but comparable to ELU. |
635 | | /// |
636 | | /// # Errors |
637 | | /// |
638 | | /// Returns `EmptyVector` if the input vector is empty. |
639 | | /// |
640 | | /// # Examples |
641 | | /// |
642 | | /// ``` |
643 | | /// use trueno::Vector; |
644 | | /// |
645 | | /// let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
646 | | /// let result = v.gelu()?; |
647 | | /// |
648 | | /// // GELU is smooth and non-monotonic near zero |
649 | | /// assert!(result.as_slice()[0] < 0.0); // Negative inputs → small negative outputs |
650 | | /// assert_eq!(result.as_slice()[2], 0.0); // gelu(0) = 0 |
651 | | /// assert!(result.as_slice()[4] > 1.5); // Large positive → ~linear |
652 | | /// # Ok::<(), trueno::TruenoError>(()) |
653 | | /// ``` |
654 | 0 | pub fn gelu(&self) -> Result<Self> { |
655 | 0 | if self.data.is_empty() { |
656 | 0 | return Err(TruenoError::EmptyVector); |
657 | 0 | } |
658 | | |
659 | | // OpComplexity::Low - GPU threshold: >100K elements |
660 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
661 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 2-800x slower, see docs/performance-analysis.md |
662 | | |
663 | | // Try GPU first for large vectors |
664 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
665 | | { |
666 | 0 | if self.data.len() >= GPU_THRESHOLD { |
667 | | use crate::backends::gpu::GpuDevice; |
668 | 0 | if GpuDevice::is_available() { |
669 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
670 | 0 | let mut result = vec![0.0; self.data.len()]; |
671 | 0 | if gpu.gelu(&self.data, &mut result).is_ok() { |
672 | 0 | return Ok(Vector::from_vec(result)); |
673 | 0 | } |
674 | 0 | } |
675 | 0 | } |
676 | | } |
677 | | |
678 | 0 | let mut result = vec![0.0; self.len()]; |
679 | | |
680 | | // Dispatch to appropriate backend |
681 | 0 | dispatch_unary_op!(self.backend, gelu, &self.data, &mut result); |
682 | | |
683 | 0 | Ok(Vector::from_vec(result)) |
684 | 0 | } |
685 | | |
686 | | /// Swish activation function (also known as SiLU - Sigmoid Linear Unit) |
687 | | /// |
688 | | /// Applies the Swish activation element-wise: swish(x) = x * sigmoid(x) = x / (1 + e^(-x)). |
689 | | /// |
690 | | /// Swish is a smooth, non-monotonic activation function that consistently matches or |
691 | | /// outperforms ReLU in deep networks. It's used in EfficientNet, MobileNet v3, and |
692 | | /// many modern architectures. The function is self-gated: it adaptively gates the |
693 | | /// input based on its value. |
694 | | /// |
695 | | /// Properties: |
696 | | /// - Smooth and differentiable everywhere |
697 | | /// - Non-monotonic: has a slight "dip" for negative values |
698 | | /// - swish(0) = 0 |
699 | | /// - swish(x) ≈ x for large positive x (linear) |
700 | | /// - swish(x) ≈ 0 for large negative x |
701 | | /// - Unbounded above, bounded below by ≈ -0.278 at x ≈ -1.278 |
702 | | /// |
703 | | /// # Performance |
704 | | /// |
705 | | /// Compute-bound operation requiring exponential and division. |
706 | | /// Future SIMD optimizations planned for Phase 9 (GPU backend). |
707 | | /// |
708 | | /// # Examples |
709 | | /// |
710 | | /// ``` |
711 | | /// use trueno::Vector; |
712 | | /// |
713 | | /// let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
714 | | /// let result = v.swish()?; |
715 | | /// |
716 | | /// // Swish is smooth and self-gated |
717 | | /// assert!(result.as_slice()[0] < 0.0); // Negative inputs → small negative outputs |
718 | | /// assert_eq!(result.as_slice()[2], 0.0); // swish(0) = 0 |
719 | | /// assert!(result.as_slice()[4] > 1.5); // Large positive → ~linear |
720 | | /// # Ok::<(), trueno::TruenoError>(()) |
721 | | /// ``` |
722 | | /// |
723 | | /// # Errors |
724 | | /// |
725 | | /// Returns `EmptyVector` if the input vector is empty. |
726 | | /// |
727 | | /// # References |
728 | | /// |
729 | | /// - Ramachandran et al. (2017): "Searching for Activation Functions" |
730 | | /// - Also known as SiLU (Sigmoid Linear Unit): Elfwing et al. (2018) |
731 | 0 | pub fn swish(&self) -> Result<Self> { |
732 | 0 | if self.data.is_empty() { |
733 | 0 | return Err(TruenoError::EmptyVector); |
734 | 0 | } |
735 | | |
736 | | // OpComplexity::Low - GPU threshold: >100K elements |
737 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
738 | | const GPU_THRESHOLD: usize = usize::MAX; // GPU DISABLED - 2-800x slower, see docs/performance-analysis.md |
739 | | |
740 | | // Try GPU first for large vectors |
741 | | #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))] |
742 | | { |
743 | 0 | if self.data.len() >= GPU_THRESHOLD { |
744 | | use crate::backends::gpu::GpuDevice; |
745 | 0 | if GpuDevice::is_available() { |
746 | 0 | let gpu = GpuDevice::new().map_err(TruenoError::InvalidInput)?; |
747 | 0 | let mut result = vec![0.0; self.data.len()]; |
748 | 0 | if gpu.swish(&self.data, &mut result).is_ok() { |
749 | 0 | return Ok(Vector::from_vec(result)); |
750 | 0 | } |
751 | 0 | } |
752 | 0 | } |
753 | | } |
754 | | |
755 | 0 | let mut result = vec![0.0; self.len()]; |
756 | | |
757 | | // Dispatch to appropriate SIMD backend |
758 | 0 | dispatch_unary_op!(self.backend, swish, &self.data, &mut result); |
759 | | |
760 | 0 | Ok(Vector::from_vec(result)) |
761 | 0 | } |
762 | | |
763 | | /// Hard Swish activation function |
764 | | /// |
765 | | /// Applies the hardswish activation element-wise: hardswish(x) = x * relu6(x + 3) / 6 |
766 | | /// |
767 | | /// Hardswish is a piece-wise linear approximation to swish, designed for efficient |
768 | | /// computation in mobile neural networks. It's used in MobileNetV3 and avoids the |
769 | | /// expensive sigmoid computation of standard swish. |
770 | | /// |
771 | | /// Properties: |
772 | | /// - Piece-wise linear: efficient to compute |
773 | | /// - hardswish(x) = 0 for x ≤ -3 |
774 | | /// - hardswish(x) = x for x ≥ 3 |
775 | | /// - hardswish(x) = x * (x + 3) / 6 for -3 < x < 3 |
776 | | /// - hardswish(0) = 0 |
777 | | /// - Smooth transitions at boundaries |
778 | | /// |
779 | | /// # Performance |
780 | | /// |
781 | | /// More efficient than swish as it uses only multiply/divide operations |
782 | | /// instead of expensive exponential functions. Ideal for inference on |
783 | | /// resource-constrained devices. |
784 | | /// |
785 | | /// # Examples |
786 | | /// |
787 | | /// ``` |
788 | | /// use trueno::Vector; |
789 | | /// |
790 | | /// let v = Vector::from_slice(&[-4.0, -3.0, 0.0, 3.0, 4.0]); |
791 | | /// let result = v.hardswish()?; |
792 | | /// |
793 | | /// // Piece-wise linear behavior |
794 | | /// assert_eq!(result.as_slice()[0], 0.0); // x ≤ -3 → 0 |
795 | | /// assert_eq!(result.as_slice()[1], 0.0); // x = -3 → 0 |
796 | | /// assert_eq!(result.as_slice()[2], 0.0); // x = 0 → 0 |
797 | | /// assert_eq!(result.as_slice()[3], 3.0); // x = 3 → x |
798 | | /// assert_eq!(result.as_slice()[4], 4.0); // x ≥ 3 → x |
799 | | /// # Ok::<(), trueno::TruenoError>(()) |
800 | | /// ``` |
801 | | /// |
802 | | /// # Errors |
803 | | /// |
804 | | /// Returns `EmptyVector` if the input vector is empty. |
805 | | /// |
806 | | /// # References |
807 | | /// |
808 | | /// - Howard et al. (2019): "Searching for MobileNetV3" |
809 | 0 | pub fn hardswish(&self) -> Result<Self> { |
810 | 0 | if self.data.is_empty() { |
811 | 0 | return Err(TruenoError::EmptyVector); |
812 | 0 | } |
813 | | |
814 | | // Scalar implementation: hardswish(x) = x * relu6(x + 3) / 6 |
815 | | // Simplified piece-wise: |
816 | | // - x <= -3: 0 |
817 | | // - x >= 3: x |
818 | | // - else: x * (x + 3) / 6 |
819 | 0 | let data: Vec<f32> = self |
820 | 0 | .data |
821 | 0 | .iter() |
822 | 0 | .map(|&x| { |
823 | 0 | if x <= -3.0 { |
824 | 0 | 0.0 |
825 | 0 | } else if x >= 3.0 { |
826 | 0 | x |
827 | | } else { |
828 | 0 | x * (x + 3.0) / 6.0 |
829 | | } |
830 | 0 | }) |
831 | 0 | .collect(); |
832 | | |
833 | 0 | Ok(Vector::from_vec(data)) |
834 | 0 | } |
835 | | |
836 | | /// Mish activation function |
837 | | /// |
838 | | /// Applies the mish activation element-wise: mish(x) = x * tanh(softplus(x)) = x * tanh(ln(1 + e^x)) |
839 | | /// |
840 | | /// Mish is a self-regularizing non-monotonic activation function that often outperforms |
841 | | /// ReLU and swish in computer vision tasks. It's used in YOLOv4 and many modern architectures. |
842 | | /// |
843 | | /// Properties: |
844 | | /// - Smooth and non-monotonic (similar to swish) |
845 | | /// - Self-regularizing: prevents dying neurons |
846 | | /// - mish(0) ≈ 0 (small positive value) |
847 | | /// - mish(x) ≈ x for large positive x (nearly linear) |
848 | | /// - mish(x) ≈ 0 for large negative x |
849 | | /// - Bounded below by ≈ -0.31 at x ≈ -1.19 |
850 | | /// |
851 | | /// # Performance |
852 | | /// |
853 | | /// Compute-bound operation requiring exponential, logarithm, and tanh. |
854 | | /// More expensive than ReLU/swish but often provides better accuracy. |
855 | | /// |
856 | | /// # Examples |
857 | | /// |
858 | | /// ``` |
859 | | /// use trueno::Vector; |
860 | | /// |
861 | | /// let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
862 | | /// let result = v.mish()?; |
863 | | /// |
864 | | /// // Mish is smooth and self-gated |
865 | | /// assert!(result.as_slice()[0] < 0.0); // Small negative output for negative inputs |
866 | | /// assert!(result.as_slice()[2].abs() < 1e-5); // mish(0) = 0 |
867 | | /// assert!(result.as_slice()[4] > 1.5); // Large positive → near linear |
868 | | /// # Ok::<(), trueno::TruenoError>(()) |
869 | | /// ``` |
870 | | /// |
871 | | /// # Errors |
872 | | /// |
873 | | /// Returns `EmptyVector` if the input vector is empty. |
874 | | /// |
875 | | /// # References |
876 | | /// |
877 | | /// - Misra (2019): "Mish: A Self Regularized Non-Monotonic Neural Activation Function" |
878 | 0 | pub fn mish(&self) -> Result<Self> { |
879 | 0 | if self.data.is_empty() { |
880 | 0 | return Err(TruenoError::EmptyVector); |
881 | 0 | } |
882 | | |
883 | | // Scalar implementation: mish(x) = x * tanh(softplus(x)) = x * tanh(ln(1 + e^x)) |
884 | 0 | let data: Vec<f32> = self |
885 | 0 | .data |
886 | 0 | .iter() |
887 | 0 | .map(|&x| { |
888 | | // Handle extreme values for numerical stability |
889 | 0 | if x < -20.0 { |
890 | | // For very negative x: softplus ≈ 0, tanh(0) ≈ 0, so mish ≈ 0 |
891 | 0 | 0.0 |
892 | 0 | } else if x > 20.0 { |
893 | | // For very positive x: softplus ≈ x, tanh(x) ≈ 1, so mish ≈ x |
894 | 0 | x |
895 | | } else { |
896 | | // Normal case: x * tanh(ln(1 + e^x)) |
897 | 0 | let softplus = (1.0 + x.exp()).ln(); |
898 | 0 | x * softplus.tanh() |
899 | | } |
900 | 0 | }) |
901 | 0 | .collect(); |
902 | | |
903 | 0 | Ok(Vector::from_vec(data)) |
904 | 0 | } |
905 | | |
906 | | /// SELU (Scaled Exponential Linear Unit) activation function |
907 | | /// |
908 | | /// Computes selu(x) = λ * (x if x > 0 else α * (exp(x) - 1)) |
909 | | /// where λ ≈ 1.0507 and α ≈ 1.6733 |
910 | | /// |
911 | | /// # Properties |
912 | | /// |
913 | | /// - **Self-normalizing**: Activations converge to zero mean and unit variance |
914 | | /// - **Vanishing gradient prevention**: Non-zero gradient for negative inputs |
915 | | /// - **Automatic normalization**: Reduces need for batch normalization |
916 | | /// |
917 | | /// # Performance |
918 | | /// |
919 | | /// Uses scalar implementation (GPU disabled for element-wise ops). |
920 | | /// |
921 | | /// # Examples |
922 | | /// |
923 | | /// ``` |
924 | | /// use trueno::Vector; |
925 | | /// |
926 | | /// let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
927 | | /// let result = v.selu()?; |
928 | | /// |
929 | | /// // Positive values scaled by λ ≈ 1.0507 |
930 | | /// assert!((result.as_slice()[3] - 1.0507).abs() < 0.001); |
931 | | /// assert!((result.as_slice()[4] - 2.1014).abs() < 0.001); |
932 | | /// |
933 | | /// // Zero stays zero |
934 | | /// assert!(result.as_slice()[2].abs() < 1e-5); |
935 | | /// |
936 | | /// // Negative values use ELU-like formula |
937 | | /// assert!(result.as_slice()[0] < 0.0); |
938 | | /// # Ok::<(), trueno::TruenoError>(()) |
939 | | /// ``` |
940 | | /// |
941 | | /// # Errors |
942 | | /// |
943 | | /// Returns `EmptyVector` if the input vector is empty. |
944 | | /// |
945 | | /// # References |
946 | | /// |
947 | | /// - Klambauer et al. (2017): "Self-Normalizing Neural Networks" |
948 | 0 | pub fn selu(&self) -> Result<Self> { |
949 | 0 | if self.data.is_empty() { |
950 | 0 | return Err(TruenoError::EmptyVector); |
951 | 0 | } |
952 | | |
953 | | // SELU constants from Klambauer et al. (2017) |
954 | | // These specific values ensure self-normalizing property |
955 | | const LAMBDA: f32 = 1.0507009873554804934193349852946; |
956 | | const ALPHA: f32 = 1.6732632423543772848170429916717; |
957 | | |
958 | | // Scalar implementation: selu(x) = λ * (x if x > 0 else α * (exp(x) - 1)) |
959 | 0 | let data: Vec<f32> = self |
960 | 0 | .data |
961 | 0 | .iter() |
962 | 0 | .map(|&x| { |
963 | 0 | if x > 0.0 { |
964 | 0 | LAMBDA * x |
965 | | } else { |
966 | 0 | LAMBDA * ALPHA * (x.exp() - 1.0) |
967 | | } |
968 | 0 | }) |
969 | 0 | .collect(); |
970 | | |
971 | 0 | Ok(Vector::from_vec(data)) |
972 | 0 | } |
973 | | } |
974 | | |
975 | | #[cfg(test)] |
976 | | mod tests { |
977 | | use super::*; |
978 | | |
979 | | // ========== Softmax ========== |
980 | | |
981 | | #[test] |
982 | | fn test_softmax_basic() { |
983 | | let v = Vector::from_slice(&[1.0, 2.0, 3.0]); |
984 | | let result = v.softmax().unwrap(); |
985 | | // Check sum = 1 |
986 | | let sum: f32 = result.as_slice().iter().sum(); |
987 | | assert!((sum - 1.0).abs() < 1e-5); |
988 | | // Check all values in [0, 1] |
989 | | for &val in result.as_slice() { |
990 | | assert!(val >= 0.0 && val <= 1.0); |
991 | | } |
992 | | // Check highest input has highest probability |
993 | | assert!(result.as_slice()[2] > result.as_slice()[1]); |
994 | | assert!(result.as_slice()[1] > result.as_slice()[0]); |
995 | | } |
996 | | |
997 | | #[test] |
998 | | fn test_softmax_empty() { |
999 | | let v = Vector::<f32>::from_slice(&[]); |
1000 | | assert!(matches!(v.softmax(), Err(TruenoError::EmptyVector))); |
1001 | | } |
1002 | | |
1003 | | #[test] |
1004 | | fn test_softmax_single() { |
1005 | | let v = Vector::from_slice(&[5.0]); |
1006 | | let result = v.softmax().unwrap(); |
1007 | | assert!((result.as_slice()[0] - 1.0).abs() < 1e-6); |
1008 | | } |
1009 | | |
1010 | | #[test] |
1011 | | fn test_softmax_uniform() { |
1012 | | let v = Vector::from_slice(&[1.0, 1.0, 1.0, 1.0]); |
1013 | | let result = v.softmax().unwrap(); |
1014 | | // All equal inputs should give equal outputs |
1015 | | for &val in result.as_slice() { |
1016 | | assert!((val - 0.25).abs() < 1e-6); |
1017 | | } |
1018 | | } |
1019 | | |
1020 | | #[test] |
1021 | | fn test_softmax_large_values() { |
1022 | | // Test numerical stability with large values |
1023 | | let v = Vector::from_slice(&[1000.0, 1001.0, 1002.0]); |
1024 | | let result = v.softmax().unwrap(); |
1025 | | let sum: f32 = result.as_slice().iter().sum(); |
1026 | | assert!((sum - 1.0).abs() < 1e-5); |
1027 | | } |
1028 | | |
1029 | | // ========== Log Softmax ========== |
1030 | | |
1031 | | #[test] |
1032 | | fn test_log_softmax_basic() { |
1033 | | let v = Vector::from_slice(&[1.0, 2.0, 3.0]); |
1034 | | let result = v.log_softmax().unwrap(); |
1035 | | // All log probabilities should be <= 0 |
1036 | | for &val in result.as_slice() { |
1037 | | assert!(val <= 0.0); |
1038 | | } |
1039 | | } |
1040 | | |
1041 | | #[test] |
1042 | | fn test_log_softmax_empty() { |
1043 | | let v = Vector::<f32>::from_slice(&[]); |
1044 | | assert!(matches!(v.log_softmax(), Err(TruenoError::EmptyVector))); |
1045 | | } |
1046 | | |
1047 | | #[test] |
1048 | | fn test_log_softmax_single() { |
1049 | | let v = Vector::from_slice(&[5.0]); |
1050 | | let result = v.log_softmax().unwrap(); |
1051 | | // log(1) = 0 |
1052 | | assert!((result.as_slice()[0] - 0.0).abs() < 1e-6); |
1053 | | } |
1054 | | |
1055 | | // ========== ReLU ========== |
1056 | | |
1057 | | #[test] |
1058 | | fn test_relu_basic() { |
1059 | | let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
1060 | | let result = v.relu().unwrap(); |
1061 | | assert!((result.as_slice()[0] - 0.0).abs() < 1e-6); |
1062 | | assert!((result.as_slice()[1] - 0.0).abs() < 1e-6); |
1063 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-6); |
1064 | | assert!((result.as_slice()[3] - 1.0).abs() < 1e-6); |
1065 | | assert!((result.as_slice()[4] - 2.0).abs() < 1e-6); |
1066 | | } |
1067 | | |
1068 | | #[test] |
1069 | | fn test_relu_empty() { |
1070 | | let v = Vector::<f32>::from_slice(&[]); |
1071 | | assert!(matches!(v.relu(), Err(TruenoError::EmptyVector))); |
1072 | | } |
1073 | | |
1074 | | #[test] |
1075 | | fn test_relu_all_negative() { |
1076 | | let v = Vector::from_slice(&[-5.0, -3.0, -1.0]); |
1077 | | let result = v.relu().unwrap(); |
1078 | | for &val in result.as_slice() { |
1079 | | assert!((val - 0.0).abs() < 1e-6); |
1080 | | } |
1081 | | } |
1082 | | |
1083 | | #[test] |
1084 | | fn test_relu_all_positive() { |
1085 | | let v = Vector::from_slice(&[1.0, 2.0, 3.0]); |
1086 | | let result = v.relu().unwrap(); |
1087 | | assert!((result.as_slice()[0] - 1.0).abs() < 1e-6); |
1088 | | assert!((result.as_slice()[1] - 2.0).abs() < 1e-6); |
1089 | | assert!((result.as_slice()[2] - 3.0).abs() < 1e-6); |
1090 | | } |
1091 | | |
1092 | | // ========== Sigmoid ========== |
1093 | | |
1094 | | #[test] |
1095 | | fn test_sigmoid_basic() { |
1096 | | let v = Vector::from_slice(&[-10.0, 0.0, 10.0]); |
1097 | | let result = v.sigmoid().unwrap(); |
1098 | | // sigmoid(-10) ≈ 0 |
1099 | | assert!(result.as_slice()[0] < 0.001); |
1100 | | // sigmoid(0) = 0.5 |
1101 | | assert!((result.as_slice()[1] - 0.5).abs() < 1e-6); |
1102 | | // sigmoid(10) ≈ 1 |
1103 | | assert!(result.as_slice()[2] > 0.999); |
1104 | | } |
1105 | | |
1106 | | #[test] |
1107 | | fn test_sigmoid_empty() { |
1108 | | let v = Vector::<f32>::from_slice(&[]); |
1109 | | assert!(matches!(v.sigmoid(), Err(TruenoError::EmptyVector))); |
1110 | | } |
1111 | | |
1112 | | #[test] |
1113 | | fn test_sigmoid_range() { |
1114 | | let v = Vector::from_slice(&[-100.0, -1.0, 0.0, 1.0, 100.0]); |
1115 | | let result = v.sigmoid().unwrap(); |
1116 | | for &val in result.as_slice() { |
1117 | | assert!(val >= 0.0 && val <= 1.0); |
1118 | | } |
1119 | | } |
1120 | | |
1121 | | // ========== Leaky ReLU ========== |
1122 | | |
1123 | | #[test] |
1124 | | fn test_leaky_relu_basic() { |
1125 | | let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
1126 | | let result = v.leaky_relu(0.01).unwrap(); |
1127 | | assert!((result.as_slice()[0] - (-0.02)).abs() < 1e-6); |
1128 | | assert!((result.as_slice()[1] - (-0.01)).abs() < 1e-6); |
1129 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-6); |
1130 | | assert!((result.as_slice()[3] - 1.0).abs() < 1e-6); |
1131 | | assert!((result.as_slice()[4] - 2.0).abs() < 1e-6); |
1132 | | } |
1133 | | |
1134 | | #[test] |
1135 | | fn test_leaky_relu_empty() { |
1136 | | let v = Vector::<f32>::from_slice(&[]); |
1137 | | assert!(matches!(v.leaky_relu(0.01), Err(TruenoError::EmptyVector))); |
1138 | | } |
1139 | | |
1140 | | #[test] |
1141 | | fn test_leaky_relu_different_slopes() { |
1142 | | let v = Vector::from_slice(&[-1.0]); |
1143 | | // slope 0.1 |
1144 | | let result = v.leaky_relu(0.1).unwrap(); |
1145 | | assert!((result.as_slice()[0] - (-0.1)).abs() < 1e-6); |
1146 | | // slope 0.2 |
1147 | | let result = v.leaky_relu(0.2).unwrap(); |
1148 | | assert!((result.as_slice()[0] - (-0.2)).abs() < 1e-6); |
1149 | | } |
1150 | | |
1151 | | // ========== ELU ========== |
1152 | | |
1153 | | #[test] |
1154 | | fn test_elu_basic() { |
1155 | | let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
1156 | | let result = v.elu(1.0).unwrap(); |
1157 | | // Positive values unchanged |
1158 | | assert!((result.as_slice()[3] - 1.0).abs() < 1e-6); |
1159 | | assert!((result.as_slice()[4] - 2.0).abs() < 1e-6); |
1160 | | // Zero stays zero |
1161 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-6); |
1162 | | // Negative values: alpha * (exp(x) - 1) |
1163 | | assert!(result.as_slice()[0] < 0.0); |
1164 | | assert!(result.as_slice()[1] < 0.0); |
1165 | | } |
1166 | | |
1167 | | #[test] |
1168 | | fn test_elu_empty() { |
1169 | | let v = Vector::<f32>::from_slice(&[]); |
1170 | | assert!(matches!(v.elu(1.0), Err(TruenoError::EmptyVector))); |
1171 | | } |
1172 | | |
1173 | | // ========== GELU ========== |
1174 | | |
1175 | | #[test] |
1176 | | fn test_gelu_basic() { |
1177 | | let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
1178 | | let result = v.gelu().unwrap(); |
1179 | | // GELU(0) = 0 |
1180 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-5); |
1181 | | // GELU is approximately linear for positive values |
1182 | | assert!(result.as_slice()[3] > 0.5); |
1183 | | assert!(result.as_slice()[4] > 1.5); |
1184 | | // Negative values are small but not zero |
1185 | | assert!(result.as_slice()[0].abs() < 0.1); |
1186 | | } |
1187 | | |
1188 | | #[test] |
1189 | | fn test_gelu_empty() { |
1190 | | let v = Vector::<f32>::from_slice(&[]); |
1191 | | assert!(matches!(v.gelu(), Err(TruenoError::EmptyVector))); |
1192 | | } |
1193 | | |
1194 | | // ========== Swish ========== |
1195 | | |
1196 | | #[test] |
1197 | | fn test_swish_basic() { |
1198 | | let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
1199 | | let result = v.swish().unwrap(); |
1200 | | // Swish(0) = 0 |
1201 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-6); |
1202 | | // Swish(x) = x * sigmoid(x) |
1203 | | // Swish(1) ≈ 0.731 |
1204 | | assert!((result.as_slice()[3] - 0.731).abs() < 0.01); |
1205 | | } |
1206 | | |
1207 | | #[test] |
1208 | | fn test_swish_empty() { |
1209 | | let v = Vector::<f32>::from_slice(&[]); |
1210 | | assert!(matches!(v.swish(), Err(TruenoError::EmptyVector))); |
1211 | | } |
1212 | | |
1213 | | // ========== Hardswish ========== |
1214 | | |
1215 | | #[test] |
1216 | | fn test_hardswish_basic() { |
1217 | | let v = Vector::from_slice(&[-4.0, -3.0, 0.0, 3.0, 4.0]); |
1218 | | let result = v.hardswish().unwrap(); |
1219 | | // x <= -3: hardswish(x) = 0 |
1220 | | assert!((result.as_slice()[0] - 0.0).abs() < 1e-6); |
1221 | | assert!((result.as_slice()[1] - 0.0).abs() < 1e-6); |
1222 | | // x >= 3: hardswish(x) = x |
1223 | | assert!((result.as_slice()[3] - 3.0).abs() < 1e-6); |
1224 | | assert!((result.as_slice()[4] - 4.0).abs() < 1e-6); |
1225 | | // x = 0: hardswish(0) = 0 |
1226 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-6); |
1227 | | } |
1228 | | |
1229 | | #[test] |
1230 | | fn test_hardswish_empty() { |
1231 | | let v = Vector::<f32>::from_slice(&[]); |
1232 | | assert!(matches!(v.hardswish(), Err(TruenoError::EmptyVector))); |
1233 | | } |
1234 | | |
1235 | | // ========== Mish ========== |
1236 | | |
1237 | | #[test] |
1238 | | fn test_mish_basic() { |
1239 | | let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
1240 | | let result = v.mish().unwrap(); |
1241 | | // Mish(0) = 0 |
1242 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-6); |
1243 | | // Mish is smooth and non-monotonic for negative values |
1244 | | assert!(result.as_slice()[0] < 0.0); |
1245 | | } |
1246 | | |
1247 | | #[test] |
1248 | | fn test_mish_empty() { |
1249 | | let v = Vector::<f32>::from_slice(&[]); |
1250 | | assert!(matches!(v.mish(), Err(TruenoError::EmptyVector))); |
1251 | | } |
1252 | | |
1253 | | // ========== SELU ========== |
1254 | | |
1255 | | #[test] |
1256 | | fn test_selu_basic() { |
1257 | | let v = Vector::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0]); |
1258 | | let result = v.selu().unwrap(); |
1259 | | // SELU(0) = 0 |
1260 | | assert!((result.as_slice()[2] - 0.0).abs() < 1e-5); |
1261 | | // Positive values scaled by λ ≈ 1.0507 |
1262 | | assert!((result.as_slice()[3] - 1.0507).abs() < 0.001); |
1263 | | assert!((result.as_slice()[4] - 2.1014).abs() < 0.001); |
1264 | | // Negative values use ELU-like formula |
1265 | | assert!(result.as_slice()[0] < 0.0); |
1266 | | assert!(result.as_slice()[1] < 0.0); |
1267 | | } |
1268 | | |
1269 | | #[test] |
1270 | | fn test_selu_empty() { |
1271 | | let v = Vector::<f32>::from_slice(&[]); |
1272 | | assert!(matches!(v.selu(), Err(TruenoError::EmptyVector))); |
1273 | | } |
1274 | | |
1275 | | // ========== Backend Tests ========== |
1276 | | |
1277 | | #[test] |
1278 | | fn test_relu_scalar_backend() { |
1279 | | let v = Vector::from_slice_with_backend(&[-1.0, 0.0, 1.0], Backend::Scalar); |
1280 | | let result = v.relu().unwrap(); |
1281 | | assert!((result.as_slice()[0] - 0.0).abs() < 1e-6); |
1282 | | assert!((result.as_slice()[2] - 1.0).abs() < 1e-6); |
1283 | | } |
1284 | | |
1285 | | #[test] |
1286 | | #[cfg(target_arch = "x86_64")] |
1287 | | fn test_relu_sse2_backend() { |
1288 | | let v = Vector::from_slice_with_backend(&[-1.0, 0.0, 1.0, 2.0], Backend::SSE2); |
1289 | | let result = v.relu().unwrap(); |
1290 | | assert!((result.as_slice()[0] - 0.0).abs() < 1e-6); |
1291 | | assert!((result.as_slice()[2] - 1.0).abs() < 1e-6); |
1292 | | } |
1293 | | |
1294 | | #[test] |
1295 | | fn test_sigmoid_scalar_backend() { |
1296 | | let v = Vector::from_slice_with_backend(&[0.0], Backend::Scalar); |
1297 | | let result = v.sigmoid().unwrap(); |
1298 | | assert!((result.as_slice()[0] - 0.5).abs() < 1e-6); |
1299 | | } |
1300 | | |
1301 | | // ========== Large Array Tests ========== |
1302 | | |
1303 | | #[test] |
1304 | | fn test_relu_large() { |
1305 | | let v = Vector::from_slice(&[-1.0; 1000]); |
1306 | | let result = v.relu().unwrap(); |
1307 | | for &val in result.as_slice() { |
1308 | | assert!((val - 0.0).abs() < 1e-6); |
1309 | | } |
1310 | | } |
1311 | | |
1312 | | #[test] |
1313 | | fn test_sigmoid_large() { |
1314 | | let v = Vector::from_slice(&[0.0; 1000]); |
1315 | | let result = v.sigmoid().unwrap(); |
1316 | | for &val in result.as_slice() { |
1317 | | assert!((val - 0.5).abs() < 1e-6); |
1318 | | } |
1319 | | } |
1320 | | |
1321 | | #[test] |
1322 | | fn test_softmax_large() { |
1323 | | let v = Vector::from_slice(&[1.0; 100]); |
1324 | | let result = v.softmax().unwrap(); |
1325 | | let sum: f32 = result.as_slice().iter().sum(); |
1326 | | assert!((sum - 1.0).abs() < 1e-4); |
1327 | | // All equal inputs should give equal probabilities |
1328 | | for &val in result.as_slice() { |
1329 | | assert!((val - 0.01).abs() < 1e-4); |
1330 | | } |
1331 | | } |
1332 | | } |