Backpropagation — التعلم العميق
الانتشار العكسي (Backpropagation)
نعرف الآن كيف تحسب الشبكة تنبؤاً (التمرير الأمامي) وكيف نقيس خطأها (الخسارة). السؤال: كيف نعدّل ملايين الأوزان لتقليل الخسارة؟
الفكرة: التدرج
لكل وزن نسأل: "إذا زدت هذا الوزن قليلاً، هل تزيد الخسارة أم تنقص، وبكم؟" هذا هو التدرج (المشتقة الجزئية ∂L/∂w). ثم نحرك الوزن عكس التدرج:
w ← w − η · ∂L/∂w (η = معدل التعلم)قاعدة السلسلة
الخسارة تعتمد على المخرج، والمخرج على الطبقة المخفية، والمخفية على الأوزان الأولى. قاعدة السلسلة تربطها:
∂L/∂W1 = ∂L/∂out · ∂out/∂h · ∂h/∂W1Backpropagation = حساب هذه المشتقات بكفاءة، بدءاً من الخسارة والرجوع للخلف طبقة طبقة، مع إعادة استخدام ما حُسب في الطبقة السابقة.
دورة تدريب واحدة
1. أمامي: احسب التنبؤات، واحفظ القيم الوسيطة.
2. الخسارة: قارن بالإجابات.
3. خلفي: احسب تدرج كل وزن.
4. تحديث: حرّك الأوزان عكس التدرج.
تحقق رقمي من المشتقة
طريقة مفيدة لاختبار أي تنفيذ يدوي: قارن المشتقة التحليلية بتقريب رقمي.
import numpy as np
def loss(w): # خسارة بسيطة لوزن واحد
return (3 * w - 6) ** 2
w, eps = 1.0, 1e-5
analytic = 2 * (3 * w - 6) * 3 # بقاعدة السلسلة
numeric = (loss(w + eps) - loss(w - eps)) / (2 * eps)
print(analytic, round(numeric, 4)) # متطابقتانمسألة XOR
XOR (أحدهما فقط صحيح) لا يمكن فصلها بخط مستقيم — نموذج خطي يفشل فيها. شبكة بطبقة مخفية واحدة تحلها. المثال الكامل ينفذ كل شيء يدوياً بـ NumPy: أوزان وانحيازات، sigmoid، Binary Cross-Entropy، وتدرجات كل طبقة. اقرأه سطراً بسطر مع هذه الملاحظات:
- مع sigmoid في المخرج و BCE، يصبح تدرج الخسارة بالنسبة لـ z المخرج ببساطة (out − y).
- تدرج الأوزان = (مدخلات الطبقة)ᵀ · (تدرج z الطبقة).
- تدرج الطبقة السابقة = (تدرج z) · Wᵀ، مضروباً بمشتقة تنشيطها.
في الممارسة
لن تكتب Backpropagation يدوياً أبداً: TensorFlow و PyTorch تحسب التدرجات تلقائياً (Automatic Differentiation) لأي شبكة تبنيها. لكن فهمه يفسر لك تلاشي التدرج، وأهمية معدل التعلم، ولماذا تنفجر القيم أحياناً إلى NaN.
تمرين
في المثال الكامل غيّر معدل التعلم إلى 0.01 ثم 10. ماذا يحدث للخسارة في الحالتين؟ ثم غيّر عدد العصبونات المخفية إلى 1 — هل ما زالت الشبكة تحل XOR؟ لماذا؟
import numpy as np
rng = np.random.default_rng(1)
# XOR: لا يمكن فصلها بخط مستقيم
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([[0], [1], [1], [0]], dtype=float)
# الأوزان والانحيازات: 2 مدخل ← 4 عصبونات مخفية ← 1 مخرج
W1, b1 = rng.normal(0, 1, (2, 4)), np.zeros((1, 4))
W2, b2 = rng.normal(0, 1, (4, 1)), np.zeros((1, 1))
def sigmoid(z):
return 1 / (1 + np.exp(-z))
lr = 1.0
for epoch in range(5001):
# === التمرير الأمامي ===
z1 = X @ W1 + b1
h = np.tanh(z1)
z2 = h @ W2 + b2
out = sigmoid(z2)
# === الخسارة: Binary Cross-Entropy ===
p = np.clip(out, 1e-7, 1 - 1e-7)
loss = -np.mean(y * np.log(p) + (1 - y) * np.log(1 - p))
# === التمرير الخلفي (قاعدة السلسلة) ===
n = len(X)
dz2 = (out - y) / n # sigmoid + BCE ← تدرج بسيط
dW2 = h.T @ dz2
db2 = dz2.sum(axis=0, keepdims=True)
dh = dz2 @ W2.T
dz1 = dh * (1 - h ** 2) # مشتقة tanh
dW1 = X.T @ dz1
db1 = dz1.sum(axis=0, keepdims=True)
# === التحديث: عكس اتجاه التدرج ===
W1 -= lr * dW1; b1 -= lr * db1
W2 -= lr * dW2; b2 -= lr * db2
if epoch % 1000 == 0:
print(f"epoch {epoch:4d} الخسارة = {loss:.4f}")
print("\nالتنبؤات:", out.round(3).ravel(), "← المطلوب: [0 1 1 0]")اختبر فهمك