Skip to main content

optirs_core/regularizers/
dropout.rs

1// Dropout regularization applied to gradients
2
3use scirs2_core::ndarray::{Array, Dimension, ScalarOperand, Zip};
4use scirs2_core::numeric::Float;
5use scirs2_core::random::Rng;
6use scirs2_core::Random;
7use std::cell::RefCell;
8use std::fmt::Debug;
9
10use crate::error::Result;
11use crate::regularizers::Regularizer;
12
13/// Dropout regularization for **gradients**
14///
15/// This type implements the [`Regularizer`] trait, so it operates on the
16/// gradient array handed to [`Regularizer::apply`], not on layer activations.
17/// While in training mode it zeroes each gradient entry independently with
18/// probability `rate` and rescales the survivors by `1 / (1 - rate)` (inverted
19/// dropout), which keeps the expected gradient unchanged. In evaluation mode —
20/// or when `rate` is zero — gradients pass through untouched.
21///
22/// Applying the same idea to activations requires a forward/backward pass that
23/// this crate's optimizer-side [`Regularizer`] interface does not model; use
24/// [`crate::regularizers::SpatialDropout`] or a neural-network layer for that.
25///
26/// A fresh mask is drawn on every [`Regularizer::apply`] call — masks are never
27/// cached or reused across calls, so consecutive calls with identical gradients
28/// generally produce different results.
29///
30/// [`Regularizer::apply`]: crate::regularizers::Regularizer::apply
31///
32/// # Examples
33///
34/// ```
35/// use scirs2_core::ndarray::Array1;
36/// use optirs_core::regularizers::Dropout;
37/// use scirs2_core::random::SeedableRng;
38/// use scirs2_core::random::rngs::SmallRng;
39///
40/// // Create a dropout regularizer with 0.5 dropout rate
41/// let seed = [0u8; 32];
42/// let mut rng = SmallRng::from_seed(seed);
43/// let mut dropout = Dropout::new(0.5f64, &mut rng);
44///
45/// // Set to training mode
46/// dropout.train();
47///
48/// // Check the dropout rate
49/// assert_eq!(dropout.rate(), 0.5);
50///
51/// // Set to evaluation mode
52/// dropout.eval();
53/// assert!(!dropout.is_training());
54/// ```
55#[derive(Debug)]
56pub struct Dropout<A: Float + Debug> {
57    /// Dropout rate (fraction of gradient entries that are dropped)
58    rate: A,
59    /// Random number generator
60    rng: RefCell<Random<scirs2_core::random::rngs::StdRng>>,
61    /// Boolean indicating whether in training mode
62    training: bool,
63}
64
65impl<A: Float + Debug + Send + Sync> Dropout<A> {
66    /// Create a new dropout regularizer
67    ///
68    /// # Arguments
69    ///
70    /// * `rate` - Dropout rate (0.0 to 1.0, fraction of entries that are dropped)
71    /// * `rng` - Random number generator used to seed this regularizer's own RNG
72    pub fn new<R: Rng>(rate: A, rng: &mut R) -> Self {
73        // Ensure rate is between 0 and 1
74        let rate = rate.max(A::zero()).min(A::one());
75
76        // Create a new RNG from the provided one
77        let mut seed_bytes = [0u8; 8];
78        rng.fill_bytes(&mut seed_bytes);
79        let seed = u64::from_ne_bytes(seed_bytes);
80        let rng = Random::seed(seed);
81
82        Self {
83            rate,
84            rng: RefCell::new(rng),
85            training: true,
86        }
87    }
88
89    /// Get the dropout rate
90    pub fn rate(&self) -> A {
91        self.rate
92    }
93
94    /// Set the dropout rate
95    ///
96    /// # Arguments
97    ///
98    /// * `rate` - Dropout rate (0.0 to 1.0, fraction of entries that are dropped)
99    pub fn set_rate(&mut self, rate: A) -> &mut Self {
100        // Ensure rate is between 0 and 1
101        self.rate = rate.max(A::zero()).min(A::one());
102        self
103    }
104
105    /// Set to training mode (apply dropout to gradients)
106    pub fn train(&mut self) -> &mut Self {
107        self.training = true;
108        self
109    }
110
111    /// Set to inference mode (gradients pass through unchanged)
112    pub fn eval(&mut self) -> &mut Self {
113        self.training = false;
114        self
115    }
116
117    /// Get the training mode
118    pub fn is_training(&self) -> bool {
119        self.training
120    }
121
122    /// Draw a fresh dropout mask for the given shape
123    ///
124    /// During training each entry is independently set to `0` with probability
125    /// `rate` and to `1 / (1 - rate)` otherwise, so the mask has unit mean and
126    /// masking leaves the expected gradient unchanged. Outside training mode (or
127    /// with a zero rate) an all-ones mask is returned.
128    fn create_mask<D: Dimension>(&self, shape: D) -> Array<A, D> {
129        if !self.training || self.rate <= A::zero() {
130            // In eval mode or with 0 dropout rate, no masking is applied
131            return Array::ones(shape);
132        }
133
134        // The scale factor for the kept entries is 1/(1-rate); this maintains
135        // the expected magnitude of the masked gradients.
136        let keep_prob = A::one() - self.rate;
137        if keep_prob <= A::zero() {
138            // rate == 1.0: everything is dropped, no finite rescaling exists.
139            return Array::zeros(shape);
140        }
141        let scale = A::one() / keep_prob;
142
143        // Compare in f64 so no fallible conversion of the random draw is needed.
144        let rate = self.rate.to_f64().unwrap_or(0.0);
145        let mut rng = self.rng.borrow_mut();
146        let mut mask = Array::zeros(shape);
147        for elem in mask.iter_mut() {
148            let rand_val: f64 = rng.gen_range(0.0..1.0);
149            if rand_val > rate {
150                *elem = scale;
151            }
152        }
153
154        mask
155    }
156}
157
158impl<A, D> Regularizer<A, D> for Dropout<A>
159where
160    A: Float + ScalarOperand + Debug + Send + Sync,
161    D: Dimension<Pattern = D>,
162{
163    /// Mask the **gradients** in place with a freshly drawn dropout mask.
164    ///
165    /// `params` is ignored: this regularizer perturbs the gradient signal, not
166    /// the parameters. Always returns a zero penalty because dropout adds no
167    /// term to the loss.
168    fn apply(&self, _params: &Array<A, D>, gradients: &mut Array<A, D>) -> Result<A> {
169        if !self.training || self.rate <= A::zero() {
170            // In eval mode or with 0 dropout rate, no dropout is applied
171            return Ok(A::zero());
172        }
173
174        // Draw a fresh mask for this call and apply it to the gradients.
175        let mask = self.create_mask(gradients.dim());
176        Zip::from(gradients).and(&mask).for_each(|grad, &mask_val| {
177            *grad = *grad * mask_val;
178        });
179
180        // Dropout doesn't add a penalty term to the loss
181        Ok(A::zero())
182    }
183
184    fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
185        // Dropout doesn't add a penalty term to the loss
186        Ok(A::zero())
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use scirs2_core::ndarray::{Array1, ArrayD};
194    use scirs2_core::random::rngs::SmallRng;
195    use scirs2_core::random::SeedableRng;
196
197    fn make_dropout(rate: f64) -> Dropout<f64> {
198        let mut rng = SmallRng::from_seed([7u8; 32]);
199        Dropout::new(rate, &mut rng)
200    }
201
202    /// The `Regularizer` impl is bound to `D: Dimension<Pattern = D>`, which only
203    /// `IxDyn` satisfies, so all fixtures are dynamic-dimension arrays.
204    fn dyn_vec(values: &[f64]) -> ArrayD<f64> {
205        Array1::from_vec(values.to_vec()).into_dyn()
206    }
207
208    fn dyn_ones(len: usize) -> ArrayD<f64> {
209        Array1::from_elem(len, 1.0).into_dyn()
210    }
211
212    #[test]
213    fn eval_mode_leaves_gradients_untouched() {
214        let mut dropout = make_dropout(0.5);
215        dropout.eval();
216
217        let params = dyn_vec(&[1.0, 2.0, 3.0, 4.0]);
218        let original = dyn_vec(&[0.1, 0.2, 0.3, 0.4]);
219        let mut gradients = original.clone();
220
221        let penalty = dropout
222            .apply(&params, &mut gradients)
223            .expect("dropout apply failed");
224
225        assert_eq!(penalty, 0.0);
226        assert_eq!(gradients, original);
227    }
228
229    #[test]
230    fn training_mode_masks_gradients_not_params() {
231        let mut dropout = make_dropout(0.5);
232        dropout.train();
233
234        let params = dyn_ones(512);
235        let params_before = params.clone();
236        let mut gradients = dyn_ones(512);
237
238        dropout
239            .apply(&params, &mut gradients)
240            .expect("dropout apply failed");
241
242        // Parameters are never touched by this regularizer.
243        assert_eq!(params, params_before);
244
245        // Gradients are either zeroed or rescaled by 1/(1-rate) = 2.
246        assert!(gradients
247            .iter()
248            .all(|&g| g == 0.0 || (g - 2.0).abs() < 1e-12));
249        let dropped = gradients.iter().filter(|&&g| g == 0.0).count();
250        assert!(dropped > 0, "no gradient entries were dropped");
251        assert!(dropped < 512, "every gradient entry was dropped");
252
253        // Inverted dropout keeps the expected gradient sum near the original.
254        let sum: f64 = gradients.sum();
255        assert!((sum - 512.0).abs() < 160.0, "unexpected gradient sum {sum}");
256    }
257
258    #[test]
259    fn mask_is_redrawn_on_every_call() {
260        // The mask must not be cached: two applies on identical gradients with a
261        // non-trivial rate produce different results with overwhelming odds.
262        let mut dropout = make_dropout(0.5);
263        dropout.train();
264
265        let params = dyn_ones(256);
266        let mut first = dyn_ones(256);
267        let mut second = dyn_ones(256);
268
269        dropout
270            .apply(&params, &mut first)
271            .expect("dropout apply failed");
272        dropout
273            .apply(&params, &mut second)
274            .expect("dropout apply failed");
275
276        assert_ne!(first, second, "dropout mask appears to be cached");
277    }
278
279    #[test]
280    fn zero_rate_is_identity_and_full_rate_zeroes_everything() {
281        let params = dyn_vec(&[1.0, 2.0, 3.0]);
282        let original = dyn_vec(&[0.5, -1.5, 2.5]);
283
284        let mut none = make_dropout(0.0);
285        none.train();
286        let mut gradients = original.clone();
287        none.apply(&params, &mut gradients)
288            .expect("dropout apply failed");
289        assert_eq!(gradients, original);
290
291        let mut all = make_dropout(1.0);
292        all.train();
293        let mut gradients = original.clone();
294        all.apply(&params, &mut gradients)
295            .expect("dropout apply failed");
296        assert_eq!(gradients, dyn_vec(&[0.0, 0.0, 0.0]));
297    }
298
299    #[test]
300    fn penalty_is_always_zero() {
301        let dropout = make_dropout(0.5);
302        let params = dyn_vec(&[1.0, 2.0, 3.0]);
303        assert_eq!(
304            Regularizer::penalty(&dropout, &params).expect("penalty failed"),
305            0.0
306        );
307    }
308}