فاز 19 · درس 36

حلقه‌ی آموزش و ارزیابی در مدل‌های زبانی: ساخت پایپ‌لاین کامل با PyTorch

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک چرخ‌دنده‌ی دقیق ساعت‌سازی در حال چرخش که به یک نمایشگر نوری بازخورد و قلم سنجش متصل است
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. تراز کردن تابع زیان
  5. تفکیک کاهش وزن در AdamW
  6. برنامه‌ی زمان‌بندی وارم‌آپ به همراه افت کسینوسی
  7. ارزیابی روی داده‌های کنارگذاشته‌شده
  8. نمونه‌گیری کیفی به عنوان سیگنال اولیه
  9. پیاده‌سازی
  10. پشته‌ی فناوری
  11. الگوهای محیط عملیاتی در پروژه‌های واقعی
  12. کاربردها
  13. تمرین‌ها
  14. اصطلاحات کلیدی
  15. برای مطالعه‌ی بیشتر

بسیاری از دوره‌های آموزشی، چرخه‌ی آموزش مدل‌های یادگیری عمیق را به یک حلقه‌ی `for` ساده با فراخوانی متوالی `loss.backward()` و `optimizer.step()` خلاصه می‌کنند. اما در عمل، اگر حلقه‌ی آموزش ابزار درستی برای سنجش نداشته باشد، ساعت‌ها منابع محاسباتی را بدون هیچ بازدهی مفیدی می‌سوزاند؛ بدون آنکه متوجه بیش‌برازش شوید یا ببینید خروجی متن واقعاً چه تغییری کرده است.

در مدل‌های زبانی بزرگ، جزئیاتی مثل تراز کردن توکن‌های هدف، تفکیک کاهش وزن (Weight Decay) در بهینه‌ساز AdamW، زمان‌بندی نرخ یادگیری با وارم‌آپ و افت کسینوسی، و پایش کیفی متن خروجی تفاوت میان یک اجرای پایدار و یک مدل واگرا را تعیین می‌کنند. پیاده‌سازی صحیح این اسکلت نرم‌افزاری پایه‌ی اصلی تمام پروژه‌های آموزش مدل دیکودرمحور است.

پیاده‌سازی این درس کاملاً متکی به کتابخانه‌ی `torch` است و اسکریپت نمایشی آن به دلیل استفاده از یک مدل کوچک و داده‌های ساختگی، بدون نیاز به کارت گرافیک گران‌قیمت یا سرویس‌های ابری، به‌راحتی در کمتر از یک دقیقه روی پردازنده‌ی معمولی (CPU) اجرا می‌شود.

حلقه‌ای که نسنجد، حلقه‌ای است که دروغ می‌گوید. در این درس، حلقه‌ی آموزشی را می‌سازیم که مدل GPT را هدایت می‌کند: AdamW با تفکیک کاهش وزن (weight decay)، برنامه‌ی زمان‌بندی نرخ یادگیری با وارم‌آپ (پیش‌گرمایش) به همراه افت کسینوسی، تابع کمکی calc_loss_batch، یک مرحله‌ی evaluate_model روی داده‌های کنارگذاشته‌شده (held out)، یک آزمون ارزیابی (probe) کیفی با نام generate_and_print_sample در هر K گام، و یک لاگ JSONL از مقادیر تابع زیان (loss) که بعداً می‌توانید نمودارش را رسم کنید. همین اسکلت اصلی، هر مدل زبانی دیکودرمحوری را که بسازید آموزش می‌دهد.

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

  • ساخت حلقه‌ی آموزشی که تابع زیان انتروپی متقاطع (cross entropy loss) را با ترازِ درست ورودی و هدف برای پیش‌بینی توکن بعدی محاسبه می‌کند.
  • پیکربندی AdamW به شکلی که کاهش وزن فقط روی تنسورهای وزن اعمال شود، نه روی تنسورهای لایه‌ی نرمال‌سازی (LayerNorm) یا بایاس (bias).
  • پیاده‌سازی برنامه‌ی زمان‌بندی نرخ یادگیری با وارم‌آپ خطی و افت کسینوسی، و بررسی مقدار نرخ یادگیری در طول زمان.
  • ارزیابی مدل روی داده‌های کنارگذاشته‌شده با تابع evaluate_model تا زیان ارزیابی بین اجراهای مختلف قابل مقایسه باشد.
  • تولید یک نمونه‌ی کیفی در هر K گام با generate_and_print_sample برای شناسایی واگرایی، پیش از آنکه در نمودار تابع زیان دیده شود.
  • ذخیره‌سازی پایدار تابع زیان در هر گام داخل فایل JSONL تا بتوانید لاگ آموزش را دوباره بارگذاری کنید، نمودارش را بکشید و به‌عنوان خروجی تحویل دهید.

