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

الوحدة 1 — ما يميّز المتتالية عن جدول المتغيّرات

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

الخيط الأحمر: استهلاك مبنى ساعةً بساعة

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

import pandas as pd

df = pd.read_csv("conso.csv", parse_dates=["ts"]).set_index("ts").sort_index()
print(df.head())
# kwh temp
# ts
# 2024-01-01 00:00:00 42.1 3.2
# 2024-01-01 01:00:00 39.6 2.8
# 2024-01-01 02:00:00 37.2 2.5

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

لماذا يفشل النموذج الكثيف

لنجرّب المقاربة الساذجة: نُطعم شبكة كثيفة سطرًا سطرًا، بمتغيّرات kwh وtemp وساعة اليوم، ونطلب منها التنبّؤ بالساعة التالية. النتيجة على مجموعة الاختبار محبطة: خطأ متوسّط في حدود عشرة كيلوواط ساعة، حيث نُذكِّر أنّ مجرّد نسخ قيمة الساعة الماضية يعطي خطأً أقلّ من ذلك بمرتين.

السبب في عمق التصميم. فالشبكة الكثيفة تعالج كلّ سطر مستقلًّا: لا تحمل أي ذاكرة من السطر السابق. وإن جرّبنا حقن الساعات الأربع والعشرين الماضية بوصفها أعمدة إضافية — ما يُسمّى تسطيح النافذة — ظهرت مشكلتان. الأولى انفجار الأبعاد كلّما وسّعنا الأفق: مئة ساعة تعني مئة عمود لـkwh وحدها. والثانية، وهي الأخطر، أنّ الشبكة تفقد بنية الترتيب: الوزن الذي يربطها بالساعة السابعة والوزن الذي يربطها بالساعة السادسة كائنان مستقلّان تمامًا، لا شيء يُعلّم النموذج أنّهما جاران في الزمن.

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

النافذة المنزلقة، صيغة التغذية

بما أنّنا سنُعالج متتاليات لا أسطرًا، فلا بدّ من صيغة تُحوّل السلسلة الطويلة إلى دُفعة من نوافذ. الاصطلاح الأشيع: كلّ مثال في التدريب هو زوج مُدخَل من TT خطوة، ومُخرَج من HH خطوة. في الفيل الأحمر: T=168T = 168 ساعة (أسبوع) وH=24H = 24 ساعة (يوم).

import numpy as np

def fenetrage(serie, entree=168, horizon=24, pas=1):
"""يقطع سلسلة أحادية البعد إلى نوافذ (X, y) متعاقبة."""
X, y = [], []
for i in range(0, len(serie) - entree - horizon + 1, pas):
X.append(serie[i:i + entree])
y.append(serie[i + entree:i + entree + horizon])
return np.stack(X), np.stack(y)

X, y = fenetrage(df["kwh"].values)
print(X.shape, y.shape) # (n_fenetres, 168) و (n_fenetres, 24)

الشكل الذي ستنتظره الشبكة التكرارية أشدّ صرامة، وهو موضوع الوحدة 2: (batch, temps, variables). لكنّ منطق النافذة يبقى هو نفسه: نمرّ على السلسلة، نأخذ نافذة مُدخَلة ثم النافذة التي تليها بوصفها هدفًا. اختيار الخطوة pas\text{pas} يُوازن بين عدد النوافذ (كلّما صغرت زادت) والاستقلال بينها (كلّما كبرت انخفض الترابط).

المصيدة الكبرى: التسرّب الزمني

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

المصدر الأشيع في هذا الفصل هو الخلط قبل التقسيم. train_test_split بلا وسيط زمني يُلقي نافذة يوم 12 مايو 2024 في التدريب، ونافذة اليوم التالي في الاختبار، فتبني الشبكة تنبّؤاتها على معرفة بحالة الشبكة الكهربائية للحيّ في تلك اللحظة. الحلّ الوحيد الصحيح هو التقسيم الزمني:

n = len(df)
tr = df.iloc[: int(n * 0.7)]
val = df.iloc[int(n * 0.7): int(n * 0.85)]
te = df.iloc[int(n * 0.85):]

ثم يُطبَّق الفنترة داخل كلّ جزء على حِدة: هكذا لا يمكن لنافذة تدريب أن تحوي ساعة من التحقّق. المصدر الثاني هو المعالجة القبلية على السلسلة كاملة: StandardScaler().fit(df["kwh"]) قبل التقسيم يحقن متوسّط المستقبل في نموذج يُفترض أنّه يجهله. سنعود إلى هذا في الوحدة 9 عند مناقشة تسوية النافذة بلا تسرّب.

تسرّب صامت لا يُطلق أي خطأ

لا ترفع مكتبة scikit-learn تحذيرًا حين تُطعمها سلسلة زمنية بوصفها جدولًا عاديًا؛ ولا يعترض PyTorch على DataLoader(..., shuffle=True) على متتاليات. العَرَض الوحيد قبل الإنتاج هو دقّة عالية بشكل مريب. ومن الحكمة، على كلّ سلسلة زمنية، أن تسأل نفسك: هل يمكن لأي جزء من X_train أن يعرف شيئًا عن أزمنة X_val؟ إن كان الجواب نعم، فأنت أمام تسرّب حتّى تُثبت العكس.

طول متغيّر، معلومة يحملها الوقت

آخر خصوصية سنعود إليها كثيرًا: الطول قد يتغيّر من متتالية إلى أخرى. جُمَل الترجمة في الوحدة 8 لا تتساوى في عدد الكلمات، وقراءات جهاز قد تنقطع دقائق. الجدول العادي يحلّ هذا بالحذف أو بالإتمام؛ أمّا في المتتاليات، يجب أن يعرف النموذج أين تنتهي المعلومة حتّى لا يحسب حالته على الحشو. هذا موضوع الوحدة 9 حين نتناول pack_padded_sequence وأخواتها. في هذه الوحدة يكفي أن نُلاحظ الحاجة، ونحتفظ بها لاحقًا.

حين تكفي الطاولة

ليست كلّ سلسلة زمنية تستحقّ شبكة تكرارية. فحين تكون التبعية قصيرة (الساعة السابقة تشرح ثلاثة أرباع التالية)، ينافس الانحدار الخطّي على متغيّرات مُهندَسة — يوم الأسبوع، الحرارة، متوسّط الأربع والعشرين ساعة الماضية — الشبكة العميقة عن قرب، ويُنشَر بسهولة أكبر بكثير. وحين يكون الطول ثابتًا وقصيرًا وذا هيكل معروف، تنافس شبكة التفافية أحادية البعد الشبكة التكرارية بأداء أفضل ووقت تدريب أقصر. سلاسل الاستهلاك الطويلة، ذات التبعيات المتعدّدة النطاقات، هي الوسط الطبيعي للـLSTM؛ خارج ذلك يُوصى بالتحقّق من مرجعية بسيطة قبل استثمار الوقت.

في الخلاصة

  • المتتالية تحمل معلومتَها في الترتيب؛ خلط الأسطر يُدمّر السلسلة، ونموذج كثيف على سطر واحد لا ذاكرة له.
  • تسطيح النافذة يُوصل إلى انفجار الأبعاد وإلى فقدان بنية الترتيب، وهذا ما تحلّه الشبكة التكرارية.
  • التقسيم على السلاسل الزمنية يكون زمنيًا صارمًا، والمعالجة القبلية تُضبَط على التدريب وحده وإلّا حدث تسرّب صامت.
  • النافذة المنزلقة بشكل (batch, temps, variables) هي صيغة التغذية القياسية، ويجب دائمًا الحذر من طول المتتالية المتغيّر.

الوحدة التالية: العصبون التكراري، الوحدة الأساس التي ترثُ حالتها من خطوة إلى أخرى وتحلّ محلّ التسطيح.