حلقة تدريب بيدك
درس 3 من 8
3حلقة تدريب بيدك
الصورة الكبيرةكل تدريب، من انحدار خطي إلى نموذج لغوي بمليارات المعاملات، هو نفس الأسطر الخمسة: تنبّأ، احسب الخسارة، صفّر الميول، اشتقّ، خطوة. اكتبها بيدك هنا حتى لا يكون أي إطار تدريب جاهز صندوقًا أسود بعد اليوم.
ماذا تشاهد في الرسم: الضوء يدور على المراحل الخمس بنفس الترتيب في كل خطوة، من الانحدار الخطي إلى نموذج لغوي ضخم. التنبّؤ يحتاجه حساب الخسارة، والاشتقاق يحتاج الخسارة، والخطوة تحتاج الميول.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
التدريب يكرر أربع خطوات مثل لاعب يتدرّب على الرمي: يرمي الكرة (تمرير أمامي)، يرى كم ابتعدت عن السلة (الخسارة)، يفهم ما الذي أخطأ فيه (تمرير خلفي)، ثم يعدّل حركته قليلًا (خطوة المحسّن). آلاف الرميات، وكل رمية أقرب قليلًا.
في كل دفعة: pred = model(x) ثم loss = loss_fn(pred, y) ثم optimizer.zero_grad() وloss.backward() وoptimizer.step(). تفصل وضع التدريب model.train() عن التقييم model.eval()، وتسجّل خسارة التدريب والتحقق في كل حقبة epoch لترى إن كان يتعلّم أم يحفظ.
قبل أن تبدأ
🔁المشتقات التلقائية
تعرف أن loss.backward() يملأ .grad لكل وزن وأن الميول تتراكم إن لم تصفّرها.
📈الانحدار الخطي
تعرف نموذجًا بسيطًا مثل y = wx + b وخسارة متوسط مربع الخطأ من درس الانحدار.
الفكرة الجوهرية
كل تدريب هو نفس الأسطر الخمسة. في هذا الدرس تكتبها بيدك في NumPy، ثم تراها في PyTorch، ثم تدرّب بها شبكة على منحنى، ثم ترى ماذا يحدث إن نسيت سطرًا منها.
1الحلقة بيدك في NumPy
نبدأ بانحدار خطي بلا أي مكتبة تدريب. البيانات مبنية من y = 3x − 1 مع ضجيج، والنموذج يبدأ من w = 0 وb = 0.
import numpy as np
rng = np.random.default_rng(0)
X = rng.uniform(-2, 2, size=(200, 1))
y = 3 * X[:, 0] - 1 + rng.normal(scale=0.2, size=200)
w, b = 0.0, 0.0
for epoch in range(6):
pred = w * X[:, 0] + b # forward
loss = ((pred - y) ** 2).mean() # loss
grad_w = 2 * ((pred - y) * X[:, 0]).mean() # backward
grad_b = 2 * (pred - y).mean()
w -= 0.3 * grad_w # step
b -= 0.3 * grad_b
print(f"epoch {epoch} loss={loss:.3f} w={w:.2f} b={b:.2f}")
الخسارة تسقط من 13.3 إلى 0.04، وw وb يقتربان من القيم الحقيقية 3 و−1 التي صنعنا بها البيانات. الخسارة لا تصل إلى صفر لأننا أضفنا ضجيجًا بانحراف 0.2، ومربّعه 0.04 هو أفضل ما يمكن.
انظر إلى الرسم: معظم التعلّم حدث في أول دورتين، ثم صارت الخطوات صغيرة. هنا كل دورة خطوة واحدة لأننا نستخدم كل النقاط المئتين دفعة واحدة. مع الدفعات، الدورة عدد خطوات يساوي عدد الدفعات.
2نفس الأسطر في PyTorch
كل سطر كتبته بيدك له مقابل في PyTorch. والمحسّن هو من ينفّذ الخطوة: SGD يطرح الميل مضروبًا في معدّل التعلّم، وAdam يكيّف الخطوة لكل وزن حسب تاريخ ميوله.
النموذج يصير nn.Module بدل معادلة، والخسارة دالة جاهزة، والاشتقاق loss.backward() بدل سطري الميل، والخطوة optimizer.step(). الترتيب والفكرة لا يتغيّران أبدًا.
3شبكة تتعلّم منحنى
الآن هدف لا يرسمه خط مستقيم. انظر إلى الرسم: مدخل واحد، وطبقة مخفية فيها ست عشرة خلية بعدها ReLU، ومخرج واحد.
الهدف y = x² مع ضجيج، وهو منحنى لا يستطيع خط مستقيم تمثيله. طبقة مخفية بستة عشر خلية وReLU تكفي.
import torch
from torch import nn
torch.manual_seed(0)
X = torch.linspace(-2, 2, 200).unsqueeze(1)
y = X ** 2 + 0.1 * torch.randn_like(X)
model = nn.Sequential(nn.Linear(1, 16), nn.ReLU(), nn.Linear(16, 1))
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
loss_fn = nn.MSELoss()
for epoch in range(301):
pred = model(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 100 == 0:
print(f"epoch {epoch:3} loss {loss.item():.4f}")
print("f(1.5) =", round(model(torch.tensor([[1.5]])).item(), 2), "true =", 1.5 ** 2)
الخسارة من 3.77 إلى 0.015 في ثلاثمئة دورة، والتنبّؤ عند 1.5 هو 2.34 مقابل القيمة الحقيقية 2.25. لاحظ الأسطر الخمسة داخل الحلقة: هي نفسها حرفيًا في كل كود تدريب ستقرؤه بعد اليوم.
unsqueeze(1). torch.linspace يعطي شكل (200,)، وnn.Linear(1, 16) يتوقّع آخر بعد بطول 1، أي (200, 1). اطبع X.shape قبل أن تمرّره إلى النموذج.4لماذا zero_grad في كل خطوة
backward يجمع على الميول السابقة. بلا تصفير، يتحرّك كل وزن بمجموع كل الميول منذ البداية. انظر إلى الرسم: نفس النموذج ونفس البيانات، والفرق سطر واحد.
import torch
from torch import nn
torch.manual_seed(0)
X = torch.randn(64, 4)
y = X.sum(dim=1, keepdim=True)
for forget in [False, True]:
torch.manual_seed(0)
model = nn.Linear(4, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
for step in range(50):
loss = ((model(X) - y) ** 2).mean()
if not forget:
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"forgot zero_grad={forget}: final loss {loss.item():.4f}")
نفس النموذج ونفس البيانات. مع التصفير الخسارة 0.0007، وبدونه 1.98، أي أسوأ من البداية تقريبًا. لا يظهر أي خطأ ولا تحذير، والكود «يعمل». هذا أخطر نوع من الأخطاء: الصامت.
كيف يعمل، خطوة بخطوة: كيف تتحرّك الأوزان في خطوة تدريب
المرور الأمامي pred = model(xb) يحوّل دفعة المدخلات إلى تنبّؤات ويبني الرسم الحسابي.
دالة الخسارة تقارن التنبّؤات بالأهداف وتعطي رقمًا واحدًا: MSELoss للانحدار و CrossEntropyLoss للتصنيف.
optimizer.zero_grad() يمسح الميول القديمة، ثم loss.backward() يحسب ميلًا جديدًا لكل وزن.
optimizer.step() يقرأ .grad ويحرّك كل وزن خطوة، ثم تبدأ الدفعة التالية من جديد.
مثال محلول
ابدأ بالحلقة بـNumPy وحدها، بلا أي مكتبة تدريب، على انحدار خطي. أربع مراحل في كل دورة، ومعلّقة بأسمائها.
softmax على مخرجات النموذج قبل CrossEntropyLoss. الدالة تطبّق log_softmax داخلها، فتصير softmax مرتين، ويتعلّم النموذج أبطأ بكثير دون أي خطأ. أعطها المخرجات الخام دائمًا.
ln(10) ≈ 2.30. إن كانت 15 أو 0.01، فهناك خطأ قبل أن يبدأ التدريب.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| أطر التدريب الجاهزة تفعل شيئًا أذكى من هذه الأسطر الخمسة. | في داخلها نفس الأسطر الخمسة. ما تضيفه هو الطباعة والحفظ وقياس الوقت حول الحلقة، لا حلقة مختلفة. |
نسيان zero_grad سيرفع خطأ تلاحظه. | لا خطأ ولا تحذير. في مثال الدرس انتهت الخسارة عند 1.98 بدل 0.0007، والكود «يعمل». |
| عدد الدورات هو عدد مرات تحديث الأوزان. | كل دفعة خطوة. 10000 مثال بدفعات من 100 لخمس دورات تعني 500 تحديث، لا 5. |
جرّب بنفسك
ما الترتيب الصحيح لخطوة تدريب واحدة؟
الخسارة تحتاج التنبّؤ، وbackward يحتاج الخسارة، وstep يحتاج الميول. والتصفير في أي مكان قبل backward. هذا الترتيب ثابت في كل كود تدريب.
بيانات من 10000 مثال، وحجم الدفعة 100، و5 دورات. كم مرة يُستدعى optimizer.step()؟
كل دورة فيها 10000 / 100 = 100 دفعة، وكل دفعة خطوة واحدة، فخمس دورات تعطي 500 خطوة. عدد الخطوات، لا عدد الدورات، هو ما يحدّد كم مرة تحرّكت الأوزان.
بطاقات مراجعة سريعة
zero_grad، ثم backward، ثم optimizer.step(). أي إطار تدريب يفعل هذا داخله..grad لكل وزن ويحدّث الوزن. SGD يطرح الميل مضروبًا في معدّل التعلّم، وAdam يكيّف الخطوة لكل وزن حسب تاريخ ميوله.zero_grad في كل خطوةbackward يجمع على الميول السابقة. بدون التصفير، تتحرّك الأوزان بمجموع كل الميول منذ البداية فيفشل التدريب.تحقق من فهمك
لماذا يأتي optimizer.zero_grad() قبل loss.backward() لا بعد optimizer.step()؟ هل يختلف الأمر؟
كلا الموضعين صحيح ما دام التصفير يحدث مرة واحدة بين كل step وbackward التالي. وضعه قبل backward أوضح لأنه يضمن أن الميول نظيفة لحظة حسابها، حتى لو أضاف أحد كودًا بين الخطوات لاحقًا. المهم ألا يحدث بين backward وstep، وإلا مسحت الميول قبل استخدامها.
في الحلقة اليدوية بـNumPy توقّفت الخسارة عند 0.04 تقريبًا ولم تصل إلى الصفر. لماذا؟
لأن البيانات صُنعت بضجيج انحرافه 0.2، ومربّعه 0.04 هو أقل خطأ ممكن. لا يستطيع أي نموذج توقّع الضجيج العشوائي.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي6
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.