optirs_core/regularizers/
label_smoothing.rs1use 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#[derive(Debug, Clone)]
34pub struct LabelSmoothing<A: Float> {
35 alpha: A,
37 num_classes: usize,
39}
40
41impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> LabelSmoothing<A> {
42 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 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 let smoothed = labels.map(|&y| one_minus_alpha * y + smooth_coef * uniform_val);
95
96 Ok(smoothed)
97 }
98
99 pub fn smooth_batch<D>(&self, labels: &Array<A, D>) -> Result<Array<A, D>>
109 where
110 D: Dimension,
111 {
112 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 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 let smoothed = labels.map(|&y| one_minus_alpha * y + smooth_coef * uniform_val);
127
128 Ok(smoothed)
129 }
130
131 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 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 let smoothed_labels = self.smooth_labels(labels)?;
157
158 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
168impl<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 Ok(A::zero())
176 }
177
178 fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
179 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 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 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 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 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 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 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 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(¶ms, &mut gradients)
301 .expect("ls.apply succeeds in test_regularizer_trait");
302
303 assert_eq!(penalty, 0.0);
305
306 assert_eq!(gradients, original_gradients);
308 }
309}