فاز 10 · درس 7

آموزش گام‌به‌گام RLHF با ساخت مدل پاداش و PPO

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python (with numpy)
یک ترازوی متوازن فلزی بر روی میز کار آزمایشگاهی، با مکعب‌های هندسی درخشان در دو کفه و یک حسگر نوری دقیق که تغییرات تراز را ثبت می‌کند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم کلیدی
  4. سه مرحله‌ی اصلی
  5. مدل پاداش
  6. الگوریتم PPO: بهینه‌سازی خط‌مشی نزدیک
  7. جزئیات تابع هدف PPO
  8. دور زدن پاداش (Reward Hacking)
  9. پایپ‌لاین‌های RLHF در دنیای واقعی
  10. پیاده‌سازی
  11. گام 1: داده‌های ترجیحات ساختگی
  12. گام 2: معماری مدل پاداش
  13. گام 3: تابع زیان برادلی-تری
  14. گام 4: حلقه‌ی ساده‌سازی‌شده‌ی PPO
  15. گام 5: مقایسه‌ی امتیاز پاداش
  16. کاربرد عملی
  17. دموی پایپ‌لاین کامل RLHF
  18. تحویل نهایی
  19. تمرین‌ها
  20. اصطلاحات کلیدی
  21. برای مطالعه‌ی بیشتر

در فرآیند تنظیم دقیق دستوری (SFT)، مدل یاد می‌گیرد دستورها را اجرا کند، اما ابزاری برای سنجش کیفیت نسبی پاسخ‌ها ندارد؛ یعنی اگر دو پاسخ درست در داده‌ها باشند، مدل تفاوت میان یک پاسخ معمولی و یک پاسخ درخشان را نمی‌فهمد. برای حل این مشکل، تکنیک یادگیری تقویتی از طریق بازخورد انسانی (RLHF) ابداع شد تا قضاوت، سلیقه و اولویت‌های کیفی انسان را مستقیماً به مدل منتقل کند.

در این درس پایپ‌لاین سه‌مرحله‌ای RLHF را از پایه می‌سازید: ابتدا یک مدل پاداش (Reward Model) با استفاده از تابع زیان مقایسه‌های زوجی برادلی-تری (Bradley-Terry) پیاده می‌کنید تا کیفیت خروجی‌ها را نمره‌دهی کند؛ سپس با اجرای حلقه‌ی بهینه‌سازی PPO و اعمال جریمه‌ی واگرایی KL، مدل زبانی را به سمت تولید پاسخ‌های بهتر بدون دچار شدن به پدیده‌ی دور زدن پاداش (Reward Hacking) هدایت خواهید کرد.

در دنیای واقعی آموزش کامل RLHF نیازمند خوشه‌های پردازشی سنگین است که به دلیل هزینه‌های بالای تأمین یا اجاره‌ی GPUهای ابری چالش‌برانگیز است؛ اما کدها و معماری پایه‌ای که در این درس پیاده‌سازی می‌کنید به‌گونه‌ای طراحی شده‌اند که روی پردازنده‌های معمولی (CPU) یا تراشه‌های شخصی نیز قابل شبیه‌سازی و اجرا باشند.

روش SFT به مدل یاد می‌دهد که دستورالعمل‌ها را دنبال کند؛ اما یاد نمی‌دهد کدام پاسخ «بهتر» است. دو پاسخ کاملاً درست از نظر دستوری و واقعیت، ممکن است از نظر میزان مفید بودن تفاوت چشمگیری با هم داشته باشند. یادگیری تقویتی از طریق بازخورد انسانی (RLHF) ابزاری است که با آن قضاوت انسان را در رفتار مدل کدگذاری می‌کنید. همین رویکرد است که Claude را این‌قدر مفید و GPT را مؤدب می‌کند.

اهداف یادگیری

  • ساخت یک مدل پاداش (reward model) که کیفیت پاسخ‌ها را بر اساس جفت‌های ترجیح انسانی (پاسخ منتخب در برابر ردشده) امتیازدهی کند
  • پیاده‌سازی حلقه‌ی آموزشی PPO برای بهینه‌سازی خط‌مشیِ (policy) مدل زبانی در برابر مدل پاداش به همراه جریمه‌ی واگرایی کولبک-لایبلر (KL)
  • توضیح این‌که چرا RLHF به سه مدل (SFT، پاداش و خط‌مشی) نیاز دارد و قید KL چطور مانع از دور زدن پاداش (Reward Hacking) می‌شود
  • ارزیابی اثر RLHF از طریق مقایسه‌ی کیفیت پاسخ‌ها پیش و پس از بهینه‌سازی ترجیحات

صورت مسئله

اگر از مدلی بخواهید «رایانش کوانتومی را توضیح بده»، ممکن است چنین پاسخ‌هایی تولید کند:

پاسخ A: «رایانش کوانتومی از کیوبیت‌هایی استفاده می‌کند که می‌توانند در حالت برهم‌نهی قرار بگیرند؛ یعنی هم‌زمان 0، 1 یا هر دو باشند. این ویژگی به رایانه‌های کوانتومی امکان می‌دهد محاسبات خاصی را با سرعتی نمایی نسبت به رایانه‌های کلاسیک انجام دهند. از الگوریتم‌های کلیدی این حوزه می‌توان به الگوریتم شور برای تجزیه‌ی اعداد بزرگ و الگوریتم گروور برای جست‌وجو در پایگاه‌های داده‌ی مرتب‌نشده اشاره کرد.»