صورت مسئله

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

حلقه‌ی این درس از سه طریق اندازه‌گیری می‌کند: تابع زیان روی دسته‌ی آموزشی در هر گام، تابع زیان روی دسته‌ی کنارگذاشته‌شده در هر K گام، و ادامه‌ی تولید متن از روی یک پرامپت ثابت در هر K گام. لاگ آموزش در قالب JSONL ذخیره می‌شود تا این دست‌سازه گواهی بر عملکرد حلقه باشد.

مفاهیم پایه

بلهبلهخیرتنسور توکنtrain + valساخت دسته‌ها (batches)جابه‌جایی ورودی و هدف با یک گامForwardلاجیت‌هاCross entropyتخت‌سازی روی دسته و زمانBackwardClip grad normگام AdamWزمان‌بندی Cosine LRافزودن رکورد گامبه losses.jsonlآیا گام، آزمون ارزیابی است؟evaluate_model روی valgenerate_and_print_sampleگام بعدی

دو بخش نه‌چندان بدیهی، تراز کردن تابع زیان و تفکیک کاهش وزن در AdamW هستند.

تراز کردن تابع زیان

مدل در هر موقعیت، توکن بعدی را پیش‌بینی می‌کند. اگر دسته‌ی ورودی توکن‌های [t0, t1, t2, t3] باشد، دسته‌ی هدف باید [t1, t2, t3, t4] باشد. انتروپی متقاطع روی شکل هموارشده‌ی (batch * seq, vocab) در برابر هدف هموارشده‌ی (batch * seq,) محاسبه می‌شود. اگر این شیفت را فراموش کنید، مدل را طوری آموزش می‌دهید که خودش را پیش‌بینی کند؛ در این حالت تابع زیان به صفر میل می‌کند، ولی مدل هیچ چیز مفیدی یاد نمی‌گیرد.

تفکیک کاهش وزن در AdamW

کاهش وزن، تنسورهای وزن را منظم‌سازی (regularize) می‌کند، نه مقیاس‌های نرمال‌سازی یا بایاس‌ها را. اعمال کاهش وزن روی مقیاس LayerNorm، مقدار آن را به‌آرامی به صفر می‌رساند و نرمال‌سازی را از کار می‌اندازد. اعمال کاهش وزن روی بایاس از نظر ریاضی بی‌خطر است، ولی چرخه‌های پردازشی را هدر می‌دهد. تفکیک استاندارد این است: تنسورهایی با شکل ماتریسی (وزن‌های لایه‌های خطی و جداول امبدینگ) مشمول کاهش وزن می‌شوند، و هر چیزی که شبیه مقیاس یا جابه‌جایی (shift) باشد، مشمول کاهش نمی‌شود.

برنامه‌ی زمان‌بندی وارم‌آپ به همراه افت کسینوسی

وارم‌آپ نرخ یادگیری را طی چند صد گام از صفر تا مقدار هدف بالا می‌برد تا وضعیت بهینه‌ساز (optimizer state) فرصت شکل‌گیری داشته باشد. برنامه‌ی زمان‌بندی کسینوسی (cosine schedule) در طول گام‌های باقی‌مانده نرخ یادگیری را دوباره به سمت صفر کاهش می‌دهد تا در فاز نهایی، وزن‌ها با اندازه‌ی گام‌های کوچک تنظیم دقیق (fine-tune) شوند. این ترکیب، رایج‌ترین برنامه‌ی زمان‌بندی در آموزش مدل‌های زبانی متن‌باز با وزن‌های عمومی است؛ چون بیشتر رفتارهای ناپایدار را در 1000 گام اول و 1000 گام آخر مهار می‌کند.

ارزیابی روی داده‌های کنارگذاشته‌شده

