انتقل إلى المحتوى الرئيسي

الوحدة 7 — الشبكات ثنائية الاتّجاه والمتراكبة

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

الشبكة ثنائية الاتّجاه: قراءة مستقبل الكلمة

على متتالية كاملة معروفة سلفًا — جملة يجب تصنيفها، مقال يجب تلخيصه، ترجمة عن جملة كاملة — لا يوجد سبب لقصر النموذج على قراءة الماضي. الشبكة ثنائية الاتّجاه تُطلق شبكتَين تكراريّتَين متوازيَتَين: واحدة تقرأ من اليسار إلى اليمين وتُنتج ht\overrightarrow{h}_t، وأخرى تقرأ من اليمين إلى اليسار وتُنتج ht\overleftarrow{h}_t. الحالة النهائية عند كلّ خطوة هي دمج الاثنتَين:

ht=[ht,ht]h_t = \left[\overrightarrow{h}_t, \overleftarrow{h}_t\right]

بعد الدمج، بُعد المُخرَج يصير 2H2H بدل HH. في PyTorch الوسيط اسمه bidirectional=True.

import torch.nn as nn

lstm_bi = nn.LSTM(input_size=64, hidden_size=32,
bidirectional=True, batch_first=True)

X = torch.randn(16, 50, 64)
sorties, (h_T, c_T) = lstm_bi(X)

print(sorties.shape) # torch.Size([16, 50, 64]) : 2 * 32
print(h_T.shape) # torch.Size([2, 16, 32]) : اتجاهان

على مهام مثل توسيم أدوار الجملة أو تصنيف المشاعر، مكسب الشبكة ثنائية الاتّجاه ملموس: كلمة «حسنًا» في «أدّى المهمّة حسنًا، لكنّه غاب طيلة الأسبوع» تتغيّر دلالتها بالكامل حسب ما يليها.

القيد الذي يُلغيها: التنبّؤ

هنا يجب التوقّف. في التنبّؤ بالسلاسل الزمنية، الشبكة ثنائية الاتّجاه ممنوعة، وذلك ببساطة لأنّ المستقبل ليس متاحًا. حين تُدرِّب شبكة ثنائية الاتّجاه على استهلاك الكهرباء الأسبوعي للتنبّؤ بالساعة القادمة، سترى نتائج مذهلة على مجموعة التحقّق — والسبب أنّ الشبكة تستعمل الساعات التالية في مُدخَلها لتتنبّأ بالساعة الحالية. عند النشر، حيث لا تُعرف تلك الساعات، النموذج لا يستطيع حتّى العمل.

ثلاث حالات تُتيح ثنائية الاتّجاه بأمان في الفيل الأحمر أو ما شابهه:

  • تصنيف بعديّ: هل هذا الأسبوع طبيعيّ أم شاذّ؟ نعم، السؤال يُطرح بعد انقضاء الأسبوع
  • رصد الحالة الشاذّة على تسجيل مُخزَّن: نعم، التسجيل كامل عند التحليل
  • تقدير قيمة ناقصة في وسط سلسلة معروفة: نعم، السياقان الأيسر والأيمن متاحان

بينما التنبّؤ بالمستقبل، حتى بخطوة واحدة، يستبعد الشبكة ثنائية الاتّجاه.

اختبار بسيط قبل استعمال ثنائي الاتّجاه

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

تكديس الطبقات

كثيرًا ما تُكدَّس عدّة طبقات LSTM أو GRU. الطبقة الثانية تستقبل hth_t للطبقة الأولى بوصفها مُدخَلاتها، فتبني تمثيلًا أكثر تجريدًا. المبدأ نفسه في الشبكات الكثيفة العميقة: طبقات دنيا تلتقط أنماطًا بسيطة، وطبقات عليا تركّبها.

lstm = nn.LSTM(input_size=2, hidden_size=64, num_layers=3,
batch_first=True, dropout=0.3)

الوسيط num_layers=3 يبني ثلاث طبقات، والوسيط dropout=0.3 يُطبِّق إسقاطًا بين الطبقات (لا بين الخطوات الزمنية). أوّل نصيحة تجريبية: ابدأ بطبقة واحدة، ولا تُضِف طبقة ثانية إلّا إن أظهر التشخيص (الوحدة 4) أنّ السعة عائق. طبقتان معيار جيّد على معظم مهام السلاسل الزمنية؛ ثلاث طبقات ممكنة على النصوص الطويلة.