پاسخ B: «رایانش کوانتومی نوعی رایانش است که از پدیده‌های مکانیک کوانتومی بهره می‌برد. این مفهوم نخستین بار در دهه‌ی 1980 مطرح شد. ریچارد فاینمن پیشنهاد داد که سیستم‌های کوانتومی را می‌توان با رایانه‌های کوانتومی شبیه‌سازی کرد. این حوزه از آن زمان تاکنون رشد چشمگیری داشته است. شرکت‌های متعددی در حال حاضر روی رایانه‌های کوانتومی کار می‌کنند. IBM، گوگل و دیگران پیشرفت‌هایی داشته‌اند. گوگل در سال 2019 ادعای برتری کوانتومی کرد.»

هر دو پاسخ از نظر علمی درست هستند، دستور زبان بی‌نقصی دارند و دستور پرامپت را اجرا کرده‌اند؛ اما پاسخ A مشخصاً بهتر است: خلاصه‌تر، آموزنده‌تر و خوش‌ساختارتر است. هر انسانی بدون تردید A را انتخاب می‌کند.

روش SFT قادر به تشخیص این تمایز نیست. این روش مدل را صرفاً روی پاسخ‌های «درست» آموزش می‌دهد، اما سازوکاری ندارد که بگوید «این پاسخ از آن یکی بهتر است». SFT با همه‌ی نمونه‌های آموزشی یکسان برخورد می‌کند. اگر هر دوی این پاسخ‌ها در دیتاست SFT حضور داشتند، مدل از هر دو به یک اندازه می‌آموخت.

فرایند RLHF این مشکل را حل می‌کند: ابتدا یک مدل پاداش آموزش می‌دهد تا پیش‌بینی کند انسان کدام پاسخ را ترجیح می‌دهد، سپس با تکیه بر این سیگنال پاداش، مدل زبانی را به سمت تولید خروجی‌های باکیفیت‌تر هدایت می‌کند. مدل InstructGPT (پیش‌درآمد ChatGPT) با استفاده از RLHF توانست مفید بودن، صداقت و بی‌ضرر بودن GPT-3 را به‌شکل چشمگیری ارتقا دهد. ارزیاب‌های داخلی OpenAI در 85% موارد پاسخ‌های InstructGPT را به خروجی‌های GPT-3 ترجیح دادند، هرچند InstructGPT حدود 135 برابر کوچک‌تر بود (1.3 میلیارد در برابر 175 میلیارد پارامتر).

مفاهیم کلیدی

سه مرحله‌ی اصلی

روش RLHF یک نوبت آموزشِ منفرد نیست؛ پایپ‌لاینی شامل سه مرحله‌ی متوالی است که هر مرحله روی دستاورد مرحله‌ی قبل بنا می‌شود:

مرحله‌ی 1: SFT. یک مدل پایه را روی جفت‌های «دستورالعمل-پاسخ» آموزش می‌دهید (درس 06). این کار مدلی در اختیارتان می‌گذارد که از دستورها پیروی می‌کند، ولی برتری نسبی پاسخ‌ها به یکدیگر را درک نمی‌کند.

مرحله‌ی 2: مدل پاداش. داده‌های ترجیحات انسانی را جمع‌آوری می‌کنید: دو پاسخ برای یک پرامپت یکسان به ارزیاب‌ها نشان می‌دهید و می‌پرسید «کدام بهتر است؟». مدلی آموزش می‌دهید که این ترجیحات را پیش‌بینی کند. این مدل پاداش جفتِ (پرامپت، پاسخ) را ورودی می‌گیرد و یک امتیاز عددی اسکالر خروجی می‌دهد.

مرحله‌ی 3: الگوریتم PPO. از مدل پاداش برای تولید سیگنال آموزشی برای مدل زبانی استفاده می‌کنید. مدل زبانی پاسخ‌هایی تولید می‌کند، مدل پاداش به آن‌ها امتیاز می‌دهد، و الگوریتم PPO مدل زبانی را به‌روزرسانی می‌کند تا پاسخ‌هایی با امتیاز بالاتر بسازد. در این میان، جریمه‌ی واگرایی کولبک-لایبلر (KL) مانع از آن می‌شود که مدل زبانی بیش از حد از چک‌پوینت SFT فاصله بگیرد.

مرحله 3: PPOمرحله 2: مدل پاداشمرحله 1: SFTتولید پاسخ‌هاراه‌اندازی خط‌مشیانجماد به عنوان مرجعتولیدسیگنال پاداشجریمه KLبه‌روزرسانیمدل پایهمدل SFTداده‌های دستورالعمل(27K نمونه)جفت‌های ترجیحی(پرامپت، برنده، بازنده)ارزیاب‌های انسانیمدل پاداشR(prompt, response) → scoreمدل خط‌مشی(در حال بهینه‌سازی)مدل مرجع(SFT منجمد)پاسخبه‌روزرسانی PPO

مدل پاداش

مدل پاداش در واقع همان مدل زبانی است که برای امتیازدهی تغییر کاربری داده است: مدل SFT را برمی‌دارید و سر (head) مدل‌سازی زبان را — که توزیع احتمالات روی واژگان را خروجی می‌داد — با یک سر اسکالر (که تنها یک عدد خروجی می‌دهد) تعویض می‌کنید. معماری این دو مدل تا قبل از لایه‌ی آخر کاملاً یکسان است.

ورودی: اتصال پرامپت به پاسخ. خروجی: یک امتیاز پاداش اسکالر واحد.

داده‌های آموزشی از جفت‌های ترجیحات انسانی تشکیل می‌شوند. ارزیاب‌ها برای هر پرامپت دو پاسخ می‌بینند و گزینه‌ی بهتر را برمی‌گزینند. حاصل این کار سه‌گانه‌های آموزشی است: (پرامپت، پاسخ_منتخب، پاسخ_ردشده).

تابع زیان (loss function) این مرحله بر پایه‌ی مدل مقایسه‌های زوجی برادلی-تری (Bradley-Terry) تعریف می‌شود:

loss=−log⁡(sigmoid(reward(preferred)−reward(rejected)))\text{loss} = -\log\big(\mathop{\mathrm{sigmoid}}(\text{reward}(\text{preferred}) - \text{reward}(\text{rejected}))\big)

