LLM 엔지니어링15장 · 모델 경량화: 양자화, 프루닝, 지식증류8 / 22← 이전목차다음 →MetaContents

15장. 모델 경량화: 양자화, 프루닝, 지식증류

프루닝

정원사는 나무가 잘 자라도록 약한 가지를 잘라 냅니다. 신경망에도 거의 0 에 가까워 결과에 별 영향이 없는 가중치가 많습니다. 프루닝은 그런 가중치를 0 으로 만들거나, 뉴런째 지워 모델을 가볍게 합니다.

흩어진 가중치를 하나씩 0 으로 만드는 비정형 프루닝과, 뉴런이나 채널을 통째로 지우는 정형 프루닝을 견준 두 카드

왼쪽 비정형 프루닝은 크기가 작은 가중치를 하나씩 골라 0 으로 만듭니다. 많이 지워도 정확도가 잘 버티지만, 0 이 흩어져 있을 뿐 행렬의 크기는 그대로라 보통의 GPU 계산에서는 빨라지지도 작아지지도 않습니다. 오른쪽 정형 프루닝은 뉴런(행렬의 행과 열)을 통째로 지워 행렬 자체가 작아지므로 실제로 빨라지지만, 정확도가 더 쉽게 떨어집니다. 여기서 약한 가지가 크기가 작은 가중치입니다.

두 방법 견주기

견줄 것비정형 프루닝정형 프루닝
지우는 단위가중치 하나뉴런, 채널, 층
정확도잘 버틴다더 떨어진다
파일 크기, 속도그대로(희소 형식과 전용 계산이 있어야 준다)실제로 준다
회복지운 뒤 조금 더 학습하면 회복된다다시 학습이 꼭 필요하다

크기로 고르기

Python
import numpy as np

w = np.array([0.8, -0.02, 0.05, -0.6, 0.01, 0.3])
threshold = np.quantile(np.abs(w), 0.5)            # 절댓값이 작은 쪽 절반을 지운다(희소도 50%)
pruned = np.where(np.abs(w) >= threshold, w, 0.0)
print(pruned, "· 0 의 몫:", (pruned == 0).mean())

코드에 나오는 함수

코드쉬운 뜻
np.abs(w)가중치마다 절댓값을 구한다. 부호를 떼고 크기만 본다
np.quantile(값들, 0.5)quantile(분위수)이다. 0.5 면 값들을 크기순으로 늘어놓았을 때 한가운데 값이다. 이보다 작은 쪽이 절반이다
np.where(조건, A, B)조건이 맞는 칸은 A, 아닌 칸은 B 를 고른다. 여기서는 큰 가중치는 그대로, 작은 가중치는 0 이다
(pruned == 0).mean()0 인 칸은 True(1), 아닌 칸은 False(0)로 보고 평균을 낸다. 곧 0 의 몫, 희소도다

이 코드가 뜻하는 것

정원사가 가지 여섯 개를 굵기순으로 세워 보고, 가는 쪽 절반을 잘라 내는 것과 같습니다. 여기서 가지의 굵기가 가중치의 절댓값, 잘라 낼 기준 굵기가 threshold 입니다.

줄하는 일쉽게 말하면
w = np.array([...])가중치 여섯 개를 둔다가지 여섯 개
threshold = np.quantile(np.abs(w), 0.5)크기의 한가운데 값을 기준으로 정한다이보다 가늘면 자른다
pruned = np.where(...)기준보다 작은 가중치를 0 으로 만든다가는 가지를 잘라 낸다
print(pruned, "· 0 의 몫:", ...)남은 가중치와 0 의 몫을 찍는다남은 가지와 자른 몫

결과에서 -0.02, 0.05, 0.01 이 0 이 됐고 0.8, -0.6, 0.3 이 남아 0 의 몫이 0.5 입니다. 비유와 다른 점은, 잘라 낸 가지는 사라지지만 프루닝한 가중치는 0 이 적힌 채 자리를 그대로 차지한다는 것입니다.

작게 줄여서 눈으로 보기

가중치를 절반 지웠을 때 출력과 배열 크기가 어떻게 되는지 봅니다. 입력을 모두 1 로 둔 것은 설명을 위해 정한 것입니다.

Python
import numpy as np

w = np.array([0.8, -0.02, 0.05, -0.6, 0.01, 0.3])
x = np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])         # 모든 입력이 1 이라고 치자(설명용)
threshold = np.quantile(np.abs(w), 0.5)
pruned = np.where(np.abs(w) >= threshold, w, 0.0)
print("원래 출력:", round(float(w @ x), 2), "· 지운 뒤 출력:", round(float(pruned @ x), 2))
print("배열 크기:", w.size, "→", pruned.size)
출력
원래 출력: 0.54 · 지운 뒤 출력: 0.5
배열 크기: 6 → 6
결과읽는 법
원래 출력: 0.54 · 지운 뒤 출력: 0.5가중치를 절반 지웠는데 출력은 0.04 만 달라졌다. 지운 것들이 원래 작았기 때문이다
배열 크기: 6 → 60 을 적었을 뿐 칸 수는 그대로다. 비정형 프루닝만으로는 파일이 작아지지 않는다

w @ x 는 가중치와 입력을 짝지어 곱해 더한 값(내적)이고, .size 는 배열의 칸 수입니다. 위 주의할 점처럼 "몇 % 지웠다"보다 "몇 MB 줄었나"를 재야 하는 까닭이 이것입니다.

주의할 점

  • LLM 은 작은 신경망보다 프루닝에 약합니다. 몇십 %만 지워도 글이 무너질 수 있습니다(실습 6). LLM 의 프루닝은 지운 뒤 다시 학습하는 비용까지 생각해야 합니다.
  • "몇 % 지웠다"보다 "몇 MB 줄고 몇 ms 빨라졌나"를 잽니다. 비정형 프루닝은 0 이 많아져도 크기와 속도가 그대로일 수 있습니다.
← 이전 절15장 · GPTQ 와 AWQ다음 절 →15장 · 지식증류