534 lines
17 KiB
Markdown
534 lines
17 KiB
Markdown
|
|
# SFT 개선 방안 심층 조사
|
|||
|
|
|
|||
|
|
> 프로젝트: 1B Korean LLM SFT (188k 샘플, 8×B200, FP8)
|
|||
|
|
> 현재 구현: NEFTune, dynamic padding, gradient checkpointing, cosine LR, BF16+FP8
|
|||
|
|
> 작성일: 2026-02-26
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Curriculum Learning (교육과정 학습)
|
|||
|
|
|
|||
|
|
### 개념
|
|||
|
|
쉬운 샘플에서 어려운 샘플 순서로 학습하여 수렴 속도와 최종 성능 향상.
|
|||
|
|
|
|||
|
|
### 구현 방법
|
|||
|
|
|
|||
|
|
**방법 A: Perplexity 기반 정렬 (권장)**
|
|||
|
|
```python
|
|||
|
|
# scripts/compute_difficulty.py
|
|||
|
|
import torch, json
|
|||
|
|
from pathlib import Path
|
|||
|
|
from tokenizers import Tokenizer
|
|||
|
|
from model import LLM
|
|||
|
|
|
|||
|
|
def compute_sample_perplexity(model, tokenizer, data_path, output_path, device="cuda:0"):
|
|||
|
|
"""현재 pretrain 모델로 각 샘플의 output perplexity 계산"""
|
|||
|
|
model.eval()
|
|||
|
|
results = []
|
|||
|
|
|
|||
|
|
with open(data_path) as f:
|
|||
|
|
samples = [json.loads(line) for line in f]
|
|||
|
|
|
|||
|
|
with torch.no_grad():
|
|||
|
|
for i, sample in enumerate(samples):
|
|||
|
|
# conversation에서 assistant turn만 추출
|
|||
|
|
messages = sample["messages"]
|
|||
|
|
# 전체 시퀀스 토크나이즈
|
|||
|
|
full_text = tokenizer.encode(
|
|||
|
|
"".join(m["content"] for m in messages)
|
|||
|
|
)
|
|||
|
|
input_ids = torch.tensor([full_text.ids[:4096]], device=device)
|
|||
|
|
|
|||
|
|
logits = model(input_ids)
|
|||
|
|
# response 토큰에 대한 CE loss = perplexity proxy
|
|||
|
|
shift_logits = logits[:, :-1, :]
|
|||
|
|
shift_labels = input_ids[:, 1:]
|
|||
|
|
loss = torch.nn.functional.cross_entropy(
|
|||
|
|
shift_logits.reshape(-1, shift_logits.size(-1)),
|
|||
|
|
shift_labels.reshape(-1),
|
|||
|
|
reduction='mean'
|
|||
|
|
)
|
|||
|
|
ppl = loss.exp().item()
|
|||
|
|
results.append({"idx": i, "ppl": ppl})
|
|||
|
|
|
|||
|
|
if i % 1000 == 0:
|
|||
|
|
print(f" {i}/{len(samples)} done")
|
|||
|
|
|
|||
|
|
# ppl 오름차순 정렬 = 쉬운 것부터
|
|||
|
|
results.sort(key=lambda x: x["ppl"])
|
|||
|
|
|
|||
|
|
with open(output_path, "w") as f:
|
|||
|
|
json.dump(results, f)
|
|||
|
|
return results
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**방법 B: 길이 기반 (가장 간단)**
|
|||
|
|
- 짧은 응답 → 긴 응답 순서로 정렬
|
|||
|
|
- SFTDataset에서 `__getitem__` 시 정렬된 인덱스 사용
|
|||
|
|
|
|||
|
|
**방법 C: IFD Score**
|
|||
|
|
- Cherry LLM 논문 (2024): `IFD = PPL(output|instruction) / PPL(output)`
|
|||
|
|
- 높은 IFD = instruction이 output 생성을 잘 유도하지 못함 = 어려움
|
|||
|
|
|
|||
|
|
### 실제 효과
|
|||
|
|
- **Curriculum Learning for LLMs (Xu et al., 2024)**: SFT에서 MT-Bench +0.3~0.5점
|
|||
|
|
- **효과 제한적 의견**: Bengio et al.의 원 연구 이후 SFT에서의 효과는 mixed results
|
|||
|
|
- **예상**: ko_ifeval +1~2%, 반복률 변화 미미
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | ko_ifeval +1~2% |
|
|||
|
|
| 구현 복잡도 | 2/5 |
|
|||
|
|
| 소요 시간 | PPL 계산 2~3시간 + 코드 수정 2시간 |
|
|||
|
|
| 적용 가능 | ✅ DataLoader sampler 수정으로 가능 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. "Less is More" 전략 (LIMA, AlpaGasus)
|
|||
|
|
|
|||
|
|
### 핵심 논문
|
|||
|
|
- **LIMA (Zhou et al., 2023)**: 1000개 고품질 > 52k 저품질. 65B 모델에서 검증.
|
|||
|
|
- **AlpaGasus (Chen et al., 2023)**: GPT-4로 품질 점수 → 9k에서 3k 선별 → Alpaca 대비 우수
|
|||
|
|
- **DEITA (Liu et al., 2024)**: complexity + quality + diversity 3축 필터링
|
|||
|
|
|
|||
|
|
### 품질 점수 계산 방법 (외부 API 없이)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# scripts/quality_filter.py
|
|||
|
|
import json, math, torch
|
|||
|
|
import numpy as np
|
|||
|
|
from collections import Counter
|
|||
|
|
|
|||
|
|
def compute_quality_scores(data_path, model, tokenizer, device="cuda:0"):
|
|||
|
|
"""다차원 품질 점수 계산"""
|
|||
|
|
with open(data_path) as f:
|
|||
|
|
samples = [json.loads(line) for line in f]
|
|||
|
|
|
|||
|
|
scored = []
|
|||
|
|
model.eval()
|
|||
|
|
|
|||
|
|
for i, sample in enumerate(samples):
|
|||
|
|
msgs = sample["messages"]
|
|||
|
|
|
|||
|
|
# 1) 길이 점수: 너무 짧거나 너무 긴 건 감점
|
|||
|
|
response = "".join(m["content"] for m in msgs if m["role"] == "assistant")
|
|||
|
|
resp_len = len(response)
|
|||
|
|
len_score = min(resp_len / 500, 1.0) * (1.0 if resp_len < 3000 else 3000 / resp_len)
|
|||
|
|
|
|||
|
|
# 2) 반복 감점: n-gram 반복률
|
|||
|
|
tokens = list(response)
|
|||
|
|
if len(tokens) > 10:
|
|||
|
|
trigrams = [tuple(tokens[j:j+3]) for j in range(len(tokens)-2)]
|
|||
|
|
unique_ratio = len(set(trigrams)) / len(trigrams)
|
|||
|
|
else:
|
|||
|
|
unique_ratio = 1.0
|
|||
|
|
rep_score = unique_ratio
|
|||
|
|
|
|||
|
|
# 3) Perplexity 점수 (중간이 좋음 - 너무 낮으면 trivial, 너무 높으면 noise)
|
|||
|
|
# 사전 계산된 ppl 사용
|
|||
|
|
|
|||
|
|
# 4) Instruction 복잡도: instruction 길이
|
|||
|
|
instruction = "".join(m["content"] for m in msgs if m["role"] == "user")
|
|||
|
|
inst_complexity = min(len(instruction) / 200, 1.0)
|
|||
|
|
|
|||
|
|
# 종합 점수
|
|||
|
|
quality = 0.3 * len_score + 0.3 * rep_score + 0.2 * inst_complexity + 0.2
|
|||
|
|
scored.append({"idx": i, "quality": quality, "sample": sample})
|
|||
|
|
|
|||
|
|
return scored
|
|||
|
|
|
|||
|
|
def select_top_k(scored, k):
|
|||
|
|
"""상위 k개 선별"""
|
|||
|
|
scored.sort(key=lambda x: x["quality"], reverse=True)
|
|||
|
|
return scored[:k]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 권장 샘플 수
|
|||
|
|
- 188k 전체 → **50k~80k** 권장 (상위 30~40%)
|
|||
|
|
- 1B 모델 규모에서는 LIMA처럼 극단적 축소보다 moderate 필터링이 적합
|
|||
|
|
- 근거: AlpaGasus는 ~30% 선별에서 최적, 1B는 65B보다 데이터 의존도 높음
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | 반복률 -30~50%, ko_ifeval +3~5% |
|
|||
|
|
| 구현 복잡도 | 2/5 |
|
|||
|
|
| 소요 시간 | 품질 계산 3~4시간, 필터링 코드 1시간 |
|
|||
|
|
| 적용 가능 | ✅ 데이터 전처리 단계 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Packing (Sequence Packing)
|
|||
|
|
|
|||
|
|
### 개념
|
|||
|
|
짧은 시퀀스들을 하나의 max_seq_len에 패킹하여 padding 낭비 제거.
|
|||
|
|
|
|||
|
|
### 현 프로젝트 상황
|
|||
|
|
- 이미 `dynamic_collate_fn`으로 batch-level dynamic padding 구현됨
|
|||
|
|
- Packing은 그 이상: **여러 샘플을 하나의 시퀀스로 concatenate**
|
|||
|
|
|
|||
|
|
### 주의사항: Cross-contamination
|
|||
|
|
- 패킹된 서로 다른 샘플 간 attention이 흐르면 안 됨
|
|||
|
|
- **해결**: Flash Attention v2의 `cu_seqlens` 파라미터 (varlen attention)
|
|||
|
|
- 또는 block diagonal attention mask
|
|||
|
|
|
|||
|
|
### 구현 방법
|
|||
|
|
```python
|
|||
|
|
# data/packed_sft_dataset.py
|
|||
|
|
class PackedSFTDataset:
|
|||
|
|
"""여러 SFT 샘플을 하나의 시퀀스로 패킹"""
|
|||
|
|
|
|||
|
|
def __init__(self, samples, tokenizer, max_seq_len=4096):
|
|||
|
|
self.packed = []
|
|||
|
|
self.cu_seqlens = [] # Flash Attention varlen용
|
|||
|
|
|
|||
|
|
buffer_ids = []
|
|||
|
|
buffer_labels = []
|
|||
|
|
seq_lens = []
|
|||
|
|
|
|||
|
|
for sample in samples:
|
|||
|
|
ids, labels = self._tokenize(sample, tokenizer)
|
|||
|
|
|
|||
|
|
if len(buffer_ids) + len(ids) > max_seq_len:
|
|||
|
|
# 현재 버퍼 저장
|
|||
|
|
if buffer_ids:
|
|||
|
|
self._save_buffer(buffer_ids, buffer_labels, seq_lens, max_seq_len)
|
|||
|
|
buffer_ids = ids
|
|||
|
|
buffer_labels = labels
|
|||
|
|
seq_lens = [len(ids)]
|
|||
|
|
else:
|
|||
|
|
buffer_ids.extend(ids)
|
|||
|
|
buffer_labels.extend(labels)
|
|||
|
|
seq_lens.append(len(ids))
|
|||
|
|
|
|||
|
|
if buffer_ids:
|
|||
|
|
self._save_buffer(buffer_ids, buffer_labels, seq_lens, max_seq_len)
|
|||
|
|
|
|||
|
|
def _save_buffer(self, ids, labels, seq_lens, max_seq_len):
|
|||
|
|
# Pad to max_seq_len
|
|||
|
|
pad_len = max_seq_len - len(ids)
|
|||
|
|
ids = ids + [0] * pad_len
|
|||
|
|
labels = labels + [-1] * pad_len
|
|||
|
|
|
|||
|
|
# cu_seqlens for varlen flash attention
|
|||
|
|
cu = [0]
|
|||
|
|
for l in seq_lens:
|
|||
|
|
cu.append(cu[-1] + l)
|
|||
|
|
|
|||
|
|
self.packed.append({
|
|||
|
|
"input_ids": torch.tensor(ids),
|
|||
|
|
"labels": torch.tensor(labels),
|
|||
|
|
"cu_seqlens": torch.tensor(cu, dtype=torch.int32),
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 속도 개선 예상
|
|||
|
|
- 현재 평균 시퀀스 길이가 max_seq_len(4096)보다 훨씬 짧다면 **1.5~3× 속도 향상**
|
|||
|
|
- SFT 데이터 특성상 평균 ~1000 토큰이면 ~3× 효율 향상 예상
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | 학습 속도 1.5~3×, 성능 변화 없거나 미미 |
|
|||
|
|
| 구현 복잡도 | 3/5 (Flash Attention varlen 연동 필요) |
|
|||
|
|
| 소요 시간 | 1~2일 |
|
|||
|
|
| 적용 가능 | ⚠️ 모델의 attention 구현이 cu_seqlens 지원해야 함 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Multi-task SFT (도메인별 Loss Weighting)
|
|||
|
|
|
|||
|
|
### 개념
|
|||
|
|
데이터 소스별로 도메인을 분류하고, 도메인별 loss weight를 다르게 적용.
|
|||
|
|
|
|||
|
|
### 현재 데이터 소스 분석 (추정)
|
|||
|
|
- `korean_safe_conv/raw/` 하위: hatespeech, square, evol, yitingxie, gamseong, koalpaca, conversation
|
|||
|
|
- 카테고리: 안전성, QA, 창작, 일반 대화
|
|||
|
|
|
|||
|
|
### 구현 방법
|
|||
|
|
```python
|
|||
|
|
# 도메인 태그를 JSONL에 추가
|
|||
|
|
# {"messages": [...], "domain": "qa"}
|
|||
|
|
# {"messages": [...], "domain": "creative"}
|
|||
|
|
|
|||
|
|
# trainer에서 도메인별 loss weight
|
|||
|
|
DOMAIN_WEIGHTS = {
|
|||
|
|
"qa": 1.0,
|
|||
|
|
"creative": 0.8,
|
|||
|
|
"safety": 1.2,
|
|||
|
|
"code": 1.0,
|
|||
|
|
"math": 1.0,
|
|||
|
|
"conversation": 0.6, # 일반 대화는 낮게
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | 특정 벤치마크 +1~3% |
|
|||
|
|
| 구현 복잡도 | 3/5 |
|
|||
|
|
| 소요 시간 | 도메인 분류 0.5일 + 구현 0.5일 |
|
|||
|
|
| 적용 가능 | ✅ loss 계산 시 weight 곱셈 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Token-level Loss Weighting / Focal Loss
|
|||
|
|
|
|||
|
|
### 개념
|
|||
|
|
모든 response 토큰에 동일 weight 대신, 모델이 예측하기 어려운 토큰에 더 높은 weight.
|
|||
|
|
|
|||
|
|
### Focal Loss 구현
|
|||
|
|
```python
|
|||
|
|
# train/focal_loss.py
|
|||
|
|
import torch
|
|||
|
|
import torch.nn.functional as F
|
|||
|
|
|
|||
|
|
def focal_cross_entropy(logits, targets, gamma=2.0, ignore_index=-1):
|
|||
|
|
"""
|
|||
|
|
Focal loss: down-weight easy tokens, up-weight hard tokens.
|
|||
|
|
Lin et al., "Focal Loss for Dense Object Detection", ICCV 2017.
|
|||
|
|
"""
|
|||
|
|
# Standard CE
|
|||
|
|
ce_loss = F.cross_entropy(
|
|||
|
|
logits.reshape(-1, logits.size(-1)),
|
|||
|
|
targets.reshape(-1),
|
|||
|
|
ignore_index=ignore_index,
|
|||
|
|
reduction='none'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# p_t = probability of correct class
|
|||
|
|
log_pt = -ce_loss
|
|||
|
|
pt = torch.exp(log_pt)
|
|||
|
|
|
|||
|
|
# Focal weight: (1 - p_t)^gamma
|
|||
|
|
focal_weight = (1 - pt) ** gamma
|
|||
|
|
loss = focal_weight * ce_loss
|
|||
|
|
|
|||
|
|
# Mask ignored tokens
|
|||
|
|
mask = (targets.reshape(-1) != ignore_index)
|
|||
|
|
loss = loss[mask].mean()
|
|||
|
|
|
|||
|
|
return loss
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 적용: trainer.py 수정
|
|||
|
|
```python
|
|||
|
|
# trainer.py의 _compute_loss에서
|
|||
|
|
# 기존: F.cross_entropy(logits, targets, ignore_index=-1)
|
|||
|
|
# 변경: focal_cross_entropy(logits, targets, gamma=2.0, ignore_index=-1)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 실제 효과
|
|||
|
|
- SFT에서 focal loss 적용 논문은 제한적
|
|||
|
|
- **SelectIT (Liu et al., 2024)**: token-level selection으로 IFEval +2~4%
|
|||
|
|
- gamma=2.0이 일반적이나 SFT에서는 gamma=1.0~1.5 권장 (너무 강하면 불안정)
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | ko_ifeval +1~3%, 반복률 영향 미미 |
|
|||
|
|
| 구현 복잡도 | 1/5 |
|
|||
|
|
| 소요 시간 | 2시간 |
|
|||
|
|
| 적용 가능 | ✅ loss 함수만 교체 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Data Augmentation for Korean
|
|||
|
|
|
|||
|
|
### 방법 A: Self-Paraphrase
|
|||
|
|
```bash
|
|||
|
|
# 현재 모델(또는 더 큰 모델)로 response 재생성
|
|||
|
|
# instruction은 유지, output만 다양화
|
|||
|
|
# → 동일 instruction에 대한 N개 다른 응답 확보
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 방법 B: Back-translation
|
|||
|
|
```python
|
|||
|
|
# 영어 고품질 데이터 (Alpaca, Dolly, OpenAssistant) → 한국어 번역
|
|||
|
|
# 서버에서 실행 가능한 방법:
|
|||
|
|
# 1) NLLB-200 3.3B (Meta): 오프라인 번역, 1GPU로 실행 가능
|
|||
|
|
# 2) 한국어 특화 모델로 직접 번역 생성
|
|||
|
|
|
|||
|
|
# pip install transformers
|
|||
|
|
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
|||
|
|
|
|||
|
|
model_name = "facebook/nllb-200-3.3B"
|
|||
|
|
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
|||
|
|
model = AutoModelForSeq2SeqLM.from_pretrained(model_name).to("cuda:7") # 여유 GPU 1개 사용
|
|||
|
|
|
|||
|
|
def translate_en_to_ko(text):
|
|||
|
|
inputs = tokenizer(text, return_tensors="pt", max_length=1024, truncation=True).to("cuda:7")
|
|||
|
|
outputs = model.generate(**inputs, forced_bos_token_id=tokenizer.lang_code_to_id["kor_Hang"], max_new_tokens=1024)
|
|||
|
|
return tokenizer.decode(outputs[0], skip_special_tokens=True)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | 다양성 증가, ko_ifeval +2~4% (좋은 소스 데이터 시) |
|
|||
|
|
| 구현 복잡도 | 3/5 |
|
|||
|
|
| 소요 시간 | 번역 파이프라인 1일 + 번역 실행 1~2일 |
|
|||
|
|
| 적용 가능 | ✅ 별도 GPU에서 병렬 실행 가능 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 학습 안정성 개선
|
|||
|
|
|
|||
|
|
### FP8 학습 주의사항
|
|||
|
|
현재 설정: MXFP8 + BF16 기반. 주요 주의점:
|
|||
|
|
|
|||
|
|
1. **Loss spike 방지**
|
|||
|
|
- `max_grad_norm: 1.0` 이미 적용됨 ✅
|
|||
|
|
- LR 2e-5는 보수적 ✅
|
|||
|
|
- 추가: gradient norm 모니터링 + 자동 LR 감소
|
|||
|
|
```python
|
|||
|
|
# 연속 3번 grad_norm > threshold면 lr 반감
|
|||
|
|
if grad_norm > 5.0:
|
|||
|
|
spike_count += 1
|
|||
|
|
if spike_count >= 3:
|
|||
|
|
for pg in optimizer.param_groups:
|
|||
|
|
pg['lr'] *= 0.5
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **Weight Decay**
|
|||
|
|
- 현재 0.01 (적절)
|
|||
|
|
- SFT에서는 0.01~0.05 범위가 표준
|
|||
|
|
|
|||
|
|
3. **Dropout**
|
|||
|
|
- 현재 `dropout: 0.0` — SFT에서는 **0.05~0.1 추가 권장**
|
|||
|
|
- 과적합 방지에 직접적 효과
|
|||
|
|
```yaml
|
|||
|
|
dropout: 0.05 # configs/korean_1b_sft.yaml
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **FP8 amax 설정**
|
|||
|
|
- `fp8_amax_history_len: 16` + `fp8_amax_compute_algo: "max"` — 적절
|
|||
|
|
- MXFP8은 DelayedScaling보다 안정적
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | 안정성 ↑, 과적합 -10~20%, 반복률 -5~10% |
|
|||
|
|
| 구현 복잡도 | 1/5 |
|
|||
|
|
| 소요 시간 | 1시간 |
|
|||
|
|
| 적용 가능 | ✅ config 변경만으로 가능 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 평가 기반 데이터 선택 (Self-Play → ORPO)
|
|||
|
|
|
|||
|
|
### 파이프라인
|
|||
|
|
```
|
|||
|
|
1. 현재 SFT 모델로 각 instruction에 대해 N=4개 응답 생성
|
|||
|
|
2. 자동 평가 (반복률, 길이, 일관성)로 best/worst 선정
|
|||
|
|
3. (chosen, rejected) 페어 구성
|
|||
|
|
4. ORPO/DPO 학습 (이미 train/orpo.py 존재!)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 구체적 단계
|
|||
|
|
```python
|
|||
|
|
# scripts/generate_self_play.py
|
|||
|
|
def generate_candidates(model, tokenizer, instructions, n=4, temp=0.8):
|
|||
|
|
pairs = []
|
|||
|
|
for inst in instructions:
|
|||
|
|
responses = []
|
|||
|
|
for _ in range(n):
|
|||
|
|
out = model.generate(inst, temperature=temp, max_new_tokens=1024)
|
|||
|
|
score = auto_evaluate(out) # 반복률, 길이, coherence
|
|||
|
|
responses.append((out, score))
|
|||
|
|
|
|||
|
|
responses.sort(key=lambda x: x[1], reverse=True)
|
|||
|
|
pairs.append({
|
|||
|
|
"instruction": inst,
|
|||
|
|
"chosen": responses[0][0],
|
|||
|
|
"rejected": responses[-1][0],
|
|||
|
|
})
|
|||
|
|
return pairs
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 평가
|
|||
|
|
| 항목 | 값 |
|
|||
|
|
|------|-----|
|
|||
|
|
| 예상 효과 | 반복률 -40~60%, ko_ifeval +3~5% |
|
|||
|
|
| 구현 복잡도 | 4/5 |
|
|||
|
|
| 소요 시간 | 생성 1~2일 + ORPO 학습 0.5일 |
|
|||
|
|
| 적용 가능 | ✅ orpo.py 이미 존재 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 종합 비교표
|
|||
|
|
|
|||
|
|
| 기법 | 예상 효과 (반복률) | 예상 효과 (ko_ifeval) | 구현 복잡도 | 소요 시간 | 우선순위 |
|
|||
|
|
|------|-------------------|---------------------|------------|----------|---------|
|
|||
|
|
| 1. Curriculum Learning | - | +1~2% | 2/5 | 5시간 | 중기 |
|
|||
|
|
| 2. Less is More | **-30~50%** | **+3~5%** | 2/5 | 5시간 | **즉시** |
|
|||
|
|
| 3. Packing | (속도만) | (변화없음) | 3/5 | 1~2일 | 중기 |
|
|||
|
|
| 4. Multi-task Weighting | - | +1~3% | 3/5 | 1일 | 중기 |
|
|||
|
|
| 5. Focal Loss | - | +1~3% | **1/5** | **2시간** | **즉시** |
|
|||
|
|
| 6. Data Augmentation | - | +2~4% | 3/5 | 2~3일 | 중기 |
|
|||
|
|
| 7. 학습 안정성 (dropout) | **-5~10%** | - | **1/5** | **1시간** | **즉시** |
|
|||
|
|
| 8. Self-Play → ORPO | **-40~60%** | **+3~5%** | 4/5 | 2~3일 | 중기 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 즉시 적용 Top 3
|
|||
|
|
|
|||
|
|
### 1위: "Less is More" 데이터 필터링
|
|||
|
|
- **근거**: LIMA, AlpaGasus 논문에서 일관되게 입증. 188k → 50~80k 필터링으로 저품질/반복적 샘플 제거
|
|||
|
|
- **예상 효과**: 반복률 -30~50%, ko_ifeval +3~5%
|
|||
|
|
- **소요**: 5시간 (PPL 계산 + 필터링 스크립트)
|
|||
|
|
- **리스크**: 낮음 (최악의 경우 전체 데이터로 롤백)
|
|||
|
|
|
|||
|
|
### 2위: Focal Loss 적용
|
|||
|
|
- **근거**: 어려운 토큰에 집중 → instruction following 능력 향상. 구현 극히 간단
|
|||
|
|
- **예상 효과**: ko_ifeval +1~3%
|
|||
|
|
- **소요**: 2시간 (loss 함수 1개 추가)
|
|||
|
|
- **리스크**: 매우 낮음 (gamma 값만 조정하면 됨)
|
|||
|
|
|
|||
|
|
### 3위: Dropout 추가 (0.05)
|
|||
|
|
- **근거**: 현재 dropout=0.0으로 과적합 위험. SFT에서 light dropout은 표준
|
|||
|
|
- **예상 효과**: 과적합 감소, 반복률 -5~10%
|
|||
|
|
- **소요**: config 한 줄 변경
|
|||
|
|
- **리스크**: 없음
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📅 중기 적용 Top 3
|
|||
|
|
|
|||
|
|
### 1위: Self-Play → ORPO (SFT 이후)
|
|||
|
|
- **근거**: SFT 완료 후 선호 학습은 반복률 감소에 가장 효과적. orpo.py 이미 구현됨
|
|||
|
|
- **예상 효과**: 반복률 -40~60%, ko_ifeval +3~5%
|
|||
|
|
- **소요**: 2~3일 (생성 + 학습)
|
|||
|
|
|
|||
|
|
### 2위: Sequence Packing
|
|||
|
|
- **근거**: 학습 속도 1.5~3× 향상. 향후 반복 실험에 필수적
|
|||
|
|
- **예상 효과**: 학습 시간 대폭 단축
|
|||
|
|
- **소요**: 1~2일
|
|||
|
|
|
|||
|
|
### 3위: Curriculum Learning + Data Augmentation (Back-translation)
|
|||
|
|
- **근거**: 데이터 다양성과 학습 효율 동시 개선
|
|||
|
|
- **예상 효과**: ko_ifeval +2~4%
|
|||
|
|
- **소요**: 3~4일
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 권장 실행 순서
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Phase 1 (즉시, SFT 전):
|
|||
|
|
1. dropout: 0.05 설정
|
|||
|
|
2. 데이터 품질 필터링 (188k → 60~80k)
|
|||
|
|
3. Focal loss 적용 (gamma=1.5)
|
|||
|
|
→ SFT 실행
|
|||
|
|
|
|||
|
|
Phase 2 (SFT 후):
|
|||
|
|
4. Self-Play 데이터 생성
|
|||
|
|
5. ORPO 학습
|
|||
|
|
|
|||
|
|
Phase 3 (다음 라운드):
|
|||
|
|
6. Packing 구현 (반복 실험 가속)
|
|||
|
|
7. Back-translation으로 데이터 확장
|
|||
|
|
8. Curriculum learning 실험
|
|||
|
|
```
|