LLM 파인튜닝 실전 가이드: LoRA부터 SFT까지

파인튜닝이란?

파인튜닝(Fine-tuning)은 사전학습된 LLM을 특정 도메인이나 작업에 맞게 추가 학습시키는 과정입니다. 비유하자면, 의대를 졸업한 의사(사전학습)가 피부과 전문의 과정(파인튜닝)을 거치는 것과 같습니다. 범용 지식은 유지하면서 특정 영역의 전문성을 높이는 것이 핵심입니다.

이 글에서는 파인튜닝을 언제 해야 하는지, 어떤 방식이 있는지, 그리고 LoRA/QLoRA로 실제 학습하는 코드까지 다룹니다.

파인튜닝 vs 프롬프트 엔지니어링 vs RAG

LLM의 답변을 개선하는 방법은 세 가지입니다. 상황에 따라 적절한 전략을 선택해야 합니다.

방법비용적합한 상황한계
프롬프트 엔지니어링낮음빠른 프로토타이핑, 일반 작업복잡한 형식/톤 유지 어려움
RAG중간최신 정보, 사내 문서 기반 답변검색 품질에 의존
파인튜닝높음특정 도메인 전문화, 일관된 스타일GPU 필요, 데이터 준비 비용

파인튜닝이 필요한 경우: 프롬프트만으로 원하는 형식/톤을 일관되게 유지하기 어려울 때, 특정 도메인 용어나 지식이 필요할 때, 모델 응답 지연을 줄여야 할 때(긴 프롬프트 대신 학습으로 해결)

파인튜닝 방식 비교

Full Fine-tuning vs PEFT

Full Fine-tuning은 모델의 모든 파라미터를 업데이트합니다. 7B 모델 기준 가중치(FP16) 14GB에 옵티마이저 상태와 그래디언트까지 포함하면 약 60GB 이상의 GPU 메모리가 필요합니다. 반면 PEFT(Parameter-Efficient Fine-Tuning)는 소수의 파라미터만 학습하여 비용을 크게 줄입니다.

방식학습 파라미터GPU 메모리 (7B 기준)성능특징
Full FT100%~60GB+최고전체 파라미터 업데이트
LoRA0.11%1624GBFull FT에 근접저순위 행렬 삽입
QLoRA0.11%1220GBLoRA와 동등4bit 양자화 + LoRA

LoRA의 원리

LoRA(Low-Rank Adaptation)는 기존 가중치 행렬 W를 직접 수정하지 않고, 작은 저순위 행렬 A와 B를 추가하여 학습합니다. 수식으로 표현하면 W' = W + BA이며, A와 B의 크기는 원래 행렬보다 훨씬 작습니다.

예를 들어 4096×4096 크기의 행렬(약 1,670만 파라미터)에 rank=8의 LoRA를 적용하면, 4096×8 + 8×4096 = 65,536개 파라미터만 학습합니다. 원래의 0.4%만 학습하는 셈입니다.

데이터셋 준비

파인튜닝의 품질은 데이터셋에 달려 있습니다. 주로 두 가지 형식이 사용됩니다.

# Alpaca 형식: 단일 턴 instruction-response
alpaca_data = [
    {
        "instruction": "다음 문장을 공손한 표현으로 바꾸세요.",
        "input": "이거 빨리 처리해.",
        "output": "이 건을 신속하게 처리해 주시면 감사하겠습니다."
    },
    {
        "instruction": "다음 코드의 버그를 수정하세요.",
        "input": "def add(a, b): return a - b",
        "output": "def add(a, b): return a + b  # 뺄셈을 덧셈으로 수정"
    }
]

# ShareGPT 형식: 멀티 턴 대화
sharegpt_data = [
    {
        "conversations": [
            {"from": "human", "value": "Python에서 리스트와 튜플의 차이가 뭔가요?"},
            {"from": "gpt", "value": "리스트는 가변(mutable)이고 튜플은 불변(immutable)입니다..."},
            {"from": "human", "value": "그럼 언제 튜플을 쓰나요?"},
            {"from": "gpt", "value": "값이 변경되면 안 되는 경우에 사용합니다..."}
        ]
    }
]

