rust_ml/core/activations/
sigmoid.rs

1use crate::core::activations::activation::Activation;
2use ndarray::{Array, Array1, Array2, Ix1, Ix2};
3
4pub struct Sigmoid;
5
6impl Activation<Ix1> for Sigmoid {
7    fn activate(z: &Array1<f64>) -> Array1<f64> {
8        1.0 / (1.0 + (-z).exp())
9    }
10
11    fn derivative(z: &Array1<f64>) -> Array1<f64> {
12        let y_hat = Self::activate(z);
13        &y_hat * (1.0 - &y_hat)
14    }
15}
16
17impl Activation<Ix2> for Sigmoid {
18    fn activate(z: &Array2<f64>) -> Array2<f64> {
19        1.0 / (1.0 + (-z).exp())
20    }
21
22    fn derivative(z: &Array<f64, Ix2>) -> Array<f64, Ix2> {
23        let y_hat = Self::activate(z);
24        &y_hat * (1.0 - &y_hat)
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31    use approx::assert_abs_diff_eq;
32    use ndarray::{arr1, arr2};
33    use ndarray_rand::rand_distr::num_traits::Float;
34
35    #[test]
36    fn test_sigmoid_activate_vector() {
37        // Test with a vector input
38        let input = arr1(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
39        let result = Sigmoid::activate(&input);
40
41        // Expected values calculated using sigmoid formula: 1 / (1 + exp(-x))
42        let expected = arr1(&[
43            0.11920292, // sigmoid(-2.0)
44            0.26894142, // sigmoid(-1.0)
45            0.5,        // sigmoid(0.0)
46            0.73105858, // sigmoid(1.0)
47            0.88079708, // sigmoid(2.0)
48        ]);
49
50        assert_eq!(result.len(), expected.len());
51
52        // Compare with a small epsilon for floating point precision
53        for (a, b) in result.iter().zip(expected.iter()) {
54            assert_abs_diff_eq!(a, b, epsilon = 1e-8);
55        }
56    }
57
58    #[test]
59    fn test_sigmoid_derivative_vector() {
60        // Test with a vector input
61        let input = arr1(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
62        let result = Sigmoid::derivative(&input);
63
64        // Expected values calculated using sigmoid derivative formula: sigmoid(x) * (1 - sigmoid(x))
65        let sigmoid_values = arr1(&[
66            0.11920292, // sigmoid(-2.0)
67            0.26894142, // sigmoid(-1.0)
68            0.5,        // sigmoid(0.0)
69            0.73105858, // sigmoid(1.0)
70            0.88079708, // sigmoid(2.0)
71        ]);
72
73        let expected = sigmoid_values.mapv(|v| v * (1.0 - v));
74
75        assert_eq!(result.len(), expected.len());
76
77        // Compare with a small epsilon for floating point precision
78        for (a, b) in result.iter().zip(expected.iter()) {
79            assert_abs_diff_eq!(a, b, epsilon = 1e-8);
80        }
81    }
82
83    #[test]
84    fn test_sigmoid_activate_matrix() {
85        // Test with a matrix input
86        let input = arr2(&[[-2.0, -1.0, 0.0], [1.0, 2.0, 3.0]]);
87
88        let result = Sigmoid::activate(&input);
89
90        // Expected values calculated using sigmoid formula: 1 / (1 + exp(-x))
91        let expected = arr2(&[
92            [0.11920292, 0.26894142, 0.5],
93            [0.73105858, 0.88079708, 0.95257413],
94        ]);
95
96        assert_eq!(result.shape(), expected.shape());
97
98        // Compare with a small epsilon for floating point precision
99        for ((i, j), value) in result.indexed_iter() {
100            assert_abs_diff_eq!(value, &expected[[i, j]], epsilon = 1e-8);
101        }
102    }
103
104    #[test]
105    fn test_sigmoid_derivative_matrix() {
106        // Test with a matrix input
107        let input = arr2(&[[-2.0, -1.0, 0.0], [1.0, 2.0, 3.0]]);
108
109        let result = Sigmoid::derivative(&input);
110
111        // Expected values calculated using sigmoid derivative formula: sigmoid(x) * (1 - sigmoid(x))
112        let sigmoid_values = arr2(&[
113            [0.11920292, 0.26894142, 0.5],
114            [0.73105858, 0.88079708, 0.95257413],
115        ]);
116
117        let expected = sigmoid_values.mapv(|v| v * (1.0 - v));
118
119        assert_eq!(result.shape(), expected.shape());
120
121        // Compare with a small epsilon for floating point precision
122        for ((i, j), value) in result.indexed_iter() {
123            assert_abs_diff_eq!(value, &expected[[i, j]], epsilon = 1e-8);
124        }
125    }
126
127    #[test]
128    fn test_sigmoid_properties() {
129        // Test that sigmoid(0) = 0.5
130        let zero = arr1(&[0.0]);
131        assert_abs_diff_eq!(Sigmoid::activate(&zero)[0], 0.5, epsilon = 1e-8);
132
133        // Test symmetry property: sigmoid(-x) = 1 - sigmoid(x)
134        let x = arr1(&[1.0, 2.0, 3.0]);
135        let neg_x = arr1(&[-1.0, -2.0, -3.0]);
136
137        let sigmoid_x = Sigmoid::activate(&x);
138        let sigmoid_neg_x = Sigmoid::activate(&neg_x);
139
140        for (i, value) in sigmoid_neg_x.iter().enumerate() {
141            assert_abs_diff_eq!(value, &(1.0 - sigmoid_x[i]), epsilon = 1e-8);
142        }
143
144        // Test that derivative is maximum at x = 0
145        let points = arr1(&[-2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0]);
146        let derivatives = Sigmoid::derivative(&points);
147
148        // Maximum value should be at x = 0 (which is 0.25)
149        let max_derivative = derivatives.iter().fold(0.0, |max, &val| max.max(val));
150        assert_abs_diff_eq!(max_derivative, 0.25, epsilon = 1e-8);
151        assert_abs_diff_eq!(derivatives[3], 0.25, epsilon = 1e-8); // index 3 is x = 0
152    }
153}