الوحدة 7 — شبكات Q العميقة وإعادة تشغيل التجربة
CartPole: حالة مكوّنة من أربعة أرقام حقيقيّة (موضع العربة، سرعتها، زاوية العصا، سرعتها الزاويّة). الفضاء مستمرّ، لا يمكن تخصيص خانة جدول لكلّ حالة. حلّ ذلك ورقة DeepMind الشهيرة 2015 بشبكة عصبيّة تتنبّأ : Deep Q-Network أو DQN. الفكرة بسيطة، لكن جعلها تعمل تطلّب حيلتين معماريّتين نُدرسهما هنا.
من الجدول إلى الشبكة
نستبدل جدول بشبكة تُخرج متّجهًا من قيم، واحدة لكلّ فعل:
import torch
import torch.nn as nn
class ReseauQ(nn.Module):
def __init__(self, dim_etat, dim_action):
super().__init__()
self.pipeline = nn.Sequential(
nn.Linear(dim_etat, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, dim_action),
)
def forward(self, s):
return self.pipeline(s) # (batch, dim_action)
هدف التدريب من بلمان مباشرة، خسارة تربيعيّة على خطأ TD:
يبدو الأمر مطابقًا للتبويب. لكن، على عكس التبويب، هذه الخوارزميّة لا تتقارب إذا نُفّذت كما هي. سببان مترابطان.
المشكلة الأولى: بيانات مترابطة
عيّنات التدريب المتتاليّة متشابهة جدًّا (الحالة قريبة من ). خوارزميّات الاستمثال المستخدمة في التعلّم العميق (SGD، Adam) تفترض عيّنات مستقلّة؛ وترابط بياناتنا يجعل التحديثات تتخذ اتّجاهًا واحدًا لفترة طويلة ثمّ ينقلب فجأة، فيتذبذب النموذج.
الحلّ: تخزين الرجعة (Experience Replay). ندع الوكيل يتفاعل، ونحفظ كلّ رباعيّة في مخزن دائري ضخم (مثلًا 100 ألف عيّنة). عند التدريب نسحب دفعة عشوائيّة من هذا المخزن.
from collections import deque
import random
memoire = deque(maxlen=100_000)
# داخل الحلقة
memoire.append((s, a, r, s_prime, terminated))
if len(memoire) >= batch_size:
dfe = random.sample(memoire, batch_size)
فوائد التخزين ثلاث: كسر الترابط الزماني، إعادة استخدام كلّ تجربة عدّة مرّات (كفاءة العيّنة)، وتنعيم توزيع البيانات الذي يراه المحلّل.
المشكلة الثانية: أهداف متحرّكة
المشكلة الثانية أخفى. عند حساب الخسارة:
نستعمل نفس لحساب الهدف وحساب التوقّع. كلّ تحديث لـ يُغيّر الهدف نفسه الذي نُحاول ملاحقته. النتيجة: تدريب غير مستقرّ، خسارة تتذبذب، وقيمة تنفجر أو تنهار.
الحلّ: الشبكة الهدف. نحتفظ بنسختين من الشبكة، (المدرَّبة) و (المجمّدة). نحسب الهدف بـ:
كلّ خطوة (مثلًا 1000)، ننسخ . بين هذه النسخات، الهدف ثابت، فيصبح التدريب استمثالًا عاديًّا مع هدف قار.
reseau = ReseauQ(4, 2) # CartPole: 4 مُدخلات، فعلان
cible = ReseauQ(4, 2)
cible.load_state_dict(reseau.state_dict())
cible.eval()
# كلّ 1000 خطوة
if pas % 1000 == 0:
cible.load_state_dict(reseau.state_dict())
تحديث ناعم بديل: بدل النسخ الكامل، حرّك مع في كلّ خطوة. أكثر سلاسة، أسهل ضبطًا، وهو ما تعتمده أغلب التنفيذات الحديثة.
حلقة DQN كاملة
gamma = 0.99
optim = torch.optim.Adam(reseau.parameters(), lr=1e-3)
for pas in range(200_000):
# فعل بإبسيلون-جشعة
if random.random() < epsilon:
a = env.action_space.sample()
else:
with torch.no_grad():
a = int(reseau(torch.tensor(s, dtype=torch.float32)).argmax())
s_prime, r, term, trunc, _ = env.step(a)
memoire.append((s, a, r, s_prime, term))
s = s_prime if not (term or trunc) else env.reset()[0]
if len(memoire) >= 64:
lot = random.sample(memoire, 64)
S = torch.tensor([x[0] for x in lot], dtype=torch.float32)
A = torch.tensor([x[1] for x in lot])
R = torch.tensor([x[2] for x in lot], dtype=torch.float32)
S2 = torch.tensor([x[3] for x in lot], dtype=torch.float32)
D = torch.tensor([x[4] for x in lot], dtype=torch.float32)
with torch.no_grad():
cible_q = R + gamma * (1 - D) * cible(S2).max(dim=1).values
pred_q = reseau(S).gather(1, A.unsqueeze(1)).squeeze()
perte = nn.functional.smooth_l1_loss(pred_q, cible_q)
optim.zero_grad()
perte.backward()
torch.nn.utils.clip_grad_norm_(reseau.parameters(), 10.0)
optim.step()
if pas % 1000 == 0:
cible.load_state_dict(reseau.state_dict())
بعد نحو 100 ألف خطوة على CartPole يحقّق الوكيل خطوة قبل السقوط، وهو حدّ الحلّ المُعلن. smooth_l1_loss (Huber) بدل MSE يحدّ من تأثير القيم الشاذّة في الهدف عند بداية التدريب.
Double DQN
مشكلة أخيرة: عامل في الهدف يُبالغ منهجيًّا في تقدير (بسبب الضجيج على التقدير). Double DQN يفصل بين اختيار الفعل وتقدير قيمته:
نستعمل لاختيار الفعل الأفضل، و لتقدير قيمته. تعديل سطر واحد يُصحّح تحيّزًا حقيقيًّا، ويحسّن الأداء دون كلفة إضافية.
- خسارة تنفجر: قصّوا التدرّجات (
clip_grad_norm_عند 10) واستعملوا Huber بدل MSE. - تنفجر: راجعوا الشبكة الهدف — نسيان تفعيلها هو السبب الأشيع.
- الوكيل لا يتعلّم: تفقّدوا تناقص إبسيلون؛ تناقص سريع جدًّا يقتل الاستكشاف قبل ملء المخزن.
- تباين هائل بين البذور: طبيعي في DQN، يجب دومًا الإبلاغ عن متوسّط ثلاث بذور على الأقلّ.
الخلاصة
- DQN يستبدل جدول بشبكة عصبيّة، فيسمح بفضاءات حالة مستمرّة أو كبيرة جدًّا.
- تخزين الرجعة يكسر ترابط البيانات الزماني ويسمح بإعادة استخدام العيّنات؛ وحده يجعل الاستمثال يعمل.
- الشبكة الهدف تُثبّت هدف الخسارة، فيصبح التدريب استمثالًا مع أهداف قارّة بين النسخات.
- Double DQN يُقلّص التحيّز التصاعدي لـ؛ تعديل يستحقّ التبنّي.
- CartPole يُحلّ بنحو 100 ألف خطوة إذا احترمت الحيل الأربع؛ وإسقاط أيّ منها يعطّل التعلّم كلّه.
الوحدة التالية: نُغيّر المنظور تمامًا. بدل تعلّم قيمة ثمّ استخلاص السياسة، نتعلّم السياسة مباشرة بتدرّج على أوزان شبكة سياسة.