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 ๊ธฐ๋ฐ ์ ๋ ฌ (๊ถ์ฅ)
# 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 ์์ด)
# 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 |
| ๊ตฌํ ๋ณต์ก๋ | 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
๊ตฌํ ๋ฐฉ๋ฒ
# 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, ์ฐฝ์, ์ผ๋ฐ ๋ํ
๊ตฌํ ๋ฐฉ๋ฒ
# ๋๋ฉ์ธ ํ๊ทธ๋ฅผ 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 ๊ตฌํ
# 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 ์์
# 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
# ํ์ฌ ๋ชจ๋ธ(๋๋ ๋ ํฐ ๋ชจ๋ธ)๋ก response ์ฌ์์ฑ
# instruction์ ์ ์ง, output๋ง ๋ค์ํ
# โ ๋์ผ instruction์ ๋ํ N๊ฐ ๋ค๋ฅธ ์๋ต ํ๋ณด
๋ฐฉ๋ฒ B: Back-translation
# ์์ด ๊ณ ํ์ง ๋ฐ์ดํฐ (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 ๊ธฐ๋ฐ. ์ฃผ์ ์ฃผ์์ :
Loss spike ๋ฐฉ์ง
max_grad_norm: 1.0์ด๋ฏธ ์ ์ฉ๋จ โ- LR 2e-5๋ ๋ณด์์ โ
- ์ถ๊ฐ: gradient norm ๋ชจ๋ํฐ๋ง + ์๋ LR ๊ฐ์
# ์ฐ์ 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.5Weight Decay
- ํ์ฌ 0.01 (์ ์ )
- SFT์์๋ 0.01~0.05 ๋ฒ์๊ฐ ํ์ค
Dropout
- ํ์ฌ
dropout: 0.0โ SFT์์๋ 0.05~0.1 ์ถ๊ฐ ๊ถ์ฅ - ๊ณผ์ ํฉ ๋ฐฉ์ง์ ์ง์ ์ ํจ๊ณผ
dropout: 0.05 # configs/korean_1b_sft.yaml- ํ์ฌ
FP8 amax ์ค์
fp8_amax_history_len: 16+fp8_amax_compute_algo: "max"โ ์ ์ - MXFP8์ DelayedScaling๋ณด๋ค ์์ ์
ํ๊ฐ
| ํญ๋ชฉ | ๊ฐ |
|---|---|
| ์์ ํจ๊ณผ | ์์ ์ฑ โ, ๊ณผ์ ํฉ -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 ์กด์ฌ!)
๊ตฌ์ฒด์ ๋จ๊ณ
# 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 |
| ๊ตฌํ ๋ณต์ก๋ | 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 +35% - ์์: 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 +35% - ์์: 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 ์คํ