0%
التعلّم العميق والشبكات العصبية

التعلّم العميق

فهم ما يحدث فعليًا داخل الشبكة.

10-15 دقيقة

التعلّم العميق والشبكات العصبية

عبارة «الشبكة العصبية» قد تبدو مرهبة، والمفردات البيولوجية لا تساعد في ذلك: فهي توحي بآلية غامضة أشبه بالدماغ، بعيدة المنال دون دكتوراه في الرياضيات. أما الحقيقة فهي أكثر تواضعًا وأكثر إثارة للاهتمام بكثير. الشبكة العصبية ليست سوى تكديس لعمليات ضرب وجمع، تُطبَّق على نتيجتها دالة بسيطة. لا شيء أكثر من ذلك.

لذلك فإن ما يستحق الفهم ليس معادلة العصبون، فهي تتسع لسطر واحد، بل أمران أقل بداهة. لماذا يجب تكديس عدة طبقات، بينما تكفي طبقة واحدة لو كان العالم بسيطًا؟ وكيف يمكن لآلة أن تجد وحدها ملايين المعاملات دون أن يدلّها أحد على الاتجاه؟

يُطلق على جواب السؤال الثاني اسم الانتشار الخلفي (backpropagation)، وكثيرًا ما يُعرض إما بوصفه أمرًا بديهيًا وإما بوصفه جدارًا من المعادلات. سنسلك هنا طريقًا ثالثًا: سنكتبه كاملًا في نحو ثلاثين سطرًا من NumPy، على مسألة يمكننا التحقق منها بالعين المجردة، ونراقب الخطأ وهو يتناقص خطوة بعد خطوة.

في نهاية هذا الدرس ستكون قد بنيت شبكة تتعلم دون أي مكتبة للتعلّم العميق، وستعرف بالضبط ما الذي ينجزه كل سطر.

العصبون: مجموع موزون وقرار

يستقبل العصبون الاصطناعي عدة أرقام مُدخلة، ويضرب كلًّا منها في وزن، ثم يجمع الناتج كله، ويضيف انحيازًا (bias)، ثم يمرر النتيجة عبر دالة. هذا كل ما في الأمر.

import numpy as np

def neurone(entrees, poids, biais):
    somme = float(np.dot(entrees, poids) + biais)
    return 1 if somme > 0 else 0     # التنشيط: عتبة بسيطة

# عصبون يحسب عملية AND المنطقية.
poids = np.array([1.0, 1.0])
biais = -1.5

for x in ([0, 0], [0, 1], [1, 0], [1, 1]):
    print(x, neurone(np.array(x, dtype=float), poids, biais))
# [0, 0] 0
# [0, 1] 0
# [1, 0] 0
# [1, 1] 1

يمكن التحقق من الحساب ذهنيًا: عندما يكون المدخلان كلاهما 1، يساوي المجموع 1 + 1 - 1,5 = 0,5، وهو موجب، فيكون الخرج 1. وعندما يكون مدخل واحد فقط 1، يساوي المجموع 1 - 1,5 = -0,5، وهو سالب، فيكون الخرج 0. يؤدي الانحياز بالضبط دور عتبة التشغيل: يجب تجاوز 1.5 لتنشيط العصبون.

هذه الآلية هي البيرسبترون (Perceptron) الذي اقترحه فرانك روزنبلات سنة 1957. هنا وُضعت الأوزان يدويًا؛ أما التعلّم فسيتمثل في جعل الآلة تعثر عليها بنفسها.

لماذا نكدّس الطبقات

يرسم العصبون الواحد حدًّا مستقيمًا في فضاء المدخلات. وما دامت الفئات قابلة للفصل بمستقيم فهذا يكفي. أما المشكلة فهي أن حالة بسيطة كحالة XOR (أو الحصرية) ليست كذلك.

      x2
       │
     1 │  ●(0,1)          ○(1,1)         ● = الفئة 1
       │                                 ○ = الفئة 0
       │
     0 │  ○(0,0)          ●(1,0)
       └──────────────────────────── x1
          0                1

   لا يمكن لأي مستقيم أن يضع النقطتين ● في جانب
   والنقطتين ○ في الجانب الآخر.

هذا ليس مجرد حدس، بل يمكن قياسه:

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([0, 1, 1, 0])            # XOR (أو الحصرية)

lineaire = LogisticRegression().fit(X, y)
print(lineaire.score(X, y), lineaire.predict(X))   # 0.5 [0 0 0 0]

