DPO — جایگزین ساده RLHF

قسمت ۸ ۱۸ دقیقه

مشکل RLHF

توی اپیزود ۲ دیدیم که RLHF (Reinforcement Learning from Human Feedback) مرحله سوم آموزش مدله. ولی RLHF خیلی پیچیده‌ست:

  • باید یه Reward Model جداگانه آموزش بدی
  • PPO (الگوریتم RL) ناپایداره و تنظیم hyperparameterهاش سخته
  • سه مدل همزمان توی حافظه لازمه: مدل اصلی، مدل مرجع، Reward Model
  • آموزش کنده و منابع زیادی مصرف می‌کنه

DPO اومده تا همون هدف RLHF رو با روش خیلی ساده‌تری به دست بیاره.

DPO چیه؟

DPO مخفف Direct Preference Optimization هست. ایده‌اش اینه: به جای آموزش یه Reward Model جداگانه و بعد استفاده از RL، مستقیم از داده‌های ترجیحی (preference data) یاد بگیر.

تشبیهش اینه: RLHF مثل اینه که یه منتقد غذا استخدام کنی (Reward Model)، بعد آشپز رو آموزش بدی بر اساس نظر منتقد (RL). DPO مثل اینه که مستقیم به آشپز بگی “این غذا بهتر از اونه” و خودش یاد بگیره.

مقایسه RLHF و DPO

# RLHF — سه مرحله
# مرحله ۱: جمع‌آوری ترجیحات (chosen/rejected)
# مرحله ۲: آموزش Reward Model
reward_model = train_reward_model(preference_data)
# مرحله ۳: آموزش مدل با PPO
ppo_trainer = PPOTrainer(model, reward_model, ...)
ppo_trainer.train()  # ناپایدار و کند

# DPO — یه مرحله
# فقط: آموزش مستقیم از ترجیحات
dpo_trainer = DPOTrainer(model, train_dataset=preference_data, ...)
dpo_trainer.train()  # ساده و پایدار

فرمت داده DPO

برای DPO فقط نیاز به جفت‌های chosen/rejected داری:

# فرمت دیتاست DPO
dpo_data = [
    {
        "prompt": "فرق بین == و === در جاوااسکریپت چیه؟",
        "chosen": "== مقایسه با تبدیل نوع (type coercion) انجام می‌ده. مثلاً '5' == 5 برابر true می‌شه. ولی === مقایسه دقیق (strict equality) انجام می‌ده و هم مقدار و هم نوع رو چک می‌کنه. '5' === 5 برابر false می‌شه. توصیه می‌شه همیشه از === استفاده کنی.",
        "rejected": "== و === هر دو برای مقایسه هستن. === بهتره."
    },
    {
        "prompt": "چطور یه لیست رو مرتب کنم؟",
        "chosen": "در پایتون دو روش اصلی داری:\n\n1. متد sort() که لیست اصلی رو تغییر می‌ده:\nmy_list = [3, 1, 2]\nmy_list.sort()\n\n2. تابع sorted() که لیست جدید برمی‌گردونه:\nmy_list = [3, 1, 2]\nnew_list = sorted(my_list)\n\nبرای مرتب‌سازی نزولی: sort(reverse=True)",
        "rejected": "از sort استفاده کن."
    }
]

تولید داده DPO

def generate_dpo_pairs(model, prompts, num_responses=4):
    """تولید جفت‌های chosen/rejected"""
    dpo_data = []
    
    for prompt in prompts:
        # تولید چند جواب مختلف
        responses = []
        for _ in range(num_responses):
            response = generate_response(
                model, prompt, 
                temperature=0.9,  # تنوع بالا
                do_sample=True
            )
            responses.append(response)
        
        # ارزیابی انسانی یا خودکار
        # اینجا ساده‌سازی شده — در عمل باید انسان انتخاب کنه
        ranked = rank_responses(responses, prompt)
        
        # بهترین → chosen، بدترین → rejected
        dpo_data.append({
            "prompt": prompt,
            "chosen": ranked[0],     # بهترین
            "rejected": ranked[-1],  # بدترین
        })
    
    return dpo_data

