1use std::f64::consts::PI;
14
15#[derive(Debug, Clone, PartialEq)]
21pub enum ActivationType {
22 ReLU,
24 LeakyReLU(f64),
26 ELU(f64),
28 Sigmoid,
30 Tanh,
32 Softmax,
34 GELU,
36 Swish,
38 Mish,
40 HardSwish,
42 Linear,
44 Threshold(f64, f64),
46}
47
48#[derive(Debug, Clone)]
54pub struct ActivationConfig {
55 pub activation_type: ActivationType,
57 pub inplace: bool,
60}
61
62impl ActivationConfig {
63 pub fn new(activation_type: ActivationType) -> Self {
65 Self {
66 activation_type,
67 inplace: false,
68 }
69 }
70
71 pub fn with_inplace(mut self) -> Self {
73 self.inplace = true;
74 self
75 }
76}
77
78#[derive(Debug, Clone, Default)]
84pub struct ActivationStats {
85 pub total_calls: u64,
87 pub total_elements: u64,
89 pub dead_relu_count: u64,
91}
92
93pub struct ActivationFunction {
100 config: ActivationConfig,
101 stats: ActivationStats,
102}
103
104impl ActivationFunction {
105 pub fn new(config: ActivationConfig) -> Self {
107 Self {
108 config,
109 stats: ActivationStats::default(),
110 }
111 }
112
113 pub fn forward(&mut self, input: &[f64]) -> Vec<f64> {
122 self.stats.total_calls += 1;
123 self.stats.total_elements += input.len() as u64;
124
125 let result = match &self.config.activation_type {
126 ActivationType::ReLU => {
127 let out: Vec<f64> = input.iter().map(|&x| Self::relu(x)).collect();
128 let dead = out.iter().filter(|&&v| v == 0.0).count() as u64;
130 self.stats.dead_relu_count += dead;
131 out
132 }
133 ActivationType::LeakyReLU(slope) => {
134 let s = *slope;
135 input.iter().map(|&x| Self::leaky_relu(x, s)).collect()
136 }
137 ActivationType::ELU(alpha) => {
138 let a = *alpha;
139 input.iter().map(|&x| Self::elu(x, a)).collect()
140 }
141 ActivationType::Sigmoid => input.iter().map(|&x| Self::sigmoid(x)).collect(),
142 ActivationType::Tanh => input.iter().map(|&x| Self::tanh_activation(x)).collect(),
143 ActivationType::Softmax => Self::softmax(input),
144 ActivationType::GELU => input.iter().map(|&x| Self::gelu(x)).collect(),
145 ActivationType::Swish => input.iter().map(|&x| Self::swish(x)).collect(),
146 ActivationType::Mish => input.iter().map(|&x| Self::mish(x)).collect(),
147 ActivationType::HardSwish => input.iter().map(|&x| Self::hard_swish(x)).collect(),
148 ActivationType::Linear => input.to_vec(),
149 ActivationType::Threshold(threshold, value) => {
150 let (t, v) = (*threshold, *value);
151 input.iter().map(|&x| if x > t { v } else { 0.0 }).collect()
152 }
153 };
154
155 result
156 }
157
158 pub fn derivative(&self, output: &[f64]) -> Vec<f64> {
173 match &self.config.activation_type {
174 ActivationType::ReLU => output
175 .iter()
176 .map(|&x| if x > 0.0 { 1.0 } else { 0.0 })
177 .collect(),
178
179 ActivationType::LeakyReLU(slope) => {
180 let s = *slope;
181 output
182 .iter()
183 .map(|&x| if x >= 0.0 { 1.0 } else { s })
184 .collect()
185 }
186
187 ActivationType::ELU(alpha) => {
188 let a = *alpha;
189 output
190 .iter()
191 .map(|&x| {
192 if x >= 0.0 {
193 1.0
194 } else {
195 a * x.exp()
197 }
198 })
199 .collect()
200 }
201
202 ActivationType::Sigmoid => output.iter().map(|&s| s * (1.0 - s)).collect(),
204
205 ActivationType::Tanh => output.iter().map(|&t| 1.0 - t * t).collect(),
207
208 ActivationType::Softmax => output.iter().map(|&s| s * (1.0 - s)).collect(),
212
213 ActivationType::GELU => output.iter().map(|&x| Self::gelu_derivative(x)).collect(),
214
215 ActivationType::Swish => output.iter().map(|&x| Self::swish_derivative(x)).collect(),
216
217 ActivationType::Mish => output.iter().map(|&x| Self::mish_derivative(x)).collect(),
218
219 ActivationType::HardSwish => output
220 .iter()
221 .map(|&x| Self::hard_swish_derivative(x))
222 .collect(),
223
224 ActivationType::Linear => vec![1.0; output.len()],
225
226 ActivationType::Threshold(threshold, _value) => {
227 let t = *threshold;
228 output
229 .iter()
230 .map(|&x| {
231 if (x - t).abs() < f64::EPSILON {
235 f64::INFINITY
236 } else {
237 0.0
238 }
239 })
240 .collect()
241 }
242 }
243 }
244
245 pub fn apply_inplace(&mut self, data: &mut [f64]) {
249 self.stats.total_calls += 1;
250 self.stats.total_elements += data.len() as u64;
251
252 match &self.config.activation_type.clone() {
253 ActivationType::ReLU => {
254 let mut dead: u64 = 0;
255 for x in data.iter_mut() {
256 if *x <= 0.0 {
257 *x = 0.0;
258 dead += 1;
259 }
260 }
261 self.stats.dead_relu_count += dead;
262 }
263 ActivationType::LeakyReLU(slope) => {
264 let s = *slope;
265 for x in data.iter_mut() {
266 if *x < 0.0 {
267 *x *= s;
268 }
269 }
270 }
271 ActivationType::ELU(alpha) => {
272 let a = *alpha;
273 for x in data.iter_mut() {
274 if *x < 0.0 {
275 *x = a * (x.exp() - 1.0);
276 }
277 }
278 }
279 ActivationType::Sigmoid => {
280 for x in data.iter_mut() {
281 *x = Self::sigmoid(*x);
282 }
283 }
284 ActivationType::Tanh => {
285 for x in data.iter_mut() {
286 *x = Self::tanh_activation(*x);
287 }
288 }
289 ActivationType::Softmax => {
290 let result = Self::softmax(data);
291 data.copy_from_slice(&result);
292 }
293 ActivationType::GELU => {
294 for x in data.iter_mut() {
295 *x = Self::gelu(*x);
296 }
297 }
298 ActivationType::Swish => {
299 for x in data.iter_mut() {
300 *x = Self::swish(*x);
301 }
302 }
303 ActivationType::Mish => {
304 for x in data.iter_mut() {
305 *x = Self::mish(*x);
306 }
307 }
308 ActivationType::HardSwish => {
309 for x in data.iter_mut() {
310 *x = Self::hard_swish(*x);
311 }
312 }
313 ActivationType::Linear => { }
314 ActivationType::Threshold(threshold, value) => {
315 let (t, v) = (*threshold, *value);
316 for x in data.iter_mut() {
317 *x = if *x > t { v } else { 0.0 };
318 }
319 }
320 }
321 }
322
323 pub fn stats(&self) -> &ActivationStats {
329 &self.stats
330 }
331
332 pub fn config(&self) -> &ActivationConfig {
334 &self.config
335 }
336
337 #[inline]
343 pub fn relu(x: f64) -> f64 {
344 x.max(0.0)
345 }
346
347 #[inline]
349 pub fn leaky_relu(x: f64, slope: f64) -> f64 {
350 if x >= 0.0 {
351 x
352 } else {
353 slope * x
354 }
355 }
356
357 #[inline]
359 pub fn elu(x: f64, alpha: f64) -> f64 {
360 if x >= 0.0 {
361 x
362 } else {
363 alpha * (x.exp() - 1.0)
364 }
365 }
366
367 #[inline]
369 pub fn sigmoid(x: f64) -> f64 {
370 1.0 / (1.0 + (-x).exp())
371 }
372
373 #[inline]
375 pub fn tanh_activation(x: f64) -> f64 {
376 x.tanh()
377 }
378
379 pub fn softmax(input: &[f64]) -> Vec<f64> {
383 if input.is_empty() {
384 return Vec::new();
385 }
386
387 let max_val = input.iter().copied().fold(f64::NEG_INFINITY, f64::max);
388
389 let exps: Vec<f64> = input.iter().map(|&x| (x - max_val).exp()).collect();
390 let sum: f64 = exps.iter().sum();
391
392 if sum == 0.0 {
393 let n = input.len();
395 return vec![1.0 / n as f64; n];
396 }
397
398 exps.iter().map(|&e| e / sum).collect()
399 }
400
401 #[inline]
405 pub fn gelu(x: f64) -> f64 {
406 let c = (2.0_f64 / PI).sqrt();
408 let inner = c * (x + 0.044715 * x * x * x);
409 0.5 * x * (1.0 + inner.tanh())
410 }
411
412 #[inline]
414 pub fn swish(x: f64) -> f64 {
415 x * Self::sigmoid(x)
416 }
417
418 #[inline]
424 pub fn mish(x: f64) -> f64 {
425 let sp = if x > 20.0 { x } else { (1.0 + x.exp()).ln() };
427 x * sp.tanh()
428 }
429
430 #[inline]
432 pub fn hard_swish(x: f64) -> f64 {
433 let relu6 = (x + 3.0).clamp(0.0, 6.0);
434 x * relu6 / 6.0
435 }
436
437 #[inline]
443 fn gelu_derivative(x: f64) -> f64 {
444 let c = (2.0_f64 / PI).sqrt();
445 let inner = c * (x + 0.044715 * x * x * x);
446 let t = inner.tanh();
447 let sech2 = 1.0 - t * t;
450 let d_inner = c * (1.0 + 3.0 * 0.044715 * x * x);
451 0.5 * (1.0 + t) + 0.5 * x * sech2 * d_inner
452 }
453
454 #[inline]
456 fn swish_derivative(x: f64) -> f64 {
457 let s = Self::sigmoid(x);
458 s + x * s * (1.0 - s)
459 }
460
461 #[inline]
466 fn mish_derivative(x: f64) -> f64 {
467 let sp = if x > 20.0 { x } else { (1.0 + x.exp()).ln() };
468 let omega = sp.tanh();
469 let sech2_sp = 1.0 - omega * omega;
470 let sig = Self::sigmoid(x);
471 omega + x * sech2_sp * sig
472 }
473
474 #[inline]
481 fn hard_swish_derivative(x: f64) -> f64 {
482 if x <= -3.0 {
483 0.0
484 } else if x >= 3.0 {
485 1.0
486 } else {
487 (2.0 * x + 3.0) / 6.0
488 }
489 }
490}
491
492#[cfg(test)]
497mod tests {
498 use super::*;
499
500 const EPS: f64 = 1e-9;
501
502 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
507 (a - b).abs() < tol
508 }
509
510 fn make_af(at: ActivationType) -> ActivationFunction {
511 ActivationFunction::new(ActivationConfig::new(at))
512 }
513
514 #[test]
519 fn test_relu_positive() {
520 assert!(approx_eq(ActivationFunction::relu(3.5), 3.5, EPS));
521 }
522
523 #[test]
524 fn test_relu_negative() {
525 assert!(approx_eq(ActivationFunction::relu(-2.0), 0.0, EPS));
526 }
527
528 #[test]
529 fn test_relu_zero() {
530 assert!(approx_eq(ActivationFunction::relu(0.0), 0.0, EPS));
531 }
532
533 #[test]
534 fn test_relu_derivative_positive() {
535 let af = make_af(ActivationType::ReLU);
536 let d = af.derivative(&[1.0, 2.0]);
537 assert!(approx_eq(d[0], 1.0, EPS));
538 assert!(approx_eq(d[1], 1.0, EPS));
539 }
540
541 #[test]
542 fn test_relu_derivative_negative() {
543 let af = make_af(ActivationType::ReLU);
544 let d = af.derivative(&[-1.0, -5.0]);
545 assert!(approx_eq(d[0], 0.0, EPS));
546 assert!(approx_eq(d[1], 0.0, EPS));
547 }
548
549 #[test]
554 fn test_dead_relu_count() {
555 let mut af = make_af(ActivationType::ReLU);
556 let _out = af.forward(&[-1.0, 2.0, -3.0, 4.0]);
557 assert_eq!(af.stats().dead_relu_count, 2);
559 }
560
561 #[test]
562 fn test_dead_relu_accumulates_across_calls() {
563 let mut af = make_af(ActivationType::ReLU);
564 let _ = af.forward(&[-1.0, 1.0]);
565 let _ = af.forward(&[-2.0, -3.0]);
566 assert_eq!(af.stats().dead_relu_count, 3);
567 }
568
569 #[test]
574 fn test_leaky_relu_negative_slope() {
575 let slope = 0.1;
576 let val = ActivationFunction::leaky_relu(-5.0, slope);
577 assert!(approx_eq(val, -0.5, EPS));
578 }
579
580 #[test]
581 fn test_leaky_relu_positive() {
582 assert!(approx_eq(
583 ActivationFunction::leaky_relu(3.0, 0.1),
584 3.0,
585 EPS
586 ));
587 }
588
589 #[test]
590 fn test_leaky_relu_derivative_negative() {
591 let slope = 0.01;
592 let af = make_af(ActivationType::LeakyReLU(slope));
593 let d = af.derivative(&[-2.0]);
594 assert!(approx_eq(d[0], slope, EPS));
595 }
596
597 #[test]
602 fn test_elu_zero() {
603 assert!(approx_eq(ActivationFunction::elu(0.0, 1.0), 0.0, EPS));
605 }
606
607 #[test]
608 fn test_elu_positive() {
609 assert!(approx_eq(ActivationFunction::elu(2.0, 1.0), 2.0, EPS));
610 }
611
612 #[test]
613 fn test_elu_negative() {
614 let alpha = 1.0;
615 let x = -1.0_f64;
616 let expected = alpha * (x.exp() - 1.0);
617 assert!(approx_eq(
618 ActivationFunction::elu(x, alpha),
619 expected,
620 1e-12
621 ));
622 }
623
624 #[test]
625 fn test_elu_continuity() {
626 let left = ActivationFunction::elu(-1e-10, 1.0);
628 let right = ActivationFunction::elu(1e-10, 1.0);
629 assert!(approx_eq(left, right, 1e-8));
630 }
631
632 #[test]
637 fn test_sigmoid_zero() {
638 assert!(approx_eq(ActivationFunction::sigmoid(0.0), 0.5, EPS));
639 }
640
641 #[test]
642 fn test_sigmoid_large_positive() {
643 assert!(ActivationFunction::sigmoid(100.0) > 0.9999);
645 }
646
647 #[test]
648 fn test_sigmoid_large_negative() {
649 assert!(ActivationFunction::sigmoid(-100.0) < 1e-10);
651 }
652
653 #[test]
654 fn test_sigmoid_derivative_from_output() {
655 let af = make_af(ActivationType::Sigmoid);
658 let d = af.derivative(&[0.5]);
659 assert!(approx_eq(d[0], 0.25, EPS));
660 }
661
662 #[test]
667 fn test_tanh_zero() {
668 assert!(approx_eq(
669 ActivationFunction::tanh_activation(0.0),
670 0.0,
671 EPS
672 ));
673 }
674
675 #[test]
676 fn test_tanh_derivative_from_output() {
677 let af = make_af(ActivationType::Tanh);
679 let d = af.derivative(&[0.0]);
680 assert!(approx_eq(d[0], 1.0, EPS));
681 }
682
683 #[test]
684 fn test_tanh_derivative_saturated() {
685 let af = make_af(ActivationType::Tanh);
687 let d = af.derivative(&[0.9999]);
688 assert!(d[0] < 0.001);
689 }
690
691 #[test]
696 fn test_softmax_sums_to_one() {
697 let input = vec![1.0, 2.0, 3.0, 4.0];
698 let out = ActivationFunction::softmax(&input);
699 let sum: f64 = out.iter().sum();
700 assert!(approx_eq(sum, 1.0, 1e-12));
701 }
702
703 #[test]
704 fn test_softmax_numerical_stability_large_values() {
705 let input = vec![1000.0, 1001.0, 1002.0];
707 let out = ActivationFunction::softmax(&input);
708 let sum: f64 = out.iter().sum();
709 assert!(!sum.is_nan());
710 assert!(approx_eq(sum, 1.0, 1e-12));
711 }
712
713 #[test]
714 fn test_softmax_uniform_input() {
715 let input = vec![2.0; 4];
717 let out = ActivationFunction::softmax(&input);
718 for &v in &out {
719 assert!(approx_eq(v, 0.25, 1e-12));
720 }
721 }
722
723 #[test]
724 fn test_softmax_empty_input() {
725 let out = ActivationFunction::softmax(&[]);
726 assert!(out.is_empty());
727 }
728
729 #[test]
730 fn test_softmax_all_outputs_positive() {
731 let input = vec![-5.0, 0.0, 5.0];
732 let out = ActivationFunction::softmax(&input);
733 for &v in &out {
734 assert!(v > 0.0);
735 }
736 }
737
738 #[test]
743 fn test_gelu_zero() {
744 assert!(approx_eq(ActivationFunction::gelu(0.0), 0.0, EPS));
746 }
747
748 #[test]
749 fn test_gelu_positive_domain_close_to_x() {
750 let x = 10.0_f64;
752 let g = ActivationFunction::gelu(x);
753 assert!(approx_eq(g, x, 1e-4));
754 }
755
756 #[test]
757 fn test_gelu_negative_domain_small() {
758 let g = ActivationFunction::gelu(-10.0);
760 assert!(g.abs() < 1e-3);
761 }
762
763 #[test]
764 fn test_gelu_approximation_bounds() {
765 for i in -30..=30 {
770 let x = i as f64 / 10.0;
771 let approx_gelu = ActivationFunction::gelu(x);
772 assert!(
774 approx_gelu > -0.2,
775 "GELU({x}) = {approx_gelu} unexpectedly low"
776 );
777 assert!(
778 approx_gelu <= x + 0.1 || x < 0.0,
779 "GELU({x}) = {approx_gelu} unexpectedly high"
780 );
781 }
782 }
783
784 #[test]
789 fn test_swish_equals_x_times_sigmoid() {
790 for i in -5..=5 {
791 let x = i as f64;
792 let swish_val = ActivationFunction::swish(x);
793 let expected = x * ActivationFunction::sigmoid(x);
794 assert!(approx_eq(swish_val, expected, EPS));
795 }
796 }
797
798 #[test]
799 fn test_swish_zero() {
800 assert!(approx_eq(ActivationFunction::swish(0.0), 0.0, EPS));
801 }
802
803 #[test]
808 fn test_mish_zero() {
809 assert!(approx_eq(ActivationFunction::mish(0.0), 0.0, EPS));
810 }
811
812 #[test]
813 fn test_mish_positive_domain_greater_than_zero() {
814 for i in 1..=10 {
816 let x = i as f64;
817 assert!(
818 ActivationFunction::mish(x) > 0.0,
819 "mish({x}) should be positive"
820 );
821 }
822 }
823
824 #[test]
825 fn test_mish_monotone_positive() {
826 let mut prev = ActivationFunction::mish(0.0);
828 for i in 1..=20 {
829 let cur = ActivationFunction::mish(i as f64 * 0.5);
830 assert!(cur >= prev, "Mish not monotone at x={}", i as f64 * 0.5);
831 prev = cur;
832 }
833 }
834
835 #[test]
840 fn test_hard_swish_clamp_negative() {
841 assert!(approx_eq(ActivationFunction::hard_swish(-3.0), 0.0, EPS));
843 assert!(approx_eq(ActivationFunction::hard_swish(-5.0), 0.0, EPS));
844 }
845
846 #[test]
847 fn test_hard_swish_clamp_positive() {
848 assert!(approx_eq(ActivationFunction::hard_swish(3.0), 3.0, EPS));
850 assert!(approx_eq(ActivationFunction::hard_swish(6.0), 6.0, EPS));
851 }
852
853 #[test]
854 fn test_hard_swish_zero() {
855 assert!(approx_eq(ActivationFunction::hard_swish(0.0), 0.0, EPS));
857 }
858
859 #[test]
864 fn test_linear_forward() {
865 let mut af = make_af(ActivationType::Linear);
866 let input = vec![1.0, -2.0, std::f64::consts::PI];
867 let out = af.forward(&input);
868 assert_eq!(out, input);
869 }
870
871 #[test]
872 fn test_linear_derivative() {
873 let af = make_af(ActivationType::Linear);
874 let d = af.derivative(&[1.0, 2.0, 3.0]);
875 for v in d {
876 assert!(approx_eq(v, 1.0, EPS));
877 }
878 }
879
880 #[test]
885 fn test_threshold_above() {
886 let mut af = make_af(ActivationType::Threshold(0.5, 1.0));
887 let out = af.forward(&[0.6, 1.0, 2.0]);
888 for v in out {
889 assert!(approx_eq(v, 1.0, EPS));
890 }
891 }
892
893 #[test]
894 fn test_threshold_below() {
895 let mut af = make_af(ActivationType::Threshold(0.5, 1.0));
896 let out = af.forward(&[0.0, 0.5, 0.4]);
897 for v in out {
898 assert!(approx_eq(v, 0.0, EPS));
899 }
900 }
901
902 #[test]
903 fn test_threshold_exact() {
904 let mut af = make_af(ActivationType::Threshold(1.0, 42.0));
906 let out = af.forward(&[1.0]);
907 assert!(approx_eq(out[0], 0.0, EPS));
908 }
909
910 #[test]
915 fn test_apply_inplace_relu() {
916 let mut af = make_af(ActivationType::ReLU);
917 let mut data = vec![-1.0, 2.0, -3.0, 4.0];
918 af.apply_inplace(&mut data);
919 assert_eq!(data, vec![0.0, 2.0, 0.0, 4.0]);
920 }
921
922 #[test]
923 fn test_apply_inplace_sigmoid() {
924 let mut af = make_af(ActivationType::Sigmoid);
925 let mut data = vec![0.0];
926 af.apply_inplace(&mut data);
927 assert!(approx_eq(data[0], 0.5, EPS));
928 }
929
930 #[test]
931 fn test_apply_inplace_softmax_sums_one() {
932 let mut af = make_af(ActivationType::Softmax);
933 let mut data = vec![1.0, 2.0, 3.0];
934 af.apply_inplace(&mut data);
935 let sum: f64 = data.iter().sum();
936 assert!(approx_eq(sum, 1.0, 1e-12));
937 }
938
939 #[test]
944 fn test_stats_total_calls() {
945 let mut af = make_af(ActivationType::Sigmoid);
946 let _ = af.forward(&[1.0]);
947 let _ = af.forward(&[2.0, 3.0]);
948 assert_eq!(af.stats().total_calls, 2);
949 }
950
951 #[test]
952 fn test_stats_total_elements() {
953 let mut af = make_af(ActivationType::Tanh);
954 let _ = af.forward(&[1.0, 2.0, 3.0]);
955 assert_eq!(af.stats().total_elements, 3);
956 }
957
958 #[test]
959 fn test_stats_inplace_increments() {
960 let mut af = make_af(ActivationType::Linear);
961 let mut data = vec![1.0; 5];
962 af.apply_inplace(&mut data);
963 assert_eq!(af.stats().total_calls, 1);
964 assert_eq!(af.stats().total_elements, 5);
965 }
966
967 #[test]
972 fn test_pi_used_in_gelu() {
973 let c = (2.0_f64 / PI).sqrt();
975 assert!(approx_eq(c, 0.7978845608028654, 1e-12));
976 }
977}