فاز 3 · درس 7

منظم‌سازی در یادگیری عمیق: از Dropout تا RMSNorm

نوع
ساختنی
زمان
حدود 75 دقیقه
زبان‌ها
Python
پیش‌نیاز
درس 03.06 (بهینه‌سازها)
یک ترازو فلزی ظریف که در یک کفه آن وزنه ساده هندسی و در کفه دیگر شاخه‌های درهم‌پیچیده و شلوغ قرار دارد
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفهوم
  4. طیف بیش‌برازش
  5. دراپ‌اوت (Dropout)
  6. کاهش وزن (منظم‌سازی L2)
  7. نرمال‌سازی دسته‌ای (Batch Normalization)
  8. نرمال‌سازی لایه‌ای (Layer Normalization)
  9. روش RMSNorm
  10. مقایسه‌ی روش‌های نرمال‌سازی
  11. داده‌افزایی به عنوان منظم‌سازی
  12. توقف زودهنگام (Early Stopping)
  13. چه زمانی از کدام تکنیک استفاده کنیم؟
  14. پیاده‌سازی از صفر
  15. گام 1: دراپ‌اوت (حالت آموزش و ارزیابی)
  16. گام 2: کاهش وزن L2
  17. گام 3: نرمال‌سازی دسته‌ای
  18. گام 4: نرمال‌سازی لایه‌ای
  19. گام 5: روش RMSNorm
  20. گام 6: آموزش با و بدون منظم‌سازی
  21. استفاده در عمل
  22. محصول خروجی
  23. تمرین‌ها
  24. اصطلاحات کلیدی
  25. برای مطالعه‌ی بیشتر

وقتی مدلی می‌سازید که روی داده‌های آموزشی دقت 99% ثبت می‌کند اما روی داده‌های آزمایشی به 60% سقوط می‌کند، با کلاسیک‌ترین کابوس یادگیری عمیق یعنی بیش‌برازش (Overfitting) مواجه شده‌اید. مدل به جای درک الگوها و قوانین کلی، داده‌های ورودی را موبه‌مو حفظ کرده است. منظم‌سازی (Regularization) مجموعه‌ای از روش‌های بنیادین است که با ایجاد محدودیت‌های هوشمندانه روی پیچیدگی شبکه، آن را ناچار به تعمیم‌پذیری و کشف ساختار واقعی داده‌ها می‌کند.

در معماری‌های مدرن—از شبکه‌های کانولوشنی گرفته تا ترنسفورمرها و مدل‌های زبانی بزرگ نظیر LLaMA—انتخاب اشتباه تکنیک منظم‌سازی کل روند آموزش را مختل می‌کند. در این درس یاد می‌گیرید که چطور روش‌های کلیدی منظم‌سازی نظیر دراپ‌اوت (Dropout)، کاهش وزن (Weight Decay)، نرمال‌سازی دسته‌ای (BatchNorm)، نرمال‌سازی لایه‌ای (LayerNorm) و RMSNorm را از صفر بنویسید و متناسب با شدت بیش‌برازش، ترکیب درستی از آن‌ها را در PyTorch پیاده کنید.

دقت مدل شما روی داده‌های آموزشی 99% و روی داده‌های آزمایشی 60% است. مدل به جای یادگیری، داده‌ها را حفظ کرده است. منظم‌سازی (regularization) مالیاتی است که بر پیچیدگی وضع می‌کنید تا مدل مجبور به تعمیم‌پذیری شود.

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

  • پیاده‌سازی دراپ‌اوت (dropout) با مقیاس‌بندی معکوس، کاهش وزن (L2 weight decay)، نرمال‌سازی دسته‌ای (batch normalization)، نرمال‌سازی لایه‌ای (layer normalization) و RMSNorm از صفر
  • اندازه‌گیری شکاف دقت آموزش-آزمون و تشخیص بیش‌برازش (overfitting) با آزمایش‌های منظم‌سازی
  • توضیح اینکه چرا ترنسفورمرها به جای BatchNorm از LayerNorm استفاده می‌کنند و چرا مدل‌های زبانی بزرگ امروزی RMSNorm را ترجیح می‌دهند
  • به‌کارگیری ترکیب درستی از تکنیک‌های منظم‌سازی متناسب با شدت بیش‌برازش

صورت مسئله

یک شبکه عصبی با تعداد پارامتر کافی می‌تواند هر مجموعه‌داده‌ای را حفظ کند. این فقط یک فرضیه نیست؛ ژانگ و همکاران (2017) این موضوع را با آموزش شبکه‌های استاندارد روی ImageNet با برچسب‌های تصادفی اثبات کردند. شبکه‌ها روی برچسب‌های کاملاً تصادفی به تابع زیان (loss) نزدیک به صفر رسیدند. آن‌ها یک میلیون جفت ورودی-خروجی تصادفی را بدون وجود کوچک‌ترین الگویی حفظ کردند. زیان آموزش بی‌نقص بود، اما دقت آزمون صفر شد.