كلفة الحساب تنمو خطّيًا بعدد الطبقات، وذاكرة التدريب أيضًا (كلّ طبقة تحفظ حالاتها الوسيطة). ثلاث طبقات LSTM بـH=128H = 128 على متتاليات T=500T = 500 ودفعة N=32N = 32 تُشغّل بضع مئات من الميغابايت على المعالج الرسومي، غير مُقلقة لكنّها تفرض الانتباه.

الإسقاط المتكرّر: dropout بين الخطوات

الإسقاط بين طبقتين معروف. لكنّ الإسقاط بين خطوة زمنية وأخرى أدقّ. تُبيّن أبحاث Gal وGhahramani (2016) أنّ الإسقاط العشوائي عند كلّ خطوة يُتلف الحالة المخفية بلا استقرار. الحلّ الصحيح: إسقاط بنفس القناع عبر الخطوات كلّها، أي نُلغي عصبونات بعينها طيلة المتتالية، ثم نُعيد اختيار عصبونات جديدة للدفعة التالية.

في PyTorch لا تُوجد هذه الوظيفة داخل nn.LSTM نفسها؛ ينبغي كتابتها يدويًا أو استعمال nn.LSTMCell مع قناع ثابت:

class LSTMAvecDropoutMemoire(nn.Module):
def __init__(self, n_vars, hidden, p=0.3):
super().__init__()
self.cellule = nn.LSTMCell(n_vars, hidden)
self.p, self.hidden = p, hidden

def forward(self, x): # x: (N, T, F)
N, T, _ = x.shape
h = torch.zeros(N, self.hidden, device=x.device)
c = torch.zeros_like(h)
# قناع واحد يبقى ثابتا طوال المتتالية
masque = (torch.rand(N, self.hidden, device=x.device) > self.p).float()

for t in range(T):
h, c = self.cellule(x[:, t, :], (h, c))
if self.training:
h = h * masque / (1 - self.p) # إسقاط معكوس

return h

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

أمثلة قرار

المهمّةمعمارية موصى بها
تنبّؤ بالساعة الكهربائية القادمةGRU طبقة واحدة، بلا ثنائية اتّجاه
تصنيف مشاعر جملة قصيرةLSTM ثنائية الاتّجاه طبقة واحدة
توسيم أنواع الكلمات في نصّ كاملLSTM ثنائية الاتّجاه طبقتان
رصد شذوذ على تسجيل مُخزَّنGRU ثنائية الاتّجاه طبقتان
ترجمة تلقائيةمرمِّز ثنائي الاتّجاه، فاكّ ترميز أحادي الاتّجاه (الوحدة 8)
توقّع لتسعير جهاز حيّ (streaming)GRU أحادي الاتّجاه، طبقة أو طبقتان

في الخلاصة

  • الشبكة ثنائية الاتّجاه تقرأ بالاتّجاهين ودمج الحالتين يعطي مُخرَجًا بُعده 2H2H؛ مكسبها ملموس على تصنيف الجملة الكاملة وتوسيمها.
  • التنبّؤ بالمستقبل يمنعها: استعمالها في هذه الحالة تسرّب من المستقبل يُنتج نتائج ممتازة في التحقّق ومستحيلة في الإنتاج.
  • تكديس الطبقات يمنح مزيدًا من التجريد؛ ابدأ بطبقة، أضف طبقة ثانية عند الحاجة، وحدّد الحدّ عند ثلاث طبقات في العادة.
  • الإسقاط المتكرّر يستعمل قناعًا ثابتًا عبر الخطوات، لا عشوائيًا؛ في PyTorch يُكتب يدويًا أو باستعمال nn.LSTMCell، بينما dropout= في nn.LSTM يسقط بين الطبقات فقط.

الوحدة التالية: كيف نجعل شبكة تكرارية تُترجم جملة إلى أخرى بمعمارية المرمّز وفاكّ الترميز، وأين تكمن حدودها التي فتحت باب الانتباه.