این معادله‌ی کلیدی ماجراست. عبارت sigmoid(reward(A) - reward(B)) احتمال ترجیح پاسخ A به پاسخ B را نشان می‌دهد. تابع زیانْ مدل پاداش را سوق می‌دهد تا به پاسخ منتخب، امتیاز بالاتری اختصاص دهد.

چرا مقایسه‌ی زوجی به‌جای امتیازدهی مطلق؟ چون انسان‌ها در نمره‌دهی مطلق بسیار ضعیف عمل می‌کنند («این پاسخ نمره‌ی 7.3 می‌گیرد یا 7.5 از 10؟»)، اما در مقایسه‌ی نسبی فوق‌العاده‌اند («آیا A بهتر از B است؟»). مدل برادلی-تری مقایسه‌های نسبی را به یک سیستم نمره‌دهی مطلق و پایدار تبدیل می‌کند.

آمارهای InstructGPT: شرکت OpenAI تعداد 33,000 جفت مقایسه‌ای را از 40 نیروی پیمانکار جمع‌آوری کرد. ثبت هر مقایسه حدود 5 دقیقه زمان برد؛ یعنی 2,750 ساعت کار انسانی تنها برای داده‌های آموزش مدل پاداش صرف شد.

الگوریتم PPO: بهینه‌سازی خط‌مشی نزدیک

الگوریتم PPO (یا Proximal Policy Optimization) یک الگوریتم یادگیری تقویتی است. در RLHF، «محیط» همان مدل پاداش است، «ایجنت» مدل زبانی است و «کنش» تولید یک توکن است.

تابع هدف:

maximize:E[R(prompt,response)]−β⋅KL(policy ∥ reference)\text{maximize:} \quad \mathbb{E}[R(\text{prompt}, \text{response})] - \beta \cdot \mathrm{KL}(\text{policy} \,\|\, \text{reference})

جمله‌ی اول مدل را ترغیب می‌کند پاسخ‌هایی با پاداش بالا بسازد. جمله‌ی دوم (جریمه‌ی واگرایی KL) سد راه انحراف بیش از حد مدل از چک‌پوینت SFT می‌شود.

چرا جریمه‌ی KL ضروری است؟ بدون این قید، مدل راه‌حل‌های تباهیده (degenerate) پیدا می‌کند. مدل پاداش روی مجموعه‌داده‌ی محدودی از ترجیحات انسانی آموزش دیده و طبیعتاً نقاط کوری دارد. مدل زبانی از این نقاط کور سوءاستفاده خواهد کرد؛ یعنی خروجی‌هایی تولید می‌کند که امتیاز بالایی از مدل پاداش می‌گیرند اما در عمل کاملاً بی‌معنی هستند. نمونه‌های کلاسیک:

  • تکرار مداوم جمله‌ی «من خیلی مفید و بی‌ضرر هستم!» که امتیاز بالایی در مدل‌های پاداشِ متمرکز بر مفید و بی‌ضرر بودن می‌گیرد
  • نوشتن پاسخ‌های پر از لغت و بسیار رسمی اما توخالی که فقط الگوی متنیِ «باکیفیت» را تقلید می‌کنند
  • سوءاستفاده از عبارت‌های خاصی که تصادفاً در داده‌های آموزشی با پاداش بالا همبستگی داشته‌اند

جریمه‌ی KL می‌گوید: می‌توانید پیشرفت کنید، اما نباید به مدلی کاملاً متفاوت تبدیل شوید! باید به نسخه‌ی SFT — که از قبل عملکردی معقول داشت — نزدیک بمانید. اگر بیش از اندازه دور شوید، هزینه‌ی جریمه‌ی KL بر کل امتیاز پاداش غلبه خواهد کرد.

آمارهای InstructGPT: در آموزش PPO از نرخ یادگیری 1.5e-5، ضریب KL برابر با β=0.02\beta=0.02، 256 هزار اپیزود (جفت‌های پرامپت-پاسخ) و 4 ایپاک PPO به‌ازای هر بچ استفاده شد. کل پایپ‌لاین RLHF چندین روز روی کلاستری از GPUها زمان برد.

حلقه آموزش PPOتکرارنمونه‌برداری پرامپتاز مجموعه دادهتولید پاسختوسط خط‌مشیامتیازدهی به پاسختوسط مدل پاداشمحاسبه واگرایی کولبک-لایبلرنسبت به مدل مرجعتابع هدف:reward - beta * KLبه‌روزرسانی گرادیان PPO(clipped surrogate loss)

جزئیات تابع هدف PPO

الگوریتم PPO از یک «تابع هدف جانشین برش‌خورده» (Clipped Surrogate Objective) برای پیشگیری از به‌روزرسانی‌های بیش از حد بزرگ استفاده می‌کند. نسبت احتمال در خط‌مشی جدید به خط‌مشی قدیم در بازه‌ی [1−ϵ,1+ϵ][1 - \epsilon, 1 + \epsilon] مهار یا اصطلاحاً برش (clip) می‌خورد که در آن مقدار ϵ\epsilon معمولاً 0.2 است.

ratio=πnew(action∣state)/πold(action∣state)clipped_ratio=clip(ratio,1−ϵ,1+ϵ)loss=−min⁡(ratio⋅advantage,clipped_ratio⋅advantage)\begin{aligned} \text{ratio} &= \pi_{\text{new}}(\text{action} \mid \text{state}) / \pi_{\text{old}}(\text{action} \mid \text{state}) \\ \text{clipped\_ratio} &= \mathop{\mathrm{clip}}(\text{ratio}, 1 - \epsilon, 1 + \epsilon) \\ \text{loss} &= -\min(\text{ratio} \cdot \text{advantage}, \text{clipped\_ratio} \cdot \text{advantage}) \end{aligned}

