Skip to content

Commit a4eca33

Browse files
committed
feat: add label smoothing regularization to machine_learning
1 parent 02680c9 commit a4eca33

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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("\nHard 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("\nSmoothed labels (epsilon=0.1):")
170+
print(f" Class 2 -> {smoother.smooth(2)}")
171+
172+
print("\nBatch smoothing for classes [0, 2, 4]:")
173+
print(smoother.smooth_batch([0, 2, 4]))
174+
175+
print("\nCross-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

Comments
 (0)