قراية
وضع الحكايات

الـ backpropagation (الانتشار الخلفي)

~22 دقيقة قراية الدرس 3 من 3 في الوحدة 6

مشكلة توزيع المسؤولية

عندك شبكة فيها ملايين الـ weights. بتدّيها input، وتحسب الـ prediction، وتقيس الغلط. وبعدين ييجي السؤال الصعب: أنهي weights مسؤولة عن الغلط، وكل واحد المفروض يتغيّر قد إيه؟ ودي الـ credit assignment problem (مشكلة إسناد الفضل) — التحدي الأساسي في تدريب الشبكات العميقة.

بالنسبة للـ output layer، الإجابة بسيطة نسبيًا: الغلط بيعتمد على طول على weights الـ output. بس weight في أول hidden layer بيأثّر على الـ output بشكل غير مباشر بس، عن طريق كل layer بعده. وإنك توزّع اللوم على layers كتير، بدقة وكفاءة، هو اللي الـ backpropagation (الانتشار الخلفي) بيحققه.

الـ backpropagation مش learning algorithm في حد ذاته — هو طريقة كفءة لحساب الـ gradients. ولما يتجوّز الـ gradient descent (النزول التدرجي) (أو أنواعه)، بيقول لكل weight بالظبط يتغيّر إزاي عشان الـ loss تقل. الـ algorithm كان معروف في الستينات والسبعينات، بس اشتهر بعد ما روملهارت وهينتون وويليامز نشروه على الـ neural networks سنة 1986، ولسه هو المحرك ورا تقريبًا كل نظام deep learning لحد النهارده.

الـ chain rule: أساس الـ backpropagation

الـ backpropagation ببساطة هو الـ chain rule (قاعدة السلسلة) في التفاضل متطبّقة بشكل منظّم وكفء على تركيب متداخل من الـ functions. والـ neural network هي بالظبط تركيب زي ده: الـ loss function في الـ output، والـ output function في آخر layer، وهكذا لحد الـ input.

الـ chain rule
\frac{\partial L}{\partial z} = \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial z} = \frac{\partial L}{\partial a} \cdot f'(z)
لو الـ loss L بتعتمد على z عن طريق متغير في النص a = f(z)، يبقى gradient الـ L بالنسبة لـ z بيساوي gradient الـ L بالنسبة لـ a مضروب في مشتقة a بالنسبة لـ z. والـ chain rule بتتطبّق مرة ورا مرة: كل layer بتضرب الـ gradient اللي جاي في المشتقة المحلية بتاعتها وتعدّيه لورا.

الفكرة الأساسية إن الـ gradients ممكن تتحسب بإننا نعدّي المعلومات لورا على نفس graph الشبكة اللي اتستخدم في الـ forward pass. كل node محتاجة حتتين معلومات بس: المشتقة المحلية بتاعتها (اتحسبت في الـ forward pass واتخزّنت)، والـ gradient اللي جاي من الـ layer اللي فوقها. اضرب الاتنين في بعض وعدّي النتيجة لورا. وده أنيق ورخيص حسابيًا في نفس الوقت.

تمريرتين: أمامي وخلفي

تدريب neural network بالـ backpropagation فيه تمريرتين ورا بعض على الشبكة لكل batch بيانات.

الـ forward pass (التمرير الأمامي) بيحسب الـ prediction. من أول الـ input، بيطبّق تحويل كل layer بالترتيب، ويطلّع activations في كل layer. والأهم، لازم كل قيمة في النص تتخزّن — كل pre-activation z[l] وكل activation a[l] — في الـ memory. والقيم المتخزّنة دي هتتستخدم في الـ backward pass عشان تحسب المشتقات المحلية.

الـ backward pass (التمرير الخلفي) بيحسب الـ gradients. من أول الـ loss عند الـ output، الـ gradient بيتنشر لورا layer ورا layer، لحد أول hidden layer. وفي كل خطوة، الـ gradient اللي جاي بيتستخدم عشان يحسب gradient الـ weights والـ biases بتوع الـ layer دي، وبعدين gradient inputs الـ layer اللي بيبقى "الـ gradient اللي جاي" للـ layer اللي قبلها.

تكلفة الـ memory

إن لازم تخزّن كل الـ activations اللي في النص أثناء الـ forward pass هو السبب إن الشبكات العميقة بتاكل memory كتير. لـ batch من 256 صورة على شبكة من 50 layer، الـ activations المتخزّنة ممكن تاكل gigabytes من memory الـ GPU. والـ gradient checkpointing — إنك تعيد حساب شوية activations أثناء الـ backward pass بدل ما تخزّنها كلها — بيبدّل حساب بـ memory لما تحتاج.

