HOME » Knowledge Distillation

Knowledge Distillation

Distilling the Knowledge in a Neural Network 논문을 다시 읽으며 knowledge distillation의 수식을 정리하고, 이를 PyTorch 코드로 옮겨 수식과 코드가 어떻게 대응되는지 한 줄씩 확인해봅니다. 온도 T를 적용한 softmax부터 soft target에 대한 cross entropy, T² 보정까지 논문의 흐름을 그대로 따라가며 구현합니다.

개요

이동수 대표님의 글을 읽다가 distillation 수식이 잘 기억나지 않아 논문을 다시 읽었고, 읽는 김에 코드로도 함께 구현해봤습니다. 논문의 수식과 알고리즘을 정리하고 실행 과정을 차트로 시각화했는데, 무엇보다 수식과 코드를 정확히 일치시키는 데 가장 많은 시간을 들였습니다. 물론 코딩 어시스턴트의 도움을 많이 받았지만 완성도를 높이는 마무리 작업은 Cursor로 전체 코드를 직접 꼼꼼히 리뷰하며 진행했습니다. 그렇게 해야 제가 원하는 결과물이 나오기 때문이죠. 어시스턴트에 모든 걸 맡겼다면 아마 이 정도 결과는 나오지 않았을 겁니다. 사람이 완전히 이해하고 책임지는 코드로 내놓는 결과물은 여전히 가치가 있고, 이렇게 정리해둔 지식은 앞으로도 두고두고 참조할 수 있습니다.

구조

원래 비전 분야에서 시작된 알고리즘이다 보니 이미지 분류 예시가 많지만, LLM에도 동일하게 적용할 수 있습니다. 다만 LLM에서는 hard loss를 계산할 때 true label 대신 teacher token을 사용한다는 차이가 있는데, 이 부분은 아래 Raschka의 그림이 더 도움이 될 것 같습니다.

논문 수식

그렇다면 Distilling the Knowledge in a Neural Network1 (Hinton et al., 2015) 논문의 수식부터 하나씩 살펴보겠습니다.

(1) 온도 T를 적용한 softmax

\[q_i = \frac{\exp(z_i/T)}{\sum_j \exp(z_j/T)}\]

(2) 로짓 \(z_i\)에 대한 cross-entropy의 gradient

\[\frac{\partial C}{\partial z_i} = \frac{1}{T}\left( q_i - p_i \right) = \frac{1}{T}\left( \frac{e^{z_i/T}}{\sum_j e^{z_j/T}} - \frac{e^{v_i/T}}{\sum_j e^{v_j/T}} \right)\]

(3) T가 로짓 크기에 비해 충분히 클 때의 근사 (\(e^x ≈ 1 + x\))

\[\frac{\partial C}{\partial z_i} \approx \frac{1}{T}\left( \frac{1 + z_i/T}{N + \sum_j z_j/T} - \frac{1 + v_i/T}{N + \sum_j v_j/T} \right)\]

(4) 각 transfer case별로 로짓이 zero-mean이라 가정하면 \((∑_j z_j = ∑_j v_j = 0)\) 수식 (3)은 다음과 같이 단순화

\[\frac{\partial C}{\partial z_i} \approx \frac{1}{NT^2}\left( z_i - v_i \right)\]

코드 구현

이를 코드로 옮기면 다음과 같습니다.

https://github.com/likejazz/knowledge-distillation

먼저 teacher (cumbersome) 모델과 student (distilled) 모델에서 각각 logit을 추출합니다. 논문의 표기를 따라 teacher의 logit은 v, student의 logit은 z로 두겠습니다.

with torch.no_grad():
    v = teacher(data)
z = student(data)

여기서는 설명을 위해 임의의 텐서 2개를 정의했습니다.

>>> v
tensor([[ 0.5032, -7.8997,  2.9085],
        [-5.1517,  3.8629, -3.6802]])
>>> z
tensor([[ 0.9008, -0.0776,  1.1376],
        [-0.4617, -0.1822, -0.2040]])

logit을 온도 T로 나눈 뒤 softmax를 취해 클래스별 확률로 바꿉니다. 이 값이 논문에서 말하는 soft target p입니다.

p = F.softmax(v / T, dim=-1)

T는 온도(temperature)로, LLM에서와 마찬가지로 값이 높을수록 분포가 더 smooth해집니다. T=1일 때와 T=3일 때를 비교해보면 차이가 분명합니다.

>>> F.softmax(v / 1, dim=-1)
tensor([[0.0828, 0.0000, 0.9172],
        [0.0001, 0.9993, 0.0005]])
