تشخيص التدريب
درس 6 من 8
6تشخيص التدريب
الصورة الكبيرةثلاثة أعطال تغطّي أغلب ما سيحدث لك: خسارة لا تنزل، وخسارة تصير NaN، ونموذج يحفظ التدريب ويفشل في غيره. لكل عطل علامة على منحنى الخسارة، ولكل علامة أول إجراء تجرّبه.
ماذا تشاهد في الرسم: حتى الدورة 100 ينزل المنحنيان معًا، ثم يواصل التدريب النزول بينما يرتفع التحقّق من 0.112 إلى 0.135. الخط المتقطّع هو النموذج الذي تحتفظ به، والفجوة بعده حفظ للضجيج لا تعلّم.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
النموذج الذي لا يتعلّم مثل سيارة لا تتحرك: قد يكون الوقود (البيانات)، أو المحرّك (النموذج)، أو أنك نسيت فرامل اليد (خطأ في الكود). تفحص بالترتيب من الأبسط: هل يستطيع أن يحفظ عشرة أمثلة فقط؟ إن لم يستطع، فالخلل في الكود لا في البيانات.
ثلاثة أعطال: خسارة لا تنزل (معدل تعلّم خطأ، أو zero_grad منسي، أو تسميات مخلوطة)، وخسارة تصبح NaN (معدل كبير، أو قسمة على صفر، أو log(0))، وإفراط في التعلّم (فجوة بين خسارة التدريب والتحقق). الاختبار الأول دائمًا الإفراط عمدًا على دفعة واحدة.
قبل أن تبدأ
⚙️حلقة تدريب بيدك
تعرف الأسطر الخمسة، فالتشخيص يعني أن تعرف أيًّا منها تفحص.
✂️تقسيم البيانات
تعرف لماذا نقيس على بيانات تحقّق منفصلة، فمنحنى التدريب وحده لا يكفي.
الفكرة الجوهرية
ثلاثة أعطال تغطّي أغلب ما سيحدث لك، ولكل عطل علامة. في هذا الدرس تفحص أول خسارة، ثم تحفظ دفعة واحدة، ثم تجد مصدر NaN، ثم تقرأ المنحنيين معًا، ثم تغيّر شيئًا واحدًا في كل مرة.
1افحص أول خسارة
مصنّف بأوزان عشوائية يوزّع احتماله بالتساوي، فأول خسارة له قريبة من ln(k)، حيث k عدد الفئات. انظر إلى الرسم: هذه الأرقام التي تتوقّعها قبل أي تدريب.
إن كانت أول خسارة 15 أو 0.01 في مصنّف من عشر فئات، فهناك خطأ قبل أن يبدأ التدريب. لا تنتظر ألف خطوة لتكتشفه.
2احفظ دفعة واحدة صغيرة
أهم اختبار: هل يستطيع النموذج أن يحفظ 16 مثالًا؟ إن فشل، فالخطأ في الكود. إن نجح، فالمشكلة في البيانات أو الإعدادات أو حجم النموذج.
هذا الكود يحتاج PyTorch على جهازك، فالناتج المعروض مسجَّل من تشغيل حقيقي. ثبّته بـpip install torch وشغّله بنفسك.
import torch
from torch import nn
torch.manual_seed(0)
X = torch.randn(16, 10)
y = torch.randint(0, 3, (16,))
model = nn.Sequential(nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 3))
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for step in range(201):
loss = nn.functional.cross_entropy(model(X), y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if step % 50 == 0:
print(f"step {step:3} loss {loss.item():.4f}")
16 مثالًا بفئات عشوائية تمامًا، ومع ذلك تسقط الخسارة من 1.07 إلى 0.0003. الشبكة حفظتها، وهذا ما نريده هنا: يثبت أن المرور الأمامي والخسارة والاشتقاق والخطوة كلها موصولة صحيحًا. إن بقيت الخسارة عند ln(3) ≈ 1.10، فابحث عن zero_grad ناقص أو مخرجات مفصولة عن الرسم.
3من أين يأتي NaN
NaN يأتي غالبًا من ثلاثة أماكن: معدّل تعلّم كبير يفجّر الأوزان، أو عملية غير مستقرة مثل log(0) أو exp لرقم كبير، أو NaN في البيانات نفسها. انظر إلى الرسم: كيف يصير رقمان عاديان NaN.
softmax تحسب exp لكل قيمة. مع قيم كبيرة، exp يتجاوز أكبر رقم يستطيع الحاسوب تمثيله، فيصير لانهاية، ولانهاية على لانهاية NaN.
import numpy as np
logits = np.array([1000.0, 1001.0])
with np.errstate(over="ignore", invalid="ignore"):
naive = np.exp(logits) / np.exp(logits).sum()
shifted = logits - logits.max()
stable = np.exp(shifted) / np.exp(shifted).sum()
print("exp(709) =", np.exp(np.float64(709)), "(float64 tops out near here)")
print("naive softmax: ", naive)
print("stable softmax:", stable.round(3))
الحل البسيط: اطرح أكبر قيمة قبل exp. النتيجة الرياضية نفسها تمامًا، لكن أكبر أُس صار صفرًا. PyTorch يفعل هذا داخل cross_entropy وlog_softmax، ولهذا يجب أن تستخدمهما بدل كتابة softmax بنفسك ثم log.
4يتعلّم أم يحفظ: اقرأ المنحنيين
منحنى التدريب وحده لا يميّز بين نموذج يتعلّم وآخر يحفظ. الفجوة بين خسارة التدريب وخسارة التحقّق هي المعلومة.
شبكة كبيرة (طبقتان بـ128 خلية) على 60 نقطة فقط من منحنى جيبي مع ضجيج. راقب الرقمين معًا.
import numpy as np
from sklearn.neural_network import MLPRegressor
rng = np.random.default_rng(0)
X = rng.uniform(-3, 3, size=(60, 1))
y = np.sin(X[:, 0]) + rng.normal(scale=0.3, size=60)
X_val = rng.uniform(-3, 3, size=(200, 1))
y_val = np.sin(X_val[:, 0]) + rng.normal(scale=0.3, size=200)
model = MLPRegressor(hidden_layer_sizes=(128, 128), learning_rate_init=0.01, random_state=0)
for epoch in range(1, 601):
model.partial_fit(X, y)
if epoch in (10, 100, 300, 600):
train = ((model.predict(X) - y) ** 2).mean()
val = ((model.predict(X_val) - y_val) ** 2).mean()
print(f"epoch {epoch:4} train={train:.3f} val={val:.3f}")
حتى الدورة 100 ينخفض الاثنان: النموذج يتعلّم الجيب. بعدها تستمر خسارة التدريب في النزول إلى 0.042، أقل من تباين الضجيج نفسه (0.09)، بينما خسارة التحقّق ترتفع من 0.112 إلى 0.135. النموذج صار يحفظ الضجيج. أفضل نموذج كان عند الدورة 100، والتوقّف المبكّر يعني بالضبط: احفظ النموذج عند أدنى خسارة تحقّق.
إن رأيت هذه العلامة فالحلول المعتادة: بيانات أكثر، أو نموذج أصغر، أو تنظيم مثل Dropout، أو توقّف مبكّر.
5غيّر شيئًا واحدًا في كل مرة
اعمل مثل الطبيب. انظر إلى الرسم: علامة، ثم فرضية، ثم تغيير واحد يختبرها، ثم تسجّل النتيجة، ثم تعود إلى العلامة التالية.
تغيير خمسة إعدادات معًا قد يصلح المشكلة، لكنك لن تعرف أيّها السبب، وستكرّر الخطأ في المشروع التالي. سجّل كل تجربة، ولو في ملف نصي بسيط.
كيف يعمل، خطوة بخطوة: كيف تشخّص تدريبًا فاشلًا
قارن أول خسارة بما تتوقّعه: ln(عدد الفئات) لمصنّف بأوزان عشوائية، فرقم بعيد عنه يعني خطأ قبل أن يبدأ التدريب.
احفظ دفعة صغيرة واحدة، 16 مثالًا مثلًا. إن لم تنزل الخسارة قرب الصفر، فالخطأ في الكود.
ارسم منحنى التدريب والتحقّق معًا، واقرأ العلامة: ثبات، أو تذبذب، أو NaN، أو فجوة تتّسع.
غيّر شيئًا واحدًا حسب العلامة، مثل قسمة المعدّل على 10 أو التوقّف المبكّر، وسجّل التجربة ونتيجتها.
أعمق قليلًا
| العلامة | السبب المرجّح | أول إجراء |
|---|---|---|
| الخسارة ثابتة من البداية | خطأ في الكود أو معدّل تعلّم صغير جدًّا | احفظ دفعة واحدة، ثم ارفع المعدّل ×10 |
| الخسارة تقفز وتتذبذب | معدّل تعلّم كبير | اقسم المعدّل على 10 |
| NaN بعد خطوات | انفجار الأوزان أو عملية غير مستقرة | خفّض المعدّل، قصّ الميل، افحص البيانات |
| التدريب ينزل والتحقّق يصعد | حفظ | توقّف مبكّر، Dropout، بيانات أكثر |
| الاثنان مرتفعان ومتقاربان | نموذج أصغر من المسألة | نموذج أكبر أو سمات أفضل |
مثال محلول
أهم اختبار في تشخيص التدريب: هل يستطيع النموذج حفظ دفعة واحدة صغيرة؟ إن لم يستطع خفض الخسارة إلى قرب الصفر على 16 مثالًا، فالمشكلة في الكود لا في البيانات ولا في الحجم.
optimizer.step(): torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0). يمنع خطوة واحدة شاذّة من تدمير أسابيع من التدريب، وهو افتراضي في أغلب تدريب المحوّلات.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| إن لم تنزل الخسارة فالنموذج يحتاج أن يكبر. | خسارة ثابتة من أول خطوة لا يصلحها نموذج أكبر. اختبار الدفعة الواحدة يحسم السؤال في دقيقة. |
| NaN يعني دائمًا أن معدّل التعلّم كبير. | قد يأتي من عملية غير مستقرة مثل log(0) أو exp لرقم كبير، أو من NaN موجود في البيانات نفسها. |
| خسارة تدريب منخفضة جدًّا تعني نموذجًا ممتازًا. | في مثال الجيب نزلت خسارة التدريب إلى 0.042، أقل من تباين الضجيج نفسه 0.09، بينما ارتفع التحقّق. هذا حفظ. |
جرّب بنفسك
خسارة التدريب 0.05 وخسارة التحقّق 0.90 وما زالت ترتفع. أي إجراء لا يساعد؟
هذه علامة حفظ واضحة: النموذج يملك قدرة زائدة على البيانات المتاحة. تكبيره يزيد القدرة على الحفظ فيوسّع الفجوة. الخيارات الثلاثة الأخرى كلها تقلّل الحفظ أو تجعله أصعب.
الخسارة صارت NaN في الخطوة 3000 بعد تدريب سليم. أول ما تفعله؟
التدريب كان سليمًا ثلاثة آلاف خطوة، فالمعدّل ليس كبيرًا بشكل عام. الأرجح دفعة واحدة فيها قيمة شاذّة أو مفقودة سبّبت ميلًا ضخمًا. احفظ الدفعة عند ظهور NaN وافحصها، وقصّ الميل يحميك من تكرار ذلك.
بطاقات مراجعة سريعة
log(0) أو exp لرقم كبير، أو NaN موجود في البيانات نفسها.تحقق من فهمك
الخسارة الأولى لتصنيف بخمس فئات كانت 0.02 قبل أي تدريب. ماذا يخبرك هذا؟
أن هناك خطأ: نموذج بأوزان عشوائية لا يعرف شيئًا، فيجب أن يعطي احتمالًا قريبًا من 1/5 لكل فئة، أي خسارة قريبة من ln(5) ≈ 1.61. خسارة 0.02 تعني أنه «يعرف» الجواب مسبقًا، والأرجح أن الفئة نفسها تسرّبت إلى المدخلات، أو أن كل الفئات في الدفعة واحدة والنموذج يخرج قيمة كبيرة لها بالصدفة.
لماذا تطرح أكبر قيمة من المدخلات قبل exp في softmax؟
لأن exp لقيم مثل 1000 يتجاوز أكبر رقم ممكن فيصير لانهاية ثم NaN. الطرح لا يغيّر النتيجة رياضيًا لكنه يجعل أكبر أسّ صفرًا.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.