11. LoRA로 LLM 파인튜닝
만드는 것: 오픈소스 LLM을 LoRA/QLoRA로 지시 데이터에 파인튜닝(SFT)한다. 소비자 GPU 한 장으로 수B 파라미터 모델을 조정하는, 현재 가장 보편적인 LLM 커스터마이징 레시피다.
선행 지식: LoRA 수식 유도, QLoRA, SFT 데이터 설계
원리 요약
가중치
1. LoRA를 손으로 먼저 — 30줄 구현
라이브러리를 쓰기 전에, LoRA가 얼마나 단순한지 직접 확인한다.
import torch
import torch.nn as nn
class LoRALinear(nn.Module):
"""기존 Linear를 감싸 W를 동결하고 BA만 학습한다."""
def __init__(self, base: nn.Linear, r=8, alpha=16):
super().__init__()
self.base = base
for p in self.base.parameters():
p.requires_grad = False # W, b 동결
self.A = nn.Parameter(torch.randn(r, base.in_features) * 0.01)
self.B = nn.Parameter(torch.zeros(base.out_features, r)) # B=0 → 초기엔 ΔW=0
self.scale = alpha / r
def forward(self, x):
return self.base(x) + self.scale * (x @ self.A.T @ self.B.T)
def merge(self):
"""추론용: ΔW를 W에 합쳐 지연 없는 일반 Linear로 되돌린다."""
self.base.weight.data += self.scale * (self.B @ self.A)
return self.baseB를 0으로 초기화하는 이유: 학습 시작 시점에 모델이 원본과 정확히 같아야, 사전학습 성능에서 출발해 점진적으로 이동한다.
2. 실전: TRL + PEFT로 SFT
"""QLoRA SFT — 단일 GPU에서 LLM 지시 튜닝.
의존성: pip install transformers peft trl datasets bitsandbytes accelerate
"""
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig
from trl import SFTTrainer, SFTConfig
BASE = "Qwen/Qwen2.5-1.5B-Instruct" # 예시. GPU 여유에 따라 더 큰 모델로 교체
def main():
# 1) 4비트 양자화 로드 (QLoRA). LoRA만 쓰려면 quantization_config를 빼면 된다.
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # 정규분포 가중치에 최적화된 4비트 형식
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
BASE, quantization_config=bnb, device_map="auto")
tok = AutoTokenizer.from_pretrained(BASE)
# 2) LoRA 설정 — 어텐션과 MLP의 선형층을 타깃으로
lora = LoraConfig(
r=16, lora_alpha=32, lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
task_type="CAUSAL_LM",
)
# 3) 데이터 — messages(chat) 형식이면 SFTTrainer가 모델의 챗 템플릿을 자동 적용
ds = load_dataset("HuggingFaceH4/no_robots", split="train") # 사람 작성 지시 데이터
# 4) 학습
cfg = SFTConfig(
output_dir="./sft-out",
num_train_epochs=1,
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 유효 배치 16
learning_rate=2e-4, # LoRA는 풀 파인튜닝보다 큰 lr을 쓴다
lr_scheduler_type="cosine",
warmup_ratio=0.03,
max_length=1024,
bf16=True,
logging_steps=20,
save_strategy="epoch",
gradient_checkpointing=True, # 활성값 메모리 절약 (재계산과 교환)
report_to="none",
)
trainer = SFTTrainer(model=model, args=cfg, train_dataset=ds,
processing_class=tok, peft_config=lora)
trainer.train()
trainer.save_model("./sft-out/final") # 어댑터만 저장된다 (수십 MB)
if __name__ == "__main__":
main()3. 추론과 병합
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
base = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.bfloat16,
device_map="auto")
model = PeftModel.from_pretrained(base, "./sft-out/final")
tok = AutoTokenizer.from_pretrained(BASE)
messages = [{"role": "user", "content": "사내 회의록 요약 규칙을 3가지로 정리해줘"}]
inputs = tok.apply_chat_template(messages, add_generation_prompt=True,
return_tensors="pt").to(model.device)
out = model.generate(inputs, max_new_tokens=300, temperature=0.7, do_sample=True)
print(tok.decode(out[0][inputs.shape[1]:], skip_special_tokens=True))
# 배포 시: 어댑터를 병합해 단일 모델로 저장 → vLLM 등에서 바로 서빙
merged = model.merge_and_unload()
merged.save_pretrained("./sft-merged")
tok.save_pretrained("./sft-merged")실무 감각
언제 파인튜닝하고 언제 안 하는가. 형식·문체·도메인 어휘를 일관되게 따르게 하는 데는 SFT가 강하다. 반면 새로운 지식 주입에는 비효율적이다 — 최신 정보·사내 문서 참조는 RAG가 먼저다. 프롬프트 엔지니어링 → RAG → 파인튜닝 순으로 시도하는 것이 비용 순서다.
데이터 품질 > 수량. 잘 만든 수천 건이 긁어모은 수십만 건을 이긴다. 응답 형식이 들쭉날쭉한 데이터로 SFT하면 모델도 들쭉날쭉해진다. (SFT 데이터 설계)
손실은 응답에만. 챗 SFT에서는 프롬프트 토큰을 손실에서 제외(마스킹)하는 것이 표준이다 — TRL이 챗 템플릿 기반으로 처리해 준다. 직접 루프를 짤 때 놓치기 쉬운 지점이다.
하이퍼파라미터 출발점. r=16, alpha=2r, lr=1e-4~3e-4, 1~3 에포크. 과적합 신호(응답이 데이터 말투를 앵무새처럼 복제)가 보이면 에포크·r을 줄인다.
확장 과제
- 수동 LoRA 검증 — 1절의
LoRALinear를 09의 GPT의qkv에 끼워 파인튜닝하고, 전체 파인튜닝과 손실 곡선을 비교하라. - r 스윕 — r=4/16/64로 학습해 성능과 어댑터 크기를 비교하라. 어디서 포화되는가?
- 평가 자동화 — 파인튜닝 전후 모델에 같은 프롬프트 50개를 넣고, 더 강한 LLM에게 어느 쪽 응답이 나은지 채점시켜(LLM-as-judge) 승률을 계산하라. (LLM 평가)
- DPO 이어가기 — SFT 모델에 선호 쌍 데이터(
trl의DPOTrainer)로 DPO를 이어 붙여라. (DPO 유도)
다음
모델을 바꾸지 않고 지식을 주입하는 다른 축 → 12. RAG 시스템 구축