فاز 19 · درس 39

آموزش فاین‌تیونینگ نظارت‌شده (SFT) با ترنسفورمر و PyTorch

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python (torch, numpy)
یک نوار دو رنگ از بلوک‌های داده که بخش اول آن خاکستری و غیرفعال است و بخش دوم با تابش نور آبی روشن ارزیابی می‌شود.
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفهوم اصلی
  4. داده‌ها
  5. توکن‌سازی و پدینگ
  6. آموزش
  7. تولید متن
  8. ارزیابی تطابق دقیق
  9. خروجی این درس
  10. چرا ماسک اهمیت دارد؟
  11. اهداف تکمیلی

یک مدل زبانی پایه (Base Model) صرفاً یاد گرفته است کلمه‌ی بعدی متن را حدس بزند؛ به همین دلیل اگر از آن سؤالی بپرسید، به‌جای پاسخ دادن ممکن است سؤال دیگری به ادامه‌ی آن بچسباند یا متنی بی‌ربط ببافد. تبدیل این مدل پیش‌آموزش‌دیده به دستیاری که دستور کاربر را بفهمد و اجرا کند، وظیفه‌ی فاین‌تیونینگ نظارت‌شده (Supervised Fine-Tuning یا SFT) است.

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

یک مدل پایه‌ی پیش‌آموزش‌دیده می‌تواند یک توالی متنی را ادامه دهد، اما از عهده‌ی اجرای دستورها برنمی‌آید. فاین‌تیونینگ نظارت‌شده (Supervised Fine-Tuning یا SFT) کوچک‌ترین تغییری است که این مشکل را حل می‌کند: جفت‌داده‌هایی شامل دستور و پاسخ مطلوب را به مدل بدهید و بدنه‌ی آن را طوری آموزش دهید که توکن‌های پاسخ را پیش‌بینی کند. ترفند کار این است: تابع زیان (loss) باید فقط پاسخ را محاسبه کند، نه دستور را. در این درس، یک حلقه‌ی SFT به سبک Alpaca می‌سازیم که با استفاده از یک تابع تجمیع (collate function) اختصاصی، توکن‌های دستور را با ignore_index=-100 ماسک می‌کند، مدل را روی 200 جفت «دستور-پاسخ» آموزش می‌دهد و آن را روی داده‌های ارزیابی (held-out split) با معیار تطابق دقیق (exact-match) می‌سنجد.

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

  • قالب‌بندی جفت‌داده‌های «دستور-پاسخ» در قالب یک توالی علّی (causal) واحد همراه با توکن‌های مرزی مشخص.
  • ساخت یک تابع تجمیع که توکن‌های دستور را ماسک کند تا آنتروپی متقاطع (Cross-Entropy) فقط توکن‌های پاسخ را به حساب بیاورد.
  • آموزش بدنه‌ی یک ترنسفورمر بسیار کوچک تحت هدف SFT و مشاهده‌ی تغییرات معیار ارزیابی.
  • پیاده‌سازی تولید متن با روش‌های رمزگشایی حریصانه (Greedy decoding) و نمونه‌گیری بر اساس دما (temperature sampling) با رعایت مرز شروع پاسخ.
  • محاسبه‌ی تطابق دقیق روی خروجی‌های تولید‌شده برای داده‌های ارزیابی.

صورت مسئله

مدل پایه‌ای که صرفاً با هدف پیش‌بینی توکن بعدی (Next-Token Prediction) آموزش دیده، هیچ درکی از «دستور» ندارد. اگر رشته‌ی "پایتخت فرانسه کجاست؟" را به آن بدهید، ادامه‌ی سؤال را می‌نویسد یا جمله‌ی جدیدی از خودش می‌سازد. این مدل زبان را می‌شناسد، اما از قرارداد قالب‌بندی چیزی سر در نمی‌آورد.

قرارداد SFT یک قالب متنی (string template) است. هر نمونه‌ی آموزشی به یک توالی واحد با سه بخش تبدیل می‌شود:

<INST> What is the capital of France? <RESP> The capital of France is Paris.

توکن‌های مرزی، توکن‌های ویژه‌ای هستند که در زمان آموزش رزرو شده‌اند. مدل یاد می‌گیرد که هر چیزی بعد از <RESP> پاسخ است و فقط همین بخشِ پاسخ نمره‌دهی می‌شود. هدف پیش‌بینی توکن بعدیِ مدل پایه همچنان پابرجاست؛ مدل فقط روی مجموعه‌داده‌ای آموزش می‌بیند که تمام نمونه‌های آن چنین ساختاری دارند.

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

مفهوم اصلی

دستورالعمل + پاسخاعمال الگوتوکن‌های INST + RESPشناسه‌های توکنماسک lossمقدار -100 روی دستورالعملبدنه ترنسفورمر + سر LMآنتروپی متقاطعignore_index=-100پس‌انتشار + گام بهینه‌ساز

