콘텐츠로 이동
Data Prep
상세

파인튜닝 원리 (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)

개념

지시-응답 쌍으로 모델 학습.

Input:  "### Instruction: 다음 문장을 영어로 번역해줘.\n### Input: 안녕하세요\n### Response:"
Target: "Hello"

구현

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단계 프로세스

1. SFT: 지시 따르기 학습
2. Reward Model: 인간 선호도 학습
3. PPO: 보상 최대화 강화학습

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은 환각 위험)

참고 자료