این همان مشکل بیش‌برازش است که با بزرگ‌تر شدن مدل‌ها وخیم‌تر هم می‌شود. مدل GPT-3 حدود 175 میلیارد پارامتر دارد، در حالی که مجموعه‌ی آموزشی آن حدود 500 میلیارد توکن (token) است. با این تعداد پارامتر، مدل ظرفیت کافی دارد تا بخش‌های قابل‌توجهی از داده‌های آموزشی را کلمه‌به‌کلمه حفظ کند. بدون منظم‌سازی، مدل به جای یادگیری الگوهای تعمیم‌پذیر، صرفاً نمونه‌های آموزشی را طوطی‌وار تکرار می‌کرد.

شکاف میان عملکرد آموزشی و عملکرد آزمایشی، «شکاف بیش‌برازش» نام دارد. تمام تکنیک‌های این درس از زوایای مختلف به این شکاف حمله می‌کنند. دراپ‌اوت شبکه را وادار می‌کند تا به هیچ نورون خاصی وابسته نشود. کاهش وزن مانع از آن می‌شود که وزن خاصی بیش از حد بزرگ شود. نرمال‌سازی دسته‌ای، چشم‌انداز زیان (loss landscape) را هموار می‌کند تا بهینه‌ساز بتواند کمینه‌های تخت‌تر و تعمیم‌پذیرتری پیدا کند. نرمال‌سازی لایه‌ای همین کار را انجام می‌دهد اما در شرایطی به کار می‌آید که نرمال‌سازی دسته‌ای شکست می‌خورد (دسته‌های کوچک، توالی‌های با طول متغیر). روش RMSNorm نیز با حذف محاسبه‌ی میانگین، همین فرایند را 10% سریع‌تر پیش می‌برد. هر یک از این تکنیک‌ها ساده‌اند، اما در کنار هم مرز میان مدلی هستند که فقط حفظ می‌کند و مدلی که تعمیم می‌دهد.

مفهوم

طیف بیش‌برازش

هر مدلی روی طیفی قرار می‌گیرد که از کم‌برازش (underfitting - آن‌قدر ساده که الگو را ثبت نمی‌کند) تا بیش‌برازش (آن‌قدر پیچیده که نویز را ثبت می‌کند) امتداد دارد. نقطه‌ی بهینه درست در میانه‌ی این دو است و منظم‌سازی، مدل‌ها را از سمت بیش‌برازش به سوی این نقطه‌ی مطلوب push می‌کند.

push به چپpush به چپpush به چپpush به چپکم‌برازشآموزش: 60%آزمون: 58%مدل بسیار سادهبرازش مناسبآموزش: 95%آزمون: 92%تعمیم‌پذیری خوببیش‌برازشآموزش: 99.9%آزمون: 65%حفظ کردن نویزDropoutکاهش وزنBatchNormافزایش داده

دراپ‌اوت (Dropout)

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

output=activation(z)⋅maskwhere mask[i]∼Bernoulli(1−p)\text{output} = \text{activation}(z) \cdot \text{mask} \quad \text{where }\text{mask}[i] \sim \text{Bernoulli}(1 - p)

با فرض p=0.5p = 0.5، در هر گذر رو به جلو (forward pass) نیمی از نورون‌ها صفر می‌شوند. بنابراین شبکه ناچار است بازنمایی‌های تکراری و مکمل یاد بگیرد، چون نمی‌تواند پیش‌بینی کند کدام نورون‌ها در دسترس خواهند بود. این کار از هم‌انطباقی (co-adaptation) یا وابستگی یک نورون به حضور نورون‌های خاص دیگر جلوگیری می‌کند.

تفسیر جمعی (ensemble interpretation): شبکه‌ای با NN نورون و دراپ‌اوت، عملاً 2N2^N زیرشبکه‌ی ممکن ایجاد می‌کند (تمام ترکیب‌های روشن یا خاموش بودن نورون‌ها). آموزش مدل با دراپ‌اوت، تقریباً معادلِ آموزش هم‌زمان همه‌ی این 2N2^N زیرشبکه روی دسته‌های کوچک (mini-batch) مختلف است. هنگام آزمون، از همه‌ی نورون‌ها استفاده می‌کنید (بدون دراپ‌اوت) و خروجی‌ها را در (1−p)(1 - p) ضرب می‌کنید تا با مقدار مورد انتظار در زمان آموزش هم‌خوانی داشته باشد. این کار معادل میانگین‌گیری از پیش‌بینی‌های 2N2^N زیرشبکه است؛ یعنی ساخت یک مدل گروهی بزرگ از دل یک مدل واحد.

