5장. LoRA 파인튜닝과 DPO 정렬
실습 1 — LoRA 붙여 보기
3장, 4장의 Qwen/Qwen2.5-0.5B-Instruct 를 fp16 으로 불러와 LoRA 를 붙이고, 학습하는 파라미터가 얼마나 되는지, 어느 층에 붙었는지 봅니다. peft 가 설치되어 있어야 합니다. 학습은 하지 않으므로 CPU 로도 됩니다.
1단계. 모델 불러오기
import torch
from transformers import AutoModelForCausalLM
name = "Qwen/Qwen2.5-0.5B-Instruct"
# fp16 으로 불러 메모리를 절반으로 줄인다(4장 "GPU 메모리 어림하기")
model = AutoModelForCausalLM.from_pretrained(name, dtype=torch.float16)
total = sum(p.numel() for p in model.parameters())
print(f"전체 파라미터: {total:,}")
# 어텐션 층의 이름을 확인한다. 첫 블록의 어텐션만 찍어 본다
print(model.model.layers[0].self_attn)전체 파라미터: 494,032,768
Qwen2Attention(
(q_proj): Linear(in_features=896, out_features=896, bias=True)
(k_proj): Linear(in_features=896, out_features=128, bias=True)
(v_proj): Linear(in_features=896, out_features=128, bias=True)
(o_proj): Linear(in_features=896, out_features=896, bias=False)
)2단계. LoRA 붙이기
from peft import LoraConfig, get_peft_model
config = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], task_type="CAUSAL_LM")
model = get_peft_model(model, config) # 원래 가중치는 얼리고 LoRA 를 붙인다
# 학습하는 파라미터 / 전체 / 비율을 한 줄로 알려 준다
model.print_trainable_parameters()
# 학습하는 파라미터의 자료형: peft 가 LoRA 만 float32 로 바꿔 둔다
print({p.dtype for p in model.parameters() if p.requires_grad})trainable params: 1,081,344 || all params: 495,114,112 || trainable%: 0.2184
{torch.float32}3단계. 붙은 자리 보기
# 이름에 lora 가 들어간 파라미터 가운데 첫 블록의 것만 찍는다
for n, p in model.named_parameters():
if "lora" in n and ".layers.0." in n:
print(n.replace("base_model.model.model.", ""), tuple(p.shape))layers.0.self_attn.q_proj.lora_A.default.weight (8, 896)
layers.0.self_attn.q_proj.lora_B.default.weight (896, 8)
layers.0.self_attn.k_proj.lora_A.default.weight (8, 896)
layers.0.self_attn.k_proj.lora_B.default.weight (128, 8)
layers.0.self_attn.v_proj.lora_A.default.weight (8, 896)
layers.0.self_attn.v_proj.lora_B.default.weight (128, 8)
layers.0.self_attn.o_proj.lora_A.default.weight (8, 896)
layers.0.self_attn.o_proj.lora_B.default.weight (896, 8)4단계. r 에 따라 늘어나는 수
from transformers import AutoModelForCausalLM
base = AutoModelForCausalLM.from_pretrained(name, dtype=torch.float16)
for r in [4, 8, 16, 32]:
cfg = LoraConfig(r=r, lora_alpha=2 * r, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], task_type="CAUSAL_LM")
m = get_peft_model(base, cfg, adapter_name=f"r{r}") # 같은 모델에 r 별로 따로 붙여 본다
n = sum(p.numel() for name_, p in m.named_parameters() if f"r{r}" in name_)
print(f"r = {r:2d}: LoRA 파라미터 {n:,}개")
base = m.unload() # 붙인 LoRA 를 떼어 원래 모델로 되돌린다r = 4: LoRA 파라미터 540,672개
r = 8: LoRA 파라미터 1,081,344개
r = 16: LoRA 파라미터 2,162,688개
r = 32: LoRA 파라미터 4,325,376개이 실습이 뜻하는 것
이 실습은 원본 그림은 그대로 두고 그 위에 얇은 투명 필름을 덧대는 것과 같습니다. 그림을 펼쳐 어느 부분이 있는지 보고(1단계), 투명 필름을 붙이고 필름에 드는 칸 수를 세고(2단계), 필름이 붙은 자리를 하나하나 확인하고(3단계), 필름의 겹 수를 바꿔 가며 칸 수가 얼마나 늘어나는지 봅니다(4단계). 여기서 원본 그림이 얼려 둔 기본 가중치, 투명 필름이 LoRA(Low-Rank Adaptation, 낮은 계수 적응)의 A·B 행렬, 겹 수가 r 입니다. 비유와 다른 점은, 필름은 그림을 덮기만 하지만 LoRA 는 B·A 로 계산한 값을 원래 층의 결과에 더한다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 전체 | 494,032,768 | 4장 실습 1과 같은 크기의 모델입니다. fp16(16비트 부동소수점)으로 불러 메모리를 절반만 씁니다 |
| 1. 어텐션 층 | q_proj, k_proj, v_proj, o_proj | LoRA 를 붙일 네 층의 이름입니다. k_proj·v_proj 는 출력이 128로 q_proj·o_proj(896)보다 작습니다 |
| 2. 학습할 수 | 1,081,344 / 0.2184% | 학습하는 숫자는 전체의 0.2% 남짓, 약 458개 가운데 1개꼴입니다 |
| 2. 자료형 | {torch.float32} | 기본 가중치는 fp16 이지만, 학습하는 A·B 는 peft 가 float32 로 바꿔 두었습니다 |
| 3. 붙은 자리 | A (8, 896), B (896, 8)·(128, 8) | 층마다 얇은 행렬 두 장이 붙었습니다. 가운데의 8 이 r 입니다 |
| 4. r 별 수 | 540,672 → 1,081,344 → 2,162,688 → 4,325,376 | r 을 두 배로 하면 학습할 숫자도 정확히 두 배가 됩니다. r 32 도 전체의 1% 가 안 됩니다 |
한 줄로 줄이면, 5억 개 가까운 숫자는 그대로 얼려 두고 108만 개쯤의 얇은 행렬만 학습하게 만든 실습입니다. 아직 아무것도 배우지 않은 상태이고, 실제로 학습해 답이 바뀌는지는 실습 3에서 봅니다. 학습할 숫자가 적다는 것은 비용이 적다는 뜻이지, 결과가 늘 좋다는 뜻은 아닙니다.
코드 설명
- 전체 파라미터는 494,032,768개입니다. 첫 블록의 어텐션에는
q_proj,k_proj,v_proj,o_proj네 층이 있고,k_proj와v_proj는 출력이 896 이 아니라 128 입니다. 키와 밸류를 여러 헤드가 나눠 써서 줄인 짜임(GQA)이라 그렇습니다. 이 이름들이target_modules에 적은 이름입니다. - 학습하는 파라미터는 1,081,344개로 전체(LoRA 를 더한 495,114,112개)의 0.2184% 입니다. 학습하는 파라미터의 자료형은
torch.float32하나뿐입니다. 기본 가중치는 fp16 으로 불러왔지만, peft 가 학습하는 A, B 만 float32 로 바꿔 둔 것입니다. - 첫 블록에
lora_A와lora_B가 층마다 하나씩 붙었습니다. A 는 모두 (8, 896)이고, B 는 층의 출력에 맞춰 q·o 는 (896, 8), k·v 는 (128, 8)입니다. 4단계처럼 r 을 4, 8, 16, 32 로 바꾸면 540,672개, 1,081,344개, 2,162,688개, 4,325,376개로 정확히 두 배씩 늘어, "LoRA 의 설정" 절의 그림과 같습니다. - 4단계의
unload()는 붙였던 LoRA 를 떼어 원래 모델로 되돌립니다. 같은 기본 모델에 여러 설정을 차례로 시험할 때 다시 불러오지 않아도 됩니다.