Skip to main content

optirs_core/regularizers/
dropconnect.rs

1// DropConnect regularization
2//
3// DropConnect is a regularization technique that randomly drops connections between layers
4// during training. Unlike Dropout which drops units, DropConnect drops individual weights.
5
6use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
7use scirs2_core::numeric::Float;
8use scirs2_core::random::thread_rng;
9use std::fmt::Debug;
10
11use crate::error::{OptimError, Result};
12use crate::regularizers::Regularizer;
13
14/// DropConnect regularizer
15///
16/// Randomly drops connections (weights) during training to prevent overfitting.
17///
18/// # Example
19///
20/// ```
21/// use scirs2_core::ndarray::Array2;
22/// use scirs2_core::ndarray::array;
23/// use optirs_core::regularizers::DropConnect;
24///
25/// let dropconnect = DropConnect::new(0.5).expect("DropConnect::new succeeds"); // 50% connection dropout
26/// let weights = array![[1.0, 2.0], [3.0, 4.0]];
27///
28/// // During training
29/// let masked_weights = dropconnect.apply_to_weights(&weights, true);
30/// // Some connections will be zeroed out randomly
31///
32/// // During inference
33/// let inference_weights = dropconnect.apply_to_weights(&weights, false);
34/// // No dropout during inference - weights are scaled appropriately
35/// ```
36#[derive(Debug, Clone)]
37pub struct DropConnect<A: Float> {
38    /// Probability of dropping a connection
39    drop_prob: A,
40}
41
42impl<A: Float + Debug + ScalarOperand + Send + Sync> DropConnect<A> {
43    /// Create a new DropConnect regularizer
44    ///
45    /// # Arguments
46    ///
47    /// * `drop_prob` - Probability of dropping each connection (0.0 to 1.0)
48    ///
49    /// # Returns
50    ///
51    /// A new DropConnect instance or error if probability is invalid
52    pub fn new(dropprob: A) -> Result<Self> {
53        if dropprob < A::zero() || dropprob > A::one() {
54            return Err(OptimError::InvalidConfig(
55                "Drop probability must be between 0.0 and 1.0".to_string(),
56            ));
57        }
58
59        Ok(Self {
60            drop_prob: dropprob,
61        })
62    }
63
64    /// Apply DropConnect to weights
65    ///
66    /// # Arguments
67    ///
68    /// * `weights` - The weight matrix to apply DropConnect to
69    /// * `training` - Whether we're in training mode (applies dropout) or inference mode
70    pub fn apply_to_weights<D: Dimension>(
71        &self,
72        weights: &Array<A, D>,
73        training: bool,
74    ) -> Array<A, D> {
75        if !training || self.drop_prob == A::zero() {
76            // During inference or if no dropout, return weights as-is
77            return weights.clone();
78        }
79
80        // Create keep probability for sampling
81        let keep_prob = A::one() - self.drop_prob;
82        let keep_prob_f64 = keep_prob
83            .to_f64()
84            .expect("DropConnect: keep_prob in [0, 1] (validated at construction) fits in f64");
85
86        // Sample mask
87        let mut rng = thread_rng();
88        let mask = Array::from_shape_fn(weights.raw_dim(), |_| rng.random_bool(keep_prob_f64));
89
90        // Apply mask and scale by keep probability
91        let mut result = weights.clone();
92        for (r, &m) in result.iter_mut().zip(mask.iter()) {
93            if !m {
94                *r = A::zero();
95            } else {
96                // Scale the kept weights to maintain expected value
97                *r = *r / keep_prob;
98            }
99        }
100
101        result
102    }
103
104    /// Apply DropConnect during gradient computation
105    ///
106    /// This method should be called during backpropagation to ensure
107    /// gradients are only computed for non-dropped connections
108    pub fn apply_to_gradients<D: Dimension>(
109        &self,
110        gradients: &Array<A, D>,
111        weightsshape: D,
112        training: bool,
113    ) -> Array<A, D> {
114        if !training || self.drop_prob == A::zero() {
115            return gradients.clone();
116        }
117
118        // Use the same mask for gradients
119        let keep_prob = A::one() - self.drop_prob;
120        let keep_prob_f64 = keep_prob
121            .to_f64()
122            .expect("DropConnect: keep_prob in [0, 1] (validated at construction) fits in f64");
123
124        // Create mask with same shape as weights
125        let mut rng = thread_rng();
126        let mask = Array::from_shape_fn(weightsshape, |_| rng.random_bool(keep_prob_f64));
127
128        // Apply mask to gradients
129        let mut result = gradients.clone();
130        for (g, &m) in result.iter_mut().zip(mask.iter()) {
131            if !m {
132                *g = A::zero();
133            } else {
134                // Scale gradients by keep probability
135                *g = *g / keep_prob;
136            }
137        }
138
139        result
140    }
141}
142
143impl<A: Float + Debug + ScalarOperand + Send + Sync, D: Dimension + Send + Sync> Regularizer<A, D>
144    for DropConnect<A>
145{
146    fn apply(&self, params: &Array<A, D>, gradients: &mut Array<A, D>) -> Result<A> {
147        // Apply DropConnect mask to gradients
148        let masked_gradients = self.apply_to_gradients(gradients, params.raw_dim(), true);
149
150        // Update gradients in place
151        gradients.assign(&masked_gradients);
152
153        // DropConnect doesn't add a penalty term
154        Ok(A::zero())
155    }
156
157    fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
158        // DropConnect doesn't add a penalty term to the loss
159        Ok(A::zero())
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use approx::assert_relative_eq;
167    use scirs2_core::ndarray::array;
168
169    #[test]
170    fn test_dropconnect_creation() {
171        // Valid creation
172        let dc = DropConnect::<f64>::new(0.5)
173            .expect("DropConnect::<f64>::new succeeds in test_dropconnect_creation");
174        assert_eq!(dc.drop_prob, 0.5);
175
176        // Invalid probabilities
177        assert!(DropConnect::<f64>::new(-0.1).is_err());
178        assert!(DropConnect::<f64>::new(1.1).is_err());
179    }
180
181    #[test]
182    fn test_dropconnect_training_mode() {
183        let dc = DropConnect::new(0.5)
184            .expect("DropConnect::new succeeds in test_dropconnect_training_mode");
185        let weights = array![[1.0, 2.0], [3.0, 4.0]];
186
187        // During training, some connections should be dropped
188        let masked_weights = dc.apply_to_weights(&weights, true);
189
190        // Check that some but not all values are zero (statistically)
191        let _zeros = masked_weights.iter().filter(|&&x| x == 0.0).count();
192
193        // The masked weights should have approximately scaled values
194        for (&original, &masked) in weights.iter().zip(masked_weights.iter()) {
195            if masked != 0.0 {
196                // Non-zero values should be scaled by 1/keep_prob = 2.0
197                assert_relative_eq!(masked, original * 2.0, epsilon = 1e-10);
198            }
199        }
200    }
201
202    #[test]
203    fn test_dropconnect_inference_mode() {
204        let dc = DropConnect::new(0.5)
205            .expect("DropConnect::new succeeds in test_dropconnect_inference_mode");
206        let weights = array![[1.0, 2.0], [3.0, 4.0]];
207
208        // During inference, weights should remain unchanged
209        let inference_weights = dc.apply_to_weights(&weights, false);
210        assert_eq!(weights, inference_weights);
211    }
212
213    #[test]
214    fn test_dropconnect_zero_probability() {
215        let dc = DropConnect::new(0.0)
216            .expect("DropConnect::new succeeds in test_dropconnect_zero_probability");
217        let weights = array![[1.0, 2.0], [3.0, 4.0]];
218
219        // With 0% dropout, weights should remain unchanged
220        let result = dc.apply_to_weights(&weights, true);
221        assert_eq!(weights, result);
222    }
223
224    #[test]
225    fn test_dropconnect_gradients() {
226        let dc =
227            DropConnect::new(0.5).expect("DropConnect::new succeeds in test_dropconnect_gradients");
228        let gradients = array![[1.0, 1.0], [1.0, 1.0]];
229        let weightsshape = gradients.raw_dim();
230
231        // Apply to gradients
232        let masked_grads = dc.apply_to_gradients(&gradients, weightsshape, true);
233
234        // Check scaling
235        for &grad in masked_grads.iter() {
236            if grad != 0.0 {
237                assert_relative_eq!(grad, 2.0, epsilon = 1e-10);
238            }
239        }
240    }
241
242    #[test]
243    fn test_regularizer_trait() {
244        let dc =
245            DropConnect::new(0.3).expect("DropConnect::new succeeds in test_regularizer_trait");
246        let params = array![[1.0, 2.0], [3.0, 4.0]];
247        let mut gradient = array![[0.1, 0.2], [0.3, 0.4]];
248
249        // Test Regularizer trait methods
250        let penalty = dc
251            .penalty(&params)
252            .expect("dc.penalty succeeds in test_regularizer_trait");
253        assert_eq!(penalty, 0.0); // DropConnect has no penalty term
254
255        // Test gradient computation
256        let penalty_from_apply = dc
257            .apply(&params, &mut gradient)
258            .expect("dc.apply succeeds in test_regularizer_trait");
259        assert_eq!(penalty_from_apply, 0.0);
260
261        // Gradient should be modified with dropout
262        let zeros = gradient.iter().filter(|&&x| x == 0.0).count();
263        assert!(zeros <= 4); // Some elements may be dropped
264    }
265}