در عمل، این مقیاس‌بندی به جای زمان آزمون، در طول آموزش اعمال می‌شود (دراپ‌اوت معکوس یا inverted dropout):

During training:output=activation(z)⋅mask/(1−p)During testing:output=activation(z)(no change needed)\begin{aligned} \text{During training:} \quad & \text{output} = \text{activation}(z) \cdot \text{mask} / (1 - p) \\ \text{During testing:} \quad & \text{output} = \text{activation}(z) \quad \text{(no change needed)} \end{aligned}

این روش تمیزتر است، چون کد آزمون اصلاً نیازی ندارد چیزی از دراپ‌اوت بداند.

نرخ‌های پیش‌فرض: مقدار p = 0.1 برای ترنسفورمرها، p = 0.5 برای MLPها و p = 0.2-0.3 برای CNNها. دراپ‌اوت بالاتر یعنی منظم‌سازی قوی‌تر که ریسک کم‌برازش را بالا می‌برد.

کاهش وزن (منظم‌سازی L2)

مجموع توان دوم تمام وزن‌ها را به تابع زیان اضافه کنید:

total_loss=task_loss+λ2∑iwi2\text{total\_loss} = \text{task\_loss} + \frac{\lambda}{2} \sum_i w_i^2

گرادیان (gradient) جمله‌ی منظم‌سازی برابر با λw\lambda w است. این یعنی در هر گام، هر وزن به اندازه‌ی نسبتی متناسب با بزرگی‌اش به سمت صفر جمع می‌شود. وزن‌های بزرگ‌تر جریمه‌ی سنگین‌تری می‌بینند. در نتیجه، مدل به سمت راه‌حل‌هایی هدایت می‌شود که در آن‌ها هیچ وزن واحدی غالب نباشد.

دلیل کمک این روش به تعمیم‌پذیری: مدل‌های بیش‌برازش‌شده معمولاً وزن‌های بزرگی دارند که نویز موجود در داده‌های آموزشی را تقویت می‌کنند. کاهش وزن، این وزن‌ها را کوچک نگه می‌دارد؛ این کار ظرفیت مؤثر مدل را محدود کرده و آن را وادار می‌کند به جای رفتارهای عجیب حفظ‌شده، به ویژگی‌های پایدار و تعمیم‌پذیر تکیه کند.

ابرپارامتر λ\lambda شدت منظم‌سازی را کنترل می‌کند. مقادیر رایج:

  • 0.01 برای AdamW در ترنسفورمرها
  • 1e-4 برای SGD در شبکه‌های CNN
  • 0.1 برای مدل‌هایی با بیش‌برازش شدید

همان‌طور که در درس 06 دیدیم: کاهش وزن و منظم‌سازی L2 در بهینه‌ساز SGD معادل یکدیگرند، اما در Adam این‌طور نیست. هنگام آموزش با Adam همیشه از AdamW (کاهش وزن تفکیک‌شده) استفاده کنید.

نرمال‌سازی دسته‌ای (Batch Normalization)

خروجی هر لایه را در طول دسته‌ی کوچک نرمال کنید و بعد به لایه‌ی بعدی بفرستید.

برای یک دسته‌ی کوچک از اکتیویشن‌ها در یک لایه‌ی دلخواه:

μ=1B∑ixi(batch mean)σ2=1B∑i(xi−μ)2(batch variance)x^=xi−μσ2+ϵ(normalize)y=γx^+β(scale and shift)\begin{aligned} \mu &= \frac{1}{B} \sum_i x_i && \text{(batch mean)} \\ \sigma^2 &= \frac{1}{B} \sum_i (x_i - \mu)^2 && \text{(batch variance)} \\ \hat{x} &= \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} && \text{(normalize)} \\ y &= \gamma \hat{x} + \beta && \text{(scale and shift)} \end{aligned}

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

تفاوت آموزش و استنتاج: هنگام آموزش، μ\mu و σ\sigma از دسته‌ی کوچک فعلی به دست می‌آیند. هنگام استنتاج، از میانگین‌های متحرکی استفاده می‌کنید که در طول آموزش جمع شده‌اند (میانگین متحرک نمایی با تکانه (momentum) برابر 0.1؛ یعنی 90% مقدار قبلی + 10% مقدار جدید).

دلیل کارکرد درست BatchNorm هنوز محل بحث است. مقاله‌ی اصلی ادعا می‌کرد که این روش «تغییر کوواریانس درونی» (تغییر توزیع ورودی لایه‌ها با به‌روزرسانی لایه‌های پیشین) را کاهش می‌دهد. سانتورکار و همکاران (2018) نشان دادند که این توضیح اشتباه است. دلیل واقعی این است: BatchNorm چشم‌انداز زیان را هموارتر می‌کند. گرادیان‌ها پیش‌بینی‌پذیرتر و ثابت‌های لیپشیتز کوچک‌تر می‌شوند؛ در نتیجه بهینه‌ساز می‌تواند با خیال راحت گام‌های بزرگ‌تری بردارد. به همین دلیل BatchNorm اجازه می‌دهد از نرخ یادگیری (learning rate) بالاتری استفاده کنید و سریع‌تر همگرا شوید.