تابع مزیت (advantage) تخمین می‌زند که پاسخ فعلی در مقایسه با کیفیت موردانتظار چقدر بهتر بوده است. در RLHF:

advantage = reward(prompt, response) - baseline

خط مبنا (baseline) اغلب میانگین پاداش پاسخ‌های اخیر در نظر گرفته می‌شود. مزیت مثبت یعنی این پاسخ بهتر از حد میانگین بوده و مزیت منفی یعنی عملکردی ضعیف‌تر از میانگین داشته است. الگوریتم PPO احتمال تولید پاسخ‌های بالاتر از میانگین را افزایش و احتمال پاسخ‌های ضعیف‌تر از میانگین را کاهش می‌دهد.

فرایند برش از به‌روزرسانی‌های فاجعه‌بار جلوگیری می‌کند. اگر پاسخی به‌طور غیرعادی پاداش بسیار بالایی بگیرد، نسبت احتمالاتِ برش‌نخورده ممکن است جهش عظیمی داشته باشد و مدل را شدیداً به سمت آن پاسخ سوق دهد. عمل برش این جهش را مهار کرده و پایداری روند آموزش را حفظ می‌کند.

دور زدن پاداش (Reward Hacking)

روی تاریک RLHF همین‌جاست. مدل زبانی در حال بهینه‌سازی در برابر مدل پاداش است؛ مدلی که خود تقریب ناقصی از ترجیحات انسانی است. با ماهرتر شدن مدل زبانی در بیشینه‌سازی پاداش، نقاط ضعف مدل پاداش شروع به استثمار شدن می‌کنند.

حالت‌های شکست رایج:

حالت شکست چه اتفاقی می‌افتد چرا
درازگویی (Verbosity) مدل پاسخ‌های طولانی و طولانی‌تر می‌دهد ارزیاب‌های انسانی اغلب پاسخ‌های مفصل‌تر را ترجیح می‌دادند، بنابراین مدل پاداش به طول متن امتیاز بالاتری می‌دهد
چاپلوسی (Sycophancy) مدل با تمام گفته‌های کاربر موافقت می‌کند ارزیاب‌ها پاسخ‌هایی را ترجیح می‌دادند که با فرض‌پایه‌ی سؤال همسو بود
طفره‌روی (Hedging) مدل از دادن پاسخ قاطع سر باز می‌زند پاسخ‌های مبهم و دست‌به‌عصا («این موضوع پیچیده است و دیدگاه‌های متعددی دارد...») به‌ندرت به‌عنوان پاسخ غلط علامت می‌خورند
بازی با فرمت مدل بیش از حد از لیست‌های نشانه‌دار و تیترها استفاده می‌کند پاسخ‌های قالب‌بندی‌شده در نگاه ارزیاب‌ها ساختاریافته‌تر و شسته‌رفته‌تر جلوه می‌کردند

راهکارهای کاهش اثر: اعمال جریمه‌ی KL قوی‌تر (تا مدل نتواند آن‌قدر دور شود که این ضعف‌ها را استثمار کند)، آموزش مدل پاداش با نمونه‌های تخاصمی (برای وصله کردن حالت‌های شکست شناخته‌شده) و استفاده از چند مدل پاداش با معماری‌های متفاوت (تا دور زدن هم‌زمان همه‌ی آن‌ها دشوارتر شود).

پایپ‌لاین‌های RLHF در دنیای واقعی

مدل جفت‌های مقایسه‌ای ارزیاب‌ها حجم مدل پاداش گام‌های PPO ضریب KL
InstructGPT 33K 40 6B 256K 0.02
Llama 2 Chat ~1M اعلام‌نشده 70B اعلام‌نشده 0.01
Claude اعلام‌نشده اعلام‌نشده اعلام‌نشده اعلام‌نشده اعلام‌نشده
مقاله‌ی RLHF انتروپیک 22K 20 52B 50K 0.001

در مقاله‌ی سال 2022 شرکت Anthropic، یک مدل پاداش 52 میلیارد پارامتری روی 22,000 مقایسه آموزش داده شد. مدل‌های پاداش بزرگ‌تر سیگنال‌های قابل‌اعتماد‌تری تولید می‌کنند که باعث پایداری بیشتر آموزش PPO می‌شود. استفاده از یک مدل پاداش کوچک برای آموزش یک مدل زبانی بزرگ ریسک بالایی دارد؛ چراکه مدل پاداش ظرفیت کافی برای درک ظرایف پاسخ‌های خوب در برابر بد را ندارد.

پایپ‌لاین RLHFمراحل را گام‌به‌گام مرور کنید
RLHF PIPELINERLHF runs in three stages. First supervised fine-tuning teaches the base model to follow instructions from human demonstrations. Then a reward model learns to score responses from preference pairs. Finally PPO optimizes the policy to maximize that reward while a KL penalty keeps it close to the SFT model so it does not drift into reward hacking.SFTفاین‌تیون نظارت‌شدهنمونه‌های انسانی ←→ خط‌مشی π₀پاداشآموزش مدل پاداشجفت‌های ترجیحی ←→ پاداش r(x,y)PPOبهینه‌سازی RLپرامپت‌ها + پاداش ←→ خط‌مشی هم‌تراز π
پایپ‌لاین RLHF در سه مرحله اجرا می‌شود. ابتدا فاین‌تیونینگ نظارت‌شده با استفاده از نمونه‌های انسانی به مدل پایه یاد می‌دهد تا دستورالعمل‌ها را دنبال کند. سپس یک مدل پاداش می‌آموزد که بر اساس جفت‌های ترجیحی به پاسخ‌ها امتیاز دهد. در نهایت، الگوریتم PPO خط‌مشی را برای بیشینه‌سازی پاداش بهینه‌سازی می‌کند، در حالی که جریمه KL آن را نزدیک به مدل SFT نگه می‌دارد تا به سمت دور زدن پاداش منحرف نشود.

