8장. PEFT와 LoRA 학습
Qwen 에 LoRA 붙이기와 하이퍼파라미터
안경점에서 렌즈를 맞출 때는 "어느 눈에(어디에), 도수를 얼마로(얼마나 강하게)"를 정합니다. LoRA 도 붙이기 전에 "어느 층에(target_modules), 필름 두께를 얼마로(r), 얼마나 세게(lora_alpha)"를 설정표(LoraConfig)에 적습니다. 그다음 get_peft_model 이 모델 안의 해당 층마다 필름을 한 번에 붙입니다.
그림의 왼쪽 칸이 Attention 의 선형층 네 곳, 가운데 칸이 FFN 의 선형층 세 곳입니다. 1일차에 본 Qwen 은 이 일곱 곳이 한 층에 있고, 층이 24개 쌓여 있습니다. 오른쪽 칸처럼 한 층에 붙는 LoRA 파라미터를 모두 더하면 366,592 개이고, 24층이면 8,798,208 개입니다. 실습 1의 print_trainable_parameters() 가 찍는 값이 바로 이것입니다.
LoRA 를 붙이는 일곱 곳
| 이름 | 위치 | 1일차 개념 | 입력 → 출력 | r = 16 일 때 LoRA 수 |
|---|---|---|---|---|
q_proj | Attention | W_Q | 896 → 896 | 16 × (896 + 896) = 28,672 |
k_proj | Attention | W_K | 896 → 128 | 16 × (896 + 128) = 16,384 |
v_proj | Attention | W_V | 896 → 128 | 16 × (896 + 128) = 16,384 |
o_proj | Attention | W_O | 896 → 896 | 28,672 |
gate_proj | FFN | Gated FFN | 896 → 4864 | 16 × (896 + 4864) = 92,160 |
up_proj | FFN | Gated FFN | 896 → 4864 | 92,160 |
down_proj | FFN | Gated FFN | 4864 → 896 | 92,160 |
k_proj 와 v_proj 의 출력이 128 인 것은 Qwen 이 Key·Value 머리를 2개(2 × 64)만 두고 여럿이 나눠 쓰기(GQA) 때문입니다. 5장에서 본 구조입니다.
LoRA 하이퍼파라미터 가이드
하이퍼파라미터(hyperparameter)는 학습으로 정해지지 않고 사람이 미리 정하는 설정값입니다.
| 설정 | 뜻 | 흔히 쓰는 값 | 조정 방향 |
|---|---|---|---|
r | LoRA 랭크(필름 두께) | 8, 16, 32 | 과제가 복잡하거나 데이터가 많으면 키운다 |
lora_alpha | 배율(alpha ÷ r) | r 의 1~2배 | r 을 바꿀 때 함께 바꾼다 |
lora_dropout | LoRA 길의 dropout | 0.0~0.1 | 데이터가 적어 과적합되면 키운다 |
target_modules | 붙일 층 | 모든 선형층("all-linear") | 파라미터를 줄이려면 Attention 만 |
| 학습률 | 한 걸음의 보폭 | 1e-4 ~ 3e-4 | Full Fine-tuning(1e-5 수준)보다 10배쯤 크게 |
| epoch | 문제집 바퀴 수 | 1~5 | 검증 손실이 다시 오르기 시작하면 멈춘다 |
처음에는 "모든 선형층 + r = 16 + alpha = 32 + 학습률 2e-4"로 시작하고, 검증 결과를 보며 고치는 것이 무난합니다. 실습도 이 값을 씁니다.
짧은 예시
# Qwen 한 층에서 LoRA 가 붙는 일곱 곳의 (입력 칸, 출력 칸)
shapes = {
"q_proj": (896, 896), "k_proj": (896, 128), "v_proj": (896, 128), "o_proj": (896, 896),
"gate_proj": (896, 4864), "up_proj": (896, 4864), "down_proj": (4864, 896),
}
r, layers = 16, 24
# 층마다 r × (입력 + 출력)을 더한다
per_layer = sum(r * (d_in + d_out) for d_in, d_out in shapes.values())
total = per_layer * layers
print(f"한 층: {per_layer:,}개, 24층: {total:,}개") # 결과: 한 층: 366,592개, 24층: 8,798,208개
# 기반 모델 494,032,768개에 LoRA 를 더한 것이 all params
all_params = 494_032_768 + total
print(f"all params: {all_params:,}, trainable%: {total / all_params * 100:.4f}")
# 결과: all params: 502,830,976, trainable%: 1.7497이 코드가 뜻하는 것
안경 렌즈 일곱 장의 넓이를 하나씩 더해 한 층의 필름 넓이를 구하고, 24층을 곱해 전체를 구한 코드입니다. 여기서 렌즈 크기표가 shapes, 필름 두께가 r 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
r * (d_in + d_out) | 한 곳의 A·B 칸 수 | 렌즈 한 장의 넓이 |
sum(... for ... in shapes.values()) | 일곱 곳을 더한다 | 한 층의 렌즈 넓이 |
per_layer * layers | 24층을 곱한다 | 모델 전체 |
total / all_params * 100 | 학습 비율(%) | 전체 중 몇 % 를 고치나 |
손계산 8,798,208 과 1.7497% 가 실습 1의 print_trainable_parameters() 출력과 한 자리도 다르지 않게 같습니다. 모델을 불러오지 않고도 층 크기만 알면 LoRA 크기를 미리 알 수 있습니다.
주의할 점
r을 두 배로 키우면 LoRA 파라미터도 정확히 두 배가 됩니다. 프로젝트에서 r = 4, 8, 16, 32 를 바꿔 가며 확인합니다.lora_alpha를 그대로 두고r만 키우면 배율(alpha ÷ r)이 작아집니다. 그래서r을 바꿀 때 alpha 도 함께 바꿉니다.- 같은 모델에
get_peft_model을 또 부르면 필름 위에 필름을 겹치게 됩니다. 실습 1처럼 확인이 끝나면unload()로 떼어 둡니다.