روش BatchNorm یک محدودیت اساسی دارد: به آماره‌های دسته وابسته است. با اندازه‌ی دسته‌ی 1، میانگین و واریانس عملاً بی‌معنی می‌شوند. با دسته‌های کوچک (کمتر از 32)، آماره‌ها نویز زیادی دارند و به عملکرد مدل ضربه می‌زنند. این مسئله در تسک‌هایی مثل تشخیص اشیا (که محدودیت حافظه، اندازه‌ی دسته را کوچک می‌کند) و مدل‌سازی زبان (که طول توالی‌ها مدام تغییر می‌کند) حسابی دردسرساز است.

نرمال‌سازی لایه‌ای (Layer Normalization)

به جای نرمال‌سازی در طول دسته، در طول ویژگی‌ها نرمال‌سازی را انجام دهید. برای یک نمونه‌ی منفرد:

μ=1D∑jxj(feature mean)σ2=1D∑j(xj−μ)2(feature variance)x^=xj−μσ2+ϵy=γx^+β\begin{aligned} \mu &= \frac{1}{D} \sum_j x_j && \text{(feature mean)} \\ \sigma^2 &= \frac{1}{D} \sum_j (x_j - \mu)^2 && \text{(feature variance)} \\ \hat{x} &= \frac{x_j - \mu}{\sqrt{\sigma^2 + \epsilon}} \\ y &= \gamma \hat{x} + \beta \end{aligned}

در اینجا DD بعدِ ویژگی است. هر نمونه کاملاً مستقل از بقیه نرمال می‌شود و هیچ وابستگی به اندازه‌ی دسته وجود ندارد. به همین دلیل ترنسفورمرها به جای BatchNorm از LayerNorm استفاده می‌کنند. در ترنسفورمرها توالی‌ها طول‌های متفاوتی دارند، اندازه‌ی دسته‌ها معمولاً کوچک است (یا حین تولید، برابر با 1 است)، و نحوه‌ی محاسبات در زمان آموزش و استنتاج عیناً یکسان باقی می‌ماند.

روش LayerNorm در ترنسفورمرها یا بعد از هر بلوک خوداتنشن (self-attention) و پیش‌خور اعمال می‌شود (Post-LN)، یا پیش از آن‌ها قرار می‌گیرد (Pre-LN که برای پایداری آموزش مناسب‌تر است).

روش RMSNorm

همان LayerNorm اما بدون کسر کردن میانگین؛ ارائه‌شده توسط ژانگ و سنریچ (2019).

rms=1D∑jxj2y=γ⋅xrms\begin{aligned} \text{rms} &= \sqrt{\frac{1}{D} \sum_j x_j^2} \\ y &= \gamma \cdot \frac{x}{\text{rms}} \end{aligned}

همین و بس. بدون محاسبه‌ی میانگین و بدون پارامتر بتا. یافته‌ی آن‌ها این بود: مرکززدایی (تفریق میانگین) در LayerNorm نقش بسیار ناچیزی در عملکرد نهایی مدل دارد، ولی هزینه‌ی محاسباتی ایجاد می‌کند. با حذف آن، همان دقت را با حدود 10% سربار محاسباتی کمتر به دست می‌آورید.

مدل‌های LLaMA، LLaMA 2، LLaMA 3، Mistral و بیشتر مدل‌های زبانی بزرگ امروزی به جای LayerNorm از RMSNorm استفاده می‌کنند. در مقیاس میلیاردها پارامتر و تریلیون‌ها توکن، این 10% صرفه‌جویی تفاوت چشمگیری رقم می‌زند.

مقایسه‌ی روش‌های نرمال‌سازی

نرمال‌سازی RMSنرمال‌سازی لایه‌ای (Layer Normalization)نرمال‌سازی دسته‌ای (Batch Normalization)نرمال‌سازی در طول BATCHبرای هر ویژگیدسته: [x1, x2, x3, x4]ویژگی 1: نرمال‌سازی [x1f1, x2f1, x3f1, x4f1]نیازمند دسته > 32تفاوت آموزش با ارزیابیکاربرد در CNNsنرمال‌سازی در طول ویژگی‌هابرای هر نمونهنمونه x1: نرمال‌سازی [f1, f2, f3, f4]مستقل از اندازه دستهیکسان در آموزش و ارزیابیکاربرد در ترنسفورمرهامشابه LayerNormاما بدون تفریق میانگینفقط تقسیم بر RMSبدون مرکزگرایی10% سریع‌تر از LayerNormدقت یکسانکاربرد در LLaMA, Mistral

