> EulerForge > 튜토리얼 > 8. PPO (RLHF) 훈련

8. PPO (RLHF) 훈련

개요

PPO(Proximal Policy Optimization)는 강화학습 기반 파인튜닝(RLHF)입니다. 모델이 프롬프트에 대한 응답을 직접 생성하고, 보상 모델(RM)이 그 응답에 점수를 매기면, 높은 점수를 받는 응답을 생성하는 방향으로 학습합니다.


PPO의 핵심 구조 — 누가 무엇을 하는가

┌─────────────────────────────────────────────────────────┐
│ 1. 정책 모델 (Policy Model) — 우리가 훈련하는 모델       │
│    = SFT 완료 모델 + LoRA                               │
│    역할: 프롬프트를 받아 응답을 생성                      │
│                                                         │
│ 2. 보상 모델 (Reward Model / RewardHead)                │
│    = 별도 RM 훈련으로 만든 "채점관"                      │
│    역할: 정책 모델이 생성한 응답에 점수(스칼라 보상) 부여  │
│                                                         │
│ 3. 참조 모델 (Reference Model)                          │
│    = 정책 모델의 adapter를 비활성화한 버전               │
│    역할: KL divergence 계산 (정책이 원본에서 너무 벗어나지 않도록)  │
└─────────────────────────────────────────────────────────┘

무엇이 훈련되는가?

구성 요소 훈련 여부 설명
정책 모델 (LoRA) O (훈련됨) 높은 보상을 받는 응답을 생성하도록 학습
보상 모델 (RewardHead) X (frozen) 이미 학습 완료된 상태로 사용
참조 모델 X (frozen) adapter disable = SFT 상태의 base model

한 스텝의 흐름

프롬프트 → [정책 모델] → 응답 생성
                         ↓
                   [보상 모델] → 점수 (reward)
                         ↓
               [PPO 알고리즘] → policy LoRA 업데이트
                    │
                    └─ KL penalty: [정책] vs [참조] 차이가 너무 크지 않도록

SFT와 RM은 필수인가?

SFT: 필수

SFT 없이 SFT 후
정책 모델이 의미 없는 응답 생성 정책 모델이 구조화된 응답 생성
보상 모델이 의미 없는 응답에 점수 → 학습 무의미 보상 모델이 유의미한 응답 비교 가능
KL divergence가 빠르게 발산 안정적 학습

RM: 사실상 필수 (random init은 무의미)

코드 상 checkpoint_path: ""이면 랜덤 초기화된 RewardHead를 사용합니다. 이것은: - 무작위 점수를 보상으로 사용 → 의미 없는 방향으로 학습 - 연구/디버깅 목적으로만 존재

실전에서는 반드시 RM 훈련 후 그 체크포인트를 지정해야 합니다.

올바른 파이프라인

SFT (필수) → RM (사실상 필수) → PPO

RM 없이 PPO를 실행하면 코드는 동작하지만, 랜덤 보상으로 학습하므로 결과에 의미가 없습니다.


RM의 크기와 policy의 관계

동일 모델 RM (기본)

# PPO 프리셋
training:
  reward_model:
    model_name: Qwen/Qwen3.5-0.8B-Base   # policy와 동일한 base
    checkpoint_path: outputs/rm_run/final  # RM 훈련 체크포인트

동작: policy 모델의 hidden state에 RewardHead를 붙여 보상 계산. 별도 모델 로드 불필요.

더 큰 모델 RM (고급 — hidden_size 불일치 주의)

원칙적으로 RM은 policy보다 큰 모델이 더 좋은 보상을 제공할 수 있습니다. 그러나 현재 EulerForge의 PPO 구현은 policy 모델의 hidden state를 사용하므로:

현재 제약: RM과 policy는 동일 base 모델이어야 합니다.


전체 파이프라인 순서

Step 1: SFT (필수)
    eulerforge train --preset qwen3.5_0.8b_dense_lora_sft.yml
    → outputs/sft_run/final

Step 2: RM (사실상 필수)
    eulerforge train --preset qwen3.5_0.8b_dense_lora_rm.yml
        --set model_name=outputs/sft_run/final
    → outputs/rm_run/final (reward_head.pt 포함)

Step 3: PPO
    eulerforge train --preset qwen3.5_0.8b_dense_lora_ppo.yml
        --set model_name=outputs/sft_run/final              ← policy = SFT 모델
        --set training.reward_model.checkpoint_path=outputs/rm_run/final  ← RM 체크포인트

프리셋에서의 역할 지정

# configs/presets/qwen3.5_0.8b_dense_lora_ppo.yml

# ── 정책 모델 (Policy) ──
# 이것이 훈련되는 모델. SFT 완료 체크포인트를 지정.
model_name: Qwen/Qwen3.5-0.8B-Base   # 또는 --set model_name=outputs/sft_run/final

