Recurrent Neural Networks — التعلم العميق
الشبكات العصبية المتكررة (RNN)
النص والكلام وأسعار الأسهم تسلسلات: الترتيب مهم، والطول متغير. "لم يكن الفيلم جيداً" تعني عكس "كان الفيلم جيداً" بإضافة كلمة واحدة. الشبكات الكثيفة تعامل المدخلات ككتلة ثابتة بلا ترتيب.
الفكرة: ذاكرة متجددة
RNN تقرأ التسلسل خطوة خطوة، وتحمل حالة مخفية h تلخص ما رأته حتى الآن:
hₜ = tanh(Wₓ·xₜ + Wₕ·hₜ₋₁ + b)نفس الأوزان تُستخدم في كل خطوة (مثل مشاركة الأوزان في CNN لكن عبر الزمن).
import numpy as np
rng = np.random.default_rng(0)
Wx, Wh, b = rng.normal(0, 0.5, (1, 3)), rng.normal(0, 0.5, (3, 3)), np.zeros(3)
sequence = [0.5, -0.1, 0.8, 0.3]
h = np.zeros(3)
for t, x in enumerate(sequence):
h = np.tanh(np.array([x]) @ Wx + h @ Wh + b)
print(f"خطوة {t}: h = {h.round(3)}")
# h الأخيرة تلخص التسلسل كله ← تمر لطبقة Dense للتنبؤمشكلة الذاكرة القصيرة
عند Backpropagation عبر الزمن، يُضرب التدرج بـ Wₕ ومشتقة tanh عند كل خطوة، فيتلاشى (أو ينفجر) مع التسلسلات الطويلة. النتيجة: SimpleRNN تنسى بداية الجمل الطويلة.
LSTM و GRU
LSTM تضيف "خلية ذاكرة" مع بوابات تتعلم ماذا تنسى وماذا تضيف وماذا تُخرج — فتحافظ على المعلومات لمسافات أطول. GRU نسخة مبسطة ببوابتين، أسرع وغالباً بنفس الجودة.
| الطبقة | متى |
|---|---|
| SimpleRNN | للفهم فقط تقريباً |
| LSTM | الخيار الكلاسيكي للتسلسلات الطويلة |
| GRU | بديل أخف لـ LSTM |
| Bidirectional(LSTM(...)) | عندما يكون التسلسل كاملاً متاحاً (تصنيف النص): تقرأ في الاتجاهين |
الأشكال في Keras
مدخلات RNN ثلاثية الأبعاد: (عينات، خطوات زمنية، ميزات لكل خطوة). سلسلة سعر يومي بنافذة 30 يوماً: (n, 30, 1).
- return_sequences=True: أعد مخرجاً لكل خطوة — لازم عند تكديس طبقة RNN فوق أخرى.
تجهيز سلسلة زمنية
import numpy as np
series = np.sin(np.linspace(0, 20, 200))
window = 10
X = np.array([series[i:i + window] for i in range(len(series) - window)])
y = series[window:]
X = X[..., np.newaxis] # (عينات، 10، 1)
print(X.shape, y.shape)⚠️ في السلاسل الزمنية لا تخلط البيانات عشوائياً عند التقسيم: درّب على الماضي واختبر على المستقبل، وإلا تسرب المستقبل إلى التدريب.
الواقع اليوم
لمعالجة اللغة حلّت Transformers محل RNN تقريباً بالكامل (درس قادم): تعالج التسلسل بالتوازي وتربط الكلمات البعيدة مباشرة. ما زالت LSTM/GRU مفيدة للسلاسل الزمنية الصغيرة والأجهزة محدودة الموارد.
تمرين
في المثال الكامل قارن SimpleRNN و LSTM و GRU على نافذة 50 بدل 10. أيها تحسّن أكثر مع النافذة الأطول؟
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
# ===== بيانات تجريبية: تنبؤ سلسلة =====
def create_sequences(data, seq_length):
X, y = [], []
for i in range(len(data) - seq_length):
X.append(data[i:i+seq_length])
y.append(data[i+seq_length])
return np.array(X), np.array(y)
# سلسلة sin
data = np.sin(np.linspace(0, 100, 1000))
seq_length = 20
X, y = create_sequences(data, seq_length)
X = X.reshape(-1, seq_length, 1) # (samples, timesteps, features)
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
# ===== Simple RNN =====
rnn_model = keras.Sequential([
layers.SimpleRNN(50, input_shape=(seq_length, 1)),
layers.Dense(1)
])
# ===== LSTM =====
lstm_model = keras.Sequential([
layers.LSTM(50, input_shape=(seq_length, 1)),
layers.Dense(1)
])
# ===== GRU =====
gru_model = keras.Sequential([
layers.GRU(50, input_shape=(seq_length, 1)),
layers.Dense(1)
])
# ===== Stacked LSTM =====
stacked_model = keras.Sequential([
layers.LSTM(50, return_sequences=True, input_shape=(seq_length, 1)),
layers.LSTM(50),
layers.Dense(1)
])
# التدريب
lstm_model.compile(optimizer='adam', loss='mse')
history = lstm_model.fit(X_train, y_train, epochs=20, validation_split=0.1, verbose=0)
# التقييم
loss = lstm_model.evaluate(X_test, y_test)
print(f"Test Loss: {loss:.4f}")
# التنبؤ
predictions = lstm_model.predict(X_test[:5])
print(f"Predictions: {predictions.flatten()}")
print(f"Actual: {y_test[:5]}")اختبر فهمك