داده‌افزایی به عنوان منظم‌سازی

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

  • تصاویر: برش تصادفی، چرخش، قرینه‌سازی، تغییر رنگ، حذف بخشی از تصویر (cutout)
  • متن: جایگزینی کلمات هم‌معنی، ترجمه‌ی رفت‌وبرگشتی، حذف تصادفی کلمات
  • صوت: کشش زمانی، تغییر گام صدا، افزودن نویز

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

توقف زودهنگام (Early Stopping)

ساده‌ترین روش منظم‌سازی: به محض اینکه زیان اعتبارسنجی شروع به افزایش کرد، آموزش را متوقف کنید. در آن نقطه مدل هنوز بیش‌برازش نشده است. در عمل، زیان اعتبارسنجی را در هر دوره (epoch) دنبال می‌کنید، بهترین نسخه از مدل را ذخیره نگه می‌دارید و آموزش را برای یک بازه‌ی معین تحت عنوان «صبر» یا patience (معمولاً بین 5 تا 20 دوره) ادامه می‌دهید. اگر زیان اعتبارسنجی در این بازه بهتر نشد، کار را متوقف کرده و همان بهترین مدل ذخیره‌شده را لود می‌کنید.

چه زمانی از کدام تکنیک استفاده کنیم؟

10%5-10%< 5%شکاف دقتآموزش و آزمون؟تنظیم‌سازی شدیدتنظیم‌سازی متوسطتنظیم‌سازی خفیفDropout p=0.3-0.5کاهش وزن 0.01-0.1افزایش داده تهاجمیتوقف زودهنگامDropout p=0.1-0.2کاهش وزن 0.001-0.01BatchNorm یا LayerNormDropout p=0.05-0.1کاهش وزن 1e-4
منظم‌سازی L2λ را بکشید
منظم‌سازی L2 مجذور نرم وزن‌ها را به تابع زیان (loss) اضافه می‌کند. افزایش λ همه‌ی ضرایب را به سمت صفر می‌کشد و مدل را هموارتر می‌سازد. اگر خیلی کم باشد بیش‌برازش رخ می‌دهد و اگر خیلی زیاد باشد سیگنال اصلی از دست می‌رود.

پیاده‌سازی از صفر

گام 1: دراپ‌اوت (حالت آموزش و ارزیابی)

import random
import math


class Dropout:
    def __init__(self, p=0.5):
        self.p = p
        self.training = True
        self.mask = None

    def forward(self, x):
        if not self.training:
            return list(x)
        self.mask = []
        output = []
        for val in x:
            if random.random() < self.p:
                self.mask.append(0)
                output.append(0.0)
            else:
                self.mask.append(1)
                output.append(val / (1 - self.p))
        return output

    def backward(self, grad_output):
        grads = []
        for g, m in zip(grad_output, self.mask):
            if m == 0:
                grads.append(0.0)
            else:
                grads.append(g / (1 - self.p))
        return grads

گام 2: کاهش وزن L2

def l2_regularization(weights, lambda_reg):
    penalty = 0.0
    for w in weights:
        penalty += w * w
    return lambda_reg * 0.5 * penalty

def l2_gradient(weights, lambda_reg):
    return [lambda_reg * w for w in weights]

گام 3: نرمال‌سازی دسته‌ای

class BatchNorm:
    def __init__(self, num_features, momentum=0.1, eps=1e-5):
        self.gamma = [1.0] * num_features
        self.beta = [0.0] * num_features
        self.eps = eps
        self.momentum = momentum
        self.running_mean = [0.0] * num_features
        self.running_var = [1.0] * num_features
        self.training = True
        self.num_features = num_features

    def forward(self, batch):
        batch_size = len(batch)
        if self.training:
            mean = [0.0] * self.num_features
            for sample in batch:
                for j in range(self.num_features):
                    mean[j] += sample[j]
            mean = [m / batch_size for m in mean]

            var = [0.0] * self.num_features
            for sample in batch:
                for j in range(self.num_features):
                    var[j] += (sample[j] - mean[j]) ** 2
            var = [v / batch_size for v in var]

            for j in range(self.num_features):
                self.running_mean[j] = (1 - self.momentum) * self.running_mean[j] + self.momentum * mean[j]
                self.running_var[j] = (1 - self.momentum) * self.running_var[j] + self.momentum * var[j]
        else:
            mean = list(self.running_mean)
            var = list(self.running_var)

        self.x_hat = []
        output = []
        for sample in batch:
            normalized = []
            out_sample = []
            for j in range(self.num_features):
                x_h = (sample[j] - mean[j]) / math.sqrt(var[j] + self.eps)
                normalized.append(x_h)
                out_sample.append(self.gamma[j] * x_h + self.beta[j])
            self.x_hat.append(normalized)
            output.append(out_sample)
        return output