تابع evaluate_model تعداد مشخصی از دسته‌های بخش اعتبارسنجی را اجرا می‌کند، تابع زیان را جمع می‌زند، بر تعداد دسته‌ها تقسیم می‌کند و خروجی می‌دهد؛ بدون گرادیان و بدون دراپ‌اوت (dropout). با داشتن مقدار دانه‌ی تصادفی (seed) یکسان و داده‌های ثابت، این عدد در تمام اجراها بازتولیدپذیر است. ارائه‌ی زیان ارزیابی در کنار زیان آموزش، همان روشی است که بیش‌برازش را مشخص می‌کند.

نمونه‌گیری کیفی به عنوان سیگنال اولیه

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

حلقه آموزشپیش‌رو · پس‌رو · گام
TRAINING LOOPEach step pulls a batch, runs the forward pass to logits, scores cross-entropy loss, backpropagates, and takes an AdamW step under the LR schedule. Repeated thousands of times, the loss curve falls and flattens. Periodic eval and sample probes catch divergence the scalar loss hides.بچگذر رو به جلوزیانپس‌انتشارAdamWهرگامزیانگام
هر گام یک بچ را دریافت کرده، گذر رو به جلو را تا رسیدن به لاجیت‌ها اجرا می‌کند، تابع زیان آنتروپی متقاطع را محاسبه می‌کند، پس‌انتشار را انجام می‌دهد و تحت برنامه‌ی زمان‌بندی نرخ یادگیری، یک گام با AdamW برمی‌دارد. با هزاران بار تکرار این فرایند، منحنی تابع زیان افت کرده و هموار می‌شود. ارزیابی‌های دوره‌ای و آزمون‌های ارزیابی نمونه‌گیری، واگرایی‌هایی را که تابع زیان اسکالر پنهان می‌کند آشکار می‌سازند.

پیاده‌سازی

فایل code/main.py این بخش‌ها را پیاده‌سازی می‌کند:

  • make_batches(token_ids, batch_size, context_length): یک تنسور طولانی از توکن‌ها را به جفت‌های ورودی و هدف برش می‌زند.
  • calc_loss_batch(model, inputs, targets): عمل عبور رو به جلو (forward) را انجام می‌دهد، خروجی را هموار می‌کند و انتروپی متقاطع اسکالر را برمی‌گرداند.
  • evaluate_model(model, val_loader, max_batches): تعداد ثابتی از دسته‌های اعتبارسنجی را بدون محاسبه‌ی گرادیان پیمایش می‌کند و میانگین زیان را تحویل می‌دهد.
  • generate_and_print_sample(model, prompt, max_new_tokens): تابع تولید متن درس 35 را روی یک پرامپت ثابت اجرا می‌کند و خروجی را چاپ می‌کند.
  • build_param_groups(model, weight_decay): لیست پارامترهای دومجموعه‌ای بهینه‌ساز AdamW را تولید می‌کند.
  • cosine_with_warmup(step, warmup_steps, total_steps, max_lr, min_lr): مقدار نرخ یادگیری را در گام داده‌شده برمی‌گرداند.
  • train(...): حلقه‌ی آموزش را اجرا می‌کند، لاگ‌ها را در outputs/losses.jsonl ذخیره می‌سازد، و در هر eval_every گام، زیان ارزیابی و نمونه‌ی متن تولیدی را چاپ می‌کند.
  • یک نسخه‌ی نمایشی (Demo) که یک مدل بسیار کوچک را روی داده‌های ساختگی در تعداد کمی گام آموزش می‌دهد، لاگ JSONL تولید می‌کند و زیان ارزیابی و نمونه‌ی متنی را در نقاط ارزیابی نشان می‌دهد. اجرای این دمو روی CPU بسیار کمتر از 1 دقیقه زمان می‌برد.

اجرای کد:

python3 code/main.py

خروجی: خط تابع زیان در هر گام، زیان ارزیابی در هر گامِ آزمون، یک نمونه‌ی تولیدشده در هر گامِ آزمون، و در نهایت فایل outputs/losses.jsonl که خط‌به‌خط با json.loads قابل خواندن است.

پشته‌ی فناوری

  • کتابخانه‌ی torch برای مشتق‌گیری خودکار (autograd)، بهینه‌ساز و ماژول‌ها.
  • فایل main.py مدل GPTModel و ماژول‌های کمکی درس 35 را به‌صورت محلی پیاده‌سازی می‌کند.

الگوهای محیط عملیاتی در پروژه‌های واقعی