reseau = MLPClassifier(hidden_layer_sizes=(4,), activation="tanh",
                       max_iter=5000, random_state=1).fit(X, y)
print(reseau.score(X, y), reseau.predict(X))       # 1.0 [0 1 1 0]

يحصل النموذج الخطي على 0.5، وهي نتيجة الصدفة في مسألة متوازنة، ويتنبأ بالفئة نفسها للنقاط الأربع: لم يستطع فعل شيء. أما الشبكة ذات الطبقة المخفية الواحدة المكوّنة من أربعة عصبونات فتحصل على 1.0.

الآلية هي التالية: الطبقة المخفية تحوّل فضاء المدخلات. في الفضاء الجديد الذي تنتجه تصبح النقاط الأربع قابلة للفصل بمستقيم، ولا يبقى على طبقة الخرج سوى رسم هذا المستقيم. تكديس الطبقات يعني تسلسل تغييرات في التمثيل إلى أن تصبح المسألة بسيطة.

لا تضم هذه الشبكة سوى 17 معاملًا: 2 × 4 من أوزان الدخل، و4 انحيازات مخفية، و4 × 1 من أوزان الخرج، و1 انحياز للخرج. وتطبّق الشبكات الحديثة المبدأ نفسه بعدة أضعاف من رتب الحجم.

دوال التنشيط

لعتبة البيرسبترون عيب قاتل بالنسبة إلى التعلّم: ميلها معدوم في كل مكان، عدا نقطة واحدة يكون فيها لانهائيًا. يستحيل استخلاص اتجاه للتصحيح منها. لذلك نستبدل بها دوال ملساء.

import numpy as np

z = np.array([-2.0, -0.5, 0.0, 0.5, 2.0])

print(np.maximum(0, z))                 # [0.  0.  0.  0.5 2. ]      ReLU
print((1 / (1 + np.exp(-z))).round(3))  # [0.119 0.378 0.5 0.622 0.881]  سيغمويد
print(np.tanh(z).round(3))              # [-0.964 -0.462 0. 0.462 0.964]  tanh
الدالةالخرجالاستخدام الشائعنقطة الانتباه
ReLU max(0, z)0 أو zالطبقات المخفية، افتراضيًاالعصبون العالق عند 0 لا يمكن تصحيحه بعد ذلك
سيغمويد (Sigmoid)بين 0 و1خرج التصنيف الثنائيميل شبه معدوم عند الأطراف
tanhبين −1 و1الطبقات المخفية، الشبكات الصغيرةالتشبع نفسه الذي تعاني منه سيغمويد

كثيرًا ما يُساء فهم دور هذه الدوال. فهي ليست موجودة من أجل «التطبيع»، بل من أجل إدخال اللاخطية. فبدونها، سيؤدي تكديس عشر طبقات من عمليات الضرب والجمع إلى… عملية ضرب وجمع واحدة. تركيب الدوال الخطية خطي، ولذلك فإن شبكة بلا دوال تنشيط، مهما بلغ عمقها، ستظل عاجزة أمام XOR عجز الانحدار اللوجستي أعلاه.

الانحدار التدرّجي: التصحيح في الاتجاه الصحيح

يبقى السؤال المحوري: كيف نجد الأوزان؟ الفكرة هي قياس الخطأ، وحساب الاتجاه الذي ينبغي أن يتحرك فيه كل معامل لتقليله، ثم اتخاذ خطوة صغيرة في هذا الاتجاه. ثم نعيد الكرّة.

لنعد إلى إيجارات الدرس السابق، ولكن دون LinearRegression: هذه المرة نكتب التعلّم بأنفسنا.

import numpy as np

x = np.array([30.0, 45.0, 60.0, 75.0, 90.0])
y = np.array([620.0, 780.0, 1010.0, 1150.0, 1400.0])

# تحجيم المدخلات: ضروري، وسنعرف السبب بعد قليل.
xn = (x - x.mean()) / x.std()

a, b = 0.0, 0.0
pas = 0.1

for etape in range(1, 201):
    prediction = a * xn + b
    erreur = prediction - y
    perte = (erreur ** 2).mean()          # متوسط مربعات الخطأ

    # مشتقات الخسارة بالنسبة إلى a وb.
    a -= pas * 2 * (erreur * xn).mean()
    b -= pas * 2 * erreur.mean()

    if etape in (1, 10, 50, 200):
        print(etape, round(perte, 1), round(a, 2), round(b, 2))