گام 4: نرمال‌سازی لایه‌ای

class LayerNorm:
    def __init__(self, num_features, eps=1e-5):
        self.gamma = [1.0] * num_features
        self.beta = [0.0] * num_features
        self.eps = eps
        self.num_features = num_features

    def forward(self, x):
        mean = sum(x) / len(x)
        var = sum((xi - mean) ** 2 for xi in x) / len(x)

        self.x_hat = []
        output = []
        for j in range(self.num_features):
            x_h = (x[j] - mean) / math.sqrt(var + self.eps)
            self.x_hat.append(x_h)
            output.append(self.gamma[j] * x_h + self.beta[j])
        return output

گام 5: روش RMSNorm

class RMSNorm:
    def __init__(self, num_features, eps=1e-6):
        self.gamma = [1.0] * num_features
        self.eps = eps
        self.num_features = num_features

    def forward(self, x):
        rms = math.sqrt(sum(xi * xi for xi in x) / len(x) + self.eps)
        output = []
        for j in range(self.num_features):
            output.append(self.gamma[j] * x[j] / rms)
        return output

گام 6: آموزش با و بدون منظم‌سازی

def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))


def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data


class RegularizedNetwork:
    def __init__(self, hidden_size=16, lr=0.05, dropout_p=0.0, weight_decay=0.0):
        random.seed(0)
        self.hidden_size = hidden_size
        self.lr = lr
        self.dropout_p = dropout_p
        self.weight_decay = weight_decay
        self.dropout = Dropout(p=dropout_p) if dropout_p > 0 else None

        self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
        self.b1 = [0.0] * hidden_size
        self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
        self.b2 = 0.0

    def forward(self, x, training=True):
        self.x = x
        self.z1 = []
        self.h = []
        for i in range(self.hidden_size):
            z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
            self.z1.append(z)
            self.h.append(max(0.0, z))

        if self.dropout and training:
            self.dropout.training = True
            self.h = self.dropout.forward(self.h)
        elif self.dropout:
            self.dropout.training = False
            self.h = self.dropout.forward(self.h)

        self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
        self.out = sigmoid(self.z2)
        return self.out

    def backward(self, target):
        eps = 1e-15
        p = max(eps, min(1 - eps, self.out))
        d_loss = -(target / p) + (1 - target) / (1 - p)
        d_sigmoid = self.out * (1 - self.out)
        d_out = d_loss * d_sigmoid

        for i in range(self.hidden_size):
            d_relu = 1.0 if self.z1[i] > 0 else 0.0
            d_h = d_out * self.w2[i] * d_relu
            self.w2[i] -= self.lr * (d_out * self.h[i] + self.weight_decay * self.w2[i])
            for j in range(2):
                self.w1[i][j] -= self.lr * (d_h * self.x[j] + self.weight_decay * self.w1[i][j])
            self.b1[i] -= self.lr * d_h
        self.b2 -= self.lr * d_out

    def evaluate(self, data):
        correct = 0
        total_loss = 0.0
        for x, y in data:
            pred = self.forward(x, training=False)
            eps = 1e-15
            p = max(eps, min(1 - eps, pred))
            total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
            if (pred >= 0.5) == (y >= 0.5):
                correct += 1
        return total_loss / len(data), correct / len(data) * 100

    def train_model(self, train_data, test_data, epochs=300):
        history = []
        for epoch in range(epochs):
            total_loss = 0.0
            correct = 0
            for x, y in train_data:
                pred = self.forward(x, training=True)
                self.backward(y)
                eps = 1e-15
                p = max(eps, min(1 - eps, pred))
                total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
                if (pred >= 0.5) == (y >= 0.5):
                    correct += 1
            train_loss = total_loss / len(train_data)
            train_acc = correct / len(train_data) * 100
            test_loss, test_acc = self.evaluate(test_data)
            history.append((train_loss, train_acc, test_loss, test_acc))
            if epoch % 75 == 0 or epoch == epochs - 1:
                gap = train_acc - test_acc
                print(f"    Epoch {epoch:3d}: train_acc={train_acc:.1f}%, test_acc={test_acc:.1f}%, gap={gap:.1f}%")
        return history

استفاده در عمل

فریم‌ورک PyTorch تمام این روش‌های نرمال‌سازی و منظم‌سازی را در قالب ماژول‌های آماده ارائه می‌دهد:

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(256, 128),
    nn.BatchNorm1d(128),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(128, 10),
)

model.train()
out_train = model(torch.randn(32, 784))

model.eval()
out_test = model(torch.randn(1, 784))