سه الگو وجود دارند که حلقه‌ی آموزشی کتابی را به سیستمی تبدیل می‌کنند که بتوانید با خیال راحت بگذارید در طول شب اجرا شود.

برش نرم گرادیان غیرقابل‌مذاکره است. یک دسته‌ی بد (داده‌های غیرعادی، جهش نرخ یادگیری، یا حالت‌های مرزی محاسبات عددی) گرادیان عظیمی تولید می‌کند که ساعت‌ها آموزش را در یک لحظه نابود می‌سازد. فراخوانی torch.nn.utils.clip_grad_norm_(params, max_norm=1.0) بعد از backward و قبل از step، بهینه‌ساز را در محدوده‌ی امن نگه می‌دارد. مقدار برش یک پارامتر آزاد است؛ اما مقدار 1 گزینه‌ی پیش‌فرضی است که در بیشتر چیدمان‌ها دوام می‌آورد.

لاگ‌گیری قابل ادامه‌دادن با JSONL، نه ذخیره‌ی وضعیت با pickle. ثبت رکوردهای هر گام به‌شکل خطوط {"step": int, "train_loss": float, "lr": float} در فایل JSONL ماندگار است: در صورت کرش کردن برنامه، یک دست‌سازه‌ی خوانا باقی می‌ماند، می‌توانید در آن جست‌وجو (grep) کنید، با 30 خط کد پایتون نمودارش را بکشید و با خواندن آخرین گام، آموزش را از سر بگیرید. وضعیت ذخیره‌شده با pickle شما را به ساختار دقیق ماژول‌هایی که فایل را ساخته‌اند وابسته می‌کند که در برابر تغییر ساختار کد بسیار شکننده است.

دسته‌های ارزیابی استخراج‌شده از یک برش ثابت. توکن‌های بخش اعتبارسنجی در ابتدای اسکریپت به دسته‌ها برش داده می‌شوند، نه حین اجرا. بازتولیدپذیری (reproducibility) وابسته به این است که دسته‌های ارزیابی در تمام اجراها کاملاً یکسان باشند؛ در غیر این صورت، مقایسه‌ی زیان ارزیابی دو اجرای مختلف، بیشتر نحوه‌ی بُر خوردن دسته‌ها را می‌سنجد تا خود مدل را.

کاربردها

  • حلقه‌ی این درس، همان ساختار پایه‌ای است که یک مدل 124M را روی داده‌های واقعی آموزش می‌دهد. کافی است تنسور توکن‌های ساختگی را با یک بارگذار به سبک کتابخانه‌ی datasets جایگزین کنید تا حلقه بدون تغییر اجرا شود.
  • لاگ JSONL همان خروجی تحویلی است که یک اجرای آموزشی را به شواهد (evidence) مستند تبدیل می‌کند. در درس بعد از این فایل برای مقایسه‌ی یک چک‌پوینت تازه‌آموزش‌دیده با یک مدل از پیش آموزش‌دیده استفاده می‌کنیم.
  • آزمون نمونه‌گیری کیفی، پوشش کاملی است که زیان اسکالر نمی‌تواند جایگزین آن شود.

تمرین‌ها

  1. تست‌های واحدی برای weight_decay_groups() بنویسید که مطمئن شوید پارامترهای مقیاس و بایاس در گروه بدون کاهش وزن، و وزن‌های لایه‌های خطی و امبدینگ در گروه کاهش وزن قرار می‌گیرند.
  2. توکن‌های تصادفی ساختگی را با بایت‌های یک فایل متنی کوچک جایگزین کنید تا مدل روی داده‌هایی خوانا آموزش ببیند. بررسی کنید که نمونه‌ی متنی تولیدشده از کاراکترهای موجود در فایل استفاده کند.
  3. یک کفِ min_lr معادل 10 درصد max_lr به برنامه‌ی زمان‌بندی کسینوسی اضافه کنید و نمودار را دوباره رسم کنید.
  4. علاوه بر لاگ JSONL، در هر eval_every گام یک چک‌پوینت ذخیره کنید. یک فلگ resume_from اضافه کنید که وضعیت مدل و بهینه‌ساز را دوباره بارگذاری کند.
  5. نرخ پردازش (throughput) هر گام (بر حسب توکن بر ثانیه) را در کنار تابع زیان لاگ کنید و مطمئن شوید در یک بازه‌ی پایدار باقی می‌ماند.

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

