파인튜닝이란?
파인튜닝(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 FT | 100% | ~60GB+ | 최고 | 전체 파라미터 업데이트 |
| LoRA | Full FT에 근접 | 저순위 행렬 삽입 | ||
| QLoRA | LoRA와 동등 | 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의 peft와 trl 라이브러리를 사용하여 실제 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_alpha | r의 1~2배 | 스케일링 팩터, alpha/r 비율이 중요 |
learning_rate | 1e-4 ~ 3e-4 | Full FT보다 높게 설정 |
num_train_epochs | 1~5 | 과적합 주의, 3이 일반적 |
gradient_accumulation_steps | 2~8 | GPU 메모리 부족 시 증가 |
학습 후 모델 사용
저장된 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 — 저장/배포/교체가 효율적