پیاده‌سازی

گام 1: داده‌های ترجیحات ساختگی

در محیط عملیاتی، ارزیاب‌های انسانی داده‌های ترجیحات را می‌سازند. ما جفت‌هایی ساختگی ایجاد می‌کنیم که پاسخ «منتخب» در آن‌ها بدون شک بهتر است (خلاصه‌تر، دقیق‌تر و مفیدتر).

import numpy as np

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.",
    },
]

پاسخ‌های منتخب مستقیم و خلاصه‌اند. در مقابل، پاسخ‌های ردشده حالت‌های شکست رایجی دارند: حاشیه‌پردازی‌های غیرضروری، طفره‌روی، توضیحات تکراری و بی‌دقتی. این‌ها دقیقاً همان تفاوت‌هایی هستند که SFT قادر به ثبت آن‌ها نیست اما RLHF از عهده‌ی آن برمی‌آید.

گام 2: معماری مدل پاداش

مدل پاداش از معماری ترنسفورمر mini GPT دوباره استفاده می‌کند، با این تفاوت که سر خروجی به اندازه‌ی دایره‌ی واژگان را با یک لایه‌ی تصویر کردن (projection) اسکالرِ تک‌خروجی جایگزین می‌کند.

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


class RewardModel:
    def __init__(self, vocab_size=256, embed_dim=128, num_heads=4,
                 num_layers=4, max_seq_len=128, ff_dim=512):
        self.embedding = Embedding(vocab_size, embed_dim, max_seq_len)
        self.blocks = [
            TransformerBlock(embed_dim, num_heads, ff_dim)
            for _ in range(num_layers)
        ]
        self.ln_f = LayerNorm(embed_dim)
        self.reward_head = np.random.randn(embed_dim) * 0.02

    def forward(self, token_ids):
        seq_len = token_ids.shape[-1]
        mask = np.triu(np.full((seq_len, seq_len), -1e9), k=1)

        x = self.embedding.forward(token_ids)
        for block in self.blocks:
            x = block.forward(x, mask)
        x = self.ln_f.forward(x)

        last_hidden = x[:, -1, :]
        reward = last_hidden @ self.reward_head

        return reward

مدل پاداش وضعیت پنهان (hidden state) در آخرین موقعیت توکن را می‌گیرد و آن را به یک عدد اسکالر تصویر می‌کند. چرا آخرین توکن؟ زیرا با توجه به ماسک اتنشن علیّتی (causal attention)، توکن پایانی به تمام توکن‌های پیش از خود اتنشن داشته است؛ بنابراین کامل‌ترین بازنمایی را از کل توالی (پرامپت، پاسخ) در بر دارد.

گام 3: تابع زیان برادلی-تری

مدل پاداش را روی جفت‌های ترجیحات با استفاده از تابع زیانِ مقایسه‌های زوجی برادلی-تری آموزش دهید.

def tokenize_for_reward(prompt, response, vocab_size=256):
    prompt_tokens = [min(t, vocab_size - 1) for t in list(prompt.encode("utf-8"))]
    response_tokens = [min(t, vocab_size - 1) for t in list(response.encode("utf-8"))]
    return prompt_tokens + [0] + response_tokens


def sigmoid(x):
    return np.where(
        x >= 0,
        1.0 / (1.0 + np.exp(-x)),
        np.exp(x) / (1.0 + np.exp(x))
    )


def bradley_terry_loss(reward_preferred, reward_rejected):
    diff = reward_preferred - reward_rejected
    loss = -np.log(sigmoid(diff) + 1e-8)
    return loss


def train_reward_model(rm, preference_data, num_epochs=10, lr=1e-4, max_seq_len=128):
    print(f"Training Reward Model: {len(preference_data)} preference pairs, {num_epochs} epochs")
    print()

    losses = []
    accuracies = []

    for epoch in range(num_epochs):
        epoch_loss = 0.0
        epoch_correct = 0
        num_pairs = 0

        indices = np.random.permutation(len(preference_data))

        for idx in indices:
            pair = preference_data[idx]

            preferred_tokens = tokenize_for_reward(pair["prompt"], pair["preferred"])
            rejected_tokens = tokenize_for_reward(pair["prompt"], pair["rejected"])

            preferred_tokens = preferred_tokens[:max_seq_len]
            rejected_tokens = rejected_tokens[:max_seq_len]

            preferred_ids = np.array(preferred_tokens).reshape(1, -1)
            rejected_ids = np.array(rejected_tokens).reshape(1, -1)

            r_preferred = rm.forward(preferred_ids)[0]
            r_rejected = rm.forward(rejected_ids)[0]

            loss = bradley_terry_loss(r_preferred, r_rejected)

            if r_preferred > r_rejected:
                epoch_correct += 1

            diff = r_preferred - r_rejected
            grad = sigmoid(diff) - 1.0

            rm.reward_head -= lr * grad * rm.ln_f.forward(
                rm.embedding.forward(preferred_ids)
            )[:, -1, :].flatten()

            epoch_loss += loss
            num_pairs += 1

        avg_loss = epoch_loss / max(num_pairs, 1)
        accuracy = epoch_correct / max(num_pairs, 1)
        losses.append(avg_loss)
        accuracies.append(accuracy)

        if epoch % 2 == 0:
            print(f"  Epoch {epoch + 1:3d} | Loss: {avg_loss:.4f} | Accuracy: {accuracy:.1%}")

    return rm, losses, accuracies