# 1   1059080.0  54.59  198.4
# 10  19587.4    243.64 885.48
# 50  518.0      272.94 991.99
# 200 518.0      272.94 992.0

تنخفض الخسارة من أكثر من مليون إلى 518 وتستقر عندها. ويُظهر تحققان أن النتيجة ليست تقريبية:

  • يتقارب الانحياز إلى 992.0، وهو بالضبط متوسط الإيجارات الخمسة: (620 + 780 + 1010 + 1150 + 1400) / 5 = 992.
  • يتقارب الميل إلى 272.94 في الفضاء المحجَّم. وبإعادته إلى المتر المربع نقسمه على الانحراف المعياري للمساحات، أي 21.213: 272,94 / 21,213 ≈ 12,86 يورو للمتر المربع. هذا هو المعامل الذي وجده LinearRegression في الدرس السابق، وقد حصلنا عليه هنا بالتجريب الموجَّه.

التحجيم ليس تجميليًا. فمع المساحات الخام والخطوة نفسها 0.1، تتباعد عملية الانحدار: تقفز الخسارة من 1.06 × 10⁶ إلى 6.9 × 10¹¹ منذ التكرار الثاني، ثم تنفجر. التدرّج متناسب مع سعة المدخلات؛ والمدخلات التي تدور حول 60 تنتج خطوات هائلة تتجاوز الحد الأدنى بدلًا من الاقتراب منه. وهذا هو السبب الأول لفشل تعلّم «لا يتقارب».

الخسارة
  │╲
  │ ╲        خطوة صغيرة جدًا: صحيح لكن بطيء
  │  ╲___
  │      ╲___
  │          ╲________
  └────────────────────► التكرارات

الخسارة
  │   ╱╲    ╱╲
  │  ╱  ╲  ╱  ╲   خطوة كبيرة جدًا: تتذبذب أو تنفجر
  │ ╱    ╲╱    ╲
  └────────────────────► التكرارات

الانتشار الخلفي، دون صيغ رياضية

في نموذج ذي معاملين، نكتب المشتقات بأيدينا. أما في الشبكة، فإن وزنًا في الطبقة الأولى يؤثر في الخرج عبر جميع الطبقات اللاحقة. الانتشار الخلفي هو طريقة تنظيم هذا الحساب: نمرّر الخطأ من الخرج نحو الدخل، طبقة بعد طبقة، مع إعادة استخدام ما سبق حسابه في كل خطوة.

إليك شبكة كاملة، بمدخلين وأربعة عصبونات مخفية وخرج واحد، مكتوبة دون أي مكتبة للتعلّم العميق.

import numpy as np

rng = np.random.default_rng(1)

X = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
y = np.array([[0.0], [1.0], [1.0], [0.0]])       # XOR (أو الحصرية)

# تهيئة عشوائية: وزنان متطابقان سيتلقيان دائمًا
# التصحيح نفسه ويظلان متطابقين إلى الأبد.
W1 = rng.normal(0, 1, (2, 4))
b1 = np.zeros((1, 4))
W2 = rng.normal(0, 1, (4, 1))
b2 = np.zeros((1, 1))

def sigmoide(z):
    return 1 / (1 + np.exp(-z))

for etape in range(1, 5001):
    # --- التمرير الأمامي: من الدخل إلى التنبؤ ---
    z1 = X @ W1 + b1
    a1 = np.tanh(z1)
    z2 = a1 @ W2 + b2
    a2 = sigmoide(z2)

    perte = float(np.mean((a2 - y) ** 2))

    # --- التمرير الخلفي: من الخطأ إلى الأوزان ---
    # d2 : حساسية الخسارة تجاه z2. العامل a2*(1-a2) هو
    # مشتقة سيغمويد؛ وهو يعبّر عن «مقدار تحرك الخرج
    # إذا تحرك z2 قليلًا».
    d2 = 2 * (a2 - y) / len(X) * a2 * (1 - a2)
    dW2 = a1.T @ d2
    db2 = d2.sum(axis=0, keepdims=True)

    # نصعد طبقة واحدة: يُعاد توزيع الخطأ على العصبونات
    # المخفية بما يتناسب مع وزنها W2، ثم يُضرب في
    # مشتقة tanh التي تساوي 1 - tanh².
    d1 = (d2 @ W2.T) * (1 - a1 ** 2)
    dW1 = X.T @ d1
    db1 = d1.sum(axis=0, keepdims=True)

    # --- التحديث ---
    for param, gradient in ((W1, dW1), (b1, db1), (W2, dW2), (b2, db2)):
        param -= 0.5 * gradient

    if etape in (1, 100, 1000, 5000):
        print(etape, round(perte, 4))

