8장. PEFT와 LoRA 학습
PEFT, 대부분은 얼리고 일부만 학습하기
학교 성적표를 고친다고 해 봅시다. 전 과목 점수를 다시 매기는 대신, 이번 학기에 새로 생긴 과목 한 칸만 채우면 일이 훨씬 줄어듭니다. 다른 칸은 그대로 두고 읽기만 합니다. PEFT(Parameter-Efficient Fine-Tuning)가 이렇게 합니다. 기반 모델의 가중치 대부분을 얼려(freeze) 두고, 적은 수의 파라미터만 학습합니다.
강사자료 그림의 위쪽 줄이 동결(freeze)입니다. 학습 전 0.50 이 학습 후에도 0.50 그대로입니다. 아래쪽 줄이 조정(tune)입니다. 학습 대상으로 정한 가중치는 0.50 에서 0.47 로 바뀝니다. 그림 아래 문장처럼, 두 경우 모두 입력을 처리하는 계산에는 가중치를 씁니다. 동결은 "계산에서 빼기"가 아니라 "고치지 않기"입니다.
동결된 것과 학습하는 것
| 구분 | 순전파 계산에 쓰나 | 그래디언트·옵티마이저 상태 | 값이 바뀌나 | 성적표로 치면 |
|---|---|---|---|---|
| 동결된 가중치 | 쓴다 | 저장하지 않는다 | 바뀌지 않는다 | 이미 채운 과목 칸 |
| 학습하는 파라미터 | 쓴다 | 저장한다 | 바뀐다 | 새로 채우는 칸 |
PEFT 의 좋은 점은 둘입니다. 하나는 학습하는 파라미터가 적어 메모리가 줄어든다는 것입니다. 다른 하나는 기반 모델을 공통으로 두고, 업무별로 작은 학습 결과(어댑터)만 따로 관리할 수 있다는 것입니다.
짧은 예시
import numpy as np
# 가중치 두 개: 첫째는 동결, 둘째는 학습 대상(강사자료 그림의 두 줄)
weights = np.array([0.50, 0.50])
trainable = np.array([False, True]) # 학습할지 표시하는 꼬리표 열
grad = np.array([0.30, 0.30]) # 두 가중치의 그래디언트(설명용 값)
lr = 0.1 # 학습률(보폭)
# 학습 대상인 칸만 "가중치 - 학습률 × 그래디언트"로 고친다
# np.where(조건, 참일 때, 거짓일 때): 표의 열 고르기처럼 칸마다 고른다
updated = np.where(trainable, weights - lr * grad, weights)
print(updated) # 결과: [0.5 0.47]
print("학습 파라미터 비율:", trainable.mean()) # 결과: 학습 파라미터 비율: 0.5이 코드가 뜻하는 것
성적표 두 칸 가운데 한 칸만 고친 코드입니다. 둘 다 같은 그래디언트를 받았지만, 꼬리표가 False 인 칸은 그대로 남습니다. 여기서 성적표가 weights, "고쳐도 되는 칸" 표시가 trainable 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
trainable = np.array([False, True]) | 학습할 칸을 고른다 | 새 과목 칸에만 표시 |
weights - lr * grad | 0.50 − 0.1 × 0.30 = 0.47 | 한 걸음 고치기 |
np.where(trainable, ..., weights) | 표시된 칸만 새 값으로 | 나머지 칸은 그대로 |
0.47 은 강사자료 그림의 "학습 후" 값과 같게 맞춘 것입니다. 실제 PyTorch 에서는 동결한 가중치에 requires_grad=False 가 붙어 그래디언트를 아예 만들지 않으므로, 그래디언트와 옵티마이저 상태의 메모리까지 아낍니다.
주의할 점
- 동결된 가중치도 순전파에 쓰이므로 GPU 에 올라가 있어야 합니다. PEFT 가 줄이는 것은 주로 그래디언트와 옵티마이저 상태입니다. 기반 모델 자체를 작게 만드는 것은 9장의 QLoRA(양자화)입니다.
- 학습하는 파라미터가 적다고 학습 시간이 꼭 그만큼 줄지는 않습니다. 순전파와 역전파는 여전히 모델 전체를 지나갑니다.