سوئیچ میان model.train() و model.eval() کاملاً حیاتی است. این کار دراپ‌اوت را خاموش و روشن می‌کند و به BatchNorm می‌گوید که از آماره‌های دسته استفاده کند یا آماره‌های انباشته‌شده. فراموش کردن model.eval() قبل از استنتاج، یکی از رایج‌ترین باگ‌ها در یادگیری عمیق است؛ چون دراپ‌اوت فعال می‌ماند و BatchNorm به جای آماره‌های کلی، از آماره‌های دسته‌ی کوچک استفاده می‌کند، دقت تست شما مدام و بی‌دلیل نوسان خواهد کرد.

در ترنسفورمرها این الگو فرق دارد:

class TransformerBlock(nn.Module):
    def __init__(self, d_model=512, nhead=8, dropout=0.1):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.ff = nn.Sequential(
            nn.Linear(d_model, d_model * 4),
            nn.GELU(),
            nn.Linear(d_model * 4, d_model),
            nn.Dropout(dropout),
        )
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        attended, _ = self.attention(x, x, x)
        x = self.norm1(x + self.dropout(attended))
        x = self.norm2(x + self.ff(x))
        return x

از LayerNorm استفاده می‌شود نه BatchNorm؛ نرخ دراپ‌اوت هم p=0.1 است نه p=0.5. این‌ها مقادیر پیش‌فرض ترنسفورمرها هستند.

محصول خروجی

خروجی این درس:

  • فایل outputs/prompt-regularization-advisor.md -- پرامپتی که بیش‌برازش را عیب‌یابی می‌کند و راهبرد منظم‌سازی مناسب را پیشنهاد می‌دهد

تمرین‌ها

  1. دراپ‌اوت مکانی (spatial dropout) را برای داده‌های دوبعدی پیاده‌سازی کنید: به جای صفر کردن تک‌تک نورون‌ها، کل کانال‌های ویژگی را صفر کنید. این کار را با دسته‌بندی ویژگی‌های متوالی به عنوان کانال و حذف کامل هر دسته شبیه‌سازی کنید. سپس شکاف دقت آموزش-آزمون را با دراپ‌اوت معمولی روی مجموعه‌داده‌ی circle با hidden_size=32 مقایسه کنید.

  2. هموارسازی برچسب‌ها (label smoothing) را از درس 05 با دراپ‌اوت این درس ترکیب کنید. مدل را در چهار حالت آموزش دهید: هیچ‌کدام، فقط دراپ‌اوت، فقط هموارسازی برچسب‌ها، و هر دو با هم. شکاف دقت نهایی آموزش-آزمون را برای هر حالت اندازه بگیرید. کدام حالت کمترین شکاف را ایجاد می‌کند؟

  3. یک لایه‌ی BatchNorm بین لایه‌ی پنهان و تابع فعال‌ساز در شبکه‌ی مجموعه‌داده‌ی circle اضافه کنید. مدل را با و بدون BatchNorm در سه نرخ یادگیری 0.01، 0.05 و 0.1 آموزش دهید. BatchNorm باید بتواند آموزش را در نرخ‌های یادگیری بالاتری که شبکه‌ی ساده در آن‌ها واگرا می‌شود، کاملاً پایدار نگه دارد.

  4. توقف زودهنگام را پیاده‌سازی کنید: در هر دوره زیان آزمون را ثبت کنید، بهترین وزن‌ها را نگه دارید و اگر زیان آزمون به مدت 20 دوره بهبود نیافت، فرایند را متوقف کنید. شبکه‌ی منظم‌شده را برای 1000 دوره اجرا کنید. گزارش دهید کدام دوره بهترین دقت آزمون را داشته و از اتلاف چند دوره محاسبات جلوگیری کرده‌اید.

  5. روش‌های LayerNorm و RMSNorm را روی یک شبکه‌ی 4 لایه (نه فقط 2 لایه) مقایسه کنید. هر دو شبکه را با وزن‌های کاملاً یکسان مقداردهی اولیه کنید. آن‌ها را برای 200 دوره آموزش دهید و در نهایت دقت، سرعت آموزش (زمان بر حسب هر دوره) و اندازه‌ی گرادیان‌ها در لایه‌ی اول را با هم بسنجید. بررسی کنید که آیا RMSNorm با حفظ همان دقت، سرعت بالاتری ارائه می‌دهد یا خیر.

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

