파인튜닝 원리 (Fine-tuning)¶
사전학습된 LLM을 특정 태스크나 도메인에 맞게 조정하는 과정.
파인튜닝 패러다임¶
사전학습 (Pre-training)
|
v
+------+------+
| |
v v
Full Fine-tuning PEFT (Parameter-Efficient)
| |
v v
SFT LoRA, QLoRA, Prefix-tuning
|
v
RLHF / DPO (선호도 정렬)
Supervised Fine-Tuning (SFT)¶
개념¶
지시-응답 쌍으로 모델 학습.
구현¶
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
from datasets import load_dataset
# 모델 로드
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
tokenizer.pad_token = tokenizer.eos_token
# 데이터셋
def format_instruction(example):
return {
"text": f"### Instruction:\n{example['instruction']}\n\n### Response:\n{example['output']}"
}
dataset = load_dataset("your_dataset")
dataset = dataset.map(format_instruction)
# 토크나이징
def tokenize(example):
return tokenizer(
example["text"],
truncation=True,
max_length=512,
padding="max_length"
)
tokenized_dataset = dataset.map(tokenize)
# 학습
training_args = TrainingArguments(
output_dir="./sft_output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-5,
warmup_steps=100,
logging_steps=10,
save_steps=500,
bf16=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
tokenizer=tokenizer,
)
trainer.train()
RLHF (Reinforcement Learning from Human Feedback)¶
3단계 프로세스¶
Reward Model 학습¶
from transformers import AutoModelForSequenceClassification
class RewardModel(nn.Module):
def __init__(self, base_model):
super().__init__()
self.model = base_model
self.reward_head = nn.Linear(base_model.config.hidden_size, 1)
def forward(self, input_ids, attention_mask):
outputs = self.model(input_ids, attention_mask=attention_mask)
hidden = outputs.last_hidden_state[:, -1, :] # 마지막 토큰
reward = self.reward_head(hidden)
return reward
# 선호도 데이터로 학습
# (prompt, chosen_response, rejected_response)
def reward_loss(chosen_reward, rejected_reward):
return -torch.log(torch.sigmoid(chosen_reward - rejected_reward)).mean()
PPO 학습¶
from trl import PPOTrainer, PPOConfig
ppo_config = PPOConfig(
model_name="sft_model",
learning_rate=1e-5,
batch_size=16,
mini_batch_size=4,
ppo_epochs=4,
)
ppo_trainer = PPOTrainer(
config=ppo_config,
model=sft_model,
ref_model=sft_model, # 참조 모델 (KL 제약)
tokenizer=tokenizer,
dataset=prompt_dataset,
reward_model=reward_model,
)
# 학습 루프
for batch in ppo_trainer.dataloader:
# 응답 생성
response_tensors = ppo_trainer.generate(batch["input_ids"])
# 보상 계산
rewards = reward_model(response_tensors)
# PPO 업데이트
stats = ppo_trainer.step(batch["input_ids"], response_tensors, rewards)
DPO (Direct Preference Optimization)¶
보상 모델 없이 직접 선호도 최적화.
손실 함수¶
\[L_{DPO} = -\log \sigma\left(\beta \log \frac{\pi_\theta(y_w|x)}{\pi_{ref}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{ref}(y_l|x)}\right)\]
구현¶
from trl import DPOTrainer, DPOConfig
dpo_config = DPOConfig(
model_name="sft_model",
learning_rate=5e-7,
beta=0.1, # KL 가중치
per_device_train_batch_size=4,
)
# 데이터셋: prompt, chosen, rejected
dpo_dataset = [
{"prompt": "What is 2+2?", "chosen": "2+2 equals 4.", "rejected": "2+2 is 5."},
...
]
dpo_trainer = DPOTrainer(
model=sft_model,
ref_model=sft_model, # 고정된 참조 모델
args=dpo_config,
train_dataset=dpo_dataset,
tokenizer=tokenizer,
)
dpo_trainer.train()
LoRA (Low-Rank Adaptation)¶
원리¶
가중치 행렬을 저랭크 행렬로 근사.
\[W' = W + \Delta W = W + BA\]
- W: 원본 가중치 (고정)
- B: (d, r) 행렬
- A: (r, k) 행렬
- r << min(d, k): 저랭크
import torch.nn as nn
class LoRALayer(nn.Module):
def __init__(self, original_layer, r=8, alpha=16):
super().__init__()
self.original = original_layer
self.original.weight.requires_grad = False
d_out, d_in = original_layer.weight.shape
# 저랭크 행렬
self.A = nn.Parameter(torch.randn(r, d_in) * 0.01)
self.B = nn.Parameter(torch.zeros(d_out, r))
self.scaling = alpha / r
def forward(self, x):
# 원본 + 저랭크 근사
original_output = self.original(x)
lora_output = (x @ self.A.T @ self.B.T) * self.scaling
return original_output + lora_output
PEFT 라이브러리 사용¶
from peft import LoraConfig, get_peft_model, TaskType
lora_config = LoraConfig(
r=8, # 저랭크 차원
lora_alpha=16, # 스케일링
lora_dropout=0.1,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.06%
QLoRA¶
4bit 양자화 + LoRA.
from transformers import BitsAndBytesConfig
from peft import prepare_model_for_kbit_training
# 4bit 양자화 설정
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# 모델 로드
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto",
)
# LoRA 준비
model = prepare_model_for_kbit_training(model)
# LoRA 적용
lora_config = LoraConfig(
r=64,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
기타 PEFT 기법¶
Prefix Tuning¶
입력에 학습 가능한 "가상 토큰" 추가.
from peft import PrefixTuningConfig
config = PrefixTuningConfig(
num_virtual_tokens=20,
task_type=TaskType.CAUSAL_LM,
)
Prompt Tuning¶
소프트 프롬프트 학습.
from peft import PromptTuningConfig
config = PromptTuningConfig(
num_virtual_tokens=10,
prompt_tuning_init="TEXT",
prompt_tuning_init_text="Classify the sentiment:",
task_type=TaskType.CAUSAL_LM,
)
Adapter¶
레이어 사이에 작은 네트워크 삽입.
from peft import AdaptionPromptConfig
config = AdaptionPromptConfig(
adapter_layers=30,
adapter_len=10,
task_type=TaskType.CAUSAL_LM,
)
파인튜닝 비교¶
| 방법 | 학습 파라미터 | 메모리 | 성능 |
|---|---|---|---|
| Full Fine-tuning | 100% | 높음 | 최고 |
| LoRA (r=8) | ~0.1% | 낮음 | 좋음 |
| QLoRA | ~0.1% | 매우 낮음 | 좋음 |
| Prefix Tuning | <0.1% | 낮음 | 보통 |
| Prompt Tuning | <0.01% | 매우 낮음 | 보통 |
실무 트레이드오프¶
파인튜닝 방법 선택¶
데이터 크기별 권장:
1K 이하: Few-shot prompting (파인튜닝 불필요)
1K-10K: LoRA (r=8-16)
10K-100K: LoRA (r=32-64) 또는 QLoRA
100K+: Full fine-tuning 고려 (리소스 충분 시)
LoRA 하이퍼파라미터¶
# LoRA rank (r) 선택 가이드
lora_configs = {
"minimal": {"r": 8, "alpha": 16}, # 파라미터 최소화
"balanced": {"r": 16, "alpha": 32}, # 일반적 선택
"quality": {"r": 64, "alpha": 128}, # 성능 우선
"full_capacity": {"r": 256, "alpha": 512}, # Full FT 근접
}
# target_modules 선택
# - 최소: ["q_proj", "v_proj"] - 가장 효과적인 레이어
# - 권장: ["q_proj", "k_proj", "v_proj", "o_proj"] - Attention 전체
# - 최대: + ["gate_proj", "up_proj", "down_proj"] - FFN 포함
메모리 요구량¶
| 모델 | Full FT (FP16) | LoRA (FP16) | QLoRA (4bit) |
|---|---|---|---|
| 7B | ~56GB | ~16GB | ~6GB |
| 13B | ~104GB | ~32GB | ~10GB |
| 70B | ~560GB | ~160GB | ~48GB |
RLHF vs DPO 선택¶
RLHF:
+ 복잡한 선호도 학습 가능
+ 보상 함수 재사용 가능
- 구현 복잡 (Reward Model + PPO)
- 학습 불안정
- 하이퍼파라미터 민감
DPO:
+ 구현 단순 (한 번에 학습)
+ 학습 안정적
+ 적은 메모리
- RLHF 대비 약간 성능 낮음 (대부분 무시 가능)
- Reference model 필요
실무 권장: DPO로 시작, 필요시 RLHF
데이터 품질 vs 양¶
# 데이터 품질이 양보다 중요
# 나쁜 예시
dataset_bad = [
{"instruction": "번역해", "output": "ok"}, # 지시 불명확
{"instruction": "...", "output": "..."}, # 짧고 무의미
]
# 좋은 예시
dataset_good = [
{
"instruction": "다음 영어 문장을 한국어로 번역하세요:\n'Hello, how are you?'",
"output": "'안녕하세요, 어떻게 지내세요?'\n\n참고: 'Hello'는 상황에 따라 '안녕', '안녕하세요', '여보세요' 등으로 번역할 수 있습니다."
}
]
# 품질 관리 체크리스트:
# - 지시가 명확한가?
# - 응답이 완전한가?
# - 형식이 일관적인가?
# - 잘못된 정보는 없는가?
흔한 실수¶
| 실수 | 증상 | 해결책 |
|---|---|---|
| Learning rate 너무 높음 | Loss 발산, catastrophic forgetting | 1e-5 ~ 5e-5 시작 |
| 에폭 과다 | 과적합, 일반화 저하 | 1-3 에폭, early stopping |
| 데이터 품질 무시 | 성능 정체 | 품질 필터링, 수작업 검수 |
| Gradient accumulation 미사용 | OOM | accumulation_steps 증가 |
| Mixed precision 미사용 | 학습 느림 | bf16=True 또는 fp16=True |
학습 모니터링¶
from transformers import TrainerCallback
import wandb
class MonitorCallback(TrainerCallback):
def on_log(self, args, state, control, logs=None, **kwargs):
if logs:
# 핵심 메트릭 추적
wandb.log({
"train/loss": logs.get("loss"),
"train/learning_rate": logs.get("learning_rate"),
"train/grad_norm": logs.get("grad_norm"),
})
def on_evaluate(self, args, state, control, metrics=None, **kwargs):
if metrics:
# Eval loss가 증가하면 과적합 신호
wandb.log({"eval/loss": metrics.get("eval_loss")})
# 과적합 징후:
# - train_loss ↓ but eval_loss ↑
# - 생성 품질 저하 (반복, 이상한 출력)
파인튜닝 vs RAG vs 프롬프팅¶
| 상황 | 권장 방법 |
|---|---|
| 최신 정보 필요 | RAG |
| 도메인 용어/스타일 | Fine-tuning |
| 특정 포맷 출력 | Fine-tuning 또는 Few-shot |
| 빠른 실험 | Prompting |
| 지식 주입 | RAG (Fine-tuning은 환각 위험) |