الدرس 7 من 8

التسلسلات: RNN و LSTM

مبيعات، أسعار، نصوص: الترتيب هنا جزء من المعنى.

ليه الترتيب مهم؟

«الأكل مش حلو» عكس «مش الأكل حلو»: نفس الكلمات بترتيب مختلف. ومبيعات بكرة مرتبطة بمبيعات النهارده وامبارح والأسبوع اللي فات. الشبكة العادية بتاخد كل المدخلات مرة واحدة ومش فاهمة الترتيب.

الـ RNN: شبكة ليها ذاكرة

الـ RNN بتقرا التسلسل خطوة خطوة، وعند كل خطوة بتاخد المدخل الجديد ومعاه ملخص للي فات (اسمه hidden state). زي ما انت بتقرا الجملة دي: كل كلمة بتفهمها في ضوء اللي قبلها.

مشكلة الـ RNN البسيطة إنها بتنسى بسرعة في التسلسلات الطويلة. الـ LSTM حلّت ده بـ «بوابات» بتقرر إيه اللي يتحفظ وإيه اللي يتنسي وإيه اللي يطلع، زي نوتة بتكتب فيها الحاجات المهمة بس.

توقع مبيعات بكرة

محل مبيعاته بتزيد مع الوقت، وبتعلى في آخر الأسبوع. هنعلّم LSTM يبص على آخر ١٤ يوم ويتوقع اليوم الجاي:

lstm_sales.py
import numpy as np
import torch
from torch import nn
import matplotlib.pyplot as plt

torch.manual_seed(0)
rng = np.random.default_rng(0)

# ---- 1 year of daily sales: trend + weekly pattern + noise ----
days = np.arange(365)
sales = 200 + 0.3 * days + 40 * np.sin(2 * np.pi * days / 7) + rng.normal(0, 8, 365)

mean, std = sales[:300].mean(), sales[:300].std()      # scale using train part only
s = (sales - mean) / std

WINDOW = 14
X = np.array([s[i:i + WINDOW] for i in range(len(s) - WINDOW)])
y = s[WINDOW:]
X = torch.tensor(X, dtype=torch.float32).unsqueeze(-1)  # (samples, 14 steps, 1 feature)
y = torch.tensor(y, dtype=torch.float32).unsqueeze(-1)
split = 300 - WINDOW
X_tr, y_tr, X_te, y_te = X[:split], y[:split], X[split:], y[split:]

class SalesLSTM(nn.Module):
    def __init__(self):
        super().__init__()
        self.lstm = nn.LSTM(input_size=1, hidden_size=32, batch_first=True)
        self.head = nn.Linear(32, 1)
    def forward(self, x):
        out, _ = self.lstm(x)           # out: output at every step
        return self.head(out[:, -1])    # use the last step to predict tomorrow

model = SalesLSTM()
opt = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(150):
    opt.zero_grad()
    loss = nn.functional.mse_loss(model(X_tr), y_tr)
    loss.backward()
    opt.step()

model.eval()
with torch.no_grad():
    pred = model(X_te).squeeze().numpy() * std + mean   # back to real units
actual = y_te.squeeze().numpy() * std + mean
print(f"average error on unseen days: {np.abs(pred - actual).mean():.1f} units")

plt.figure(figsize=(7, 3.2))
plt.plot(actual, label="actual", color="#14213D")
plt.plot(pred, label="LSTM forecast", color="#E0A800", linewidth=2)
plt.title("Next-day sales forecast (65 unseen days)")
plt.legend(); plt.grid(alpha=0.3)
plt.show()

الموديل لقط لوحده النمط الأسبوعي كويس جداً. بس بص تاني: توقعاته بتطلع أقل شوية من الحقيقي، والفرق بيكبر مع الوقت. ليه؟ لأن المبيعات بتزيد، ووصلت لأرقام الموديل عمره ما شافها في التدريب. ده عيب معروف في الموديلات دي مع الـ trend، والحل الشائع إنك تعلّمه يتوقع التغيير عن امبارح بدل الرقم نفسه. والدرس الأهم: دايماً ارسم التوقعات، الرقم لوحده ماكانش هيوريك المشكلة دي.

💡 الـ shape بتاع التسلسلاتالـ LSTM بياخد tensor شكله (batch, sequence_length, features). هنا (286, 14, 1): ٢٨٦ مثال، كل واحد ١٤ يوم، وكل يوم فيه رقم واحد. لو عندك كمان درجة الحرارة وهل اليوم إجازة، الـ features تبقى ٣.
🤖 وبعدين جات الـ Transformersالـ LSTM كان ملك النصوص لسنين. في ٢٠١٧ ظهر الـ Transformer اللي بيبص على التسلسل كله مرة واحدة بآلية اسمها Attention بدل خطوة خطوة، فبقى أسرع وأقوى بكتير. ده الأساس اللي مبني عليه Claude و GPT، وموضوع المرحلة الجاية بالكامل.
الدرس اللي فات