44. Knowledge Distillation Loss (Soft + Hard Target)
Problem
Distillation trains a smaller/cheaper "student" model to match a larger "teacher" model's behavior, so the student can serve at a fraction of the teacher's cost/latency. The classic recipe blends two signals: a "soft" loss that pulls the student's output distribution toward the teacher's full softened probability distribution (encoding which wrong classes the teacher considers "less wrong"), and a "hard" loss against the ground-truth labels the model must ultimately get right.
Implement the combined loss: a temperature-softened KL divergence term plus a standard cross-entropy term, weighted by alpha.
Source: src/44_knowledge_distillation_loss.py
def distillation_loss(
student_logits: torch.Tensor,
teacher_logits: torch.Tensor,
labels: torch.Tensor,
temperature: float = 2.0,
alpha: float = 0.5,
) -> torch.Tensor:
...
Examples:
>>> student = torch.randn(4, 5, requires_grad=True)
>>> teacher = torch.randn(4, 5)
>>> labels = torch.randint(0, 5, (4,))
>>> loss = distillation_loss(student, teacher, labels)
>>> loss.shape
torch.Size([])
Step-by-Step Approach
- Soften both distributions by dividing logits by the temperature
Tbefore the softmax — this widens the probability mass on non-argmax classes, exposing the teacher's relative confidence across wrong answers ("dark knowledge"). - Compute the teacher's softened probabilities with
F.softmax(teacher_logits / T, dim=-1)and the student's softened log-probabilities withF.log_softmax(student_logits / T, dim=-1). - Feed both into
F.kl_div(student_log_probs, teacher_probs, reduction="batchmean")— note the argument order: PyTorch'skl_divtakes (input=log-probs, target=probs). - Rescale the KL term by
T²: softened gradients shrink by roughly1/T², so this keeps the soft loss's gradient magnitude comparable to the hard loss's regardless of what temperature was chosen. - Compute the hard loss as plain
F.cross_entropy(student_logits, labels)at the original (unsoftened) logits. - Combine as
alpha * soft_loss + (1 - alpha) * hard_lossand return the scalar.
The key insight is that temperature scaling and the T² gradient correction aren't optional details — without them the soft-target signal either vanishes (too little useful gradient at low T) or is scaled inconsistently against the hard-label term as you tune T.
Reference solution
def distillation_loss(student_logits, teacher_logits, labels, temperature=2.0, alpha=0.5):
# soft loss: KL(teacher || student) at temperature T
teacher_probs = F.softmax(teacher_logits / temperature, dim=-1)
student_log_probs = F.log_softmax(student_logits / temperature, dim=-1)
soft_loss = F.kl_div(student_log_probs, teacher_probs, reduction="batchmean")
# T^2 rescale: softened gradients shrink by ~1/T^2, this corrects for it
soft_loss = soft_loss * (temperature ** 2)
# hard loss: standard supervised cross-entropy at original logits
hard_loss = F.cross_entropy(student_logits, labels)
return alpha * soft_loss + (1.0 - alpha) * hard_loss
Key Functions & Tricks
F.log_softmax/F.softmax— computed at temperature-scaled logits, not the raw logits, to soften the distributions.F.kl_div(..., reduction="batchmean")— PyTorch's KL divergence expects log-probabilities as input and probabilities as target;batchmeandivides by batch size only, matching the textbook KD formula (notmean, which would also divide by the class count).F.cross_entropy— combines log-softmax and negative log-likelihood internally; takes raw logits and integer class labels directly.temperature ** 2gradient correction — the detail most likely to be missed if implementing this from memory.
How to Recognize This Pattern
The signal is "train a cheaper model to imitate a more expensive one's output distribution, not just its hard predictions." Variations include distilling on intermediate hidden states or attention maps instead of (or in addition to) output logits, distilling across different architectures (as in transformer-to-recurrent distillation), or annealing alpha/temperature over training. Common pitfalls: forgetting the T² rescale (soft loss ends up far weaker than intended at high T), passing raw probabilities instead of log-probabilities into F.kl_div's first argument (silently wrong gradients, no error), and accidentally letting gradients flow into the teacher (it should always be run under torch.no_grad() or with logits already detached).