데이터 품질 체크리스트: 최소 500~1,000개 샘플 준비, instruction과 output의 일관된 톤 유지, 중복 데이터 제거, 출력 길이가 극단적으로 다르지 않도록 필터링.

LoRA 파인튜닝 실전 코드

Hugging Face의 pefttrl 라이브러리를 사용하여 실제 LoRA 파인튜닝을 수행합니다. 아래 예제는 4bit 양자화(QLoRA)를 적용하여 소비자 GPU에서도 실행 가능합니다.

# 필요한 패키지 설치
pip install transformers peft trl datasets bitsandbytes accelerate
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
import torch

# 1. 4bit 양자화 설정 (QLoRA)
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NormalFloat4 양자화
    bnb_4bit_compute_dtype=torch.bfloat16 # 연산 시 bfloat16 사용
)

# 2. 모델과 토크나이저 로드
model_name = "meta-llama/Llama-3.1-8B"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

# 3. LoRA 설정
lora_config = LoraConfig(
    r=16,                  # 저순위 행렬의 랭크 (높을수록 표현력 증가)
    lora_alpha=32,         # 스케일링 팩터 (보통 r의 2배)
    lora_dropout=0.05,     # 과적합 방지용 드롭아웃
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],  # Attention 레이어
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 13,631,488 || all params: 8,043,384,832 || trainable%: 0.1695

# 4. 데이터셋 로드
dataset = load_dataset("json", data_files="train_data.json", split="train")

# 5. SFT 학습 실행
training_config = SFTConfig(
    output_dir="./lora-output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,  # 실효 배치 크기 = 4 × 4 = 16
    learning_rate=2e-4,
    logging_steps=10,
    save_strategy="epoch",
    bf16=True,
)

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=training_config,
    tokenizer=tokenizer,
)
trainer.train()

# 6. LoRA 어댑터 저장 (전체 모델이 아닌 어댑터만 저장)
model.save_pretrained("./lora-adapter")

위 코드에서 핵심 하이퍼파라미터를 정리하면 다음과 같습니다.

파라미터설명
r (rank)8~64높을수록 표현력 증가, 메모리 사용 증가
lora_alphar의 1~2배스케일링 팩터, alpha/r 비율이 중요
learning_rate1e-4 ~ 3e-4Full FT보다 높게 설정
num_train_epochs1~5과적합 주의, 3이 일반적
gradient_accumulation_steps2~8GPU 메모리 부족 시 증가

학습 후 모델 사용

저장된 LoRA 어댑터를 원본 모델에 다시 적용하여 추론에 사용합니다.

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

# 베이스 모델 로드 후 LoRA 어댑터 적용
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
model = PeftModel.from_pretrained(base_model, "./lora-adapter")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

# 추론
inputs = tokenizer("Python에서 데코레이터란?", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=200)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

LoRA 어댑터는 보통 수십 MB에 불과하므로, 원본 모델(수 GB) 대비 저장/배포가 매우 효율적입니다. 하나의 베이스 모델에 용도별 어댑터를 여러 개 만들어 교체할 수도 있습니다.

정리

파인튜닝은 LLM을 특정 도메인에 특화시키는 강력한 방법입니다. 핵심 포인트를 정리하면 다음과 같습니다.

  • 프롬프트로 부족할 때 파인튜닝을 고려하세요 — 먼저 프롬프트 엔지니어링과 RAG를 시도
  • QLoRA로 16~24GB GPU에서 7B 모델 학습 가능 (A100 불필요)
  • 데이터 품질이 핵심 — 500~1,000개 이상의 일관된 고품질 데이터 준비
  • QLoRA: 4bit 양자화로 메모리를 절반 이하로 줄이면서 LoRA와 동등한 성능
  • 학습된 LoRA 어댑터는 수십 MB — 저장/배포/교체가 효율적

이 글이 도움이 되었나요?