Skip to main content

optirs_core/regularizers/
stochastic_depth.rs

1// Stochastic Depth regularization
2//
3// Stochastic Depth is a regularization technique that randomly skips
4// certain layers during training, which helps prevent overfitting and
5// improves gradient flow in very deep networks.
6
7use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::fmt::Debug;
10
11use crate::error::Result;
12use crate::regularizers::Regularizer;
13
14/// Stochastic Depth regularization
15///
16/// Implements stochastic depth by randomly skipping layers during training.
17/// During inference, all layers are used with a scaling factor.
18///
19/// # Example
20///
21/// ```
22/// use scirs2_core::ndarray::array;
23/// use optirs_core::regularizers::StochasticDepth;
24///
25/// let stochastic_depth = StochasticDepth::new(0.2, 10, 50);
26/// let features = array![[1.0, 2.0], [3.0, 4.0]];
27///
28/// // Apply stochastic depth for layer 5 during training
29/// let output = stochastic_depth.apply_layer(5, &features, true);
30/// ```
31#[derive(Debug, Clone)]
32pub struct StochasticDepth<A: Float> {
33    /// Probability of dropping a layer
34    drop_prob: A,
35    /// Current layer index
36    layer_idx: usize,
37    /// Total number of layers
38    num_layers: usize,
39    /// Random state for drop decision
40    rng_state: u64,
41}
42
43impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> StochasticDepth<A> {
44    /// Create a new stochastic depth regularization
45    ///
46    /// # Arguments
47    ///
48    /// * `drop_prob` - The base probability of dropping a layer
49    /// * `layer_idx` - The index of the current layer
50    /// * `num_layers` - The total number of layers in the network
51    pub fn new(drop_prob: A, layer_idx: usize, numlayers: usize) -> Self {
52        Self {
53            drop_prob,
54            layer_idx,
55            num_layers: numlayers,
56            rng_state: 0,
57        }
58    }
59
60    /// Set layer index
61    ///
62    /// # Arguments
63    ///
64    /// * `layer_idx` - New layer index
65    pub fn set_layer(&mut self, layeridx: usize) {
66        self.layer_idx = layeridx;
67    }
68
69    /// Set the RNG state for deterministic behavior
70    pub fn set_rng_state(&mut self, state: u64) {
71        self.rng_state = state;
72    }
73
74    /// Get the survival probability for the current layer
75    ///
76    /// The survival probability typically decreases for deeper layers,
77    /// following a linear decay schedule.
78    fn survival_probability(&self) -> A {
79        // Linear decay of survival probability with depth
80        let layer_ratio = A::from_usize(self.layer_idx)
81            .expect("StochasticDepth: layer_idx must fit in A (f32/f64)")
82            / A::from_usize(self.num_layers)
83                .expect("StochasticDepth: num_layers must fit in A (f32/f64)");
84        A::one() - (self.drop_prob * layer_ratio)
85    }
86
87    /// Decide whether to drop the current layer
88    fn should_drop(&self) -> bool {
89        // Use simple random hash function for reproducibility
90        let hash = (self
91            .rng_state
92            .wrapping_mul(0x7fffffff)
93            .wrapping_add(self.layer_idx as u64))
94            % 10000;
95        let random_val = A::from_f64(hash as f64 / 10000.0)
96            .expect("StochasticDepth: a ratio in [0, 1) always fits in A (f32/f64)");
97
98        random_val > self.survival_probability()
99    }
100
101    /// Apply stochastic depth to a layer
102    ///
103    /// # Arguments
104    ///
105    /// * `layer_idx` - Index of the layer
106    /// * `features` - Input features
107    /// * `training` - Whether in training mode
108    ///
109    /// # Returns
110    ///
111    /// The output features, which are either:
112    /// - The identity (input) if the layer is dropped during training
113    /// - The input scaled by the survival probability during inference
114    /// - The input if not dropped during training
115    pub fn apply_layer<D>(
116        &self,
117        layer_idx: usize,
118        features: &Array<A, D>,
119        training: bool,
120    ) -> Array<A, D>
121    where
122        D: Dimension,
123    {
124        let survival_prob = self.survival_probability();
125
126        if training {
127            let mut sd = self.clone();
128            sd.set_layer(layer_idx);
129
130            if sd.should_drop() {
131                // Skip this layer
132                features.clone()
133            } else {
134                // Use this layer normally
135                features.clone()
136            }
137        } else {
138            // During inference, scale by survival probability
139            features * survival_prob
140        }
141    }
142}
143
144// Implement Regularizer trait (although the main functionality is in apply_layer)
145impl<A: Float + Debug + ScalarOperand + FromPrimitive, D: Dimension + Send + Sync> Regularizer<A, D>
146    for StochasticDepth<A>
147{
148    fn apply(&self, _params: &Array<A, D>, _gradients: &mut Array<A, D>) -> Result<A> {
149        // This method is not the primary way to use stochastic depth,
150        // prefer apply_layer for layer-wise applications
151        Ok(A::zero())
152    }
153
154    fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
155        // Stochastic depth doesn't add a direct penalty term
156        Ok(A::zero())
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use scirs2_core::ndarray::array;
164
165    #[test]
166    fn test_stochastic_depth_creation() {
167        let sd = StochasticDepth::<f64>::new(0.2, 5, 10);
168        assert_eq!(sd.drop_prob, 0.2);
169        assert_eq!(sd.layer_idx, 5);
170        assert_eq!(sd.num_layers, 10);
171    }
172
173    #[test]
174    fn test_survival_probability() {
175        // For layer 0 of 10 with drop_prob 0.5, survival prob is 1.0
176        let sd1 = StochasticDepth::<f64>::new(0.5, 0, 10);
177        assert_eq!(sd1.survival_probability(), 1.0);
178
179        // For layer 10 of 10 with drop_prob 0.5, survival prob is 0.5
180        let sd2 = StochasticDepth::<f64>::new(0.5, 10, 10);
181        assert_eq!(sd2.survival_probability(), 0.5);
182
183        // For layer 5 of 10 with drop_prob 0.5, survival prob is 0.75
184        let sd3 = StochasticDepth::<f64>::new(0.5, 5, 10);
185        assert_eq!(sd3.survival_probability(), 0.75);
186    }
187
188    #[test]
189    fn test_should_drop() {
190        // With fixed RNG states, we can test deterministic behavior
191        let mut sd = StochasticDepth::<f64>::new(0.5, 5, 10);
192
193        // Try different RNG states
194        sd.set_rng_state(12345);
195        let _result1 = sd.should_drop();
196
197        sd.set_rng_state(54321);
198        let _result2 = sd.should_drop();
199
200        // The results should be deterministic for given RNG states
201        // result1 is already a boolean, no need to assert
202        // result2 is already a boolean, no need to assert
203    }
204
205    #[test]
206    fn test_apply_layer_training() {
207        let sd = StochasticDepth::<f64>::new(0.5, 5, 10);
208        let features = array![[1.0, 2.0], [3.0, 4.0]];
209
210        // In training mode, the output is either features or modified features
211        let output = sd.apply_layer(5, &features, true);
212
213        // Output should be 2D array with same shape
214        assert_eq!(output.shape(), features.shape());
215    }
216
217    #[test]
218    fn test_apply_layer_inference() {
219        let sd = StochasticDepth::<f64>::new(0.5, 5, 10);
220        let features = array![[1.0, 2.0], [3.0, 4.0]];
221
222        // In inference mode, output is always scaled by survival probability
223        let output = sd.apply_layer(5, &features, false);
224        let survival_prob = sd.survival_probability();
225
226        // Check that each element is scaled by survival probability
227        for (i, j) in output.indexed_iter() {
228            assert_eq!(*j, features[i] * survival_prob);
229        }
230    }
231
232    #[test]
233    fn test_regularizer_trait() {
234        let sd = StochasticDepth::<f64>::new(0.5, 5, 10);
235        let params = array![[1.0, 2.0], [3.0, 4.0]];
236        let mut gradients = array![[0.1, 0.2], [0.3, 0.4]];
237        let original_gradients = gradients.clone();
238
239        let penalty = sd
240            .apply(&params, &mut gradients)
241            .expect("sd.apply succeeds in test_regularizer_trait");
242
243        // Penalty should be zero
244        assert_eq!(penalty, 0.0);
245
246        // Gradients should be unchanged
247        assert_eq!(gradients, original_gradients);
248    }
249}