معیار دقت سرراست است: مدل پاداش چند درصد از جفت‌های ترجیحات را در رتبه‌بندی درست تشخیص می‌دهد؟ یک مدل تصادفی امتیاز 50% می‌گیرد. مدل پاداشی که خوب آموزش دیده باشد، روی داده‌های تمیز باید از 70% فراتر رود. دقت مدل پاداش InstructGPT روی داده‌های ارزیابیِ جدا نگه‌داشته‌شده حدود 72% بود؛ این عدد شاید پایین به نظر برسد اما در واقع نتیجه‌ی خوبی است، چون خیلی از جفت‌های ترجیحات حتی برای انسان‌ها هم مبهم و بحث‌برانگیزند (میزان توافق بین ارزیاب‌های انسانی حدود 73% بود).

گام 4: حلقه‌ی ساده‌سازی‌شده‌ی PPO

پیاده‌سازی کامل PPO پیچیده است. کدی که در ادامه می‌آید سازوکار اصلی را پیاده می‌کند: تولید پاسخ‌ها، امتیازدهی به آن‌ها، محاسبه‌ی مزیت و به‌روزرسانی خط‌مشی همراه با جریمه‌ی KL.

def compute_kl_divergence(policy_logits, reference_logits):
    policy_probs = np.exp(policy_logits - policy_logits.max(axis=-1, keepdims=True))
    policy_probs = policy_probs / policy_probs.sum(axis=-1, keepdims=True)
    policy_probs = np.clip(policy_probs, 1e-10, 1.0)

    ref_probs = np.exp(reference_logits - reference_logits.max(axis=-1, keepdims=True))
    ref_probs = ref_probs / ref_probs.sum(axis=-1, keepdims=True)
    ref_probs = np.clip(ref_probs, 1e-10, 1.0)

    kl = np.sum(policy_probs * np.log(policy_probs / ref_probs), axis=-1)
    return kl.mean()


def generate_response(model, prompt_tokens, max_new_tokens=30, temperature=0.8, max_seq_len=128):
    tokens = list(prompt_tokens)

    for _ in range(max_new_tokens):
        context = np.array(tokens[-max_seq_len:]).reshape(1, -1)
        logits = model.forward(context)
        next_logits = logits[0, -1, :]

        next_logits = next_logits / max(temperature, 1e-8)
        probs = np.exp(next_logits - next_logits.max())
        probs = probs / probs.sum()
        probs = np.clip(probs, 1e-10, 1.0)
        probs = probs / probs.sum()

        next_token = np.random.choice(len(probs), p=probs)
        tokens.append(int(next_token))

    return tokens


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 ppo_training(policy_model, reference_model, reward_model, prompts,
                 num_episodes=20, lr=1.5e-5, kl_coeff=0.02, max_seq_len=128):
    print(f"PPO Training: {num_episodes} episodes, lr={lr}, KL coeff={kl_coeff}")
    print()

    rewards_history = []
    kl_history = []

    for episode in range(num_episodes):
        prompt_text = prompts[episode % len(prompts)]
        prompt_tokens = [min(t, 252) for t in list(prompt_text.encode("utf-8"))]

        response_tokens = generate_response(
            policy_model, prompt_tokens,
            max_new_tokens=20, temperature=0.8, max_seq_len=max_seq_len
        )

        response_ids = np.array(response_tokens[:max_seq_len]).reshape(1, -1)
        reward = reward_model.forward(response_ids)[0]

        policy_logits = policy_model.forward(response_ids)
        ref_logits = reference_model.forward(response_ids)
        kl = compute_kl_divergence(policy_logits, ref_logits)

        total_reward = reward - kl_coeff * kl

        rewards_history.append(float(reward))
        kl_history.append(float(kl))

        for block in policy_model.blocks:
            update_scale = lr * total_reward
            block.ffn.W1 += update_scale * np.random.randn(*block.ffn.W1.shape) * 0.01
            block.ffn.W2 += update_scale * np.random.randn(*block.ffn.W2.shape) * 0.01

        if episode % 5 == 0:
            avg_reward = np.mean(rewards_history[-5:]) if rewards_history else 0
            avg_kl = np.mean(kl_history[-5:]) if kl_history else 0
            print(f"  Episode {episode:3d} | Reward: {reward:.4f} | KL: {kl:.4f} | "
                  f"Avg Reward: {avg_reward:.4f}")

    return policy_model, rewards_history, kl_history

حلقه‌ی اصلی: (1) نمونه‌گیری یک پرامپت، (2) تولید پاسخ، (3) امتیازدهی با مدل پاداش، (4) محاسبه‌ی واگرایی KL نسبت به مدل مرجع منجمد، (5) محاسبه‌ی پاداش تعدیل‌شده (پاداش منهای جریمه‌ی KL)، (6) به‌روزرسانی خط‌مشی. هرچه خط‌مشی از مدل مرجع بیشتر فاصله بگیرد، جریمه‌ی KL سنگین‌تر می‌شود و همین موضوع به‌طور خودکار مانع دور زدن پاداش خواهد شد.

گام 5: مقایسه‌ی امتیاز پاداش

پس از پایان RLHF، پاسخ‌های مدل خط‌مشی باید در مدل پاداش نسبت به پاسخ‌های مدل اولیه‌ی SFT امتیاز بالاتری کسب کنند.

