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(¶ms, &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}