1+ """
2+ Label Smoothing is a regularization technique used during training of
3+ classification models. Instead of using hard one-hot encoded targets
4+ (e.g. [0, 0, 1, 0]), it softens the labels by distributing a small
5+ amount of probability mass (epsilon) uniformly across all classes.
6+
7+ This prevents the model from becoming overconfident and improves
8+ generalization, especially when training data is noisy or limited.
9+
10+ Formula:
11+ smoothed_label = (1 - epsilon) * one_hot + epsilon / num_classes
12+
13+ Reference:
14+ Szegedy et al., "Rethinking the Inception Architecture for Computer
15+ Vision", https://arxiv.org/abs/1512.00567
16+
17+ Example usage:
18+ >>> import numpy as np
19+ >>> smoother = LabelSmoother(num_classes=4, epsilon=0.1)
20+ >>> smoother.smooth(2)
21+ array([0.025, 0.025, 0.925, 0.025])
22+ """
23+
24+ import numpy as np
25+
26+
27+ class LabelSmoother :
28+ """
29+ Applies label smoothing to a one-hot encoded target vector.
30+
31+ Attributes:
32+ num_classes: Total number of classes.
33+ epsilon: Smoothing factor in the range [0.0, 1.0).
34+ 0.0 means no smoothing (standard one-hot).
35+
36+ >>> smoother = LabelSmoother(num_classes=3, epsilon=0.0)
37+ >>> smoother.smooth(1)
38+ array([0., 1., 0.])
39+
40+ >>> smoother = LabelSmoother(num_classes=3, epsilon=0.3)
41+ >>> smoother.smooth(0)
42+ array([0.8, 0.1, 0.1])
43+ """
44+
45+ def __init__ (self , num_classes : int , epsilon : float = 0.1 ) -> None :
46+ """
47+ Initialize LabelSmoother.
48+
49+ Args:
50+ num_classes: Number of target classes (must be >= 2).
51+ epsilon: Smoothing factor. Must satisfy 0.0 <= epsilon < 1.0.
52+
53+ Raises:
54+ ValueError: If num_classes < 2 or epsilon is out of range.
55+
56+ >>> LabelSmoother(num_classes=1, epsilon=0.1)
57+ Traceback (most recent call last):
58+ ...
59+ ValueError: num_classes must be at least 2.
60+
61+ >>> LabelSmoother(num_classes=3, epsilon=1.0)
62+ Traceback (most recent call last):
63+ ...
64+ ValueError: epsilon must be in [0.0, 1.0).
65+ """
66+ if num_classes < 2 :
67+ raise ValueError ("num_classes must be at least 2." )
68+ if not (0.0 <= epsilon < 1.0 ):
69+ raise ValueError ("epsilon must be in [0.0, 1.0)." )
70+ self .num_classes = num_classes
71+ self .epsilon = epsilon
72+
73+ def smooth (self , true_class : int ) -> np .ndarray :
74+ """
75+ Return a smoothed label vector for the given true class index.
76+
77+ Args:
78+ true_class: The index of the correct class (0-indexed).
79+
80+ Returns:
81+ A numpy array of shape (num_classes,) with smoothed probabilities.
82+ All values sum to 1.0.
83+
84+ Raises:
85+ ValueError: If true_class is out of range.
86+
87+ >>> smoother = LabelSmoother(num_classes=4, epsilon=0.1)
88+ >>> smoother.smooth(2)
89+ array([0.025, 0.025, 0.925, 0.025])
90+
91+ >>> smoother.smooth(0)
92+ array([0.925, 0.025, 0.025, 0.025])
93+
94+ >>> float(round(smoother.smooth(1).sum(), 10))
95+ 1.0
96+
97+ >>> smoother.smooth(5)
98+ Traceback (most recent call last):
99+ ...
100+ ValueError: true_class index 5 is out of range for 4 classes.
101+ """
102+ if not (0 <= true_class < self .num_classes ):
103+ raise ValueError (
104+ f"true_class index { true_class } is out of range "
105+ f"for { self .num_classes } classes."
106+ )
107+ # Start with uniform distribution weighted by epsilon
108+ labels = np .full (self .num_classes , self .epsilon / self .num_classes )
109+ # Add the remaining probability mass to the true class
110+ labels [true_class ] += 1.0 - self .epsilon
111+ return labels
112+
113+ def smooth_batch (self , true_classes : list [int ]) -> np .ndarray :
114+ """
115+ Return smoothed label vectors for a batch of true class indices.
116+
117+ Args:
118+ true_classes: List of true class indices.
119+
120+ Returns:
121+ A numpy array of shape (batch_size, num_classes).
122+
123+ >>> smoother = LabelSmoother(num_classes=3, epsilon=0.3)
124+ >>> smoother.smooth_batch([0, 2])
125+ array([[0.8, 0.1, 0.1],
126+ [0.1, 0.1, 0.8]])
127+ """
128+ return np .array ([self .smooth (c ) for c in true_classes ])
129+
130+
131+ def cross_entropy_loss (
132+ smoothed_labels : np .ndarray , predicted_probs : np .ndarray
133+ ) -> float :
134+ """
135+ Compute cross-entropy loss between smoothed labels and predicted
136+ probability distribution.
137+
138+ Args:
139+ smoothed_labels: Target distribution, shape (num_classes,).
140+ predicted_probs: Predicted probabilities, shape (num_classes,).
141+ Values must be in (0, 1] and sum to 1.
142+
143+ Returns:
144+ Scalar cross-entropy loss value.
145+
146+ >>> import numpy as np
147+ >>> labels = np.array([0.025, 0.025, 0.925, 0.025])
148+ >>> preds = np.array([0.01, 0.01, 0.97, 0.01])
149+ >>> round(cross_entropy_loss(labels, preds), 4)
150+ 0.3736
151+ """
152+ # Clip to avoid log(0)
153+ predicted_probs = np .clip (predicted_probs , 1e-12 , 1.0 )
154+ return float (- np .sum (smoothed_labels * np .log (predicted_probs )))
155+
156+
157+ if __name__ == "__main__" :
158+ import doctest
159+
160+ doctest .testmod (verbose = True )
161+
162+ print ("\n --- Label Smoothing Demo ---" )
163+ smoother = LabelSmoother (num_classes = 5 , epsilon = 0.1 )
164+
165+ print ("\n Hard one-hot (no smoothing, epsilon=0.0):" )
166+ hard = LabelSmoother (num_classes = 5 , epsilon = 0.0 )
167+ print (f" Class 2 -> { hard .smooth (2 )} " )
168+
169+ print ("\n Smoothed labels (epsilon=0.1):" )
170+ print (f" Class 2 -> { smoother .smooth (2 )} " )
171+
172+ print ("\n Batch smoothing for classes [0, 2, 4]:" )
173+ print (smoother .smooth_batch ([0 , 2 , 4 ]))
174+
175+ print ("\n Cross-entropy loss with smoothed target vs confident prediction:" )
176+ smoothed = smoother .smooth (2 )
177+ confident_pred = np .array ([0.01 , 0.01 , 0.96 , 0.01 , 0.01 ])
178+ print (f" Loss = { cross_entropy_loss (smoothed , confident_pred ):.4f} " )
0 commit comments