def compare_models(sft_model, rlhf_model, reward_model, prompts, max_seq_len=128):
    print("Model Comparison (reward scores)")
    print("-" * 60)
    print(f"  {'Prompt':<35} {'SFT':>10} {'RLHF':>10}")
    print("  " + "-" * 55)

    sft_total = 0.0
    rlhf_total = 0.0

    for prompt in prompts:
        prompt_tokens = [min(t, 252) for t in list(prompt.encode("utf-8"))]

        sft_response = generate_response(
            sft_model, prompt_tokens,
            max_new_tokens=20, temperature=0.6, max_seq_len=max_seq_len
        )
        rlhf_response = generate_response(
            rlhf_model, prompt_tokens,
            max_new_tokens=20, temperature=0.6, max_seq_len=max_seq_len
        )

        sft_ids = np.array(sft_response[:max_seq_len]).reshape(1, -1)
        rlhf_ids = np.array(rlhf_response[:max_seq_len]).reshape(1, -1)

        sft_reward = reward_model.forward(sft_ids)[0]
        rlhf_reward = reward_model.forward(rlhf_ids)[0]

        sft_total += sft_reward
        rlhf_total += rlhf_reward

        truncated_prompt = prompt[:33] + ".." if len(prompt) > 35 else prompt
        print(f"  {truncated_prompt:<35} {sft_reward:>10.4f} {rlhf_reward:>10.4f}")

    n = len(prompts)
    print("  " + "-" * 55)
    print(f"  {'Average':<35} {sft_total/n:>10.4f} {rlhf_total/n:>10.4f}")

    return sft_total / n, rlhf_total / n

کاربرد عملی

دموی پایپ‌لاین کامل RLHF

if __name__ == "__main__":
    np.random.seed(42)

    print("=" * 70)
    print("RLHF PIPELINE: REWARD MODEL + PPO")
    print("=" * 70)
    print()

    print("STAGE 1: SFT Model (from Lesson 06)")
    print("-" * 40)
    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("STAGE 2: Train Reward Model")
    print("-" * 40)
    rm = RewardModel(
        vocab_size=256, embed_dim=128, num_heads=4,
        num_layers=4, max_seq_len=128, ff_dim=512
    )

    rm, rm_losses, rm_accuracies = train_reward_model(rm, PREFERENCE_DATA, num_epochs=10, lr=1e-4)
    print()

    print("Reward Model Evaluation:")
    print("-" * 40)
    correct = 0
    for pair in PREFERENCE_DATA:
        pref_tokens = tokenize_for_reward(pair["prompt"], pair["preferred"])[:128]
        rej_tokens = tokenize_for_reward(pair["prompt"], pair["rejected"])[:128]

        r_pref = rm.forward(np.array(pref_tokens).reshape(1, -1))[0]
        r_rej = rm.forward(np.array(rej_tokens).reshape(1, -1))[0]

        if r_pref > r_rej:
            correct += 1
        print(f"  Preferred: {r_pref:+.4f} | Rejected: {r_rej:+.4f} | {'Correct' if r_pref > r_rej else 'Wrong'}")

    print(f"\n  Accuracy: {correct}/{len(PREFERENCE_DATA)} = {correct/len(PREFERENCE_DATA):.1%}")
    print()

    print("STAGE 3: PPO Training")
    print("-" * 40)

    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)

    train_prompts = [pair["prompt"] for pair in PREFERENCE_DATA]

    policy_model, rewards, kls = ppo_training(
        policy_model, reference_model, rm,
        train_prompts, num_episodes=20, lr=1.5e-5, kl_coeff=0.02
    )
    print()

    print("=" * 70)
    print("COMPARISON: SFT vs RLHF")
    print("=" * 70)
    print()

    eval_prompts = [
        "What is the capital of France?",
        "Explain gravity.",
        "Name three programming languages.",
    ]

    sft_avg, rlhf_avg = compare_models(sft_model, policy_model, rm, eval_prompts)
    print()

    print("=" * 70)
    print("KL DIVERGENCE ANALYSIS")
    print("=" * 70)
    print()

    if kls:
        print(f"  Initial KL: {kls[0]:.4f}")
        print(f"  Final KL:   {kls[-1]:.4f}")
        print(f"  Max KL:     {max(kls):.4f}")
        kl_threshold = 0.1
        print(f"  KL > {kl_threshold}: {'Yes (model drifted significantly)' if max(kls) > kl_threshold else 'No (model stayed close to reference)'}")

تحویل نهایی

خروجی این درس فایلی در مسیر outputs/prompt-reward-model-designer.md است: پرامپتی برای طراحی پایپ‌لاین‌های آموزش مدل پاداش. با تعیین یک رفتار هدف (میزان مفید بودن، توانایی کدنویسی، ایمنی)، این پرامپت یک پروتکل جمع‌آوری داده، شیوه‌نامه‌ی ارزیاب‌ها و معیارهای سنجش مدل پاداش را تولید می‌کند.