معادلات الـ backpropagation

خلّي L هي الـ loss، والـ superscript [l] يدل على الـ layer رقم l، وL هي العدد الكلي للـ layers. الـ backward pass بيحسب error signal (إشارة خطأ) δ[l] لكل layer، وهي المشتقة الجزئية للـ loss بالنسبة للـ pre-activation z[l]. والـ deltas دي بتتستخدم بعد كده عشان تحسب gradients الـ weights.

الـ delta بتاعة الـ output layer
\boldsymbol{\delta}^{[L]} = \nabla_{\mathbf{a}^{[L]}} L \odot f'\!\left(\mathbf{z}^{[L]}\right)
الـ error signal عند الـ output layer هي الضرب عنصر بعنصر بين gradient الـ loss بالنسبة لـ activations الـ output ومشتقة activation function الـ output. ومع الـ cross-entropy loss وoutput بـ softmax، ده بيتبسّط بشكل حلو لـ δ[L] = â − y، وâ هو vector الاحتمالات المتوقع وy هو vector الـ label الـ one-hot.
الـ delta بتاعة الـ hidden layer
\boldsymbol{\delta}^{[l]} = \left(W^{[l+1]\top} \boldsymbol{\delta}^{[l+1]}\right) \odot f'\!\left(\mathbf{z}^{[l]}\right)
الـ error signal عند hidden layer بتتحسب بإنك تعمل projection لـ delta الـ layer اللي بعدها لورا عن طريق matrix الـ weights (الـ transpose)، وبعدين تضرب عنصر بعنصر في مشتقة الـ activation function عند الـ pre-activation المتخزّن. ودي الـ recursion الأساسية: delta كل layer بتعتمد على delta الـ layer اللي فوقها.
gradients الـ weights والـ biases
\frac{\partial L}{\partial W^{[l]}} = \boldsymbol{\delta}^{[l]} \mathbf{a}^{[l-1]\top} \qquad \frac{\partial L}{\partial \mathbf{b}^{[l]}} = \boldsymbol{\delta}^{[l]}
gradient الـ loss بالنسبة لـ matrix الـ weights W[l] هو الـ outer product بين delta الـ layer وactivations الـ layer اللي قبلها (الـ transpose). وgradient الـ bias هو الـ delta نفسها ببساطة. والـ gradients دي بيستخدمها الـ optimizer بعد كده — SGD أو Adam أو غيرهم — عشان يحدّث الـ weights.

الـ vanishing والـ exploding gradients

الـ backward pass بيضرب الـ gradients في بعض وهو ماشي على الـ layers. وفي شبكة عميقة، حاصل ضرب حدود كتير ده ممكن يبقى صغير بشكل كارثي أو كبير بشكل كارثي.

الـ vanishing gradients (التدرجات المتلاشية) بتحصل لما كل layer تضرب الـ gradient في رقم أصغر من 1. والسبب الكلاسيكي هو الـ sigmoid activation: مشتقته أقصاها 0.25 (عند z = 0) وبتقرّب من الصفر لما |z| يكبر. وبعد 20 layer من الضرب في 0.25، الـ gradient بيوصل لأول layer بمقدار أقل من 10−12. والـ layers الأولى بتاخد إشارة تدريب تقريبًا معدومة والـ weights بتاعتها بالكاد بتتغيّر — والشبكة بتفشل تتعلّم representations هرمية.

الـ exploding gradients (التدرجات المتفجرة) هي العكس: كل layer بتضرب الـ gradient في رقم أكبر من 1 فبيكبر أُسّيًا. وبعد 20 layer من الضرب في 2، الـ gradient بيعدّي المليون. وتحديثات الـ weights بتبقى ضخمة جدًا، فبتهز الـ optimization وغالبًا بتطلّع قيم NaN.

الحلول العملية

الـ vanishing gradients: استخدم activations الـ ReLU (الـ gradient بيساوي 1 للـ inputs الموجبة)، وزوّد skip connections (الـ residual networks)، واستخدم الـ batch normalization عشان الـ activations تفضل في نطاقات كويسة، أو استخدم architectures زي الـ LSTMs للـ sequences اللي فيها gates بتتعلّم وبتتحكم في مرور الـ gradient.

الـ exploding gradients: طبّق الـ gradient clipping (قص التدرج) — لو طول الـ gradient عدّى حد معيّن (عادةً 1.0 أو 5.0)، صغّر vector الـ gradient كله لحد ما طوله يساوي الحد. والحيلة البسيطة دي بتثبّت التدريب في الـ recurrent networks والـ transformers. وكمان الـ initialization الكويسة للـ weights (Xavier/He) بتمنع الـ gradients إنها تنفجر من أول التدريب.