>>> F.softmax(v / 3, dim=-1)
tensor([[0.3039, 0.0185, 0.6776],
        [0.0438, 0.8846, 0.0716]])

이제 student가 teacher의 분포를 얼마나 잘 따라가는지 cross entropy로 측정합니다. 논문에서 첫 번째 목적 함수를 정의한 부분으로 soft target과의 cross entropy이며, teacher와 student가 동일한 T를 사용합니다.

\[C = -\sum_i p_i \log q_i\]
log_q = F.log_softmax(z / T, dim=-1)
C = -(p * log_q).sum(dim=-1).mean()

각 단계의 값을 확인해보면 다음과 같습니다.

>>> F.softmax(v / T, dim=-1)     # p
tensor([[0.3039, 0.0185, 0.6776],
        [0.0438, 0.8846, 0.0716]])
>>> F.log_softmax(z / T,dim=-1)  # log_q
tensor([[-1.0310, -1.3571, -0.9520],
        [-1.1592, -1.0660, -1.0733]])
>>> -(p * log_q)
tensor([[0.3134, 0.0251, 0.6451],
        [0.0508, 0.9430, 0.0768]])

-log_q는 logit이 클수록 작은 값, 작을수록 큰 값을 갖습니다. 여기에 teacher의 확률 p를 곱해 더하므로, teacher가 높은 확률을 준 클래스에 student도 큰 logit을 주면 loss는 줄어들고, 그렇지 않으면 커집니다. 또한 teacher의 변별력이 떨어져 p가 평평해질수록 C는 전반적으로 커집니다. 아래처럼 student의 logit을 바꿔가며 확인해보면, teacher가 정답으로 본 위치에 가장 큰 logit을 줬을 때 loss가 가장 작은걸 확인할 수 있습니다.

>>> -(p * torch.log_softmax(torch.tensor([[1.0,0.0,0.0],[0.0,1.0,0.0]]), dim=-1)).sum(dim=-1)
tensor([1.2475, 0.6668])
>>> -(p * torch.log_softmax(torch.tensor([[1.0,0.0,0.0],[1.0,0.0,0.0]]), dim=-1)).sum(dim=-1)
tensor([1.2475, 1.5076])
>>> -(p * torch.log_softmax(torch.tensor([[1.0,0.0,0.0],[0.0,0.0,1.0]]), dim=-1)).sum(dim=-1)
tensor([1.2475, 1.4799])

다만 정답이 일치해도, 심지어 logit이 완전히 동일해도 loss가 0이 되지는 않습니다. cross entropy의 최솟값은 0이 아니라 p 자체의 entropy이기 때문입니다. entropy는 변별력이 클수록 줄어듭니다.

마지막으로 온도가 높을 때 gradient가 지나치게 작아지지 않도록 C에 \(T^2\)을 곱해 보정합니다. 수식 (4)에 등장하는 \(1/T^2\) 항을 상쇄하는 것이죠.

soft_loss = (T**2) * C

실험

분류 문제를 풀이해봅시다.

이 같은 데이터가 있다고 가정했을 때 위 코드로 학습 시 다음과 같은 distillation이 잘 진행된 결과를 확인할 수 있습니다.

기타

with torch.no_grad():
    v = teacher(data)
z = student(data)

loss = F.mse_loss(v, z)
# or
# loss = F.cross_entropy(p, q)

distillation 자체가 목적이라면 복잡한 구현 없이 이처럼 두 logit 행렬의 MSE만 구해도 충분합니다. LLM처럼 정답 label이 없는 문제라면 hard loss 역시 필요하지 않습니다.

그러나 Hinton이 KD의 존재 이유로 든 “오답들 사이의 상대적 유사도”를 파악하기가 어렵습니다. 또한 꼬리의 noise까지 똑같이 맞추려고 하는 문제가 있습니다. 만약 softmax MSE를 한다면 반대로 상위 한두 개 클래스만 보고 나머지는 무시하게 됩니다. KL은 그 중간에 있고, T로 그 균형을 조절하는 형태입니다.

Recent Updates:
2026/09/06 초안 작성
자바 알고리즘 인터뷰 파이썬 알고리즘 인터뷰

카카오 코딩 테스트 출제위원이 직접 집필한,
리트코드(LeetCode) 문제로 풀어보는,
구글, 마이크로소프트, 네이버, 카카오
코딩 테스트 완벽 가이드
『자바 알고리즘 인터뷰』,
『파이썬 알고리즘 인터뷰』

© 2000 - Sang Park Except where otherwise noted, content on this site is licensed under a CC BY-NC 4.0.
This site design was brought from Distill. Logo and wiki background image was brought from Bear.