قابلیت ignore_index در torch.nn.functional.cross_entropy دقیقاً برای همین کار است. هر موقعیت هدف (target) که مقدارش برابر با ignore_index باشد، زیان و گرادیان آن صفر در نظر گرفته می‌شود. مقدار قراردادی این متغیر در PyTorch برابر با -100 است. تابع تجمیع برای هر نمونه دو تنسور می‌سازد: input_ids (کل توالی) و labels (نسخه‌ای از input_ids که جایگاه‌های دستور در آن با -100 بازنویسی شده‌اند).

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

داده‌ها

200 جفت «دستور-پاسخ» به‌صورت قطعی (deterministic) در main.py تولید می‌شوند. این داده‌ها 6 نوع وظیفه (task) را پوشش می‌دهند:

  • اطلاعات واقعی تک‌شات (پایتخت فلان کشور)
  • محاسبات ریاضی
  • استخراج فهرست
  • خلاصه‌سازی تک‌جمله‌ای
  • کدنویسی (print و sort)
  • تعریف مفاهیم

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

داده‌ها به دو بخش 160 تایی برای آموزش و 40 تایی برای تست تقسیم می‌شوند. مجموعه‌ی تست هر 6 نوع وظیفه را در بر می‌گیرد تا بتوانیم تطابق دقیق را به تفکیک هر دسته‌بندی گزارش کنیم.

توکن‌سازی و پدینگ

توکن‌ساز در سطح بایت (byte-level) کار می‌کند و 3 توکن ویژه دارد:

  • INST_ID = 256: شروع بخش دستور را مشخص می‌کند.
  • RESP_ID = 257: مرز میان دستور و پاسخ را تعیین می‌کند.
  • PAD_ID = 258: پدینگ (حاشیه‌گذاری) برای یک‌دست کردن طول دسته‌ها.

توالی به‌صورت [INST] inst_bytes [RESP] resp_bytes [PAD]* ساخته می‌شود. تابع تجمیع این مراحل را طی می‌کند:

  1. هر نمونه را توکن‌سازی می‌کند.
  2. تمام نمونه‌های دسته را با پدینگ به اندازه‌ی طولانی‌ترین توالیِ همان دسته می‌رساند.
  3. تنسور labels را می‌سازد که برابر است با input_ids شیفت‌یافته به‌اندازه‌ی یک گام (هدف causal LM)، همراه با اعمال قواعد زیر:
    • بخش دستور با -100 جایگزین می‌شود.
    • بخش پدینگ با -100 جایگزین می‌شود.
    • جایگاه خودِ توکن مرزی RESP_ID هم با -100 جایگزین می‌شود (مدل را طوری آموزش نمی‌دهید که توکن مرزی را پیش‌بینی کند، بلکه باید آنچه بعد از آن می‌آید را پیش‌بینی کند).
نمونه‌هاکدگذاری + درج توکن‌های ویژهپدینگ تا طولانی‌ترینشیفت برچسب‌ها به اندازه یک واحدتنظیم -100 رویدستور / پدینگ / مرزinput_ids, labels

این جابه‌جایی، ترفند استاندارد مدل‌های علّی است: موقعیت i در input_ids موقعیت i+1 را پیش‌بینی می‌کند؛ بنابراین labels[i] = input_ids[i+1] (که در آن آخرین موقعیت از ورودی و اولین موقعیت از هدف حذف می‌شود). ماسک بعد از این شیفت اعمال می‌شود تا دقیقاً روی موقعیت‌های درست بنشیند.

آموزش

لودر آموزش200 جفتپیش‌روB x T x VCE با ماسک -100پس‌انتشاربهینه‌ساز Adamبدنه به‌روزرسانی‌شده

حلقه‌ی آموزش، همان حلقه‌ی استاندارد SFT در PyTorch است: Adam، نرخ یادگیری حدود 3e-4 تا 1e-3، 10 تا 20 اپوک روی این داده‌ها، و بدون اسکژولر. مدل به اندازه‌ای کوچک است (ابعاد مخفی 96، 2 بلوک، حداکثر طول 64) که ظرف کمتر از 2 دقیقه روی CPU همگرا می‌شود.

هر 5 اپوک یک بار، حلقه یک مرحله‌ی ارزیابی کوتاه روی داده‌های held-out اجرا می‌کند و تطابق دقیق را نمایش می‌دهد. تماشای افزایش تطابق دقیق از 0.0 در اپوک اول به حدود 0.85 در اپوک پانزدهم، پاداش زحمات این درس است: می‌بینید که مدل هم‌زمان هم قالب پاسخ‌دهی و هم خود پاسخ‌ها را یاد می‌گیرد.

تولید متن