# یا از یه مدل قوی‌تر برای ارزیابی استفاده کن
def rank_with_llm(responses, prompt, judge_model="gpt-4"):
    """رتبه‌بندی جواب‌ها با LLM"""
    judge_prompt = f"""
    به سوال زیر {len(responses)} جواب داده شده.
    جواب‌ها رو از بهترین تا بدترین رتبه‌بندی کن.
    
    سوال: {prompt}
    
    جواب‌ها:
    """
    for i, resp in enumerate(responses):
        judge_prompt += f"\n[{i+1}] {resp}\n"
    
    judge_prompt += "\nرتبه‌بندی (بهترین اول): "
    
    ranking = call_llm(judge_prompt, model=judge_model)
    return parse_ranking(ranking, responses)

پیاده‌سازی DPO با TRL

from unsloth import FastLanguageModel
from trl import DPOTrainer, DPOConfig
from datasets import load_dataset

# ۱. لود مدل SFT شده
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="./my-sft-model",  # مدلی که قبلاً SFT شده
    max_seq_length=2048,
    load_in_4bit=True,
)

# LoRA برای DPO
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    lora_alpha=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                     "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0,
    bias="none",
    use_gradient_checkpointing="unsloth",
)

# ۲. لود دیتاست DPO
dataset = load_dataset("json", data_files="dpo_data.jsonl", split="train")

# ۳. فرمت‌دهی
def format_dpo(example):
    prompt_messages = [
        {"role": "user", "content": example["prompt"]},
    ]
    
    chosen_messages = prompt_messages + [
        {"role": "assistant", "content": example["chosen"]},
    ]
    
    rejected_messages = prompt_messages + [
        {"role": "assistant", "content": example["rejected"]},
    ]
    
    return {
        "prompt": tokenizer.apply_chat_template(
            prompt_messages, tokenize=False, add_generation_prompt=True
        ),
        "chosen": tokenizer.apply_chat_template(
            chosen_messages, tokenize=False
        ),
        "rejected": tokenizer.apply_chat_template(
            rejected_messages, tokenize=False
        ),
    }

dataset = dataset.map(format_dpo)

# ۴. تنظیمات DPO
dpo_config = DPOConfig(
    output_dir="dpo-output",
    num_train_epochs=2,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    learning_rate=5e-6,          # خیلی کمتر از SFT
    beta=0.1,                    # پارامتر کلیدی DPO
    warmup_ratio=0.1,
    bf16=True,
    optim="adamw_8bit",
    logging_steps=10,
    save_strategy="epoch",
)

# ۵. شروع آموزش
trainer = DPOTrainer(
    model=model,
    ref_model=None,        # Unsloth خودش handle می‌کنه
    args=dpo_config,
    train_dataset=dataset,
    tokenizer=tokenizer,
)

trainer.train()

پارامتر Beta (β)

beta مهم‌ترین hyperparameter توی DPO هست. کنترل می‌کنه مدل چقدر از مدل مرجع (reference model) فاصله بگیره:

# beta کوچک (0.05) → مدل بیشتر تغییر می‌کنه
# beta بزرگ (0.5) → مدل محافظه‌کارتره و کمتر تغییر می‌کنه

# مقادیر معمول:
beta_values = {
    0.05: "تغییرات زیاد — ریسک بالا",
    0.1:  "مقدار پیش‌فرض — تعادل خوب",
    0.2:  "تغییرات متوسط",
    0.5:  "محافظه‌کارانه — تغییرات کم",
}

# شروع با 0.1 و اگه مدل خیلی تغییر کرد، بالا ببر
dpo_config = DPOConfig(beta=0.1, ...)

کِی از DPO استفاده کنی؟

  • بعد از SFT: وقتی مدل SFT شده ولی هنوز جواب‌های ناخواسته می‌ده
  • بهبود لحن: وقتی می‌خوای مدل مودب‌تر، دقیق‌تر یا مختصرتر جواب بده
  • کاهش هلوسینیشن: وقتی مدل اطلاعات نادرست تولید می‌کنه
  • فرمت بهتر: وقتی می‌خوای مدل خروجی ساختارمندتری بده