الـ weight initialization

القيم الأولانية للـ weights مهمة جدًا. لو كل الـ weights بدأت بصفر، كل الـ neurons في الـ layer بتحسب نفس الـ output وبتاخد نفس الـ gradient — بيبقوا متماثلين وعمرهم ما هيختلفوا. ودي مشكلة الـ symmetry breaking (كسر التماثل): الـ initialization العشوائية بتكسر التماثل، فالـ neurons المختلفة تقدر تتعلّم features مختلفة.

بس العشوائية لوحدها مش كفاية — الحجم مهم. لو الـ weights كبيرة أوي، الـ activations بتتشبّع؛ ولو صغيرة أوي، الـ activations والـ gradients بينكمشوا للصفر. وفيه طريقتين initialization مبنيين على مبدأ مسيطرين في الممارسة:

الـ initialization بتاع Xavier وHe
W \sim \mathcal{N}\!\left(0,\, \frac{1}{n_{\text{in}}}\right) \text{ (Xavier)} \qquad W \sim \mathcal{N}\!\left(0,\, \frac{2}{n_{\text{in}}}\right) \text{ (He)}
الـ Xavier (Glorot) initialization بتعمل scaling للـ weights بـ 1/√nin، عشان تثبّت variance الـ activation في الـ forward pass لشبكات الـ sigmoid/tanh. والـ He initialization بتعمل scaling بـ √(2/nin)، فبتصلّح حقيقة إن الـ ReLU بتصفّر نص الـ inputs بتاعتها في المتوسط. وإنك تستخدم الـ initialization الغلط مع الـ activation الغلط مصدر مشهور لعدم استقرار التدريب.

نجمّع الحتت: لفّة التدريب

بالـ forward pass والـ backward pass وقاعدة تحديث الـ weights، الـ training algorithm الكامل لـ mini-batch بيبقى كده:

لفّة تدريب الـ mini-batch

1. اسحب mini-batch من B مثال من الـ training set.

2. الـ forward pass: احسب الـ predictions وخزّن كل الـ activations والـ pre-activations اللي في النص.

3. احسب الـ loss: قيّم الـ loss function على predictions الـ batch قصاد الـ labels الحقيقية.

4. الـ backward pass: احسب δ[L]، وبعدين انشر لورا عشان تحسب δ[l] لكل layer و∇W[l]، ∇b[l] لكل matrix weights وbias.

5. حدّث الـ weights: طبّق الـ optimizer (مثلًا، W ← W − η∇W). وعيد من الخطوة 1.

كل لفّة كاملة على الـ training set اسمها epoch (حقبة). والشبكات الحديثة عادةً محتاجة من عشرات لمئات الـ epochs. وحجم الـ mini-batch B هو hyperparameter — الـ batches الأكبر بتدّي تقديرات gradient أثبت بس محتاجة memory أكتر؛ والـ batches الأصغر بتدخّل noise ممكن فعلًا تساعد تطلع من الـ local minima.

أهم النقط
  • الـ backpropagation بيحل الـ credit assignment problem بإنه يحسب gradient الـ loss بالنسبة لكل weight في الشبكة بكفاءة ودقة.
  • هو الـ chain rule في التفاضل متطبّقة مرة ورا مرة: كل layer بتضرب الـ gradient اللي جاي في المشتقة المحلية بتاعتها وتعدّي النتيجة لورا.
  • الـ forward pass بيحسب ويخزّن كل الـ activations اللي في النص؛ والـ backward pass بيستخدم القيم المتخزّنة دي عشان يحسب الـ gradients layer ورا layer بالترتيب العكسي.
  • delta الـ output layer هي ∇aL ⊙ f′(z[L])؛ ومع softmax + cross-entropy بتتبسّط لـ â − y.
  • deltas الـ hidden layers بتتحسب بإنك تعمل projection لـ delta الـ layer اللي بعدها عن طريق matrix الـ weights الـ transpose، وبعدين تضرب في مشتقة الـ activation المحلية.
  • الـ vanishing gradients (بسبب activations بتتشبّع في الشبكات العميقة) بتتعالج بالـ ReLU والـ skip connections والـ batch normalization. والـ exploding gradients بيتحكم فيها الـ gradient clipping.
  • الـ weight initialization مهمة: Xavier للـ sigmoid/tanh، وHe للـ ReLU. الـ initialization العشوائية بتكسر التماثل؛ والـ scaling المناسب بيخلّي الـ gradients متصرّفة كويس من الأول.
اللي فات الـ neural networks متعددة الـ layers (الشبكات العصبية متعددة الطبقات) نظرة عامة اللي بعده تدريب الـ neural networks (الشبكات العصبية)