در زمان ارزیابی، پیشوند دستور یعنی [INST] inst_bytes [RESP] به مدل داده می‌شود و مدل توکن‌ها را تا زمانی تولید می‌کند که یکی از این دو شرط رخ دهد:

  • طول توالی به max_len برسد، یا
  • مدل نشانه‌ی توقف تعریف‌شده را تولید کند: 2 بایت متوالیِ پایان‌بخش جمله (.، !، ?).

در این درس کد رمزگشایی حریصانه به همراه یک نمونه‌گیر اختیاری با دما ارائه شده است. برای تطابق دقیق از رمزگشایی حریصانه استفاده می‌شود؛ چون دما معیار را تصادفی و متغیر می‌کند. سیستم‌های واقعی معمولاً ابتدا نمونه‌گیری می‌کنند و سپس ارزیابی فازی انجام می‌دهند؛ این پایپ‌لاین در درس 41 بررسی می‌شود.

ارزیابی تطابق دقیق

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

پایپ‌لاین‌های واقعی SFT، تطابق دقیق را با F1 در سطح توکن (درس 41) و یک مدل داور تکمیل می‌کنند. با این حال تطابق دقیق همچنان مفید است؛ چون هیچ ابهامی ندارد: اگر 0.7 را نشان دهد، یعنی دقیقاً 70 درصد از دستورهای آزمایشی پاسخی مو‌به‌مو و نویسه‌به‌نویسه مطابق با پاسخ الگو تولید کرده‌اند.

ماسک تابع زیان در SFTدستورالعمل خاموش · پاسخ امتیازدهی‌شده
SFT LOSS MASKInstruction tuning packs each example into one sequence with boundary tokens, but the instruction is given, not learned. The collate function sets those positions to ignore_index -100 so cross-entropy skips them entirely. Only the tokens after the response boundary contribute loss, so the gradient teaches the model to produce the answer rather than to memorize the prompt.یک توالی علیتی<INST>-100پایتخت-100of-100فرانسه-100<RESP>-100پاریسCEisCEitCEدستورالعمل · ماسک‌شده، گرادیان صفرپاسخ · محاسبه زیانبه‌روزرسانی
تنظیم با دستورالعمل (Instruction Tuning) هر نمونه را به همراه توکن‌های مرزی در یک توالی بسته‌بندی می‌کند، اما دستورالعمل ورودی داده شده است و نباید یاد گرفته شود. تابع collate این موقعیت‌ها را روی ignore_index برابر با -100 تنظیم می‌کند تا آنتروپی متقاطع آن‌ها را کاملاً نادیده بگیرد. تنها توکن‌های پس از مرز پاسخ در تابع زیان نقش دارند؛ در نتیجه گرادیان به مدل می‌آموزد که به جای حفظ کردن پرامپت، پاسخ را تولید کند.

خروجی این درس

پیاده‌سازی ما شامل یک فایل main.py به همراه تست‌ها است:

  1. InstructionTokenizer: انکودر سطح بایت با توکن‌های ویژه‌ی رزروشده که هم پیشوند دستور و هم جفت کامل را کدگذاری می‌کند.
  2. make_dataset: تولید 200 جفت‌داده در 6 نوع وظیفه با seed ثابت.
  3. SFTDataset: خروجی (input_ids, labels) را برای هر نمونه، همراه با ماسکِ آماده برمی‌گرداند.
  4. sft_collate: پدینگ پویا، ساخت تنسور دسته، و تنظیم مقدار -100 روی موقعیت‌های دستور و پدینگ.
  5. TinyGPT: بدنه‌ی ترنسفورمر به همراه سر مدل زبانی (LM head) با وزن‌های مقید (tied) یا مستقل.
  6. train_sft: حلقه‌ی آموزش SFT به همراه هوک‌های ارزیابی در انتهای هر اپوک.
  7. generate: رمزگشایی علّی بر اساس پیشوند، به روش حریصانه یا نمونه‌گیری‌شده، به همراه منطق توقف.
  8. exact_match: مقایسه‌ی رشته‌های نرمال‌شده که یک مقدار اعشاری در بازه‌ی [0, 1] برمی‌گرداند.
  9. run_demo: ساخت داده‌ها، 20 اپوک آموزش، ارزیابی، چاپ تفکیکی نتایج برای هر دسته، و خروج با کد صفر در صورت موفقیت.

چرا ماسک اهمیت دارد؟

