همراستاسازی مدلهای زبانی با روش سنتی RLHF همواره دردسرهای فنی و محاسباتی بزرگی دارد. در RLHF باید دستکم سه مدل مستقل را همزمان مدیریت کنید: مدل تنظیم دقیق نظارتشده (SFT)، مدل پاداش (Reward Model) و مدل خطمشی مبتنی بر یادگیری تقویتی (PPO). تنظیم ناپایدار PPO، احتمال بروز رفتارهایی مانند دور زدن پاداش (Reward Hacking) و نیاز به حافظهی کارت گرافیک بسیار بالا، پیادهسازی این روش را برای بسیاری از پروژهها دشوار میکند.
الگوریتم بهینهسازی مستقیم ترجیحات یا DPO (Direct Preference Optimization) این فرایند پیچیده را به یک گام سادهی یادگیری نظارتشده تبدیل میکند. در DPO نیازی به آموزش مدل پاداش جداگانه یا اجرای حلقههای ناپایدار یادگیری تقویتی نیست؛ چرا که پاداش بهینهی مدل بهصورت ریاضی از دل احتمالات توکنهای خود مدل زبانی استخراج میشود.
در شرایطی که دسترسی به کلاسترهای بزرگ پردازش ابری یا کارتهای گرافیک حرفهای محدود و پرهزینه است، DPO با کاهش نیازهای حافظه و حذف دو مدل از حافظهی همزمان، آموزش مدلهای تراز اول را با منابع پردازشی بسیار کمتر روی سیستم شخصی یا کارتهای گرافیک اقتصادی ممکن میسازد.
یادگیری تقویتی بر اساس بازخورد انسانی (RLHF) واقعاً جواب میدهد؛ اما آموزش سه مدل (SFT، مدل پاداش و خطمشی)، مهار ناپایداریهای PPO و تنظیم جریمهی واگرایی کولبک-لایبلر (KL) را هم به دنبال دارد. بهینهسازی مستقیم ترجیحات (DPO) یک پرسش اساسی مطرح میکند: اگر بتوان همهی این مراحل را دور زد چه؟ DPO مدل زبانی را مستقیماً روی جفتهای ترجیحی بهینهسازی میکند؛ بدون مدل پاداش، بدون PPO، تنها با یک حلقهی آموزش و با همان نتایج قبلی.
اهداف یادگیری
- پیادهسازی آموزش DPO برای بهینهسازی مستقیم مدل زبانی روی جفتهای ترجیحی بدون نیاز به مدل پاداش جداگانه
- اثبات ریاضی تابع زیان (Loss function) روش DPO و توضیح نحوهی بازنمایی ضمنی مدل پاداش از طریق لگاریتم احتمالات خطمشی
- مقایسهی DPO و RLHF از نظر پایداری آموزش، هزینهی پردازشی و تعداد مدلهای موردنیاز
- تنظیم ابرپارامتر بتا برای کنترل میزان انحراف خطمشی آموزشدیده از مدل مرجع
صورت مسئله
در درس 07 یک پایپلاین (Pipeline) کامل RLHF ساختید: سه مرحله و سه مدل شامل مدل SFT، مدل پاداش و مدل خطمشی که با PPO بهینهسازی میشد. تنها مدل پاداش به هزاران جفت ترجیح انسانی و یک حلقهی آموزش جداگانه نیاز داشت. PPO نیز نیازمند تنظیم دقیق ضریب KL، نرخ یادگیری (Learning rate)، نسبت برش (clip ratio) و تعداد دورهها بود.
در عمل، آموزش PPO به ناپایداری شدید مشهور است؛ تغییرات کوچک در ابرپارامترها آموزش را به واگرایی میکشاند. مدل پاداش هم بازتاب کاملی از ترجیحات انسانی نیست و خطمشی همیشه راههایی برای سوءاستفاده از نقاط ضعف آن پیدا میکند. جریمهی KL تا حدی اوضاع را بهتر میکند، ولی تنظیم آن هم داستان خودش را دارد؛ اگر خیلی کم باشد به دور زدن پاداش (Reward Hacking) برمیخورید و اگر خیلی زیاد باشد، مدل عملاً چیزی یاد نمیگیرد.
همین پیچیدگی باعث شد تا سالها پس از انتشار InstructGPT، اغلب مدلهای متنباز با RLHF دستبهگریبان باشند. پایپلاین سهمرحلهای بسیار شکننده است؛ هر مرحله حالتهای شکست (Failure modes) خاص خود را دارد و خطاها روی هم تلنبار میشوند.
در مه 2023، رافائل رافائلوف (Rafael Rafailov)، آرچیت شارما (Archit Sharma) و همکارانشان در دانشگاه استنفورد مقالهای با عنوان «بهینهسازی مستقیم ترجیحات: مدل زبانی شما در باطن یک مدل پاداش است» منتشر کردند. ایدهی کلیدی آنها این بود: اصلاً نیازی به مدل پاداش مجزا ندارید؛ تابع پاداش بهینه از نظر ریاضی مستقیماً با احتمالات توکنهای خودِ مدل زبانی تعیین میشود. بنابراین میتوانید مدل پاداش را کلاً کنار بگذارید و مدل زبانی را مستقیماً روی جفتهای ترجیحی بهینهسازی کنید.
روش DPO فرایند RLHF را به یک گام یادگیری نظارتشده (Supervised Learning) ساده تبدیل میکند: یک مدل، یک تابع زیان، یک حلقهی آموزش و بدون هیچ یادگیری تقویتی (Reinforcement Learning). مدل Zephyr-7B، یکی از نخستین مدلهایی که DPO را در مقیاس بزرگ به کار گرفت، در چندین بنچمارک با مدلهایی که با RLHF کامل آموزش دیده بودند برابری کرد یا حتی از آنها جلو زد. شرکت Meta نیز از DPO در پایپلاین همراستاسازی Llama 3 بهره برد و Anthropic هم در پژوهشهای همراستاسازی خود به روشهای مبتنی بر DPO ارجاع داده است.
مبانی نظری
ایدهی کلیدی
الگوریتم RLHF این تابع هدف را بهینهسازی میکند:
که در آن مدل پاداش، خطمشی، مدل مرجع و ضریب جریمهی KL است.
مقالهی DPO نشان داد که این تابع هدف یک راهحل بهینهی فرم بسته (Closed-form) دارد. به ازای هر تابع پاداش ، خطمشی بهینه برابر است با:
که در آن یک ثابت نرمالسازی است. با بازآرایی این معادله داریم:
دستاورد بزرگ دقیقاً همینجاست: پاداش کاملاً بر حسب احتمالات مدل خطمشی و مدل مرجع تعریف میشود. نیازی به آموزش مدل پاداش جداگانه ندارید؛ پاداش بهصورت ضمنی در نسبت این احتمالات وجود دارد.
با جایگذاری این رابطه در مدل ترجیحات برادلی-تری (Bradley-Terry):
جملات با هم خط میخورند؛ چون هر دو پاسخ به یک پرامپت (Prompt) یکسان وابستهاند (Dependent). آنچه باقی میماند تابعی است که فقط و فقط به لگاریتم احتمالات مدل خطمشی و مدل مرجع روی پاسخهای مرجح و مردود بستگی دارد.
تابع زیان DPO
بیایید اجزای فرمول را باز کنیم:
- = پاسخ مرجح (برنده)
- = پاسخ مردود (بازنده)
- = پرامپت
- = مدل فعلی (در حال آموزش)
- = مدل مرجع (چکپوینت منجمد SFT)
- = پارامتر دما برای کنترل انحراف از مدل مرجع (معمولاً بین 0.1 تا 0.5)
نسبت log pi(y|x) / pi_ref(y|x) در واقع نسبت لگاریتم احتمال است. مثبت بودن این نسبت نشان میدهد مدل فعلی در مقایسه با مدل مرجع، احتمال بالاتری به پاسخ میدهد؛ منفی بودن آن یعنی احتمال کمتری به آن اختصاص داده است.
تابع زیان DPO کاری میکند که مدل نسبت لگاریتم احتمال را برای پاسخهای مرجح بالا ببرد و برای پاسخهای مردود کم کند. پارامتر بتا تعیین میکند مدل چقدر اجازه دارد از مرجع فاصله بگیرد؛ بتای کوچک اجازهی انحرافهای بزرگ را میدهد و بتای بزرگ مدل را نزدیک مرجع نگه میدارد.
چرا DPO سادهتر است؟
| جنبه | RLHF (PPO) | DPO |
|---|---|---|
| تعداد مدلهای نیازمند آموزش | 3 (مدلهای SFT، پاداش و خطمشی) | 1 (فقط خطمشی) |
| حلقههای آموزش | 3 (آموزش SFT، آموزش RM و PPO) | 2 (آموزش SFT و DPO) |
| ابرپارامترها | نرخ یادگیری، ضریب KL، نسبت برش، نرخ یادگیری RM، تعداد دورهها برای هر 3 مدل | نرخ یادگیری، بتا، تعداد دورهها |
| مدل پاداش | الزامی (آموزش جداگانه) | ضمنی در احتمالات مدل |
| الگوریتم یادگیری تقویتی | PPO (پیچیده و ناپایدار) | یادگیری نظارتشده (پایدار) |
| حافظهی کارت گرافیک | 3 تا 4 مدل همزمان در حافظه در طول PPO | 2 مدل (مدل فعلی + مدل مرجع) |
| پایداری آموزش | حساس به ابرپارامترها | مقاوم، مشابه SFT |
روش DPO هنگام آموزش فقط دو مدل را در حافظه نگه میدارد: مدل فعلی و مدل مرجع منجمد. در مقابل، RLHF به سه یا چهار مدل نیاز دارد: خطمشی، مدل مرجع، مدل پاداش و در صورت نیاز یک خط مبنا (Baseline) برای تابع ارزش. برای یک مدل 70B، هر نسخه در فرمت FP16 حدود 140 گیگابایت حافظه میخواهد؛ بنابراین حذف مدل پاداش صرفهجویی چشمگیری در حافظه ایجاد میکند.
در چه مواردی DPO از RLHF بهتر کار میکند؟
دیتاستهای کوچک. با 5,000 تا 20,000 جفت ترجیحی، DPO معمولاً عملکردی برابر یا حتی بهتر از RLHF دارد. مدل پاداش در RLHF برای تعمیمپذیری به دادههای زیادی نیاز دارد؛ اگر دادهها کم باشند، دچار بیشبرازش (Overfitting) میشود و سیگنالهای پاداش غیرقابلاعتمادی تولید میکند. DPO این مشکل را با حذف کامل مدل پاداش کنار میزند.
محدودیت منابع پردازشی. روش DPO تقریباً به یکسوم توان پردازشی RLHF کامل نیاز دارد (یک حلقهی آموزش به جای سه حلقه). برای تیمهایی که کلاسترهای بزرگ GPU ندارند، این روش بهترین گزینهی عملی است.
تکرار سریع آزمایشها. اگر بخواهید 10 مجموعهدادهی ترجیحی مختلف را بسنجید تا بهترین مدل را پیدا کنید، DPO به شما امکان میدهد هر آزمایش را ظرف چند ساعت اجرا کنید؛ در حالی که در RLHF باید مدل پاداش را برای تکتک دیتاستها دوباره آموزش دهید.
در چه مواردی RLHF از DPO بهتر است؟
آموزش در مقیاس بسیار بزرگ. در مقیاس مدلهایی مثل GPT-4 یا Claude، مدل پاداش جداگانه در RLHF میتواند سیگنالهای ترجیحی دقیقتر و ظریفتری را ثبت کند. مدل پاداش در اینجا مثل یک تابع زیانِ آموزشدیده عمل میکند که خودش را با معیارهای کیفی پیچیده تطبیق میدهد.
سیگنالهای پاداش چندبعدی و پیچیده. وقتی مفهوم «پاسخ بهتر» ابعاد مختلفی داشته باشد (مفید بودن، بیضرر بودن، صداقت)، مدل پاداش میتواند تعادل بین این چند هدف را یاد بگیرد. DPO با هر جفت ترجیحی مثل یک سیگنال دودویی برخورد میکند (یکی بهتر است و دیگری بدتر)، بدون آنکه دلیل این تفاوت را مدل کند.
همراستاسازی تعاملی و چرخهای. پایپلاینهای RLHF میتوانند با خطمشی فعلی پاسخهای تازهای تولید کنند، امتیازدهی آنها را به انسان بسپارند و مدل پاداش را در یک حلقهی آنلاین دوباره آموزش دهند. اما DPO روی یک مجموعهدادهی ثابت از جفتهای ترجیحی کار میکند. رویکرد Constitutional AI در شرکت Anthropic از همین ویژگی تعاملی و چرخهای RLHF نهایت استفاده را میبرد.
روشهای پس از DPO: الگوهای KTO، ORPO و SimPO
موفقیت DPO الهامبخش خانوادهای از روشهای سادهتر برای همراستاسازی شد.
روش KTO (بهینهسازی کانمن-تورسکی، 2024): در این روش حتی نیازی به جفت ترجیحی ندارید. KTO با بازخوردهای بدون جفت کار میکند؛ فقط کافی است به هر پاسخ برچسب «خوب» یا «بد» بزنید، بدون اینکه آن را با پاسخ دیگری مقایسه کنید. این رویکرد جمعآوری داده را فوقالعاده راحت میکند. به جای اینکه دو پاسخ جلوِ ارزیاب بگذارید و بپرسید «کدام بهتر است؟»، یک پاسخ نشان میدهید و میپرسید «این خوب است یا نه؟». تابع زیان این روش از اصل زیانگریزی در نظریهی چشمانداز الهام گرفته است: پاسخهای بد بیش از پاداشی که به پاسخهای خوب داده میشود، جریمه میشوند.
روش ORPO (بهینهسازی ترجیحات بر اساس نسبت شانس، 2024): این روش SFT و همراستاسازی را در یک مرحلهی آموزشی ترکیب میکند. به جای آنکه اول SFT و سپس DPO را اجرا کنید، ORPO تابع زیان SFT را تغییر میدهد تا سیگنال ترجیح را در دل خود داشته باشد. این تابع زیان دو بخش دارد: یک تابع زیان استاندارد پیشبینی توکن بعدی (Next-Token Prediction) روی پاسخهای مرجح، به اضافهی یک جملهی نسبت شانس که فاصلهی احتمالاتی بین پاسخ مرجح و مردود را بیشتر میکند. یعنی تنها یک حلقهی آموزش به جای دو حلقه.
روش SimPO (بهینهسازی سادهی ترجیحات، 2024): مدل مرجع را هم کلاً حذف میکند. SimPO به جای محاسبهی نسبت لگاریتم احتمال در برابر یک مدل مرجع منجمد، میانگین لگاریتم احتمال پاسخ را (با نرمالسازی بر اساس طول) به عنوان پاداش ضمنی در نظر میگیرد. این کار مصرف حافظه را کم میکند (چون مدل مرجع نمیخواهد) و آموزش را سادهتر میسازد. نرمالسازی بر اساس طول هم جلوی این را میگیرد که مدل همیشه پاسخهای کوتاهتر را ترجیح دهد.
| روش | سال | مدلهای درون حافظه | نیاز به جفت داده؟ | نیاز به مدل مرجع؟ | حلقههای آموزش |
|---|---|---|---|---|---|
| RLHF | 2022 | 3-4 | بله (برای RM) | بله | 3 |
| DPO | 2023 | 2 | بله | بله | 2 |
| KTO | 2024 | 2 | خیر (دادههای تکی) | بله | 2 |
| ORPO | 2024 | 1 | بله | خیر | 1 |
| SimPO | 2024 | 1 | بله | خیر | 1 |
مسیر تحولات کاملاً مشخص است: هر روش جدید یکی از بخشهای دستوپاگیر را حذف میکند. RLHF به مدل پاداش و PPO نیاز داشت؛ DPO هر دو را برداشت. KTO نیاز به دادههای جفتشده (نظیربهنظیر) (Paired Data) را حذف کرد. ORPO مرحلهی SFT مجزا را کنار گذاشت و SimPO هم مدل مرجع را برچید. به این ترتیب «مالیات همراستاسازی» (Alignment tax) — یعنی هزینهی پردازشی و دردسرهای رساندن مدل پایه به یک مدل همراستا — روزبهروز کمتر میشود.
نمونههای پیادهسازی واقعی DPO
مدل Zephyr-7B (هاگینگفیس، اکتبر 2023): با مدل پایهی Mistral 7B، ابتدا SFT روی UltraChat (شامل 200 هزار نمونه) و سپس DPO روی UltraFeedback (شامل 60 هزار جفت ترجیحی) اعمال شد. این مدل امتیاز 6.47 را در MT-Bench به دست آورد که تا آن زمان بالاترین امتیاز میان مدلهای 7B بود. برای مقایسه، Llama 2 Chat 70B امتیاز 6.86 را ثبت کرده بود؛ یعنی Zephyr تنها با تکیه بر همراستاسازی DPO به فاصلهی 6 درصدی مدلی رسید که 10 برابر از خودش بزرگتر بود.
مدل Llama 3 (متا، آوریل 2024): متا پس از مراحل اولیهی RLHF از DPO استفاده کرد. این ترکیب نشان میدهد که DPO و RLHF میتوانند مکمل هم باشند؛ یعنی RLHF برای همراستاسازی کلی و DPO برای تنظیم دقیق و هدفمند به کار رود.
مدل Neural Magic / nm-chat (2024): با اعمال DPO روی چندین مدل متنباز، همواره 5 تا 15 درصد بهبود در بنچمارکهای همراستاسازی نسبت به خط مبناهای صرفاً مبتنی بر SFT به دست آمد.
پیادهسازی
گام 1: مجموعهدادهی ترجیحات
فرمت دادهها درست مثل RLHF است؛ سهگانههای (Triple) شامل (پرامپت، مرجح، مردود). DPO مستقیماً این دادهها را میگیرد و بدون نیاز به مدل پاداش میانی از آنها استفاده میکند.
import numpy as np
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "04-pre-training-mini-gpt", "code"))
from main import MiniGPT, LayerNorm, Embedding, TransformerBlock
PREFERENCE_DATA = [
{
"prompt": "What is the capital of France?",
"preferred": "The capital of France is Paris.",
"rejected": "France is a country in Europe. It has many cities. The capital is Paris. Paris is known for the Eiffel Tower.",
},
{
"prompt": "Explain gravity in one sentence.",
"preferred": "Gravity is the force that attracts objects with mass toward each other.",
"rejected": "Gravity is something that makes things fall down when you drop them.",
},
{
"prompt": "What is 15 times 7?",
"preferred": "15 times 7 is 105.",
"rejected": "Let me think about this. 15 times 7. Well, 10 times 7 is 70, and 5 times 7 is 35, so the answer might be around 105.",
},
{
"prompt": "Name three programming languages.",
"preferred": "Python, Rust, and TypeScript.",
"rejected": "There are many programming languages. Some popular ones include various languages like Python and others.",
},
{
"prompt": "What year did World War II end?",
"preferred": "World War II ended in 1945.",
"rejected": "World War II was a major global conflict. It involved many countries. The war ended in the mid-1940s, specifically in 1945.",
},
{
"prompt": "Define machine learning.",
"preferred": "Machine learning is a field where algorithms learn patterns from data to make predictions without being explicitly programmed.",
"rejected": "Machine learning is a type of AI. AI stands for artificial intelligence. Machine learning uses data to learn.",
},
]
گام 2: لگاریتم احتمال توالی
تابع زیان DPO باید مجموع لگاریتم احتمال یک پاسخ به یک پرامپت مشخص را حساب کند. این یعنی اجرای مدل روی کل توالی (پرامپت + پاسخ) و سپس جمع زدن لگاریتم احتمال تکتک توکنهای بخش پاسخ.
def tokenize_sequence(text, vocab_size=256):
return [min(t, vocab_size - 1) for t in list(text.encode("utf-8"))]
def compute_sequence_log_prob(model, prompt_tokens, response_tokens, max_seq_len=128):
full_sequence = prompt_tokens + response_tokens
if len(full_sequence) > max_seq_len:
full_sequence = full_sequence[:max_seq_len]
if len(full_sequence) < 2:
return 0.0
input_ids = np.array(full_sequence[:-1]).reshape(1, -1)
target_ids = np.array(full_sequence[1:])
logits = model.forward(input_ids)
logits = logits[0]
max_logits = logits.max(axis=-1, keepdims=True)
log_probs = logits - max_logits - np.log(
np.exp(logits - max_logits).sum(axis=-1, keepdims=True)
)
prompt_len = len(prompt_tokens)
response_start = max(0, prompt_len - 1)
response_end = len(target_ids)
if response_start >= response_end:
return 0.0
response_log_probs = log_probs[response_start:response_end, :]
response_targets = target_ids[response_start:response_end]
total_log_prob = 0.0
for i, target in enumerate(response_targets):
total_log_prob += response_log_probs[i, target]
return total_log_prob
این تابع بخش حیاتی و پرکار DPO است. برای هر جفت ترجیحی، این تابع چهار بار اجرا میشود: اجرای مدل روی پاسخ مرجح، اجرای مدل روی پاسخ مردود، اجرای مدل مرجع روی پاسخ مرجح، و اجرای مدل مرجع روی پاسخ مردود. این یعنی 4 گذر رو به جلو (forward pass) به ازای هر نمونهی آموزشی، در برابر فرایند پیچیدهی RLHF که شامل تولید پاسخ، امتیازدهی پاداش، تخمین تابع ارزش و بهروزرسانی PPO بود. سادهتر، سریعتر و بسیار پایدارتر.
گام 3: تابع زیان DPO
پیادهسازی اصل ایدهی مقاله در قالب کد: یک تابع، یک زیان، بدون مدل پاداش.
def sigmoid(x):
return np.where(
x >= 0,
1.0 / (1.0 + np.exp(-x)),
np.exp(x) / (1.0 + np.exp(x))
)
def dpo_loss(policy_logprob_preferred, policy_logprob_rejected,
ref_logprob_preferred, ref_logprob_rejected, beta=0.1):
preferred_ratio = policy_logprob_preferred - ref_logprob_preferred
rejected_ratio = policy_logprob_rejected - ref_logprob_rejected
logit = beta * (preferred_ratio - rejected_ratio)
loss = -np.log(sigmoid(logit) + 1e-8)
preferred_reward = beta * preferred_ratio
rejected_reward = beta * rejected_ratio
return loss, {
"preferred_ratio": float(preferred_ratio),
"rejected_ratio": float(rejected_ratio),
"logit": float(logit),
"implicit_preferred_reward": float(preferred_reward),
"implicit_rejected_reward": float(rejected_reward),
"reward_margin": float(preferred_reward - rejected_reward),
}
متغیرهای preferred_ratio و rejected_ratio همان نسبتهای لگاریتم احتمال حاصل از روابط فرمول DPO هستند. وقتی مدل فعلی در مقایسه با مدل مرجع، احتمال بیشتری به پاسخ مرجح و احتمال کمتری به پاسخ مردود بدهد، مقدار لاجیت (Logit) مثبت میشود و تابع زیان کاهش مییابد. سیگنال آموزش هم مدل را دقیقاً در همین مسیر هل میدهد.
خروجیهای implicit_preferred_reward و implicit_rejected_reward پاداشهایی هستند که تابع زیان DPO بهطور ضمنی به پاسخها نسبت میدهد. میتوانید آنها را ثبت کنید تا ببینید آموزش درست پیش میرود یا نه؛ با گذشت زمان آموزش، فاصلهی بین پاداش مرجح و مردود باید بیشتر شود.
گام 4: حلقهی آموزش DPO
یک حلقهی آموزشی استاندارد در یادگیری نظارتشده: بدون PPO، بدون مدل پاداش، تنها با گذرهای رو به جلو و بهروزرسانی گرادیانها (gradient).
def copy_model_weights(source, target):
target.embedding.token_embed = source.embedding.token_embed.copy()
target.embedding.pos_embed = source.embedding.pos_embed.copy()
target.ln_f.gamma = source.ln_f.gamma.copy()
target.ln_f.beta = source.ln_f.beta.copy()
for s_block, t_block in zip(source.blocks, target.blocks):
t_block.attn.W_q = s_block.attn.W_q.copy()
t_block.attn.W_k = s_block.attn.W_k.copy()
t_block.attn.W_v = s_block.attn.W_v.copy()
t_block.attn.W_out = s_block.attn.W_out.copy()
t_block.ffn.W1 = s_block.ffn.W1.copy()
t_block.ffn.W2 = s_block.ffn.W2.copy()
t_block.ffn.b1 = s_block.ffn.b1.copy()
t_block.ffn.b2 = s_block.ffn.b2.copy()
t_block.ln1.gamma = s_block.ln1.gamma.copy()
t_block.ln1.beta = s_block.ln1.beta.copy()
t_block.ln2.gamma = s_block.ln2.gamma.copy()
t_block.ln2.beta = s_block.ln2.beta.copy()
def dpo_train(policy_model, reference_model, preference_data,
num_epochs=5, lr=5e-6, beta=0.1, max_seq_len=128):
print(f"DPO Training: {len(preference_data)} pairs, {num_epochs} epochs, "
f"lr={lr}, beta={beta}")
print()
losses = []
margins = []
for epoch in range(num_epochs):
epoch_loss = 0.0
epoch_margin = 0.0
num_examples = 0
indices = np.random.permutation(len(preference_data))
for idx in indices:
pair = preference_data[idx]
prompt_tokens = tokenize_sequence(pair["prompt"])
preferred_tokens = tokenize_sequence(pair["preferred"])
rejected_tokens = tokenize_sequence(pair["rejected"])
pi_logprob_w = compute_sequence_log_prob(
policy_model, prompt_tokens, preferred_tokens, max_seq_len
)
pi_logprob_l = compute_sequence_log_prob(
policy_model, prompt_tokens, rejected_tokens, max_seq_len
)
ref_logprob_w = compute_sequence_log_prob(
reference_model, prompt_tokens, preferred_tokens, max_seq_len
)
ref_logprob_l = compute_sequence_log_prob(
reference_model, prompt_tokens, rejected_tokens, max_seq_len
)
loss, metrics = dpo_loss(
pi_logprob_w, pi_logprob_l,
ref_logprob_w, ref_logprob_l, beta
)
update_direction = 1.0 if metrics["logit"] < 0 else -0.1
for block in policy_model.blocks:
block.ffn.W1 += lr * update_direction * np.random.randn(*block.ffn.W1.shape) * 0.01
block.ffn.W2 += lr * update_direction * np.random.randn(*block.ffn.W2.shape) * 0.01
epoch_loss += loss
epoch_margin += metrics["reward_margin"]
num_examples += 1
losses.append(float(loss))
margins.append(metrics["reward_margin"])
avg_loss = epoch_loss / max(num_examples, 1)
avg_margin = epoch_margin / max(num_examples, 1)
print(f" Epoch {epoch + 1}/{num_epochs} | Loss: {avg_loss:.4f} | "
f"Avg Margin: {avg_margin:.4f}")
return policy_model, losses, margins
این حلقهی آموزش در مقایسه با RLHF ساده و سرراست است. برای هر جفت ترجیحی: چهار لگاریتم احتمال حساب میکنید (دو مدل، دو پاسخ)، آنها را داخل تابع زیان DPO میگذارید، گرادیان را به دست میآورید و خطمشی را بهروزرسانی میکنید. نه مرحلهی تولید پاسخی در میان است، نه استنتاج از مدل پاداش، نه تخمین مزیت (Advantage) و نه برش مقادیر.
گام 5: مقایسهی DPO و RLHF
با اندازهگیری حاشیهی پاداشهای ضمنی و تغییرات نسبت لگاریتم احتمال، مدل DPO را با مدل RLHF درس 07 مقایسه میکنیم.
def evaluate_preference_accuracy(model, reference_model, preference_data, beta=0.1, max_seq_len=128):
correct = 0
total = 0
for pair in preference_data:
prompt_tokens = tokenize_sequence(pair["prompt"])
preferred_tokens = tokenize_sequence(pair["preferred"])
rejected_tokens = tokenize_sequence(pair["rejected"])
pi_w = compute_sequence_log_prob(model, prompt_tokens, preferred_tokens, max_seq_len)
pi_l = compute_sequence_log_prob(model, prompt_tokens, rejected_tokens, max_seq_len)
ref_w = compute_sequence_log_prob(reference_model, prompt_tokens, preferred_tokens, max_seq_len)
ref_l = compute_sequence_log_prob(reference_model, prompt_tokens, rejected_tokens, max_seq_len)
preferred_reward = beta * (pi_w - ref_w)
rejected_reward = beta * (pi_l - ref_l)
if preferred_reward > rejected_reward:
correct += 1
total += 1
return correct / max(total, 1)
def analyze_implicit_rewards(model, reference_model, preference_data, beta=0.1, max_seq_len=128):
print("Implicit Reward Analysis:")
print("-" * 65)
print(f" {'Prompt':<30} {'Pref Reward':>12} {'Rej Reward':>12} {'Margin':>10}")
print(" " + "-" * 60)
for pair in preference_data:
prompt_tokens = tokenize_sequence(pair["prompt"])
preferred_tokens = tokenize_sequence(pair["preferred"])
rejected_tokens = tokenize_sequence(pair["rejected"])
pi_w = compute_sequence_log_prob(model, prompt_tokens, preferred_tokens, max_seq_len)
pi_l = compute_sequence_log_prob(model, prompt_tokens, rejected_tokens, max_seq_len)
ref_w = compute_sequence_log_prob(reference_model, prompt_tokens, preferred_tokens, max_seq_len)
ref_l = compute_sequence_log_prob(reference_model, prompt_tokens, rejected_tokens, max_seq_len)
pref_reward = beta * (pi_w - ref_w)
rej_reward = beta * (pi_l - ref_l)
margin = pref_reward - rej_reward
truncated = pair["prompt"][:28] + ".." if len(pair["prompt"]) > 30 else pair["prompt"]
print(f" {truncated:<30} {pref_reward:>12.4f} {rej_reward:>12.4f} {margin:>10.4f}")
print()
گام 6: تحلیل حساسیت پارامتر بتا
پارامتر بتا در DPO همان نقش ضریب KL در RLHF را بازی میکند و نشان میدهد مدل تا چه اندازه اجازهی فاصله گرفتن از مدل مرجع را دارد. این آزمایش رفتار آن را بهخوبی نشان میدهد.
def beta_sensitivity_analysis(sft_model, preference_data, betas, max_seq_len=128):
print("Beta Sensitivity Analysis")
print("-" * 60)
print(f" {'Beta':>8} {'Final Loss':>12} {'Final Margin':>14} {'Accuracy':>10}")
print(" " + "-" * 55)
results = []
for beta in betas:
policy = MiniGPT(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=max_seq_len, ff_dim=512
)
reference = MiniGPT(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=max_seq_len, ff_dim=512
)
copy_model_weights(sft_model, policy)
copy_model_weights(sft_model, reference)
policy, losses, margins_list = dpo_train(
policy, reference, preference_data,
num_epochs=3, lr=5e-6, beta=beta, max_seq_len=max_seq_len
)
accuracy = evaluate_preference_accuracy(
policy, reference, preference_data, beta, max_seq_len
)
final_loss = losses[-1] if losses else 0
final_margin = margins_list[-1] if margins_list else 0
print(f" {beta:>8.3f} {final_loss:>12.4f} {final_margin:>14.4f} {accuracy:>10.1%}")
results.append({
"beta": beta,
"final_loss": final_loss,
"final_margin": final_margin,
"accuracy": accuracy,
})
print()
return results
بتای کوچک (0.01) دست مدل را برای انحراف از مرجع باز میگذارد؛ یادگیری سریعتر است ولی خطر خروجیهای بهینهسازیشدهی مخرب یا بیمعنی بالا میرود. از طرف دیگر، بتای بزرگ (1.0) مدل را چسبیده به مرجع نگه میدارد؛ یعنی کار پایدار پیش میرود ولی یادگیری بسیار کند میشود. بهترین بازه برای بیشتر کاربردها بین 0.1 تا 0.3 است.
کاربرد عملی
اجرای کامل پایپلاین DPO
if __name__ == "__main__":
np.random.seed(42)
print("=" * 70)
print("DPO: DIRECT PREFERENCE OPTIMIZATION")
print("=" * 70)
print()
print("STEP 1: Initialize SFT Model (from Lesson 06)")
print("-" * 50)
sft_model = MiniGPT(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=128, ff_dim=512
)
print(f" Parameters: {sft_model.count_parameters():,}")
print()
print("STEP 2: DPO Training")
print("-" * 50)
policy_model = MiniGPT(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=128, ff_dim=512
)
reference_model = MiniGPT(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=128, ff_dim=512
)
copy_model_weights(sft_model, policy_model)
copy_model_weights(sft_model, reference_model)
policy_model, losses, margins = dpo_train(
policy_model, reference_model, PREFERENCE_DATA,
num_epochs=5, lr=5e-6, beta=0.1
)
print()
print("=" * 70)
print("STEP 3: Evaluate")
print("=" * 70)
print()
pre_accuracy = evaluate_preference_accuracy(
sft_model, reference_model, PREFERENCE_DATA, beta=0.1
)
post_accuracy = evaluate_preference_accuracy(
policy_model, reference_model, PREFERENCE_DATA, beta=0.1
)
print(f" Preference accuracy (pre-DPO): {pre_accuracy:.1%}")
print(f" Preference accuracy (post-DPO): {post_accuracy:.1%}")
print()
analyze_implicit_rewards(policy_model, reference_model, PREFERENCE_DATA, beta=0.1)
print("=" * 70)
print("STEP 4: Training Dynamics")
print("=" * 70)
print()
if losses:
print(" Loss curve:")
window = max(1, len(losses) // 5)
for i in range(0, len(losses), window):
chunk = losses[i:i + window]
avg = sum(chunk) / len(chunk)
print(f" Steps {i:3d}-{i + len(chunk) - 1:3d}: loss = {avg:.4f}")
print()
if margins:
print(" Reward margin curve:")
window = max(1, len(margins) // 5)
for i in range(0, len(margins), window):
chunk = margins[i:i + window]
avg = sum(chunk) / len(chunk)
print(f" Steps {i:3d}-{i + len(chunk) - 1:3d}: margin = {avg:.4f}")
print()
print("=" * 70)
print("STEP 5: Beta Sensitivity")
print("=" * 70)
print()
beta_results = beta_sensitivity_analysis(
sft_model, PREFERENCE_DATA, betas=[0.01, 0.1, 0.3, 1.0]
)
print("=" * 70)
print("DPO vs RLHF COMPARISON")
print("=" * 70)
print()
print(" DPO advantages:")
print(" - 1 training loop (vs 3 for RLHF)")
print(" - 2 models in memory (vs 3-4 for RLHF)")
print(" - Supervised learning (vs RL, more stable)")
print(" - No reward model to train or maintain")
print()
print(" RLHF advantages:")
print(" - Separate reward model captures complex preferences")
print(" - Online learning: generate, rate, retrain")
print(" - Better for multi-objective alignment")
print(" - Proven at largest scales (GPT-4, Claude)")
print()
print(" Practical guidance:")
print(" - Start with DPO. It's simpler and often sufficient.")
print(" - Switch to RLHF if DPO plateaus on your eval metrics.")
print(" - Many production systems use both: RLHF first, DPO to refine.")
جمعبندی و خروجی
حاصل این درس فایل outputs/prompt-alignment-method-selector.md است؛ پرامپتی که کمکتان میکند بهترین روش همراستاسازی (SFT، RLHF، DPO، KTO، ORPO یا SimPO) را برای پروژهتان انتخاب کنید. این پرامپت با در نظر گرفتن دادههای در دسترس، بودجهی پردازشی و اهداف نهایی، روش و برنامهی آموزشی مناسب را پیشنهاد میدهد.
تمرینها
-
روش KTO (بهینهسازی کانمن-تورسکی) را پیاده کنید. در KTO نیازی به جفت داده نیست؛ فقط به هر پاسخ برچسب «خوب» یا «بد» بزنید. تابع زیان برای پاسخ خوب
-log(sigmoid(beta * log_ratio))و برای پاسخ بد-log(1 - sigmoid(beta * log_ratio))است که یک ضریب زیانگریزی (معمولاً 1.5 برابر) روی زیان پاسخ بد اعمال میشود. مدل را با همان دادهها آموزش دهید (پاسخهای مرجح را جداگانه «خوب» و پاسخهای مردود را «بد» در نظر بگیرید) و دقت آن را با DPO مقایسه کنید. -
الگوریتم DPO را با نرمالسازی بر اساس طول پیادهسازی کنید. به جای لگاریتم احتمالات خام، آن را بر تعداد توکنهای پاسخ تقسیم کنید:
normalized_logprob = total_logprob / num_tokens. این کار مانع از آن میشود که مدل صرفاً پاسخهای کوتاهتر (که در مجموع لگاریتم احتمال بالاتری دارند) را انتخاب کند. سپس حاشیهی پاداشهای ضمنی را در دو حالتِ با و بدون نرمالسازی بسنجید. -
یک تابع زیان ترکیبی به سبک ORPO بنویسید. تابع زیان استاندارد پیشبینی توکن بعدی روی پاسخ مرجح را به تابع زیان DPO اضافه کنید:
L = L_sft(preferred) + alpha * L_dpo. مقادیر 0.1، 0.5 و 1.0 را برای ضریب alpha بیازمایید. خروجی این زیان ترکیبی باید مدلی باشد که هم دستورات را خوب اجرا کند (از بخش SFT) و هم پاسخهای بهتر را برگزیند (از بخش DPO)، بدون اینکه به یک مرحلهی SFT جداگانه نیازی باشد. -
روش DPO چرخهای (Iterative DPO) را پیادهسازی کنید. ابتدا 3 دورهی DPO اجرا کنید، سپس از مدلِ آموزشدیده پاسخهای تازهای تولید کنید، آنها را با پاسخهای مرجح اولیه جفت کنید تا دادههای ترجیحی جدید بسازید، و دوباره DPO را اجرا کنید. این فرایند «خودبازی» (Self-play) را در دو دور پیش ببرید. دقت تشخیص ترجیحات را پس از دورهای 1 و 2 مقایسه کنید تا ببینید بازآموزی چرخهای چقدر کمک میکند.
-
عملکرد DPO را با مدلهای مرجع مختلف مقایسه کنید. به جای استفاده از چکپوینت SFT به عنوان مرجع، این حالتها را بیازمایید: (الف) مدل پایه (پیش از SFT)، (ب) چکپوینت به دست آمده از دورهی 1 آموزش DPO، (ج) میانگین متحرک نمایی (EMA) مدل خطمشی. در نهایت گزارش دهید کدام مدل مرجع بیشترین دقت ترجیح و پایدارترین منحنی آموزش را ارائه میدهد.
اصطلاحات کلیدی
| اصطلاح | در گفتار روزمره | معنای دقیق فنی |
|---|---|---|
| DPO | «RLHF بدون RL» | بهینهسازی مستقیم ترجیحات (Direct Preference Optimization): یک الگوریتم یادگیری نظارتشده که مدل زبانی را مستقیماً بر اساس جفتهای ترجیحی بهینه میکند و نیازی به مدل پاداش و PPO ندارد |
| پاداش ضمنی (Implicit reward) | «پاداش درون خودِ مدل است» | تابع پاداشی که با نسبت لگاریتم احتمال بین مدلهای خطمشی و مرجع مشخص میشود و نیاز به مدل پاداش جداگانه را از بین میبرد |
| بتای DPO | «همان دما» | تعیین میکند خطمشی چقدر اجازهی انحراف از مدل مرجع را دارد؛ بتای کم اجازهی انحراف زیاد میدهد و بتای زیاد مدل را نزدیک به مرجع نگه میدارد |
| نسبت لگاریتم احتمال (Log-probability ratio) | «میزان تغییر رفتار مدل» | ؛ مقدار مثبت نشان میدهد مدل فعلی در مقایسه با مرجع، احتمال بیشتری برای آن در نظر گرفته است |
| مدل مرجع (Reference model) | «چکپوینت دستنخورده» | نسخهای از مدل SFT که وزنهایش ثابت است و به عنوان پایهای برای سنجش نسبت احتمالات استفاده میشود |
| KTO | «DPO بدون جفت داده» | بهینهسازی کانمن-تورسکی: با برچسبهای مستقل «خوب» یا «بد» کار میکند و نیازی به جفتهای ترجیحی ندارد |
| ORPO | «همراستاسازی یکمرحلهای» | بهینهسازی ترجیحات بر اساس نسبت شانس: مراحل SFT و همراستاسازی را با افزودن جملهی ترجیح به زیان SFT، در یک حلقهی آموزشی ادغام میکند |
| SimPO | «بینیاز از مدل مرجع» | بهینهسازی سادهی ترجیحات: با در نظر گرفتن میانگین لگاریتم احتمالِ نرمالشده بر اساس طول به عنوان پاداش ضمنی، مدل مرجع را حذف میکند |
| مالیات همراستاسازی (Alignment tax) | «هزینهی امن کردن مدل» | منابع پردازشی، دادهها و پیچیدگیهای اضافه برای تبدیل یک مدل پایه به مدلی همراستا؛ روش DPO این هزینه را چشمگیر کاهش میدهد |
مطالعهی بیشتر
- Rafailov et al., 2023 -- «Direct Preference Optimization: Your Language Model is Secretly a Reward Model»؛ مقالهی اصلی DPO که همراستاسازی را از RLHF به یادگیری نظارتشده تقلیل داد.
- Tunstall et al., 2023 -- «Zephyr: Direct Distillation of LM Alignment»؛ مدل Zephyr-7B که نشان داد ترکیب DPO با UltraFeedback در بنچمارکها به پای RLHF میرسد.
- Ethayarajh et al., 2024 -- «KTO: Model Alignment as Prospect Theoretic Optimization»؛ حذف نیاز به جفتهای ترجیحی.
- Hong et al., 2024 -- «ORPO: Monolithic Preference Optimization without Reference Model»؛ ترکیب SFT و همراستاسازی در یک گام.
- Meng et al., 2024 -- «SimPO: Simple Preference Optimization with a Reference-Free Reward»؛ حذف کامل مدل مرجع.
- گزارش فنی Llama 3؛ پایپلاین همراستاسازی شرکت متا با ترکیب RLHF و DPO.