کِی از DPO استفاده نکنی؟

  • وقتی هنوز SFT نکردی — اول SFT کن، بعد DPO
  • وقتی داده ترجیحی کافی نداری (حداقل ۵۰۰ جفت)
  • وقتی مشکل اصلی کمبود دانشه — DPO دانش جدید اضافه نمی‌کنه

ساخت داده ترجیحی باکیفیت

def create_preference_dataset(model, prompts, evaluator="human"):
    """ساخت دیتاست ترجیحی"""
    dataset = []
    
    for prompt in prompts:
        # تولید دو جواب مختلف
        response_a = generate_response(model, prompt, temperature=0.7)
        response_b = generate_response(model, prompt, temperature=0.9)
        
        if evaluator == "human":
            # نمایش به ارزیاب انسانی
            print(f"Prompt: {prompt}")
            print(f"\nA: {response_a}")
            print(f"\nB: {response_b}")
            choice = input("کدوم بهتره؟ (A/B): ")
            
            chosen = response_a if choice == "A" else response_b
            rejected = response_b if choice == "A" else response_a
        
        elif evaluator == "llm":
            # استفاده از LLM قوی‌تر
            chosen, rejected = llm_evaluate(prompt, response_a, response_b)
        
        dataset.append({
            "prompt": prompt,
            "chosen": chosen,
            "rejected": rejected,
        })
    
    return dataset

# نکته: کیفیت ارزیابی مهمه
# chosen باید واقعاً بهتر از rejected باشه
# اگه فرق‌شون کمه، DPO خوب یاد نمی‌گیره

DPO vs RLHF: مقایسه عملی

  • سادگی پیاده‌سازی: DPO خیلی ساده‌تره — فقط یه loop آموزش معمولی
  • پایداری: DPO پایدارتره — PPO ممکنه diverge کنه
  • مصرف حافظه: DPO کمتر — نیاز به Reward Model نداره
  • کیفیت: تحقیقات نشون داده DPO در اکثر task‌ها نتایج مشابه RLHF می‌ده
  • سرعت: DPO سریع‌تره — یه فاز آموزش به جای سه فاز

DPO تقریباً توی همه سناریوها جایگزین خوبی برای RLHF هست. مگه اینکه scale خیلی بالایی داشته باشی (مثل آموزش ChatGPT) که اونجا RLHF ممکنه کمی بهتر باشه.

فرآیند کامل: SFT + DPO

# مسیر کامل Fine-tuning

# مرحله ۱: SFT با دیتای instruction
# (اپیزود ۶ — با Unsloth)
sft_model = sft_train(base_model, instruction_data)

# مرحله ۲: تولید جواب‌ها از مدل SFT شده
# برای ساخت دیتاست DPO
responses = generate_multiple_responses(sft_model, prompts)

# مرحله ۳: ارزیابی و ساخت جفت‌های ترجیحی
preference_data = create_preference_pairs(responses)

# مرحله ۴: DPO
final_model = dpo_train(sft_model, preference_data)

# نتیجه: مدلی که هم task-specific هست (SFT)
# و هم align شده با ترجیحات (DPO)

جمع‌بندی

DPO یه ابزار قدرتمند و ساده برای بهبود کیفیت مدل بعد از SFT هست. نیازی به Reward Model جداگانه نداره و با داده ترجیحی (chosen/rejected pairs) مستقیم مدل رو بهینه می‌کنه. اگه مدل SFT شده‌ات ۸۰ درصد خوبه، DPO می‌تونه اون ۲۰ درصد آخر رو بهتر کنه.

توی اپیزود بعدی، چالش‌های خاص Fine-tuning برای زبان فارسی رو بررسی می‌کنیم — از مشکل tokenization تا انتخاب مدل مناسب.

نظرات

هنوز نظری ثبت نشده. اولین نفر باشید!

نظر خود را بنویسید