مشروع: شبكة من الصفر
درس 8 من 8
8مشروع: شبكة من الصفر
الصورة الكبيرةمشروع هذا المسار: شبكة تكتبها من أول سطر لآخر سطر، بحلقة تدريب يدوية ومنحنى تحقّق تقرؤه، ثم ثلاثة أعطال نزرعها في الكود لتكتشفها من علاماتها قبل أن تقرأ الحل.
ماذا تشاهد في الرسم: الأعطال الثلاثة تُعرف من رقمين فقط: الدقة والخسارة. المسار المضيء يصل إلى أخطرها، softmax الزائد: دقة 93% تبدو سليمة، لكن الخسارة 0.408 بدل 0.284 تفضحه.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
في هذا المشروع تكتب حلقة التدريب كاملة بيدك دون أي أداة جاهزة، ثم نخبّئ لك ثلاثة أخطاء في الكود، وعليك أن تجدها كما يجد الميكانيكي عطل السيارة: بالملاحظة والاختبار، لا بالتخمين.
تبني نموذجًا صغيرًا وDataset وDataLoader وحلقة تدريب وتقييم مع تسجيل المنحنيات. الأعطال المزروعة من نوع zero_grad المنسي، وتسميات مزاحة، ونسيان model.eval()، وتشخّص كل واحد بالمنحنى أو باختبار الإفراط على دفعة واحدة.
قبل أن تبدأ
⚙️حلقة تدريب بيدك
تكتب الأسطر الخمسة دون نظر، مع model.train() و model.eval() و torch.no_grad() في أماكنها.
🩺تشخيص التدريب
تقرأ منحنى التدريب والتحقّق معًا، وتعرف أن ln(عدد الفئات) خسارة نموذج يخمّن بالتساوي.
الفكرة الجوهرية
مشروع هذه المرحلة شبكة تكتبها من أول سطر. تبني الشبكة على بيانات الهلالين، ثم تدرّبها وتقرأ منحناها، ثم تحفظ أفضل نموذج، ثم تزرع ثلاثة أعطال وتتعرّف عليها من أرقامها.
1البيانات والشبكة
بيانات الهلالين نقاط على مستوٍ في شكلين متداخلين لا يفصلهما خط مستقيم. نولّد 1000 نقطة، 800 للتدريب و200 للتحقّق، ونثبّت البذرة.
انظر إلى الرسم: مدخلان، وطبقتان مخفيتان في كل منهما 32 خلية بعدها ReLU، ومخرجان. الشبكة صغيرة عن قصد حتى يأخذ التدريب ثواني.
2الحلقة والمنحنى
الحلقة هي الأسطر الخمسة، مع model.train() قبل التدريب وmodel.eval() وtorch.no_grad() قبل التقييم. انظر إلى الرسم: خسارة التحقّق تنزل حتى الدورة 10، ثم ترتفع بينما خسارة التدريب تستمر في النزول.
هذا الكود يحتاج PyTorch على جهازك، فالناتج المعروض مسجَّل من تشغيل حقيقي. ثبّته بـpip install torch وشغّله بنفسك.
import torch
from sklearn.datasets import make_moons
from torch import nn
X, y = make_moons(n_samples=1000, noise=0.25, random_state=0)
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.long)
X_train, y_train, X_val, y_val = X[:800], y[:800], X[800:], y[800:]
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 2))
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
for epoch in range(1, 201):
model.train()
for i in range(0, 800, 64):
xb, yb = X_train[i:i + 64], y_train[i:i + 64]
loss = loss_fn(model(xb), yb)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch in (1, 10, 50, 200):
model.eval()
with torch.no_grad():
train_loss = loss_fn(model(X_train), y_train).item()
val_logits = model(X_val)
val_loss = loss_fn(val_logits, y_val).item()
val_acc = (val_logits.argmax(dim=1) == y_val).float().mean().item()
print(f"epoch {epoch:3} train {train_loss:.3f} val {val_loss:.3f} val acc {val_acc:.3f}")
بعد عشر دورات: دقة تحقّق 93.5% وخسارة تحقّق 0.206، وهذا أفضل نموذج في التشغيل. بعدها تنخفض خسارة التدريب ببطء وترتفع خسارة التحقّق إلى 0.314، بينما الدقة تبقى 93%. الدقة لم تتغيّر لكن النموذج صار أكثر ثقة في أخطائه، وهذا ما تكشفه الخسارة ولا تكشفه الدقة.
3احفظ أفضل نموذج
النموذج الذي تريده هو الذي عند أدنى خسارة تحقّق، لا الذي في آخر دورة. هذا هو التوقّف المبكّر في أبسط صوره.
التوقّف المبكّر في أبسط صوره: تابع أدنى خسارة تحقّق واحفظ الأوزان عندها، ثم حمّلها في النهاية.
best_loss, best_state, patience, waited = float("inf"), None, 10, 0
for epoch in range(200):
train_one_epoch(model, train_loader)
val_loss = evaluate(model, val_loader)
if val_loss < best_loss:
best_loss, waited = val_loss, 0
best_state = {k: v.clone() for k, v in model.state_dict().items()}
else:
waited += 1
if waited >= patience:
break
model.load_state_dict(best_state)
patience يعطي النموذج عشر دورات ليتحسّن قبل التوقّف، لأن خسارة التحقّق تتذبذب ولا تصعد بخط مستقيم. وclone() ضروري: بدونه تحفظ مراجع للأوزان الحالية التي ستتغيّر، لا نسخة منها.
4ثلاثة أعطال مزروعة
انسخ الملف ثلاث مرات وازرع في كل نسخة عطلًا واحدًا: حذف zero_grad، وsoftmax زائدة، وفئات مخلوطة عشوائيًا. انظر إلى الرسم: آخر خسارة لكل نسخة، والمعلَّمة هي الأخطر.
قبل أن تقرأ الشرح، خمّن من الأرقام: أي عطل يسبّب أي سطر؟
import torch
from sklearn.datasets import make_moons
from torch import nn
X, y = make_moons(n_samples=1000, noise=0.25, random_state=0)
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.long)
X_train, y_train, X_val, y_val = X[:800], y[:800], X[800:], y[800:]
loss_fn = nn.CrossEntropyLoss()
def run(bug):
torch.manual_seed(0)
layers = [nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 2)]
if bug == "softmax":
layers.append(nn.Softmax(dim=1))
model = nn.Sequential(*layers)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(10):
for i in range(0, 800, 64):
xb, yb = X_train[i:i + 64], y_train[i:i + 64]
if bug == "labels":
yb = y_train[torch.randperm(800)[:len(xb)]]
loss = loss_fn(model(xb), yb)
if bug != "zero_grad":
optimizer.zero_grad()
loss.backward()
optimizer.step()
with torch.no_grad():
acc = (model(X_val).argmax(dim=1) == y_val).float().mean().item()
return loss.item(), acc
for bug in ["none", "zero_grad", "softmax", "labels"]:
loss, acc = run(bug)
print(f"{bug:9} last loss {loss:.3f} val acc {acc:.3f}")
zero_grad محذوف: خسارة 4.03، أعلى من البداية بكثير، ودقة 53.5%، أي صدفة تقريبًا. softmax زائدة: دقة 93% تكاد تساوي السليم، لكن الخسارة 0.408 مقابل 0.284، لأن الميول صارت أضعف فالتعلّم أبطأ. الفئات المخلوطة: خسارة 0.697 وهي ln(2) = 0.693 تقريبًا، ودقة 50.5%. هذا الرقم بالذات، ln لعدد الفئات، علامة على أن النموذج يخمّن بالتساوي.
الهدف ذاكرة بصرية: حين ترى هذه العلامات في مشروع حقيقي بعد أشهر، تعرف أين تنظر فورًا.
كيف يعمل، خطوة بخطوة: كيف تبني المشروع وتزرع فيه الأعطال
ولّد 1000 نقطة من الهلالين، 800 للتدريب و200 للتحقّق، وثبّت البذرة.
درّب شبكة من ثلاث طبقات Linear بينها ReLU بحلقة يدوية، واطبع خسارة التدريب والتحقّق ودقة التحقّق.
احتفظ بالأوزان عند أدنى خسارة تحقّق بنسخ state_dict مع clone()، وارسم المنحنيين في ملف صورة.
انسخ الملف ثلاث مرات وأدخل عطلًا واحدًا في كل نسخة، ثم سجّل خسارتها ودقتها وعلامتها في جدول.
مثال محلول
البيانات: «الهلالان» من scikit-learn، نقاط على مستوى في شكلين متداخلين لا يفصلهما خط مستقيم. سهلة الرسم، وكافية لتظهر كل ظاهرة رأيتها في هذا المسار.
model.eval() مع Dropout، أو معدّل تعلّم 10، وأضفه للجدول. كلما كبر جدولك، صار تشخيصك أسرع.
جرّب بلا كود
ln(4) ≈ 1.386 هي خسارة نموذج يخمّن بالتساوي، و25% هي الصدفة مع 4 فئات.خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| إن لم تتغيّر الدقة فالنموذج لم يتغيّر. | بقيت الدقة 93% من الدورة 10 إلى 200 بينما ارتفعت خسارة التحقّق من 0.206 إلى 0.314: صار النموذج أكثر ثقة في أخطائه. |
| الأعطال الخطيرة هي التي ترفع أخطاء. | العطل الذي يفشل بصوت عالٍ يُصلح في دقائق. الخطير هو الصامت، مثل softmax الزائد الذي يعطي دقة تبدو مقبولة. |
حفظ أفضل نموذج يكفيه best_state = model.state_dict(). | هذا يحفظ مراجع للأوزان الحالية التي ستتغيّر. تحتاج نسخة بـ clone() لكل موتر. |
جرّب بنفسك
شبكة لتصنيف عشر فئات، خسارتها عالقة عند 2.30 من أول دورة إلى آخرها. ما الأرجح؟
ln(10) ≈ 2.30 هي خسارة نموذج يعطي 10% لكل فئة، أي يخمّن بالتساوي. ثباتها التام يعني أنه لم يتعلّم شيئًا على الإطلاق، وحتى نموذج صغير كان سيتعلّم شيئًا. افحص البيانات والفئات وتدفّق الميول باختبار الدفعة الواحدة.
لماذا عطل softmax الزائدة أخطر عمليًا من عطل zero_grad؟
الدقة 93% تبدو نجاحًا، فينتقل الكود إلى الإنتاج بنموذج أبطأ تعلّمًا وأسوأ معايرة مما يمكن. العطل الذي يفشل بصوت عالٍ يُصلح في دقائق، والعطل الصامت يعيش لشهور.
بطاقات مراجعة سريعة
zero_gradCrossEntropyLossln(عدد الفئات) تمامًا، ودقة تساوي الصدفة. النموذج لا يجد أي علاقة لأن العلاقة غير موجودة.تحقق من فهمك
في النسخة السليمة، الدقة بقيت 93% من الدورة 10 إلى 200 بينما خسارة التحقّق ارتفعت. أي نموذج تختار، ولماذا؟
نموذج الدورة 10، صاحب أدنى خسارة تحقّق. الدقة متساوية، لكن ارتفاع الخسارة يعني أن النموذج المتأخّر يعطي احتمالات أكثر تطرّفًا لأخطائه، فإن استخدمت احتماله لاحقًا كدرجة ثقة أو لاختيار عتبة، سيضلّلك. والنموذج المبكّر أيضًا أقل حفظًا، فيعمّم أفضل على بيانات تختلف قليلًا عن التحقّق.
لماذا يعطي patience = 10 النموذج عشر دورات قبل التوقّف بدل التوقّف عند أول ارتفاع في خسارة التحقّق؟
لأن خسارة التحقّق تتذبذب ولا ترتفع في خط مستقيم، فارتفاع واحد قد يتبعه انخفاض جديد. الصبر يمنع التوقّف المبكّر جدًّا بسبب ضجيج عابر.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.