تمرین‌ها

  1. مدل پاداش را طوری تغییر دهید که به‌جای تکیه بر توکن آخر، میانگین تمام وضعیت‌های پنهان را بگیرد. سپس دقت را مقایسه کنید. رویکرد میانگین‌گیری (mean pooling) به همه‌ی توکن‌ها وزن یکسانی می‌دهد، در حالی که روش موقعیت آخر برای جمع‌آوری اطلاعات به اتنشن علیّتی متکی است. هر دو روش را روی 6 جفت ترجیحات بیازمایید و گزارش دهید کدام‌یک دقت بالاتری به دست می‌آورد.

  2. کالیبراسیون (calibration) مدل پاداش را پیاده‌سازی کنید. پس از پایان آموزش، تمام جفت‌های ترجیحات را به مدل پاداش بدهید و این موارد را محاسبه کنید: (الف) میانگین پاداش پاسخ‌های منتخب، (ب) میانگین پاداش پاسخ‌های ردشده، (ج) حاشیه‌ی اختلاف (پاسخ منتخب منهای ردشده). یک مدل خوب‌کالیبره‌شده باید حاشیه‌ی اختلاف مشخصی داشته باشد. سپس 4 جفت ترجیحات جدید اضافه کنید و ببینید آیا این اختلاف روی داده‌های ندیده هم پابرجا می‌ماند یا خیر.

  3. دور زدن پاداش را شبیه‌سازی کنید. مدل پاداشی بسازید که به پاسخ‌های طولانی امتیاز بالایی می‌دهد (پاداش = 100 / طول پاسخ). حلقه‌ی PPO را با این مدل پاداش معیوب اجرا کنید و ببینید مدل خط‌مشی چطور خروجی‌های بی‌نهایت طولانی و تکراری تولید می‌کند. سپس جریمه‌ی KL با ضریب 0.1 را به آن بیفزایید و نشان دهید که چطور مانع از این رفتار تباهیده می‌شود.

  4. پاداش چندهدفه را پیاده‌سازی کنید. دو مدل پاداش جداگانه آموزش دهید: یکی برای مفید بودن و دیگری برای اختصار و ایجاز. این دو را با فرمول R=0.7⋅Rhelpful+0.3⋅RconciseR = 0.7 \cdot R_{\text{helpful}} + 0.3 \cdot R_{\text{concise}} ترکیب کنید. نشان دهید که این تابع هدف ترکیبی چگونه خروجی‌هایی خلق می‌کند که هم‌زمان مفید و کوتاه‌اند و در تله‌ی پرحرفیِ ناشی از یک مدل پاداش صرفاً مبتنی بر مفید بودن نمی‌افتند.

  5. مقایسه‌ای میان ضرایب مختلف KL انجام دهید. الگوریتم PPO را با مقادیر β=0.001\beta=0.001 (بسیار کم، منجر به دور زدن پاداش)، β=0.02\beta=0.02 (استاندارد) و β=0.5\beta=0.5 (بسیار زیاد، بدون یادگیری) اجرا کنید. نمودار پاداش و نمودار KL را برای هر حالت رسم کنید. اجرای مربوط به ضریب β=0.02\beta=0.02 باید نشان‌دهنده‌ی بهبود پیوسته‌ی پاداش همراه با مقادیر مهارشده‌ی KL باشد.

اصطلاحات کلیدی

اصطلاح در گفت‌وگوهای عامیانه معنای دقیق فنی
RLHF «آموزش با بازخورد انسانی» یادگیری تقویتی از طریق بازخورد انسانی: پایپ‌لاینی سه‌مرحله‌ای (SFT، مدل پاداش، PPO) که خروجی‌های مدل زبانی را با استفاده از سیگنال‌های ترجیح انسانی بهینه‌سازی می‌کند
مدل پاداش (Reward model) «مدلی که به پاسخ‌ها نمره می‌دهد» یک ترنسفورمر مجهز به سر خروجی اسکالر که روی جفت‌های ترجیحات انسانی و با تابع زیان برادلی-تری آموزش دیده است
برادلی-تری (Bradley-Terry) «مدل مقایسه‌ای» یک مدل احتمالات که با رابطه‌ی P(A>B)=sigmoid(score(A)−score(B))P(A > B) = \mathop{\mathrm{sigmoid}}(\text{score}(A) - \text{score}(B))، ترجیحات زوجی را به یک سیستم نمره‌دهی مطلق و پایدار تبدیل می‌کند
PPO «الگوریتم RL» بهینه‌سازی خط‌مشی نزدیک: به‌روزرسانی خط‌مشی جهت بیشینه‌سازی پاداش هم‌زمان با مهار بزرگی گام‌های به‌روزرسانی برای جلوگیری از ناپایداری
واگرایی KL (KL divergence) «میزان تفاوت دو توزیع» معیاری برای سنجش تفاوت میان توزیع توکن‌های مدل خط‌مشی و مدل مرجع — که به‌عنوان جریمه برای پیشگیری از دور زدن پاداش به کار می‌رود
جریمه‌ی KL (KL penalty) «افسار مدل» مقدار β⋅KL(policy∥reference)\beta \cdot \mathrm{KL}(\text{policy} \Vert \text{reference}) که از سیگنال پاداش کسر می‌شود تا مانع فاصله گرفتن بیش از حد خط‌مشی از چک‌پوینت SFT شود
دور زدن پاداش (Reward hacking) «دور زدن سیستم پاداش» حالتی که در آن مدل بدون بهبود واقعی، با سوءاستفاده از نقاط ضعف مدل پاداش خروجی‌های تباهیده‌ای می‌سازد که امتیاز بالایی می‌گیرند
جفت ترجیحات (Preference pair) «کدام بهتر است، A یا B؟» یک نمونه‌ی آموزشی متشکل از (پرامپت، پاسخ_منتخب، پاسخ_ردشده) — واحد بنیادین داده‌های آموزشی RLHF
مدل مرجع (Reference model) «چک‌پوینت فریز شده‌ی SFT» یک کپی از مدل SFT که وزن‌های آن هرگز تغییر نمی‌کند — و به‌عنوان لنگری برای محاسبه‌ی واگرایی KL به کار می‌رود

برای مطالعه‌ی بیشتر

سؤالات متداول

چرا روش SFT به‌تنهایی برای تولید بهترین پاسخ کافی نیست؟

روش SFT تمام پاسخ‌های صحیح را با وزنی یکسان می‌بیند و مکانیزمی ندارد تا نشان دهد یک پاسخ ساختاریافته‌تر، خلاصه‌تر و مفیدتر از پاسخ صحیح دیگری است.

نقش جریمه‌ی واگرایی KL در الگوریتم PPO چیست؟

جریمه‌ی KL مانع فاصله گرفتن بیش از حد مدل از نسخه‌ی مرجع اولیه (SFT) می‌شود تا مدل از نقاط کور مدل پاداش سوءاستفاده نکند (جلوگیری از پدیده‌ی دور زدن پاداش).

چرا برای آموزش مدل پاداش از مقایسه‌های زوجی به‌جای امتیازدهی عددی استفاده می‌شود؟

انسان‌ها در دادن نمره‌ی مطلق ضعیف عمل می‌کنند اما در مقایسه‌ی نسبی دو گزینه دقت بالایی دارند؛ مدل برادلی-تری این ترجیحات نسبی را به یک سیستم امتیازدهی پایدار تبدیل می‌کند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.