الترتيب بيفرق
أغلب الـ models اللي بنيناها لحد دلوقتي بتعامل كل input على إنه vector مستقل حجمه ثابت. بندخّل صورة على CNN فترجّع prediction — من غير أي معرفة بالصور اللي قبلها. بس فيه نوع واسع ومهم من البيانات طبيعته تسلسلية: معنى الكلمة بيعتمد على اللي قبلها، وقيمة قراية الـ sensor بتعتمد على القرايات اللي فاتت، والنوتة الموسيقية مبتتفهمش غير في سياق النوتات اللي جت قبلها.
البيانات التسلسلية (sequential data) فيها اللغة الطبيعية، والـ time series (أسعار الأسهم، إشارات EEG، الطقس)، والإشارات الصوتية، والفيديو (frames مترتّبة زمنيًا)، وsequences الـ DNA، وlogs الأحداث. والسمة الأساسية إن المكان مهم — لو لخبطت العناصر المعنى بيبوظ. والـ models اللي بتعالج sequences لازم تمسك العلاقات اللي بتمتد على مسافات عشوائية في الزمن أو المكان.
ليه الـ feedforward networks مبتقدرش تعالج الـ sequences؟
الـ fully connected network بتاخد input vector حجمه ثابت وتطلّع output حجمه ثابت. وعشان تعالج sequence طولها T، أبسط طريقة إنك تدمج كل الـ T inputs في vector واحد كبير وتعدّيه على feedforward network. وده بيفشل لسببين: الأول، الـ sequences أطوالها مختلفة — الجمل مش فيها نفس عدد الكلمات، والـ time series ليها مدد مختلفة. والـ input اللي حجمه ثابت مبيستوعبش sequences أطوالها متغيّرة من غير قص بيضيّع معلومات أو padding.
والتاني، والأهم في الأساس، الـ feedforward network اللي بتشوف الـ input المتسلسل مرة واحدة مبتقدرش تشارك الـ parameters على الـ time steps. الـ weights اللي على المكان 1 مستقلة تمامًا عن اللي على المكان 5. ومعنى ده إن الشبكة لازم تتعلّم نفس الفكرة من جديد (زي "الـ pattern اللغوي ده معناه نفي") في كل مكان لوحده، وده بيبدّد القدرة ومبيعمّمش على أماكن مشافهاش في التدريب. ومشاركة الـ parameters على الزمن هي الفكرة الأساسية ورا الـ recurrent networks (الشبكات التكرارية).
حل بديل مشهور إنك تستخدم sliding window (نافذة منزلقة): تاخد آخر k time steps كـ input لـ feedforward network. وده بيتعامل مع الـ sequences اللي أطوالها متغيّرة (بإنك تحرّك الشباك) وبيفرض نوع من مشاركة الـ parameters (نفس الشبكة بتعالج كل شباك). بس بيثبّت طول السياق k — أي علاقة بتمتد أكتر من k خطوة بتبقى مش ظاهرة. وفهم اللغة كتير بيحتاج سياق راجع مئات الأماكن. وكمان الشبابيك بتهمل المعلومات عن المكان بالظبط لكل خطوة بالنسبة لبداية الـ sequence.
الـ recurrent connections: شيل hidden state
الحل إنك تدّي الشبكة hidden state (حالة مخفية) ht بتشتغل كـ memory. في كل time step t، الشبكة بتقرا الـ input الحالي xt، وتجمعه مع الـ hidden state اللي فاتت ht−1، وتطلّع hidden state جديدة ht. والـ hidden state دي ملخّص مضغوط لكل اللي الشبكة شافته من المكان 1 لحد المكان t. ولأن نفس matrices الـ weights بتتستخدم في كل time step، الشبكة بتشارك الـ parameters على كل الأماكن أوتوماتيك.
والـ output في كل خطوة بيتحسب من الـ hidden state: yt = Wy ht + by. وعلى حسب المهمة، الشبكة ممكن تطلّع output في كل خطوة (الترجمة التسلسلية)، أو في آخر خطوة بس (text classification)، أو في خطوات مختارة (named entity recognition).
وبالشكل، الـ recurrent network شبكة فيها loop: الـ hidden state بتغذّي نفسها في الشبكة في الخطوة اللي بعدها. ولما نفك الـ loop دي — نرسم كل time step كنسخة منفصلة من الشبكة متوصّلين في سلسلة — بنشوف شبكة عميقة جدًا: عمق في بُعد الزمن بدل عمق الـ layers. والفك ده بيوضّح إزاي الـ gradients لازم تعدّي لورا على كل الـ T خطوات عشان تدرّب الشبكة على العلاقات اللي مداها طويل.
الـ unfolding على الزمن
الـ unfolding (التكشّف) بيعيد كتابة الـ recurrence كـ computational graph مفيهوش دوائر. كل time step t بياخد نسخته من الـ hidden state ht، والـ input xt، ويطلّع الـ output بتاعه yt. وmatrices الـ weights Wh وWx وWy مشتركة (مربوطة) على كل النسخ — وده اللي بيطبّق مشاركة الـ parameters.
وأول ما تعمل unfolding، الـ backpropagation العادي بينطبق. والـ loss الكلية هي مجموع losses كل خطوة: L = Σt Lt. وgradients الـ L بالنسبة للـ weights المشتركة بتتراكم من كل time step. والـ algorithm ده اسمه الـ backpropagation through time (الانتشار الخلفي عبر الزمن) (BPTT).
وعمليًا، الـ BPTT الكامل على sequences طويلة جدًا مكلّف حسابيًا ومش مستقر عدديًا. والـ truncated BPTT (المبتور) بيحدّ انتشار الـ gradient لعدد ثابت من الخطوات لورا (مثلًا 20–100 خطوة)، بإنه يقسم الـ sequence الطويلة لحتت. والـ gradients مبتعدّيش حدود الحتت، وده بيقيّد القدرة على تعلّم العلاقات البعيدة جدًا بس بيخلّي التدريب ممكن.
مشكلة الـ vanishing gradient
الـ gradient اللي راجع لورا على k time step فيه حاصل ضرب k من Jacobian matrices مربوطة بـ matrix الـ recurrent weights Wh. ولما الـ spectral radius (نصف القطر الطيفي) بتاع Wh (أكبر singular value ليها) يبقى أقل من 1، الحاصل ده بيصغر أُسّيًا مع k. ولما يبقى أكبر من 1، بينفجر أُسّيًا. والحالتين بيدمّروا تعلّم العلاقات البعيدة.
الـ vanishing gradients (التدرجات المتلاشية) معناها إن إشارة الـ gradient من loss في الخطوة T بالكاد بتوصل للخطوة T−50. الشبكة فعليًا بتنسى الماضي البعيد — مبتقدرش تتعلّم إن كلمة في المكان 1 من الجملة بتحدّد معنى العبارة في المكان 50. ودي مش مسألة دقة عددية؛ دي مشكلة في بنية الـ simple RNN نفسها.
لما الـ spectral radius بتاع matrix الـ recurrent weights يبقى أكبر من 1، الـ gradients بتكبر أُسّيًا مع طول الـ sequence، وده بيعمل تحديثات ضخمة للـ parameters — مشكلة "الـ exploding gradients". وعلى عكس الـ vanishing gradients، دي ممكن تتكشف (losses بـ NaN، وقيم parameters بتجري لما لا نهاية) وليها حل بسيط: الـ gradient clipping (قطع التدرج) بيحدّد طول الـ gradient قبل خطوة التحديث. لو ‖∇L‖ عدّى threshold θ، بنعيد قياسه: ∇L ← θ · ∇L / ‖∇L‖. والـ gradient clipping تقريبًا موجود في كل تدريب للـ recurrent networks.
مشكلة الـ vanishing gradient اتشخّصت على إيد Bengio et al. (1994) وHochreiter (1991)، وهي اللي زقّت لتطوير شبكات الـ long short-term memory (الذاكرة طويلة-قصيرة الأمد) (LSTM) اللي بتحلّها بآلية gates. وقبل ما الـ LSTM تسيطر، الناس كانت بتتحايل على المشكلة بـ initialization حذرة للـ weights (matrices متعامدة أو identity لـ Wh)، والـ gradient clipping، وأطوال sequences قصيرة جدًا. ودي لسه حيل مفيدة حتى مع الـ architectures الحديثة.
الـ simple RNNs تقدر تعمل modeling لإيه؟
مع قيد الـ vanishing gradient، الـ simple RNNs تقدر تتعلّم العلاقات القصيرة المدى بكفاءة. مهام عملية كتير مش محتاجة memory بتمتد مئات الخطوات: تصنيف مشاعر جملة قصيرة، والتنبؤ بالـ frame الجاي في فيديو، وتوقّع الـ time series على المدى القصير. وفي المهام دي، الـ vanilla RNN المتظبطة كويس لسه بتنافس.
والـ hidden state بتاعة RNN متدرّبة بتطوّر بنية ينفع تتفسّر. في الـ language models على مستوى الحروف، hidden units فردية بتتعلّم تتابع حاجات زي "دلوقتي جوه اقتباس"، أو "عمق المسافة البادئة في الـ code"، أو "عدد الأقواس المفتوحة". والـ memory اللي بتطلع لوحدها دي — من غير برمجة صريحة — بتكشف إن قاعدة التحديث التكرارية البسيطة تقدر تطلّع بنية تسلسلية غنية بشكل يدهش من البيانات.
الـ architecture اللي اتوصفت هنا — اللي الـ hidden state فيها بتغذّي الخطوة المخفية اللي بعدها — اسمها Elman network (Elman, 1990). والنوع القريب منها، Jordan network، بيغذّي الـ output yt بدل ht. والـ Elman networks مشهورة أكتر بكتير في الممارسة الحديثة لأن الـ hidden state تمثيل أغنى من الـ output، ولأنها أسهل في التدريب للمهام اللي فضاء الـ output فيها صغير (زي الـ binary classification في آخر خطوة).
architectures الـ sequence-to-sequence
مهام مختلفة محتاجة أشكال input–output مختلفة. الـ RNN one-to-many بتاخد input vector واحد (زي صورة) وتطلّع sequence outputs (زي وصف) — بتتستخدم في وصف الصور. والـ many-to-one بتقرا sequence وتطلّع output واحد (زي تصنيف المشاعر) — آخر hidden state بتشفّر الـ sequence كلها. والـ many-to-many بتطلّع output في كل خطوة — بتتستخدم في الـ language modeling والـ tagging والترجمة الفورية.
وللمهام اللي طول الـ input فيها مختلف عن طول الـ output (الترجمة، التلخيص)، architecture الـ encoder–decoder هي المعيار. الـ RNN الـ encoder بتقرا الـ sequence كلها وتضغطها في context vector حجمه ثابت (آخر hidden state). وبعدين الـ decoder بيولّد الـ sequence الخارجة بشرط الـ vector ده. ونقطة الخنقة إنك لازم تضغط معنى الـ input كله في vector واحد — قيد بتحلّه آلية الـ attention (الانتباه) (اللي هتتقدّم في الدرس 9.1) بإنها تخلّي الـ decoder يسأل كل hidden states الـ encoder على طول.
- البيانات التسلسلية (النص، الـ time series، الصوت، الفيديو) محتاجة models بتحترم الترتيب وبتمسك العلاقات البعيدة. والـ feedforward networks بتفشل لأنها محتاجة inputs حجمها ثابت ومبتقدرش تشارك الـ parameters على الـ time steps.
- الـ recurrent networks بتحتفظ بـ hidden state ht بتتحدّث في كل خطوة: ht = tanh(Whht−1 + Wxxt + b). ونفس matrices الـ weights مشتركة على كل الـ time steps عشان تمسك نفس الـ patterns مهما كان المكان.
- الـ unfolding بتاع الـ RNN بيحوّل الـ recurrence لـ graph مفيهوش دوائر فبيسمح بالـ backpropagation العادي. والـ BPTT بيراكم الـ gradients من كل الخطوات؛ والـ truncated BPTT بيحدّ ده لعدد ثابت من الخطوات عشان الكفاءة.
- الـ vanishing gradients بتطلع لأن حواصل ضرب الـ gradient على sequences طويلة بتصغر أُسّيًا لما الـ spectral radius بتاع Wh < 1. والـ exploding gradients (spectral radius > 1) بتتعالج بالـ gradient clipping. والـ vanishing gradients بتستدعي architectures الـ LSTM والـ GRU اللي في الدرس الجاي.
- مهام مختلفة بتستخدم أشكال recurrent مختلفة: one-to-many (وصف الصور)، many-to-one (الـ classification)، many-to-many (الـ tagging والـ language modeling). ومهام الـ sequence-to-sequence بتستخدم architecture encoder–decoder.