مشكلة توزيع المسؤولية
عندك شبكة فيها ملايين الـ 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.
الفكرة الأساسية إن الـ 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 اللي قبلها.
إن لازم تخزّن كل الـ 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.
الـ 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 مبنيين على مبدأ مسيطرين في الممارسة:
نجمّع الحتت: لفّة التدريب
بالـ forward pass والـ backward pass وقاعدة تحديث الـ weights، الـ training algorithm الكامل لـ 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 متصرّفة كويس من الأول.