اصطلاح در گفتار عامیانه معنای دقیق فنی
بیش‌برازش (Overfitting) «مدل داده‌ها را حفظ کرده» زمانی که عملکرد مدل روی داده‌های آموزش به شکل محسوسی بهتر از آزمون باشد؛ نشانه‌ی اینکه مدل به جای سیگنال واقعی، نویز را یاد گرفته است
منظم‌سازی (Regularization) «جلوگیری از بیش‌برازش» هر روشی که با محدود کردن پیچیدگی مدل، تعمیم‌پذیری را بالا می‌برد: دراپ‌اوت، کاهش وزن، نرمال‌سازی، داده‌افزایی
دراپ‌اوت (Dropout) «حذف تصادفی نورون‌ها» صفر کردن تصادفی نورون‌ها هنگام آموزش با احتمال pp که مدل را وادار به یادگیری الگوهای افزونه و مستقل می‌کند؛ معادل آموزش یک مدل گروهی
کاهش وزن (Weight decay) «جریمه‌ی L2» کوچک کردن وزن‌ها به سمت صفر با کسر کردن λw\lambda w در هر گام؛ جریمه کردن پیچیدگی از طریق کنترل بزرگی وزن‌ها
نرمال‌سازی دسته‌ای (Batch normalization) «نرمال‌سازی در سطح دسته» نرمال کردن خروجی لایه‌ها در بُعد دسته با استفاده از آماره‌های دسته در زمان آموزش و میانگین‌های متحرک در زمان استنتاج
نرمال‌سازی لایه‌ای (Layer normalization) «نرمال‌سازی به ازای هر نمونه» نرمال کردن ویژگی‌ها درون هر نمونه؛ مستقل از اندازه‌ی دسته و بسیار کاربردی در ترنسفورمرها به دلیل متغیر بودن طول توالی‌ها
روش RMSNorm «LayerNorm بدون میانگین» نرمال‌سازی جذر میانگین مربعات؛ تفریق میانگین را از LayerNorm حذف می‌کند تا 10% افزایش سرعت بدون افت دقت فراهم شود
توقف زودهنگام (Early stopping) «توقف قبل از بیش‌برازش» قطع فرایند آموزش وقتی زیان اعتبارسنجی دیگر بهتر نمی‌شود؛ ساده‌ترین ابزار منظم‌سازی که معمولاً در کنار روش‌های دیگر می‌آید
داده‌افزایی (Data augmentation) «تولید داده‌ی بیشتر از داده‌های موجود» تغییر شکل دادن ورودی‌های آموزش (چرخش، برش، نویز) برای افزایش اندازه‌ی مؤثر داده‌ها و مجبور کردن مدل به یادگیری الگوهای تغییرناپذیر
شکاف تعمیم‌پذیری (Generalization gap) «شکاف آموزش و تست» تفاوت میان عملکرد مدل روی داده‌های آموزش و آزمون؛ تمام هدف منظم‌سازی این است که این شکاف به حداقل برسد

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

  • مقاله‌ی Srivastava et al. با عنوان "Dropout: A Simple Way to Prevent Neural Networks from Overfitting" (2014) -- مقاله‌ی اصلی دراپ‌اوت با ارائه‌ی تفسیر مدل گروهی و آزمایش‌های گسترده
  • مقاله‌ی Ioffe & Szegedy با عنوان "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" (2015) -- معرفی لایه‌ی BatchNorm و فرایند آموزش آن؛ یکی از پرارجاع‌ترین مقالات تاریخ یادگیری عمیق
  • مقاله‌ی Zhang & Sennrich با عنوان "Root Mean Square Layer Normalization" (2019) -- اثبات این موضوع که RMSNorm به دقت LayerNorm می‌رسد اما با بار محاسباتی کمتر؛ به‌کاررفته در LLaMA و Mistral
  • مقاله‌ی Zhang et al. با عنوان "Understanding Deep Learning Requires Rethinking Generalization" (2017) -- مقاله‌ی تاریخ‌ساز و تکان‌دهنده‌ای که نشان داد شبکه‌های عصبی می‌توانند برچسب‌های کاملاً تصادفی را حفظ کنند و دیدگاه‌های سنتی درباره‌ی تعمیم‌پذیری را به چالش کشید

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

چرا ترنسفورمرها به جای BatchNorm از LayerNorm استفاده می‌کنند؟

روش BatchNorm به شدت به آماره‌های دسته و اندازه آن وابسته است و روی دسته‌های کوچک یا داده‌های متنی با طول توالی متغیر ناپایدار می‌شود؛ در مقابل، LayerNorm ویژگی‌ها را مستقل از اندازه‌ی دسته و درون هر نمونه نرمال می‌کند.

تفاوت RMSNorm با LayerNorm چیست و چرا در مدل‌های جدید استفاده می‌شود؟

روش RMSNorm مرحله‌ی کسر میانگین را از LayerNorm حذف می‌کند. این کار بدون کاهش دقت نهایی، هزینه‌ی محاسبات را حدود 10% کاهش می‌دهد که در مدل‌های زبانی میلیاردی صرفه‌جویی بزرگی است.

تفاوت حالت model.train و model.eval در PyTorch چیست؟

این دستور رفتار دراپ‌اوت (خاموش/روشن شدن) و نرمال‌سازی دسته‌ای (استفاده از آماره‌های دسته یا میانگین‌های متحرک) را تغییر می‌دهد؛ فراموش کردن آن هنگام تست سبب نوسان غیرواقعی دقت می‌شود.

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

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