Skip to main content

optirs_core/regularizers/
label_smoothing.rs

1// Label Smoothing regularization
2//
3// Label smoothing is a regularization technique that prevents the model from
4// becoming over-confident by replacing hard one-hot encoded targets with
5// soft targets that include some probability for incorrect classes.
6
7use scirs2_core::ndarray::{Array, Array1, Dimension, ScalarOperand};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::fmt::Debug;
10
11use crate::error::{OptimError, Result};
12use crate::regularizers::Regularizer;
13
14/// Label Smoothing regularization
15///
16/// Implements label smoothing by replacing one-hot encoded target vectors with
17/// "smoother" target distributions, where some probability mass is assigned to
18/// non-target classes.
19///
20/// # Example
21///
22/// ```
23/// use scirs2_core::ndarray::array;
24/// use optirs_core::regularizers::LabelSmoothing;
25///
26/// let label_smooth = LabelSmoothing::new(0.1, 3).expect("LabelSmoothing::new succeeds");
27/// let one_hot_target = array![0.0, 1.0, 0.0];
28///
29/// // Apply label smoothing to one-hot targets
30/// let smoothed_target = label_smooth.smooth_labels(&one_hot_target).expect("label_smooth.smooth_labels succeeds");
31/// // Result will be [0.033..., 0.933..., 0.033...]
32/// ```
33#[derive(Debug, Clone)]
34pub struct LabelSmoothing<A: Float> {
35    /// Smoothing factor (between 0 and 1)
36    alpha: A,
37    /// Number of classes
38    num_classes: usize,
39}
40
41impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> LabelSmoothing<A> {
42    /// Create a new label smoothing regularizer
43    ///
44    /// # Arguments
45    ///
46    /// * `alpha` - Smoothing factor, where 0 gives one-hot encoding and 1 gives uniform distribution
47    /// * `num_classes` - Number of classes in the classification task
48    ///
49    /// # Errors
50    ///
51    /// Returns an error if alpha is not between 0 and 1
52    pub fn new(alpha: A, numclasses: usize) -> Result<Self> {
53        if alpha < A::zero() || alpha > A::one() {
54            return Err(OptimError::InvalidConfig(
55                "Alpha must be between 0 and 1".to_string(),
56            ));
57        }
58
59        Ok(Self {
60            alpha,
61            num_classes: numclasses,
62        })
63    }
64
65    /// Smooth the one-hot encoded target labels
66    ///
67    /// # Arguments
68    ///
69    /// * `labels` - One-hot encoded target labels
70    ///
71    /// # Returns
72    ///
73    /// The smoothed labels
74    ///
75    /// # Example
76    ///
77    /// For a 3-class problem with smoothing factor 0.1:
78    /// [0, 1, 0] -> [0.033..., 0.933..., 0.033...]
79    pub fn smooth_labels(&self, labels: &Array1<A>) -> Result<Array1<A>> {
80        if labels.len() != self.num_classes {
81            return Err(OptimError::InvalidConfig(format!(
82                "Expected {} classes, got {} in label vector",
83                self.num_classes,
84                labels.len()
85            )));
86        }
87
88        let num_classes: A = crate::regularizers::cast_scalar(self.num_classes)?;
89        let uniform_val = A::one() / num_classes;
90        let smooth_coef = self.alpha;
91        let one_minus_alpha = A::one() - smooth_coef;
92
93        // Compute (1 - alpha) * y + alpha * uniform
94        let smoothed = labels.map(|&y| one_minus_alpha * y + smooth_coef * uniform_val);
95
96        Ok(smoothed)
97    }
98
99    /// Apply label smoothing to a batch of one-hot encoded targets
100    ///
101    /// # Arguments
102    ///
103    /// * `labels` - Batch of one-hot encoded target labels
104    ///
105    /// # Returns
106    ///
107    /// The smoothed labels for the batch
108    pub fn smooth_batch<D>(&self, labels: &Array<A, D>) -> Result<Array<A, D>>
109    where
110        D: Dimension,
111    {
112        // Ensure the last dimension is the class dimension
113        if labels.shape().last().unwrap_or(&0) != &self.num_classes {
114            return Err(OptimError::InvalidConfig(
115                "Last dimension must match number of classes".to_string(),
116            ));
117        }
118
119        // Apply smoothing to each label vector
120        let num_classes: A = crate::regularizers::cast_scalar(self.num_classes)?;
121        let uniform_val = A::one() / num_classes;
122        let smooth_coef = self.alpha;
123        let one_minus_alpha = A::one() - smooth_coef;
124
125        // Compute (1 - alpha) * y + alpha * uniform for each element
126        let smoothed = labels.map(|&y| one_minus_alpha * y + smooth_coef * uniform_val);
127
128        Ok(smoothed)
129    }
130
131    /// Compute cross-entropy loss with label smoothing
132    ///
133    /// # Arguments
134    ///
135    /// * `logits` - Raw model outputs (unnormalized)
136    /// * `labels` - One-hot encoded target labels
137    /// * `eps` - Small value for numerical stability
138    ///
139    /// # Returns
140    ///
141    /// The smoothed cross-entropy loss
142    pub fn cross_entropy_loss(&self, logits: &Array1<A>, labels: &Array1<A>, eps: A) -> Result<A> {
143        if logits.len() != self.num_classes || labels.len() != self.num_classes {
144            return Err(OptimError::InvalidConfig(
145                "Logits and labels must match number of classes".to_string(),
146            ));
147        }
148
149        // Compute softmax probabilities
150        let max_logit = logits.fold(A::neg_infinity(), |max, &v| if v > max { v } else { max });
151        let exp_logits = logits.map(|&l| (l - max_logit).exp());
152        let sum_exp = exp_logits.sum();
153        let probs = exp_logits.map(|&e| e / (sum_exp + eps));
154
155        // Smooth the labels
156        let smoothed_labels = self.smooth_labels(labels)?;
157
158        // Compute cross-entropy with smoothed labels
159        let mut loss = A::zero();
160        for (p, y) in probs.iter().zip(smoothed_labels.iter()) {
161            loss = loss - *y * (*p + eps).ln();
162        }
163
164        Ok(loss)
165    }
166}
167
168// Implement Regularizer trait (though it's not the primary interface for label smoothing)
169impl<A: Float + Debug + ScalarOperand + FromPrimitive, D: Dimension + Send + Sync> Regularizer<A, D>
170    for LabelSmoothing<A>
171{
172    fn apply(&self, _params: &Array<A, D>, _gradients: &mut Array<A, D>) -> Result<A> {
173        // Label smoothing is not applied to model parameters directly
174        // It's applied to the target labels during loss computation
175        Ok(A::zero())
176    }
177
178    fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
179        // Label smoothing doesn't add a parameter penalty term
180        Ok(A::zero())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use approx::assert_relative_eq;
188    use scirs2_core::ndarray::array;
189
190    #[test]
191    fn test_label_smoothing_creation() {
192        let ls = LabelSmoothing::<f64>::new(0.1, 3)
193            .expect("LabelSmoothing::<f64>::new succeeds in test_label_smoothing_creation");
194        assert_eq!(ls.alpha, 0.1);
195        assert_eq!(ls.num_classes, 3);
196
197        // Alpha out of range should fail
198        assert!(LabelSmoothing::<f64>::new(-0.1, 3).is_err());
199        assert!(LabelSmoothing::<f64>::new(1.1, 3).is_err());
200    }
201
202    #[test]
203    fn test_smooth_labels() {
204        let ls = LabelSmoothing::new(0.1, 3)
205            .expect("LabelSmoothing::new succeeds in test_smooth_labels");
206        let one_hot = array![0.0, 1.0, 0.0];
207
208        let smoothed = ls
209            .smooth_labels(&one_hot)
210            .expect("ls.smooth_labels succeeds in test_smooth_labels");
211
212        // Expected: [0.033..., 0.933..., 0.033...]
213        let uniform_val = 1.0 / 3.0;
214        let expected_1 = 0.9 * 1.0 + 0.1 * uniform_val;
215        let expected_0 = 0.9 * 0.0 + 0.1 * uniform_val;
216
217        assert_relative_eq!(smoothed[0], expected_0, epsilon = 1e-5);
218        assert_relative_eq!(smoothed[1], expected_1, epsilon = 1e-5);
219        assert_relative_eq!(smoothed[2], expected_0, epsilon = 1e-5);
220
221        // Sum should still be 1
222        assert_relative_eq!(smoothed.sum(), 1.0, epsilon = 1e-5);
223    }
224
225    #[test]
226    fn test_full_smoothing() {
227        let ls = LabelSmoothing::new(1.0, 4)
228            .expect("LabelSmoothing::new succeeds in test_full_smoothing");
229        let one_hot = array![0.0, 0.0, 1.0, 0.0];
230
231        let smoothed = ls
232            .smooth_labels(&one_hot)
233            .expect("ls.smooth_labels succeeds in test_full_smoothing");
234
235        // With alpha=1, should be uniform distribution [0.25, 0.25, 0.25, 0.25]
236        for i in 0..4 {
237            assert_relative_eq!(smoothed[i], 0.25, epsilon = 1e-5);
238        }
239    }
240
241    #[test]
242    fn test_no_smoothing() {
243        let ls =
244            LabelSmoothing::new(0.0, 3).expect("LabelSmoothing::new succeeds in test_no_smoothing");
245        let one_hot = array![0.0, 1.0, 0.0];
246
247        let smoothed = ls
248            .smooth_labels(&one_hot)
249            .expect("ls.smooth_labels succeeds in test_no_smoothing");
250
251        // With alpha=0, should be identical to input
252        for i in 0..3 {
253            assert_relative_eq!(smoothed[i], one_hot[i], epsilon = 1e-5);
254        }
255    }
256
257    #[test]
258    fn test_smooth_batch() {
259        let ls =
260            LabelSmoothing::new(0.2, 2).expect("LabelSmoothing::new succeeds in test_smooth_batch");
261        let batch = array![[1.0, 0.0], [0.0, 1.0]];
262
263        let smoothed = ls
264            .smooth_batch(&batch)
265            .expect("ls.smooth_batch succeeds in test_smooth_batch");
266
267        // With alpha=0.2 and 2 classes, uniform_val = 0.5
268        // For label 1.0: (1 - 0.2) * 1.0 + 0.2 * 0.5 = 0.8 + 0.1 = 0.9
269        // For label 0.0: (1 - 0.2) * 0.0 + 0.2 * 0.5 = 0.0 + 0.1 = 0.1
270        assert_relative_eq!(smoothed[[0, 0]], 0.9, epsilon = 1e-5);
271        assert_relative_eq!(smoothed[[0, 1]], 0.1, epsilon = 1e-5);
272        assert_relative_eq!(smoothed[[1, 0]], 0.1, epsilon = 1e-5);
273        assert_relative_eq!(smoothed[[1, 1]], 0.9, epsilon = 1e-5);
274    }
275
276    #[test]
277    fn test_cross_entropy_loss() {
278        let ls = LabelSmoothing::new(0.1, 3)
279            .expect("LabelSmoothing::new succeeds in test_cross_entropy_loss");
280        let labels = array![0.0, 1.0, 0.0];
281        let logits = array![1.0, 2.0, 0.5];
282
283        let loss = ls
284            .cross_entropy_loss(&logits, &labels, 1e-8)
285            .expect("cross_entropy_loss succeeds in test_cross_entropy_loss");
286
287        // Loss should be positive and finite
288        assert!(loss > 0.0 && loss.is_finite());
289    }
290
291    #[test]
292    fn test_regularizer_trait() {
293        let ls = LabelSmoothing::new(0.1, 3)
294            .expect("LabelSmoothing::new succeeds in test_regularizer_trait");
295        let params = array![[1.0, 2.0], [3.0, 4.0]];
296        let mut gradients = array![[0.1, 0.2], [0.3, 0.4]];
297        let original_gradients = gradients.clone();
298
299        let penalty = ls
300            .apply(&params, &mut gradients)
301            .expect("ls.apply succeeds in test_regularizer_trait");
302
303        // Penalty should be zero
304        assert_eq!(penalty, 0.0);
305
306        // Gradients should be unchanged
307        assert_eq!(gradients, original_gradients);
308    }
309}