# 1     0.24
# 100   0.1182
# 1000  0.0018
# 5000  0.0003

print(a2.round(3).ravel())    # [0.007 0.983 0.983 0.021]

تنطلق الشبكة من خطأ قدره 0.24 وتنزل إلى 0.0003. أما الخرجات الأربعة النهائية، 0.007 / 0.983 / 0.983 / 0.021، فتُقارَن بالأهداف 0 / 1 / 1 / 0. وهكذا حُلّت مسألة XOR ببرنامج يتسع منطق التعلّم الكامل فيه لثمانية أسطر.

هناك ثلاث ملاحظات تستحق التذكر:

  • التمرير الخلفي مرآة للتمرير الأمامي. فالعملية X @ W1 في الذهاب تصبح X.T @ d1 في العودة. وهذا ليس مصادفة: إنه قاعدة الاشتقاق المتسلسل مطبّقة على ضرب المصفوفات.
  • لا تعرف أي طبقة ما الذي تكتشفه. لقد بنت الطبقة المخفية تمثيلًا مفيدًا للنقاط الأربع، لكن أحدًا لم يخبرها بأيّ تمثيل. هذا ما يسمى تعلّم التمثيلات، وهو الإسهام الرئيسي للتعلّم العميق.
  • لا شيء هنا يتوقف على عدد الطبقات. مع عشر طبقات، نكرر الكتلة نفسها عشر مرات. هذا الانتظام هو ما جعل الشبكات العميقة ممكنة، ولا تفعل مكتبات مثل PyTorch أو TensorFlow شيئًا سوى اشتقاق هذه التمريرات الخلفية تلقائيًا.

ما الذي جعل التعلّم العميق ممكنًا

شاعت الانتشار الخلفي سنة 1986 على يد روميلهارت وهينتون وويليامز. أما ازدهار التعلّم العميق فيعود إلى العقد الثاني من الألفية. وهذا التأخر البالغ خمسًا وعشرين سنة لا يعود إلى اكتشاف نظري ناقص، بل إلى ثلاثة شروط مادية لم تكن متوفرة.

الشرطالوضع في التسعينياتالوضع في العقد 2010
بيانات موسومةبضعة آلاف من الأمثلةImageNet: نحو 1.2 مليون صورة تدريب، و1000 فئة
الحوسبةمعالجات عامة الأغراضبطاقات رسومية متوازية على نطاق واسع
وصفات التدريبتشبع دوال سيغمويد، تهيئات عشوائية غير مدروسةReLU، تهيئات مناسبة، تطبيعات، تنظيم (regularization)

المضاعِف هائل. فكل تكرار في حلقتنا يعالج 4 أمثلة ويحدّث 17 معاملًا. أما تدريب شبكة رؤية شائعة فيعالج ملايين الصور ويحدّث عشرات الملايين من المعاملات، عشرات المرات. النظرية هي نفسها؛ لكن الحجم يتغير في طبيعته، ومعه الكلفة التي سنقيس عواقبها في الدرس الأخير.

الخلاصة

  • العصبون مجموع موزون تتبعه دالة. يعمل الانحياز عمل العتبة، ويمكن التحقق من الحساب يدويًا على AND المنطقية.
  • الطبقة الواحدة لا ترسم سوى حدٍّ مستقيم: فهي تفشل في XOR بنتيجة 0.5. وتكديس الطبقات يخدم تحويل الفضاء إلى أن تصبح المسألة قابلة للفصل خطيًا.
  • توجد دوال التنشيط لإدخال اللاخطية. وبدونها تكافئ شبكة من عشر طبقات طبقةً واحدة، مهما كان حجمها.
  • يقيس الانحدار التدرّجي الخطأ، ويحسب اتجاه التصحيح، ويتخذ خطوة صغيرة. والخطوة وسلّم المدخلات حاسمان: فالبيانات نفسها بلا تحجيم تجعل الحلقة نفسها تتباعد.
  • الانتشار الخلفي ليس سوى اشتقاق متسلسل منظَّم من الخرج نحو الدخل. يتسع لبضعة أسطر، ويبقى هو نفسه مهما كان عدد الطبقات، وهذا ما يجعله قابلًا للتصنيع على نطاق واسع.

التعليقات

التعليقات مدعومة عبر GitHub Discussions

سجّل الدخول باستخدام GitHub للمشاركة في النقاش

تم نسخ الرابط!