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(¶ms, &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(¶ms, &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(¶ms, &mut first)
271 .expect("dropout apply failed");
272 dropout
273 .apply(¶ms, &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(¶ms, &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(¶ms, &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, ¶ms).expect("penalty failed"),
305 0.0
306 );
307 }
308}