الوحدة 9 — تقطيع المتتاليات وحشوها عمليًا
سبع وحدات نظرية، ووحدة معمارية إضافية. تُختَم هذه السلسلة بوحدة عملانية حصرًا: كيف نُطعم PyTorch أو Keras متتاليات حقيقية دون هدر ذاكرة ودون تسرّب من المستقبل، ودون إغراق النموذج في الحشو الفارغ. القرارات هنا لا تتغيّر معماريّتك، لكنّها تُغيّر جودة نموذجك.
من سلسلة طويلة إلى دفعة من نوافذ
يعود الفيل الأحمر: سلسلة الاستهلاك الكهربائي الساعي على سنتين. نحتاج تحويلها إلى دفعة نوافذ متتالية بشكل (N, T, F). الأداة الجاهزة في PyTorch هي TensorDataset مع DataLoader:
import torch
from torch.utils.data import Dataset, DataLoader
class FenetresConso(Dataset):
"""يقطع سلسلة موحدة السلم إلى نوافذ (X, y) بلا تسرب."""
def __init__(self, serie, T=168, H=24):
self.serie, self.T, self.H = serie, T, H
def __len__(self):
return len(self.serie) - self.T - self.H + 1
def __getitem__(self, i):
X = self.serie[i:i + self.T] # (T, F)
y = self.serie[i + self.T:i + self.T + self.H, 0] # (H,) هدف kWh
return torch.tensor(X, dtype=torch.float32), \
torch.tensor(y, dtype=torch.float32)
train = DataLoader(FenetresConso(serie_tr), batch_size=32, shuffle=True)
shuffle=True سليم على مستوى الدفعات لأنّ كلّ نافذة مكتملة في ذاتها؛ لا نخلط الخطوات الزمنية داخل نافذة، بل ننتقي نوافذ متعاقبة بترتيب عشوائي. هذا كلا سيكيّ ولا يُنتج تسرّبًا.
التسوية بلا تسرّب
في الدورة 7 كنّا نُسوّي بـStandardScaler().fit(X_train) ثمّ نُطبّق على التحقّق. المبدأ نفسه ينطبق هنا، مع تحذير إضافي: لا تُدرَّب المُسوّية إلّا على البيانات الأقدم زمنيًا، لا على المتتاليات المخلوطة.
from sklearn.preprocessing import StandardScaler
# التقسيم زمني كما في الوحدة 1.
tr = df.iloc[: int(len(df) * 0.7)]
val = df.iloc[int(len(df) * 0.7): int(len(df) * 0.85)]
te = df.iloc[int(len(df) * 0.85):]
echelle = StandardScaler().fit(tr[["kwh", "temp"]]) # على التدريب فقط
tr_norm = echelle.transform(tr[["kwh", "temp"]])
val_norm = echelle.transform(val[["kwh", "temp"]])
te_norm = echelle.transform(te[["kwh", "temp"]])
إذا فعلتَ العكس (fit على السلسلة كاملة قبل التقسيم)، متوسّط 2025 يتسرّب إلى نموذج يُفترض أنّه تدرّب عام 2024 وحده. النتيجة: تحقّق ممتاز، إنتاج سيّئ. المطابقة نفسها لكلّ مُسوّية تعرف الحدّ الأقصى أو الأدنى (MinMaxScaler، QuantileTransformer، وسواها).
الحشو والتقنيع: حين تختلف الأطوال
على الاستهلاك الكهربائي، كلّ نوافذنا بطول 168 بالضبط. لا مشكلة. لكن على النصوص (جُمَل الترجمة في الوحدة 8)، أو على تسجيلات جهاز تتوقّف وتشتغل بأطوال مختلفة، الأطوال متغيّرة داخل الدفعة نفسها. الحلّ: نحشو المتتاليات القصيرة بقيمة خاصّة (0 أو رمز <pad> مخصوص) حتى يتساوى طول كلّ الدفعة، ثم نُخبر النموذج أن يتجاهل الحشو.
from torch.nn.utils.rnn import pad_sequence
phrases = [torch.tensor([1, 5, 8, 3]),
torch.tensor([2, 7]),
torch.tensor([4, 9, 6, 1, 8])]
dette = pad_sequence(phrases, batch_first=True, padding_value=0)
print(dette)
# tensor([[1, 5, 8, 3, 0],
# [2, 7, 0, 0, 0],
# [4, 9, 6, 1, 8]])
pack_padded_sequence: الأداة الحاسمة
مجرّد حشو المتتاليات لا يكفي. إذا مرّرت الدفعة المحشوّة مباشرة إلى nn.LSTM، الشبكة ستحسب الحالة على الحشو: تُلوّث الحالة المخفية بمعلومة معدومة، وتُعطي مُخرَجات مختلفة حسب مقدار الحشو. الحلّ في PyTorch اسمه pack_padded_sequence:
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
longueurs = torch.tensor([4, 2, 5])
# نرتب الدفعة تنازليا حسب الطول (شرط الحزم).
longueurs, ordre = longueurs.sort(descending=True)
dette = dette[ordre]
paquet = pack_padded_sequence(dette, longueurs, batch_first=True)
sorties_paquet, h_T = lstm(paquet)
sorties, _ = pad_packed_sequence(sorties_paquet, batch_first=True)
الحزمة paquet تخزّن كلّ خطوة فعلية مرّة واحدة فقط، وتحمل معها قائمة الأطوال. تعالجها nn.LSTM بكفاءة، ولا تعبر أبدًا خطوة حشو. الحالة النهائية h_T هي فعليًا حالة الخطوة الأخيرة الفعلية لكلّ متتالية، لا حالة الخطوة الحشوّة. هذا مهمّ جدًّا للتصنيف الجملي.
pad_packed_sequence تُعيد فكّ ال حزمة إلى موتِّر مُبطَّن حين نحتاجه للطبقات التالية.
الفرز بالطول
اشتراط ترتيب الدفعة تنازليًا (enforce_sorted=True وهو الافتراض) عقبة صغيرة. الحلّ هو دالة تجميع دفعة (collate_fn) تفرز الدفعة قبل الحزم:
def collate_fn(dfa):
"""تنشئ دفعة مرتبة تنازليا حسب الطول من قائمة (X, y, طول)."""
dfa.sort(key=lambda x: x[2], reverse=True)
X, y, longueurs = zip(*dfa)
X_dette = pad_sequence(X, batch_first=True, padding_value=0)
return X_dette, torch.stack(y), torch.tensor(longueurs)
charg = DataLoader(dataset, batch_size=32, shuffle=True, collate_fn=collate_fn)
بديل الفرز الكامل: enforce_sorted=False، الذي يفرز داخليًا. أبطأ قليلًا، لكنّه يُبسِّط الكود.
Bucketing: دفعات متجانسة الطول
خدعة عمليّة أخيرة لكسب الأداء: الدفعات المتجانسة. حين تحوي دفعتك متتالية بطول 5 وأخرى بطول 500، يُحسَب الحشو في الأخيرة عبثًا لأنّ الأولى تُغذّي الحساب بحشو 495. الحلّ: نُنشئ دلاء (buckets) من المتتاليات ذات الأطوال المتقاربة، ونُشكِّل ا لدفعات داخل كلّ دلو.
المكاسب على المدوّنات المتغيّرة الأطوال بشدّة (كوثائق قانونية أو نصوص علمية) قد تصل إلى تسريع مضاعف بلا أي أثر على الجودة. BucketIterator في torchtext ومقابلاته في مكتبات المعالجة اللغوية تُطبّق هذا آليًا.
نمط سلسلة زمنية بدلًا من pack
على السلاسل الزمنية ذات النافذة الثابتة (فيلنا الأحمر)، لا حاجة إلى pack_padded_sequence لأنّ الأطوال متساوية. يبقى تحذيران:
- الفواصل المفقودة داخل النافذة: قراءة جهاز انقطعت ساعة. الحلّ ملء بالاستكمال الخطّي أو بمتوسّط الساعة نفسها من الأيّام السابقة، مع علم أنّ فَرق بيانات مفقودة عن أخرى لا يُعالَج بسهولة بدون تقنيع.
- الفروقات الموسمية: نافذة أسبوعية على الشتاء وأخرى على الصيف لهما إحصائيات مختلفة. المُسوّية الواحدة تخلطهما؛ متغيّرات موسمية إضافية (شهر، فصل) تساعد على تعويض هذا الاختلاف.
مغرٍ أن نُسوّي كلّ نافذة على متوسّطها الخاصّ (تسوية على مستوى النافذة). لكن هذا يمحو معلومة السلّم بين النوافذ: الأسبوع الأسخن يبدو مطابقًا للأسبوع الأبرد. على الاستهلاك الكهربائي هذه معلومة أساسية. القاعدة العامّة: سوِّ على مستوى السلسلة، ودع النموذج يتعلّم التغيّرات النسبية. الاستثناء الوحيد: حين يكون الفرق المطلق غير مفيد أصلًا (تصنيف نمط بغضّ النظر عن السلّم)، وحتى ثمّة يجب أن يكون القرار واعيًا.
في الخلاصة
- النافذة المنزلقة بشكل
(N, T, F)هي صيغة التغذية المعيار؛shuffle=Trueعلى مستوى الدفعات سليم لأنّ كلّ نافذة مكتملة في ذاتها. - التسوية تُضبَط على البيانات الأقدم زمنيًا حصرًا، وإلّا حدث تسرّب من المستقبل يُنتج نموذجًا لا يعمل في الإنتاج.
- الحشو والتقنيع لا غنى عنهما حين تختلف الأطوال؛
pack_padded_sequenceفي PyTorch يمنع الشبكة من حساب حالتها على الحشو ويوفّر حالة نهاية فعلية. - الفرز والدلاء يوفّران أداءً بلا مقابل في الجودة على المدوّنات المتغيّرة الأطوال بشدّة، ويجب تجنّب التسوية داخل نافذة إذا كان السلّم المطلق يحمل معلومة.
الوحدة التالية: نجمع كلّ ما سبق في مشروع تنبّؤ كامل على استهلاك الكهرباء، مع مرجعية ساذجة، فترات ثقة، والأخطاء التي يجب ألّا نقع فيها.