اصطلاح در گفتار روزمره مفهوم فنی واقعی
تراز کردن تابع زیان (Loss alignment) «شیفت یک‌واحدی» توکن‌های ورودی در موقعیت‌های 0..T-1 و توکن‌های هدف در موقعیت‌های 1..T؛ انتروپی متقاطع روی شکل‌های هموارشده محاسبه می‌شود
تفکیک کاهش وزن (Decay split) «دو گروه» AdamW تنسورهای ماتریسی را با کاهش وزن و تنسورهای مقیاس یا بایاس را بدون کاهش دریافت می‌کند
وارم‌آپ (Warmup) «صعود تدریجی» نرخ یادگیری طی تعداد مشخصی گام از صفر به مقدار هدف می‌رسد تا وضعیت بهینه‌ساز شکل بگیرد
دسته‌های ارزیابی (Eval batches) «دسته‌های کنارگذاشته‌شده» برش ثابتی از تنسور توکن‌های اعتبارسنجی که یک بار در ابتدای اسکریپت ساخته شده و در تمام آزمون‌ها عیناً استفاده می‌شود
آزمون ارزیابی کیفی (Qualitative probe) «چاپ نمونه» متنی کوتاه از یک پرامپت ثابت که در هر K گام چاپ می‌شود تا حالت‌های شکستی را که تابع زیان به تنهایی پنهان می‌کند پیدا کند

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

  • فاز 19 درس 35: برای مدلی که این حلقه آن را هدایت می‌کند.
  • فاز 19 درس 37: برای بارگذاری وزن‌های از پیش آموزش‌دیده روی همین مدل.
  • فاز 10 درس 04 (پیش‌آموزش mini GPT): برای روند آموزش روی داده‌های واقعی.
  • فاز 10 درس 10 (ارزیابی): برای دامنه‌ی ارزیابی گسترده‌تر فراتر از تابع زیان انتروپی متقاطع.

خودآزمایی

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

  1. پیش از درسچرا تنسور هدف (target) باید همان تنسور ورودی باشد که یک موقعیت شیفت پیدا کرده است؟
  2. پیش از درستابع evaluate_model چه کاری انجام می‌دهد که گام آموزش انجام نمی‌دهد؟
  3. حین درسکدام پارامترهای AdamW کاهش وزن دریافت می‌کنند؟
  4. حین درسبرنامه‌ی زمان‌بندی وارم‌آپ همراه با کسینوسی از گام 0 تا گام پایانی چه شکلی را ترسیم می‌کند؟
  5. حین درسچرا نرم گرادیان قبل از optimizer.step() برش (clip) داده می‌شود؟
  6. پس از درسچرا ثبت رکوردهای هر گام به صورت JSONL به ذخیره وضعیت پیکل‌شده (pickle) ترجیح داده می‌شود؟

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

چرا در پیش‌بینی توکن بعدی تراز کردن ورودی و هدف اهمیت دارد؟

چون مدل در هر گام باید توکنِ بعد از ورودی فعلی را حدس بزند؛ ورودی از توکن 0 تا T-1 و هدف از توکن 1 تا T برش می‌خورد. اگر این شیفت یک‌واحدی انجام نشود، مدل یاد می‌گیرد خودش را پیش‌بینی کند و تابع زیان بدون یادگیری واقعی صفر می‌شود.

چرا نباید کاهش وزن (Weight Decay) را روی لایه‌های نرمال‌سازی یا بایاس اعمال کرد؟

اعمال کاهش وزن روی مقیاس LayerNorm مقدار آن را به‌مرور به صفر می‌رساند و نرمال‌سازی را مختل می‌کند. روی پارامترهای بایاس نیز کاهش وزن فایده‌ی منظم‌سازی ندارد و تنها بار محاسباتی اضافه تحمیل می‌کند.

برش نرم گرادیان (Gradient Norm Clipping) چه مشکلی را در حین آموزش حل می‌کند؟

اگر یک دسته‌ی غیرعادی یا جهش عددی باعث تولید گرادیان‌های بیش از حد بزرگ شود، برش نرم گرادیان با مقیاس‌بندی آن‌ها، مانع از به‌هم‌ریختن وزن‌ها و نابودی ساعت‌ها روند آموزش می‌شود.

چرا در لاگ‌گیری آموزش، قالب JSONL به ذخیره‌سازی وضعیت با pickle ترجیح داده می‌شود؟

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

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

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