بدون ماسک، تابع زیان با توکن‌های دستور مثل توکن‌های هدف رفتار می‌کند؛ در نتیجه مدل یاد می‌گیرد خودِ دستور را پیش‌بینی کند. این یعنی یک هدف کاملاً متفاوت که به دو دلیل مدل ضعیف‌تری تحویل می‌دهد: اول اینکه ظرفیت مدل صرف بازسازی ورودی‌هایی می‌شود که کاربر همیشه خودش وارد می‌کند. دوم اینکه در جمع گرادیان‌ها، زیانِ مربوط به پاسخ سهم کمتری خواهد داشت؛ چون در بیشتر دسته‌ها تعداد توکن‌های دستور از پاسخ بیشتر است. بنابراین، نرخ یادگیری مؤثر بهینه‌ساز روی بخشی که واقعاً برایتان اهمیت دارد، کمتر از حد انتظار خواهد بود. ماسک صرفاً یک کار تمیزکاری ظاهری نیست؛ خودِ هدف یادگیری است.

اهداف تکمیلی

  • افزودن وارم‌آپ (پیش‌گرمایش) به نرخ یادگیری و سپس کاهش کسینوسی (cosine decay). فاین‌تیونینگ نظارت‌شده نسبت به پیش‌آموزش حساسیت بسیار بیشتری به نرخ یادگیری دارد.
  • ثبت زیان به ازای هر توکن و رسم نمودار تابع زیان در طول آموزش. دقت کنید که در اپوک‌های اولیه، توکن‌های قالب (<RESP> و پیشوندهای رایج) بر تابع زیان غلبه دارند و در اپوک‌های بعدی، توکن‌های پاسخ واقعی بخش اصلی را تشکیل می‌دهند.
  • ارتقای ارزیابی با معیارهای BLEU-1 یا chrF. تطابق دقیق توانایی مدل‌هایی را که پاسخی یکسان اما با بیانی دیگر ارائه می‌دهند، دست‌کم می‌گیرد.
  • افزودن الگوی چت (chat template) با قالب‌بندی چندنوبته (multi-turn) و آموزش روی داده‌هایی که شامل دنباله‌ی گفت‌وگو هستند.

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

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسپیش از مطالعه: پرامپت «پایتخت فرانسه کجاست؟» را به یک مدل پایه خام می‌دهید. خروجی می‌دهد: «و نظرتان در مورد ایتالیا و اسپانیا چیست؟». کدام مورد علت این شکاف رفتاری را به بهترین نحو توصیف می‌کند؟
  2. حین درستابع collate مقدار `labels[i] = -100` را روی موقعیت‌های دستورالعمل و پدینگ قرار می‌دهد. چرا آنتروپی متقاطع PyTorch این موقعیت‌ها را به‌درستی مدیریت می‌کند؟
  3. حین درسچرا `shifted_loss` برش‌های `logits[:, :-1, :]` و `labels[:, 1:]` را انجام می‌دهد، به جای اینکه از همان موقعیت‌های یکسان استفاده کند؟
  4. حین درسمدل را بدون ماسک آموزش می‌دهید: برچسب‌ها دقیقاً برابر `input_ids` بدون هیچ مقدار -100 هستند. مدل آموزش می‌بیند اما تطابق دقیق (exact-match) روی پاسخ بدتر از حالت همراه با ماسک می‌شود. محتمل‌ترین دلیل چیست؟
  5. پس از درسمعیار Exact-match روی داده‌های ارزیابی 0.85 است. یک بازبین می‌گوید مدل پاسخ‌های درست را بازنویسی (نقل به مضمون) می‌کند و exact-match آن‌ها را نادیده می‌گیرد. برای رفع این مشکل کدام معیار را اضافه می‌کنید؟
  6. پس از درسمی‌خواهید از مدل SFT در باتی استفاده کنید که از گفت‌وگوی چندنوبته پشتیبانی می‌کند. حداقل تغییر موردنیاز در داده‌ها و ماسک چیست؟

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

چرا باید توکن‌های دستور را در فاین‌تیونینگ SFT ماسک کنیم؟

چون دستور توسط کاربر وارد می‌شود و مدل نباید ظرفیت خود را صرف پیش‌بینی مجدد آن کند؛ با قرار دادن مقدار 100- روی توکن‌های دستور، تابع زیان و گرادیان فقط برای تولید توکن‌های پاسخ محاسبه می‌شوند.

معیار ارزیابی تطابق دقیق (Exact-Match) چگونه کار می‌کند؟

متن تولید‌شده و پاسخ الگو پس از نرمال‌سازی (حذف فاصله‌های اضافی و کوچک‌کردن حروف) با یکدیگر مقایسه می‌شوند؛ اگر پاسخ‌ها کاراکتر به کاراکتر هم‌خوانی داشته باشند امتیاز 1 و در غیر این صورت 0 ثبت می‌شود.

آیا برای اجرای کد این درس به کارت گرافیک (GPU) نیاز داریم؟

خیر؛ مدل ترنسفورمر ارائه‌شده در این درس بسیار سبک (hidden 96 با 2 بلوک) طراحی شده و ظرف کمتر از 2 دقیقه روی CPU معمولی همگرا می‌شود.

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

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