بعد أن تنتج الشبكة تنبّؤًا (الانتشار الأمامي) ويحسب دالة الخسارة مدى الخطأ، الخطوة التالية هي: أيّ وزن مسؤول عن هذا الخطأ، وإلى أيّ اتجاه نُحرّكه ليقلّ الخطأ؟
هذا بالضبط ما يفعله الانتشار العكسي (Backpropagation).
الحدس أوّلًا
تخيّل أنك رميت سهمًا بعيدًا عن الهدف. كيف تُصحّح رميتك؟
- تنظر إلى أين وصل السهم (التنبّؤ).
- تنظر كم يبعد عن الهدف (الخسارة).
- تُحدّد أيّ عاملٍ سبب الخطأ: زاوية الذراع؟ اتجاه الريح؟ قوة الرمي؟
- تُعدّل ذلك العامل بمقدار صغير في الاتجاه الصحيح.
- تُكرّر.
الانتشار العكسي يفعل الشيء نفسه:
- يحسب تنبّؤ الشبكة (forward pass).
- يحسب الخسارة (loss).
- يحسب تدرّج كل وزن — أي تأثير هذا الوزن على الخسارة.
- يُحرّك الوزن في الاتجاه المعاكس للتدرّج بمقدار مُحدَّد بـ learning rate.
- يُكرّر آلاف المرات.
ما هو التدرّج (gradient)؟
التدرّج = مُشتقّ دالة الخسارة بالنسبة لمُعامل معيّن.
∂L/∂w = "لو غيّرت w بمقدار صغير جدًا، كم ستتغيّر L؟"
- موجب: زيادة w تزيد L → نُنقِص w.
- سالب: زيادة w تنقِص L → نُزيد w.
- صفر: w في نقطة مثالية محليًا (أو dead ReLU، سنتناوله).
chain rule — مفتاح الانتشار العكسي
لو L تعتمد على y_hat، و y_hat يعتمد على w، فيمكننا حساب تأثير w على L عبر y_hat:
∂L/∂w = (∂L/∂y_hat) · (∂y_hat/∂w)
هذا هو chain rule. في شبكة بطبقات كثيرة، نطبّقه مرات ومرات حتى نصل لكل وزن.
مثال صغير محسوب يدويًا
سنأخذ شبكة من معاملَين، ونحسب كل تدرّج يدويًا ثم نتحقّق منه بـ PyTorch.
البنية
y_hat = w2 · ReLU(w1 · x)
L = (y_hat - y)²
المُعطيات
x = 2.0y = 3.0(القيمة الحقيقية)w1 = 0.5،w2 = 1.5
Forward pass (يدوي)
z = w1 · x = 0.5 · 2.0 = 1.0
a = ReLU(z) = max(0, 1.0) = 1.0
y_hat = w2 · a = 1.5 · 1.0 = 1.5
L = (1.5 - 3.0)² = 2.25
Backward pass (يدوي)
نبدأ من الخسارة ونرجع خطوة خطوة:
∂L/∂y_hat = 2 · (y_hat - y) = 2 · (-1.5) = -3.0
∂L/∂w2 = ∂L/∂y_hat · ∂y_hat/∂w2 = -3.0 · a = -3.0 · 1.0 = -3.0
∂L/∂a = ∂L/∂y_hat · ∂y_hat/∂a = -3.0 · w2 = -3.0 · 1.5 = -4.5
∂L/∂z = ∂L/∂a · ∂a/∂z = -4.5 · ReLU'(1.0) = -4.5 · 1.0 = -4.5
(لأن z > 0، مُشتقّ ReLU = 1)
∂L/∂w1 = ∂L/∂z · ∂z/∂w1 = -4.5 · x = -4.5 · 2.0 = -9.0
النتيجة:
∂L/∂w1 = -9.0
∂L/∂w2 = -3.0
تحديث الأوزان (خطوة واحدة من gradient descent)
بـ learning rate = 0.01:
w1_new = w1 - lr · ∂L/∂w1 = 0.5 - 0.01 · (-9.0) = 0.5 + 0.09 = 0.59
w2_new = w2 - lr · ∂L/∂w2 = 1.5 - 0.01 · (-3.0) = 1.5 + 0.03 = 1.53
التدرّجات سالبة، فالأوزان ستزيد → ستقترب y_hat من y = 3.0. هذا هو التعلّم.
التحقّق بـ PyTorch (autograd)
PyTorch يحسب التدرّجات تلقائيًا. نتأكّد أن حساباتنا اليدوية صحيحة:
import torch
x = torch.tensor([2.0])
y = torch.tensor([3.0])
w1 = torch.tensor([0.5], requires_grad=True)
w2 = torch.tensor([1.5], requires_grad=True)
# Forward pass
z = w1 * x
a = torch.clamp(z, min=0) # ReLU
y_hat = w2 * a
loss = ((y_hat - y) ** 2).sum()
# Backward pass
loss.backward()
print(f"∂L/∂w1 (autograd) = {w1.grad.item()}") # -9.0
print(f"∂L/∂w2 (autograd) = {w2.grad.item()}") # -3.0
النتيجة: -9.0 و -3.0 — مطابقتان تمامًا للحساب اليدوي.
التحقّق بالتدرّج العددي (numerical gradient)
طريقة أخرى للتحقّق: حرّك الوزن بمقدار صغير جدًا، وراقب تغيّر الخسارة:
∂L/∂w ≈ (L(w + ε) - L(w - ε)) / (2ε)
import torch
x = torch.tensor([2.0])
y = torch.tensor([3.0])
def loss_at(w1v, w2v):
z = w1v * x
a = torch.clamp(z, min=0)
y_hat = w2v * a
return ((y_hat - y) ** 2).sum()
eps = 1e-3
dw1_num = (loss_at(0.5 + eps, 1.5) - loss_at(0.5 - eps, 1.5)) / (2 * eps)
dw2_num = (loss_at(0.5, 1.5 + eps) - loss_at(0.5, 1.5 - eps)) / (2 * eps)
print(f"∂L/∂w1 (numerical) = {dw1_num.item():.4f}") # ≈ -9.0
print(f"∂L/∂w2 (numerical) = {dw2_num.item():.4f}") # ≈ -3.0
القاعدة: إذا تدرّج autograd يطابق التدرّج اليدوي والتدرّج العددي (خلال تسامح صغير)، فهو صحيح. هذا اختبار sanity ممتاز.
ماذا يحدث إذا كان z سالبًا؟ (Dying ReLU)
لو غيّرنا x إلى -3.0:
z = 0.5 · (-3.0) = -1.5 (سالب)
a = ReLU(-1.5) = 0
y_hat = 1.5 · 0 = 0
L = (0 - 3.0)² = 9
∂L/∂y_hat = -6.0
∂L/∂w2 = -6.0 · a = -6.0 · 0 = 0 ← لأن a = 0
∂L/∂z = ... · ReLU'(-1.5) = ... · 0 = 0 ← لأن z < 0
∂L/∂w1 = 0 · x = 0
كلا التدرّجَين صفر. لا تعلّم. هذا ما يُسمّى dying ReLU (شرح في دوال التنشيط).
import torch
x = torch.tensor([-3.0])
y = torch.tensor([3.0])
w1 = torch.tensor([0.5], requires_grad=True)
w2 = torch.tensor([1.5], requires_grad=True)
z = w1 * x
a = torch.clamp(z, min=0)
y_hat = w2 * a
loss = ((y_hat - y) ** 2).sum()
loss.backward()
print(f"∂L/∂w1 = {w1.grad.item()}") # 0.0
print(f"∂L/∂w2 = {w2.grad.item()}") # 0.0
الرسم البياني الحسابي (computational graph)
PyTorch يبني رسمًا بيانيًا للعمليات أثناء الـ forward pass، ثم يستخدمه في backward:
x ──┐
├──► [w1·x] ──► z ──► [ReLU] ──► a ──► [w2·a] ──► y_hat ──► [(y_hat-y)²] ──► L
w1 ─┤ w2 ──┘ y ─────┘
- forward pass: نمشي من اليسار إلى اليمين، نخزّن النتائج الوسيطة.
- backward pass: نمشي من اليمين إلى اليسار، نضرب المشتقّات (chain rule).
- autograd في PyTorch يفعل ذلك تلقائيًا.
الخطوات الثلاث — لا تخلط بينها
1. Forward pass: حساب التنبّؤ والخسارة
2. Backward pass: حساب التدرّجات (loss.backward())
3. Optimizer step: تحديث الأوزان (optimizer.step())
ترتيبها حرج — وسنتناولها بالتفصيل في Training Loop في PyTorch.
أخطاء شائعة
- "autograd = backpropagation": autograd يحسب التدرّجات تلقائيًا عبر chain rule. الانتشار العكسي هو الخوارزمية الكاملة.
- نسيان
loss.backward(): لن تُحدَّث التدرّجات، والأوزان لن تتحرّك. - نسيان
optimizer.zero_grad()قبل backward: تدرّجات الخطوة السابقة تتراكم مع الحالية. سنتناوله. - "تدرّج صغير يعني تعلّم بطيء دائمًا": ليس بالضرورة. أحيانًا LR صغير جدًا يجمّد التدريب. أحيانًا LR كبير جدًا يقفز فوق الحل.
- "autograd يعمل فقط لـ float32": يدعم أنماطًا أخرى (float64, complex) لكن الأداء يختلف. float32 هو الافتراضي للتدريب.
الخطوات التالية
- Optimizers و Regularization — كيف تُستخدم التدرّجات لتحديث الأوزان (SGD, Adam, ...).
- Training Loop في PyTorch — تجميع forward + backward + step.
- دوال الخسارة — ما الذي نحسب تدرّجه.
- الشبكات العصبية — الأساسيات — تعريف الأوزان التي نُحدِّثها.
🔍 لفهم أعمق للـ chain rule في RNN أو attention، راجع معمارية Transformers.