# ── 보상 모델 (Reward) ──
training:
  reward_model:
    model_name: Qwen/Qwen3.5-0.8B-Base   # RM의 base 모델 (policy와 동일해야)
    checkpoint_path: ""                    # ← RM 체크포인트 경로 (비우면 random init)
    # 실전에서는 반드시 지정:
    # checkpoint_path: outputs/rm_run/final

# ── 참조 모델 (Reference) ──
# 별도 지정 불필요. policy 모델의 adapter를 disable하여 자동 생성.

사전 요구 사항

RM 없이 PPO를 실행하면 random init RewardHead가 사용됩니다. 이 경우 무작위 보상으로 학습하므로 실전에서는 의미 없습니다. 연구/디버깅 목적으로만 사용하세요.


데이터 포맷

PPO 전용 데이터 (prompt_only)

PPO는 프롬프트만 필요합니다. 응답은 정책 모델이 직접 생성합니다.

{"prompt": "인공지능의 역사에 대해 설명해주세요."}
{"prompt": "파이썬에서 리스트 정렬 방법을 알려주세요."}
data:
  format: raw
  path: data/ppo_1k_raw.jsonl
  task: prompt_only
  max_length: 256

SFT 데이터 재사용

기존 SFT raw 데이터(data/sft_10k_raw.jsonl)의 prompt 컬럼도 사용 가능합니다:

# SFT 데이터에서 prompt만 추출
python -c "
import json
with open('data/sft_10k_raw.jsonl') as f, open('data/ppo_prompts.jsonl', 'w') as out:
    for line in f:
        row = json.loads(line)
        out.write(json.dumps({'prompt': row['prompt']}) + '\n')
"

프리셋

training:
  type: ppo
  lr: 1.0e-6               # SFT/DPO보다 낮은 lr 권장
  ppo:
    clip_range: 0.2         # PPO 클리핑 ε
    kl_coef: 0.1            # KL 페널티 계수 (너무 크면 학습 안 됨, 너무 작으면 발산)
    epochs: 4               # 배치당 PPO 업데이트 에폭
    max_gen_len: 64          # 최대 생성 토큰 수
    temperature: 1.0         # 샘플링 온도

  reward_model:
    checkpoint_path: outputs/rm_run/final  # RM 체크포인트 (필수 지정 권장)

실행

풀 파이프라인 (SFT → RM → PPO)

# Step 1: SFT
eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_sft.yml \
    --set data.format=raw --set data.task=sft \
    --set data.path=data/sft_10k_raw.jsonl \
    --output-dir outputs/ppo_pipeline/01_sft

# Step 2: RM (SFT 모델 기반)
eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_rm.yml \
    --set model_name=outputs/ppo_pipeline/01_sft/final \
    --set data.format=raw --set data.task=preference \
    --set data.path=data/dpo_10k_raw.jsonl \
    --output-dir outputs/ppo_pipeline/02_rm

# Step 3: PPO (SFT policy + RM reward)
eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_ppo.yml \
    --set model_name=outputs/ppo_pipeline/01_sft/final \
    --set training.reward_model.checkpoint_path=outputs/ppo_pipeline/02_rm/final \
    --set data.path=data/ppo_1k_raw.jsonl \
    --output-dir outputs/ppo_pipeline/03_ppo

SFT 데이터로 실행

eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_ppo.yml \
    --set model_name=outputs/sft_run/final \
    --set training.reward_model.checkpoint_path=outputs/rm_run/final \
    --set data.path=data/ppo_prompts.jsonl

주요 메트릭

메트릭 의미 기대
reward_mean 평균 보상 점진적 증가
kl_divergence policy-reference KL 안정적 (폭발하면 kl_coef 증가)
ppo_loss PPO surrogate loss 감소
entropy 생성 다양성 너무 낮아지면 과적합

디버깅 및 트러블슈팅

증상 원인 해결
reward_mean이 변하지 않음 RM random init (checkpoint 미지정) reward_model.checkpoint_path 지정
reward_mean이 초기에 음수 RM 품질 낮음 또는 hidden_size 불일치 RM 재훈련, base 모델 일치 확인
kl_divergence 폭발 lr 너무 높음 또는 kl_coef 너무 낮음 lr 감소, kl_coef 증가
생성 품질 하락 reward hacking (RM 취약점 악용) RM 데이터 다양화, kl_coef 증가
OOM generate + 3× forward (policy, ref, new) batch_size 감소, max_gen_len 축소
SDPA tensor size mismatch generate()에서 right-padding 또는 gradient checkpointing 충돌 자동 처리됨 (left-padding + gc 임시 비활성화)

관련 문서