در برنامهنویسی سنتی، باگها واضح و سرراست هستند: مفسر متوقف میشود، استثنایی بالا میآید و شمارهی خطِ خطا را میبینید. اما در یادگیری عمیق، وحشتناکترین سناریو زمانی رخ میدهد که کد بدون هیچ اخطاری تا انتها اجرا میشود، ولی مدل هیچ چیزی یاد نمیگیرد یا خروجی کاملاً پرت تولید میکند. ساعتها یا روزها صبر کردن برای پایان فرآیند آموزش و سپس مواجه شدن با یک مدل بیمصرف، چالش مشترک تمام مهندسان یادگیری ماشین است.
این درس به شما یک متدولوژی دقیق و چکلیست کاربردی برای کشف خطاهای خاموش میدهد. به جای حدسزدنهای بیپایان یا تغییر تصادفی هایپرپارامترها، یاد میگیرید که چطور با تکنیکهایی نظیر آزمون «بیشبرازش روی یک بچ»، رصد جریان گرادیان و بررسی توزیع فعالسازی لایهها، ریشهی باگ را در عرض چند دقیقه و پیش از شروع آموزش طولانیمدت پیدا کنید.
شبکهتان کامپایل شد. اجرا شد. یک عدد هم خروجی داد. عدد کاملاً اشتباه است، ولی نه خطایی رخ داده و نه برنامه کرش کرده است. به سختترین نوع دیباگ خوش آمدید؛ جایی که هیچ پیام خطایی در کار نیست.
اهداف یادگیری
- عیبیابی خرابیهای متداول شبکههای عصبی (تابع زیان NaN، نمودار زیانِ تخت و راکد، بیشبرازش و نوسان شدید) با بهکارگیری استراتژیهای سیستماتیک دیباگ
- استفاده از روش «بیشبرازش روی یک بچ» (overfit-one-batch) برای سنجش صحت معماری مدل و حلقهی آموزش
- بررسی بزرگی گرادیانها، توزیع فعالسازیها (activations) و نُرم وزنها جهت شناسایی مشکلات محوشدگی یا انفجار گرادیان
- ایجاد یک چکلیست دیباگ جامع برای پوشش خطاهای پایپلاین داده، معماری مدل، تابع زیان (loss function)، بهینهساز و نرخ یادگیری (learning rate)
صورت مسئله
نرمافزارهای سنتی وقتی خراب میشوند، کرش میکنند. یک اشارهگر تهی (null pointer) استثنا تولید میکند. عدم تطابق نوع متغیرها همان موقع کامپایل به خطا میخورد. یک خطای یکی کم یا زیاد (off-by-one error) خروجی آشکارا غلطی تحویل میدهد.
اما شبکههای عصبی چنین لطفی در حق شما نمیکنند.
یک شبکه عصبیِ معیوب تا آخر اجرا میشود، مقدار تابع زیان را چاپ میکند و خروجیها را تحویل میدهد. حتی ممکن است تابع زیان کاهش یابد و پیشبینیها هم معقول به نظر برسند؛ ولی مدل در خفا خراب است: میانبرها را یاد میگیرد، نویز را حفظ میکند یا در یک کمینهی محلی (local minimum) بیفایده گیر میافتد. پژوهشگران گوگل تخمین میزنند که 60 تا 70 درصد از زمان دیباگ در یادگیری ماشین (Machine learning) صرف باگهای «پنهان» یا خاموش میشود؛ خطاهایی که پیامی چاپ نمیکنند اما کیفیت مدل را به باد میدهند.
تفاوت میان یک مدل درست و یک مدل خراب، اغلب در یک خط کد جا مانده یا جابهجا خلاصه میشود: فراموش کردن zero_grad()، یک بعد ترانهادهشده (transposed)، یا نرخ یادگیریای که 10 برابر بزرگتر یا کوچکتر از حد مجاز است. راهنمای مرجع «دستورالعمل آموزش شبکههای عصبی» (Recipe for Training Neural Networks، سال 2019) با همین جمله آغاز میشود: «رایجترین اشتباهات شبکههای عصبی، باگهایی هستند که برنامه را متوقف نمیکنند.»
در این درس یاد میگیرید که چطور مچ این باگها را بگیرید.
مفاهیم کلیدی
ذهنیت دیباگ
دیباگ به روش «پرینت بگیر و دعا کن» را فراموش کنید. دیباگ شبکه عصبی به رویکردی ساختاریافته و قدمبهقدم نیاز دارد؛ چون حلقهی بازخورد (feedback loop) کند است (از چند دقیقه تا چند ساعت برای هر اجرای آموزش) و علائم هم مبهم و گولزنندهاند (یک تابع زیان نامناسب میتواند 20 علت کاملاً مختلف داشته باشد).
قانون طلایی: ساده شروع کنید، پیچیدگی را تکهتکه بیفزایید و صحت هر بخش را به صورت مستقل بیازمایید.
نشانهی اول: کاهش نیافتن تابع زیان
این مورد رایجترین شکایت است. حلقهی آموزش اجرا میشود، ایپاکها (epochs) یکی پس از دیگری میگذرند، اما مقدار تابع زیان راکد میماند یا به شدت نوسان میکند.
نرخ یادگیری نامناسب. اگر خیلی بالا باشد: تابع زیان نوسان میکند یا ناگهان به NaN میپرد. اگر خیلی پایین باشد: تابع زیان آنقدر آهسته کاهش مییابد که به نظر تخت و راکد میآید. برای Adam کار را با 1e-3 شروع کنید. برای SGD، از 1e-1 یا 1e-2 شروع کنید. همیشه قبل از اینکه نتیجه بگیرید مشکل از جای دیگری است، 3 نرخ یادگیری با اختلاف مرتبه بزرگیِ 10 (مثلاً 1e-2، 1e-3 و 1e-4) را امتحان کنید.
نورونهای مردهی ReLU (Dead ReLUs). اگر به یک نورون ReLU ورودی منفیِ بزرگی وارد شود، خروجی آن 0 و گرادیانش صفر میشود و دیگر هرگز هم فعال نخواهد شد. اگر تعداد زیادی از نورونها بمیرند، شبکه دیگر قادر به یادگیری نیست. روش بررسی: نسبت فعالسازیهایی را که پس از هر لایهی ReLU دقیقاً برابر با 0 هستند چاپ کنید. اگر بیشتر از 50 درصد نورونها مرده بودند، به LeakyReLU سوییچ کنید یا نرخ یادگیری را کاهش دهید.
محوشدگی گرادیان (Vanishing gradients). در شبکههای عمیق با توابع فعالسازی sigmoid یا tanh، با حرکت گرادیانها به سمت لایههای عقبتر، مقدار آنها به صورت نمایی کوچک میشود؛ تا جایی که وقتی به لایهی اول میرسند، تقریباً 0 شدهاند. در نتیجه لایههای اول یادگیری را متوقف میکنند. راهحل: از ReLU/GELU استفاده کنید، اتصالات باقیمانده (residual connections) را بیفزایید یا از نرمالسازی دستهای (batch normalization) کمک بگیرید.
انفجار گرادیان (Exploding gradients). مشکل برعکس حالت قبلی؛ گرادیانها به صورت نمایی رشد میکنند. این حالت در شبکههای RNN و مدلهای بسیار عمیق متداول است و مقدار تابع زیان ناگهان به NaN میپرد. راهحل: برش گرادیان (gradient clipping با متد torch.nn.utils.clip_grad_norm_)، کاهش نرخ یادگیری یا افزودن لایههای نرمالسازی.
نشانهی دوم: کاهش تابع زیان با وجود عملکرد ضعیف مدل
تابع زیان پایین میآید و دقت آموزش به 99 درصد میرسد، اما دقت روی دادههای تست 55 درصد است؛ یا اینکه مدل روی دادههای واقعی خروجیهای بیمعنی و غیرعقلانی تحویل میدهد.
بیشبرازش (Overfitting). مدل به جای الگوها، دادههای آموزشی را حفظ میکند. شکاف بین زیان آموزش و اعتبارسنجی (validation loss) با گذشت زمان بیشتر میشود. راهحل: دادههای بیشتر، استفاده از dropout، کاهش وزن (weight decay)، توقف زودهنگام (early stopping) و دادهافزایی (data augmentation).
نشت داده (Data Leakage). دادههای تست وارد فرآیند آموزش شدهاند و به همین دلیل دقت مدل مشکوک و غیرواقعی بالاست. علتهای رایج: بر زدن دادهها قبل از تفکیک، پیشپردازش با استفاده از آمارههای کل مجموعهداده، یا وجود نمونههای تکراری در بخشهای آموزش و تست. راهحل: اول دادهها را تفکیک کنید و بعد پیشپردازش را انجام دهید، و حتماً وجود دادههای تکراری را بررسی کنید.
خطاهای برچسبگذاری (Label errors). در بیشتر مجموعهدادههای واقعی، بین 5 تا 10 درصد برچسبها اشتباهاند (Northcutt et al., 2021 در مقالهی «Pervasive Label Errors in Test Sets»). در این حالت مدل نویزها را یاد میگیرد. راهحل: استفاده از یادگیری بااطمینان (confident learning) برای یافتن و اصلاح نمونههای برچسبگذاریشدهی اشتباه، یا استفاده از روش برش زیان (loss truncation) جهت نادیده گرفتن نمونههایی با زیان بسیار بالا.
نشانهی سوم: وجود NaN یا Inf در تابع زیان
مقدار تابع زیان تبدیل به nan یا inf میشود؛ یعنی فرآیند آموزش به بنبست رسیده است.
نرخ یادگیری بیش از حد بالا. جهشهای بهروزرسانی گرادیان چنان بزرگ است که وزنها منفجر میشوند. راهحل: نرخ یادگیری را 10 برابر کاهش دهید.
محاسبهی log(0) یا لگاریتم اعداد منفی. تابع زیان آنتروپی متقاطع (Cross-Entropy) مقدار log(p) را محاسبه میکند. اگر خروجی مدل شما دقیقاً 0 یا عددی منفی باشد، لگاریتم منفجر میشود. راهحل: خروجیها را روی بازهی [eps, 1-eps] برش بزنید (clamp کنید) که در آن eps=1e-7 است.
تقسیم بر صفر. نرمالسازی دستهای مقادیر را بر انحراف معیار تقسیم میکند. اگر یک بچ شامل مقادیر ثابت باشد، انحراف معیار آن 0 میشود. راهحل: یک مقدار اپسیلون به مخرج کسر اضافه کنید (PyTorch این کار را به صورت پیشفرض انجام میدهد، اما در پیادهسازیهای دستی ممکن است فراموش شود).
سرریز عددی (Numerical overflow). ورود فعالسازیهای بزرگ به تابع exp() مقدار Inf تولید میکند. لایهی سافتمکس (Softmax) به شدت مستعد این خطا است. راهحل: قبل از به توان رساندن، مقدار بیشینه (max) را از ورودیها کم کنید (ترفند log-sum-exp).
تکنیک اول: بررسی گرادیان (Gradient Checking)
گرادیانهای تحلیلی خود را (که از پسانتشار به دست آمدهاند) با گرادیانهای عددی (که با تفاضل متناهی محاسبه شدهاند) مقایسه کنید. اگر با هم نخوانند، گذر رو به عقب (backward pass) شما باگ دارد.
گرادیان عددی برای پارامتر w:
معیار همخوانی (اختلاف نسبی):
اگر rel_diff < 1e-5 باشد: پیادهسازی درست است. اگر rel_diff > 1e-3 باشد: تقریباً مطمئن باشید که باگ دارید.
تکنیک دوم: آمارههای فعالسازی
میانگین و انحراف معیار فعالسازیها را پس از هر لایه در طول آموزش زیر نظر بگیرید. شبکههای سالم مقادیر فعالسازی خود را با میانگینی نزدیک به 0 و انحراف معیاری نزدیک به 1 (پس از نرمالسازی) یا دستکم در بازهای کراندار نگه میدارند.
| شاخص سلامت | میانگین | انحراف معیار | وضعیت |
|---|---|---|---|
| سالم | ~0 | ~1 | شبکه به طور عادی در حال یادگیری است |
| اشباعشده | >>0 یا <<0 | ~0 | فعالسازیها در مقادیر انتهایی گیر کردهاند |
| مرده | 0 | 0 | نورونها مردهاند (تماماً صفر) |
| در حال انفجار | >>10 | >>10 | فعالسازیها بیرویه رشد میکنند |
تکنیک سوم: مصورسازی جریان گرادیان
میانگین بزرگی گرادیان را برای هر لایه رسم کنید. در یک شبکهی سالم، بزرگی گرادیانها در تمام لایهها باید تقریباً یکدست و هماندازه باشد. اگر گرادیانهای لایههای ابتدایی 1000 برابر کوچکتر از لایههای انتهایی باشند، با محوشدگی گرادیان طرفید.
تکنیک چهارم: آزمون بیشبرازش روی یک بچ (Overfit-One-Batch)
مهمترین تکنیک دیباگ در کل یادگیری عمیق (Deep learning).
یک بچ کوچک (بین 8 تا 32 نمونه) بردارید و مدل را برای 100 تکرار یا بیشتر روی همان آموزش دهید. تابع زیان باید تقریباً صفر شود و دقت آموزش به 100 درصد برسد. اگر چنین نشد، مدل یا حلقهی آموزش شما باگی اساسی دارد؛ تا این را حل نکردهاید سراغ آموزش کامل نروید.
این آزمون خطاهای زیر را شکار میکند:
- خراب بودن توابع زیان
- گذر رو به عقبِ معیوب
- کوچک بودن بیش از حد معماری مدل برای بازنمایی دادهها
- متصل نبودن بهینهساز به پارامترهای مدل
- ناهماهنگی میان دادهها و برچسبها
این تست فقط 30 ثانیه زمان میبرد، اما شما را از ساعتها دیباگ و آموزش بینتیجه نجات میدهد.
تکنیک پنجم: ابزار یافتن نرخ یادگیری (Learning Rate Finder)
لزلی اسمیت (Leslie Smith، سال 2017) روشی پیشنهاد داد که در آن در طول یک ایپاک، نرخ یادگیری را به صورت تصاعدی از مقداری بسیار کوچک (1e-7) تا مقداری بسیار بزرگ (10) افزایش میدهیم و همزمان تابع زیان را ثبت میکنیم. سپس نمودار تابع زیان بر حسب نرخ یادگیری را رسم میکنیم. نرخ یادگیری بهینه معمولاً حدوداً 10 برابر کوچکتر از نقطهای است که تابع زیان در آن با بیشترین سرعت شروع به کاهش میکند.
بهترین نرخ یادگیری در این مثال: حدود 1e-3 (یک مرتبه بزرگی قبل از تندترین شیبِ کاهش).
باگهای متداول در PyTorch
این باگها بیشترین وقت را از برنامهنویسان PyTorch تلف میکنند:
| باگ | نشانه | راهحل |
|---|---|---|
فراموش کردن optimizer.zero_grad() |
گرادیانها در طول بچها روی هم جمع میشوند و تابع زیان نوسان میکند | اضافه کردن optimizer.zero_grad() قبل از loss.backward() |
فراموش کردن model.eval() در زمان ارزیابی |
رفتار Dropout و Batch Normalization تغییر میکند و دقت تست در دفعات مختلف دستخوش نوسان میشود | اضافه کردن model.eval() و استفاده از بلوک torch.no_grad() |
| ابعاد اشتباه تنسورها (Tensors) | برادکستینگ (broadcasting) پنهان و بیصدا نتایج نادرست تولید میکند بدون اینکه خطایی رخ دهد | چاپ ابعاد تنسورها بعد از هر عملیات در حین دیباگ |
| عدم تطابق CPU و GPU | خطای RuntimeError: expected CUDA tensor |
اعمال متد .to(device) هم روی مدل و هم روی دادهها |
| جدا نکردن تنسورها (عدم استفاده از detach) | گراف محاسباتی بیوقفه رشد میکند و خطای کمبود حافظه (OOM) میدهد | استفاده از متد .detach() یا بلوک with torch.no_grad() |
| عملیات درجا (In-place) که autograd را مختل میکنند | خطای RuntimeError: modified by in-place operation |
جایگزینی x += 1 با عبارت x = x + 1 |
| نرمال نشدن دادهها | مقدار تابع زیان در سطح حدس تصادفی قفل میشود | نرمال کردن ورودیها با میانگین 0 و انحراف معیار 1 |
| نوع دادهی (dtype) اشتباه برای برچسبها | آنتروپی متقاطع انتظار نوع Long دارد ولی Float دریافت کرده است |
تبدیل نوع دادهی برچسبها با: labels.long() |
جدول مرجع عیبیابی
| نشانه | علت محتمل | اولین اقدامی که باید تست شود |
|---|---|---|
ثابت ماندن تابع زیان روی مقدار log(1/num_classes)- |
مدل توزیع یکنواخت پیشبینی میکند | بررسی پایپلاین داده و اطمینان از تطابق برچسبها با ورودیها |
| زیان بعد از چند گام NaN میشود | نرخ یادگیری بیش از حد بالاست | کاهش نرخ یادگیری به میزان 10 برابر |
| زیان از همان ابتدا فوراً NaN میشود | رخداد log(0) یا تقسیم بر صفر | افزودن اپسیلون به عملیاتهای لگاریتم یا تقسیم |
| نوسان شدید تابع زیان | نرخ یادگیری بیش از حد بالا یا اندازهی بچ بسیار کوچک است | کاهش نرخ یادگیری و افزایش اندازهی بچ |
| کاهش تابع زیان و سپس درجا زدن آن روی یک کفه (plateau) | نرخ یادگیری برای مرحلهی فاینتیونینگ (Fine-tuning) بیش از حد بالاست | اضافه کردن زمانبندی نرخ یادگیری (cosine یا step decay) |
| دقت آموزش بالا، دقت تست پایین | بیشبرازش | اضافه کردن dropout، کاهش وزن و افزایش حجم دادهها |
| دقت آموزش = دقت تست = در حد شانس تصادفی | مدل اصلاً چیزی یاد نمیگیرد | اجرای تست بیشبرازش روی یک بچ |
| دقت آموزش = دقت تست، اما هر دو پایین | کمبرازش (Underfitting) | بزرگتر کردن مدل، لایههای بیشتر، ویژگیهای ورودی بیشتر |
| همهی گرادیانها صفرند | نورونهای مردهی ReLU یا قطع بودن گراف محاسباتی | سوییچ به LeakyReLU و بررسی تنظیم بودن .requires_grad |
| خطای کمبود حافظه (OOM) در حین آموزش | اندازهی بچ خیلی بزرگ است یا گراف محاسباتی آزاد نمیشود | کوچکتر کردن اندازهی بچ و استفاده از torch.no_grad() هنگام ارزیابی |
پیادهسازی عملی
در این بخش جعبهابزاری تشخیصی میسازید که فعالسازیها، گرادیانها و نمودارهای تابع زیان را رصد کند. سپس عمداً چند شبکه را خراب میکنید و با این جعبهابزار به عیبیابی هرکدام میپردازید.
گام 1: کلاس NetworkDebugger
اتصال هوکها (hooks) به مدل PyTorch برای ثبت آمارههای فعالسازی و گرادیانها به تفکیک لایهها.
import torch
import torch.nn as nn
import math
class NetworkDebugger:
def __init__(self, model):
self.model = model
self.activation_stats = {}
self.gradient_stats = {}
self.loss_history = []
self.lr_losses = []
self.hooks = []
self._register_hooks()
def _register_hooks(self):
for name, module in self.model.named_modules():
if isinstance(module, (nn.Linear, nn.Conv2d, nn.ReLU, nn.LeakyReLU)):
hook = module.register_forward_hook(self._make_activation_hook(name))
self.hooks.append(hook)
hook = module.register_full_backward_hook(self._make_gradient_hook(name))
self.hooks.append(hook)
def _make_activation_hook(self, name):
def hook(module, input, output):
with torch.no_grad():
out = output.detach().float()
self.activation_stats[name] = {
"mean": out.mean().item(),
"std": out.std().item(),
"fraction_zero": (out == 0).float().mean().item(),
"min": out.min().item(),
"max": out.max().item(),
}
return hook
def _make_gradient_hook(self, name):
def hook(module, grad_input, grad_output):
if grad_output[0] is not None:
with torch.no_grad():
grad = grad_output[0].detach().float()
self.gradient_stats[name] = {
"mean": grad.mean().item(),
"std": grad.std().item(),
"abs_mean": grad.abs().mean().item(),
"max": grad.abs().max().item(),
}
return hook
def record_loss(self, loss_value):
self.loss_history.append(loss_value)
def check_loss_health(self):
if len(self.loss_history) < 2:
return "NOT_ENOUGH_DATA"
recent = self.loss_history[-10:]
if any(math.isnan(v) or math.isinf(v) for v in recent):
return "NAN_OR_INF"
if len(self.loss_history) >= 20:
first_half = sum(self.loss_history[:10]) / 10
second_half = sum(self.loss_history[-10:]) / 10
if second_half >= first_half * 0.99:
return "NOT_DECREASING"
if len(recent) >= 5:
diffs = [recent[i+1] - recent[i] for i in range(len(recent)-1)]
if max(diffs) - min(diffs) > 2 * abs(sum(diffs) / len(diffs)):
return "OSCILLATING"
return "HEALTHY"
def check_activations(self):
issues = []
for name, stats in self.activation_stats.items():
if stats["fraction_zero"] > 0.5:
issues.append(f"DEAD_NEURONS: {name} has {stats['fraction_zero']:.0%} zero activations")
if abs(stats["mean"]) > 10:
issues.append(f"EXPLODING_ACTIVATIONS: {name} mean={stats['mean']:.2f}")
if stats["std"] < 1e-6:
issues.append(f"COLLAPSED_ACTIVATIONS: {name} std={stats['std']:.2e}")
return issues if issues else ["HEALTHY"]
def check_gradients(self):
issues = []
grad_magnitudes = []
for name, stats in self.gradient_stats.items():
grad_magnitudes.append((name, stats["abs_mean"]))
if stats["abs_mean"] < 1e-7:
issues.append(f"VANISHING_GRADIENT: {name} abs_mean={stats['abs_mean']:.2e}")
if stats["abs_mean"] > 100:
issues.append(f"EXPLODING_GRADIENT: {name} abs_mean={stats['abs_mean']:.2e}")
if len(grad_magnitudes) >= 2:
first_mag = grad_magnitudes[0][1]
last_mag = grad_magnitudes[-1][1]
if last_mag > 0 and first_mag / last_mag > 100:
issues.append(f"GRADIENT_RATIO: first/last = {first_mag/last_mag:.0f}x (vanishing)")
return issues if issues else ["HEALTHY"]
def print_report(self):
print("\n=== NETWORK DEBUGGER REPORT ===")
print(f"\nLoss health: {self.check_loss_health()}")
if self.loss_history:
print(f" Last 5 losses: {[f'{v:.4f}' for v in self.loss_history[-5:]]}")
print("\nActivation diagnostics:")
for item in self.check_activations():
print(f" {item}")
print("\nGradient diagnostics:")
for item in self.check_gradients():
print(f" {item}")
print("\nPer-layer activation stats:")
for name, stats in self.activation_stats.items():
print(f" {name}: mean={stats['mean']:.4f} std={stats['std']:.4f} zero={stats['fraction_zero']:.1%}")
print("\nPer-layer gradient stats:")
for name, stats in self.gradient_stats.items():
print(f" {name}: abs_mean={stats['abs_mean']:.2e} max={stats['max']:.2e}")
def remove_hooks(self):
for hook in self.hooks:
hook.remove()
self.hooks.clear()
گام 2: آزمون بیشبرازش روی یک بچ
def overfit_one_batch(model, x_batch, y_batch, criterion, lr=0.01, steps=200):
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
model.train()
print("\n=== OVERFIT ONE BATCH TEST ===")
print(f"Batch size: {x_batch.shape[0]}, Steps: {steps}")
for step in range(steps):
optimizer.zero_grad()
output = model(x_batch)
loss = criterion(output, y_batch)
loss.backward()
optimizer.step()
if step % 50 == 0 or step == steps - 1:
with torch.no_grad():
preds = (output > 0).float() if output.shape[-1] == 1 else output.argmax(dim=1)
targets = y_batch if y_batch.dim() == 1 else y_batch.squeeze()
acc = (preds.squeeze() == targets).float().mean().item()
print(f" Step {step:3d} | Loss: {loss.item():.6f} | Accuracy: {acc:.1%}")
final_loss = loss.item()
if final_loss > 0.1:
print(f"\n FAIL: Loss did not converge ({final_loss:.4f}). Model or training loop is broken.")
return False
print(f"\n PASS: Loss converged to {final_loss:.6f}")
return True
گام 3: ابزار یافتن نرخ یادگیری
def find_learning_rate(model, x_data, y_data, criterion, start_lr=1e-7, end_lr=10, steps=100):
import copy
original_state = copy.deepcopy(model.state_dict())
optimizer = torch.optim.SGD(model.parameters(), lr=start_lr)
lr_mult = (end_lr / start_lr) ** (1 / steps)
model.train()
results = []
best_loss = float("inf")
current_lr = start_lr
print("\n=== LEARNING RATE FINDER ===")
for step in range(steps):
optimizer.zero_grad()
output = model(x_data)
loss = criterion(output, y_data)
if math.isnan(loss.item()) or loss.item() > best_loss * 10:
break
best_loss = min(best_loss, loss.item())
results.append((current_lr, loss.item()))
loss.backward()
optimizer.step()
current_lr *= lr_mult
for param_group in optimizer.param_groups:
param_group["lr"] = current_lr
model.load_state_dict(original_state)
if len(results) < 10:
print(" Could not complete LR sweep -- loss diverged too quickly")
return results
min_loss_idx = min(range(len(results)), key=lambda i: results[i][1])
suggested_lr = results[max(0, min_loss_idx - 10)][0]
print(f" Swept {len(results)} steps from {start_lr:.0e} to {results[-1][0]:.0e}")
print(f" Minimum loss {results[min_loss_idx][1]:.4f} at lr={results[min_loss_idx][0]:.2e}")
print(f" Suggested learning rate: {suggested_lr:.2e}")
return results
گام 4: ابزار بررسی گرادیان
def _flat_to_multi_index(flat_idx, shape):
multi_idx = []
remaining = flat_idx
for dim in reversed(shape):
multi_idx.insert(0, remaining % dim)
remaining //= dim
return tuple(multi_idx)
def gradient_check(model, x, y, criterion, eps=1e-4):
model.train()
x_double = x.double()
y_double = y.double()
model_double = model.double()
print("\n=== GRADIENT CHECK ===")
overall_max_diff = 0
checked = 0
for name, param in model_double.named_parameters():
if not param.requires_grad:
continue
layer_max_diff = 0
model_double.zero_grad()
output = model_double(x_double)
loss = criterion(output, y_double)
loss.backward()
analytical_grad = param.grad.clone()
num_checks = min(5, param.numel())
for i in range(num_checks):
idx = _flat_to_multi_index(i, param.shape)
original = param.data[idx].item()
param.data[idx] = original + eps
with torch.no_grad():
loss_plus = criterion(model_double(x_double), y_double).item()
param.data[idx] = original - eps
with torch.no_grad():
loss_minus = criterion(model_double(x_double), y_double).item()
param.data[idx] = original
numerical = (loss_plus - loss_minus) / (2 * eps)
analytical = analytical_grad[idx].item()
denom = max(abs(numerical), abs(analytical), 1e-8)
rel_diff = abs(numerical - analytical) / denom
layer_max_diff = max(layer_max_diff, rel_diff)
checked += 1
overall_max_diff = max(overall_max_diff, layer_max_diff)
status = "OK" if layer_max_diff < 1e-5 else "MISMATCH"
print(f" {name}: max_rel_diff={layer_max_diff:.2e} [{status}]")
model.float()
print(f"\n Checked {checked} parameters")
if overall_max_diff < 1e-5:
print(" PASS: Gradients match (rel_diff < 1e-5)")
elif overall_max_diff < 1e-3:
print(" WARN: Small differences (1e-5 < rel_diff < 1e-3)")
else:
print(" FAIL: Gradient mismatch detected (rel_diff > 1e-3)")
return overall_max_diff
گام 5: شبکههایی که عمداً خراب شدهاند
اکنون این جعبهابزار را روی شبکههای خراب اجرا و علت خرابی هر کدام را پیدا کنید.
def demo_broken_networks():
torch.manual_seed(42)
x = torch.randn(64, 10)
y = (x[:, 0] > 0).long()
print("\n" + "=" * 60)
print("BUG 1: Learning rate too high (lr=10)")
print("=" * 60)
model1 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger1 = NetworkDebugger(model1)
optimizer1 = torch.optim.SGD(model1.parameters(), lr=10.0)
criterion = nn.CrossEntropyLoss()
for step in range(20):
optimizer1.zero_grad()
out = model1(x)
loss = criterion(out, y)
debugger1.record_loss(loss.item())
loss.backward()
optimizer1.step()
debugger1.print_report()
debugger1.remove_hooks()
print("\n" + "=" * 60)
print("BUG 2: Dead ReLUs from bad initialization")
print("=" * 60)
model2 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 2))
with torch.no_grad():
for m in model2.modules():
if isinstance(m, nn.Linear):
m.weight.fill_(-1.0)
m.bias.fill_(-5.0)
debugger2 = NetworkDebugger(model2)
optimizer2 = torch.optim.Adam(model2.parameters(), lr=1e-3)
for step in range(50):
optimizer2.zero_grad()
out = model2(x)
loss = criterion(out, y)
debugger2.record_loss(loss.item())
loss.backward()
optimizer2.step()
debugger2.print_report()
debugger2.remove_hooks()
print("\n" + "=" * 60)
print("BUG 3: Missing zero_grad (gradients accumulate)")
print("=" * 60)
model3 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger3 = NetworkDebugger(model3)
optimizer3 = torch.optim.SGD(model3.parameters(), lr=0.01)
for step in range(50):
out = model3(x)
loss = criterion(out, y)
debugger3.record_loss(loss.item())
loss.backward()
optimizer3.step()
debugger3.print_report()
debugger3.remove_hooks()
print("\n" + "=" * 60)
print("HEALTHY NETWORK: Correct setup for comparison")
print("=" * 60)
model_good = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger_good = NetworkDebugger(model_good)
optimizer_good = torch.optim.Adam(model_good.parameters(), lr=1e-3)
for step in range(50):
optimizer_good.zero_grad()
out = model_good(x)
loss = criterion(out, y)
debugger_good.record_loss(loss.item())
loss.backward()
optimizer_good.step()
debugger_good.print_report()
debugger_good.remove_hooks()
print("\n" + "=" * 60)
print("OVERFIT-ONE-BATCH TEST (healthy model)")
print("=" * 60)
model_test = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
overfit_one_batch(model_test, x[:8], y[:8], criterion)
print("\n" + "=" * 60)
print("LEARNING RATE FINDER")
print("=" * 60)
model_lr = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
find_learning_rate(model_lr, x, y, criterion)
print("\n" + "=" * 60)
print("GRADIENT CHECK")
print("=" * 60)
model_grad = nn.Sequential(nn.Linear(10, 8), nn.ReLU(), nn.Linear(8, 2))
gradient_check(model_grad, x[:4], y[:4], criterion)
کاربرد عملی
ابزارهای توکار PyTorch
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(768, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
with torch.autograd.detect_anomaly():
output = model(input_tensor)
loss = criterion(output, target)
loss.backward()
for name, param in model.named_parameters():
if param.grad is not None:
print(f"{name}: grad_mean={param.grad.abs().mean():.2e}")
یکپارچهسازی با Weights & Biases
import wandb
wandb.init(project="debug-training")
for epoch in range(100):
loss = train_one_epoch()
wandb.log({
"loss": loss,
"lr": optimizer.param_groups[0]["lr"],
"grad_norm": torch.nn.utils.clip_grad_norm_(model.parameters(), float("inf")),
})
for name, param in model.named_parameters():
if param.grad is not None:
wandb.log({f"grad/{name}": wandb.Histogram(param.grad.cpu().numpy())})
استفاده از TensorBoard
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/debug_experiment")
for epoch in range(100):
loss = train_one_epoch()
writer.add_scalar("Loss/train", loss, epoch)
for name, param in model.named_parameters():
writer.add_histogram(f"weights/{name}", param, epoch)
if param.grad is not None:
writer.add_histogram(f"gradients/{name}", param.grad, epoch)
چکلیست دیباگ (پیش از شروع آموزش کامل)
- تست بیشبرازش روی یک بچ را اجرا کنید؛ اگر رد شد، کار را متوقف کنید.
- خلاصهی معماری مدل را چاپ کنید و مطمئن شوید تعداد پارامترها منطقی است.
- یک بار گذر رو به جلو (forward pass) را با دادههای تصادفی اجرا کنید و ابعاد خروجی را بسنجید.
- مدل را به مدت 5 ایپاک آموزش دهید تا مطمئن شوید تابع زیان کاهش مییابد.
- آمارههای فعالسازی را بررسی کنید؛ نباید لایهی مرده یا فعالسازیِ در حال انفجار داشته باشید.
- جریان گرادیان را چک کنید؛ خبری از محوشدگی یا انفجار گرادیان نباشد.
- پایپلاین داده را بررسی کنید؛ 5 نمونهی تصادفی را همراه با برچسبهایشان چاپ و نگاه کنید.
تحویل نهایی
خروجیهای این درس:
- فایل
outputs/prompt-nn-debugger.md: پرامپتی (Prompt) برای عیبیابی خطاهای آموزش شبکههای عصبی - فایل
outputs/skill-debug-checklist.md: چکلیستی درختی برای دیباگ مشکلات آموزش
الگوهای کلیدی استقرار برای دیباگ:
- افزودن هوکهای نظارتی به اسکریپتهای آموزش در محیط عملیاتی (production)
- ثبت آمارههای فعالسازی و گرادیان در W&B یا TensorBoard هر چند گام یک بار
- تنظیم هشدارهای خودکار برای وقوع زیان NaN، نورونهای مرده (بیش از 80 درصد صفر) یا انفجار گرادیان
- اجرای همیشگی آزمون بیشبرازش روی یک بچ هنگام اعمال تغییر در معماری مدل یا پایپلاینهای داده
تمرینها
-
یک شناساگر انفجار گرادیان بسازید. کلاس
NetworkDebuggerرا طوری دستکاری کنید که عبور گرادیانها از یک آستانه (Threshold) را تشخیص دهد و به صورت خودکار مقداری را برای برش گرادیان پیشنهاد کند. عملکرد آن را روی یک شبکهی 20 لایه بدون هیچ لایهی نرمالسازی بسنجید. -
ابزاری برای احیای نورونهای مرده بسازید. تابعی بنویسید که نورونهای مردهی ReLU (نورونهایی که همیشه خروجی 0 دارند) را شناسایی کند و وزنهای ورودی آنها را دوباره با روش مقداردهی Kaiming مقداردهی اولیه کند. نشان دهید که این روش چطور میتواند شبکهای را که بیش از 70 درصد نورونهایش مردهاند احیا کند.
-
ابزار یافتن نرخ یادگیری را همراه با رسم نمودار پیادهسازی کنید. تابع
find_learning_rateرا طوری توسعه دهید که نتایج را در یک فایل CSV ذخیره کند. سپس اسکریپت جداگانهای بنویسید که دادههای این فایل را بخواند و نمودار نرخ یادگیری در برابر تابع زیان را با کتابخانهی matplotlib رسم کند. نرخ یادگیری بهینه را برای مدل ResNet-18 روی مجموعهدادهی CIFAR-10 پیدا کنید. -
اعتبارسنجی پایپلاین داده بسازید. تابعی بنویسید که این موارد را بررسی کند: نمونههای تکراری میان دادههای آموزش و تست، عدم تعادل شدید در توزیع برچسبها (نسبت بیشتر از 10 به 1)، نرمالسازی ورودیها (میانگین نزدیک به 0 و انحراف معیار نزدیک به 1) و وجود مقادیر NaN یا Inf در دادهها. این تابع را روی یک مجموعهداده که عمداً خرابش کردهاید اجرا کنید.
-
یک خرابی واقعی را دیباگ کنید. مینیفریمورک درس 10 را بردارید، باگی نامحسوس به آن تزریق کنید (مثلاً ماتریس وزن را در گذر رو به عقب ترانهاده کنید) و با تکنیک بررسی گرادیان دقیقاً مشخص کنید کدام پارامتر گرادیان نادرستی دارد. مراحل دیباگ را مستند کنید.
اصطلاحات کلیدی
| اصطلاح | در گفتوگوها چه میگویند | معنای دقیق فنی |
|---|---|---|
| باگ خاموش (Silent bug) | «کد اجرا میشه ولی خروجی چرته» | باگی که هیچ خطایی تولید نمیکند اما کیفیت مدل را کاهش میدهد؛ شایعترین نوع خرابی در یادگیری ماشین |
| نورون مردهی ReLU (Dead ReLU) | «نورونها سوختن» | نورون ReLU که ورودی آن همیشه منفی است؛ بنابراین خروجی آن همیشه 0 بوده و گرادیان آن برای همیشه صفر میماند |
| محوشدگی گرادیان (Vanishing gradients) | «لایههای اول اصلاً یاد نمیگیرن» | گرادیانها حین عبور از لایهها به صورت نمایی کوچک میشوند و وزنهای لایههای ابتدایی عملاً دستنخورده و ثابت میمانند |
| انفجار گرادیان (Exploding gradients) | «زیان مدل NaN شد» | گرادیانها در گذر از لایهها به صورت نمایی رشد میکنند و مقدار بهروزرسانی وزنها چنان بزرگ میشود که سرریز رخ میدهد |
| بررسی گرادیان (Gradient checking) | «چک کردن درستی backprop» | مقایسهی گرادیانهای تحلیلی روش پسانتشار با گرادیانهای عددی حاصل از تفاضل متناهی |
| بیشبرازش روی یک بچ (Overfit-one-batch) | «مهمترین تست دیباگ» | آموزش مدل روی یک بچ بسیار کوچک برای اطمینان از اینکه مدل توانایی یادگیری دارد؛ اگر نتواند، یعنی یک جای کار شدیداً میلنگد |
| ابزار یافتن نرخ یادگیری (LR finder) | «پیدا کردن نرخ یادگیری مناسب» | افزایش نمایی نرخ یادگیری در طول یک ایپاک و انتخاب مقداری که درست قبل از واگرایی تابع زیان قرار دارد |
| نشت داده (Data leakage) | «دادههای تست لو رفته توی ترین» | نفوذ اطلاعات مجموعهدادهی تست به فرآیند آموزش، که به تولید دقتی کاذب و غیرواقعی میانجامد |
| آمارههای فعالسازی (Activation statistics) | «بررسی سلامت لایهها» | ثبت و رصد میانگین، انحراف معیار و نسبت صفرهای خروجی هر لایه برای کشف نورونهای مرده، اشباعشده یا در حال انفجار |
| برش گرادیان (Gradient clipping) | «سقف گذاشتن برای گرادیان» | کاهش مقیاس گرادیانها در شرایطی که نُرم آنها از یک آستانه بیشتر شود، برای پیشگیری از بهروزرسانیهای مخرب وزنها |
برای مطالعهی بیشتر
- مقالهی Smith با عنوان «Cyclical Learning Rates for Training Neural Networks» (سال 2017)؛ مقالهای که آزمون محدودهی نرخ یادگیری (LR finder) را معرفی کرد
- مقالهی Northcutt et al. با عنوان «Pervasive Label Errors in Test Sets Destabilize Machine Learning Benchmarks» (سال 2021)؛ نشان میدهد که بین 3 تا 6 درصد از برچسبها در ImageNet، CIFAR-10 و سایر بنچمارکهای بزرگ اشتباه هستند
- مقالهی Zhang et al. با عنوان «Understanding Deep Learning Requires Rethinking Generalization» (سال 2017)؛ اثبات میکند شبکههای عصبی حتی میتوانند برچسبهای کاملاً تصادفی را هم حفظ کنند، و دقیقاً به همین دلیل است که آزمون بیشبرازش روی یک بچ کار میکند
- مستندات PyTorch دربارهی
torch.autograd.detect_anomalyوtorch.autograd.set_detect_anomalyبرای شناسایی خودکار خطاهای NaN و Inf به صورت توکار
