في المقالة السابقة تحدثنا عن طريقة الإنتروبيا المتقاطعة ، و قمنا بإستخدمها مع بيئة Cartpole من Gym في مقالة اليوم سنقوم بإستخدام طريقة الإنتروبيا المتقاطعة في بيئة أخرى و هي بيئة البحيرة المتجمدة
طريقة الانتروبيا المتقاطعة على بيئة FrozenLake
عالم بيئة FrozenLake هو من فئة عالم الشبكة (grid world) ، عندما يعيش وكيلك في شبكة بحجم 4 × 4 ويمكنه التحرك في أربعة اتجاهات: لأعلى ولأسفل ولليسار ولليمين. يبدأ الوكيل دائمًا في الموضع العلوي الأيسر ، وهدفه هو الوصول إلى الخلية اليمنى السفلية من الشبكة. توجد ثقوب في الخلايا الثابتة للشبكة وإذا دخلت في تلك الثقوب ، تنتهي الحلقة وتكون مكافأتك صفرًا. إذا وصل الوكيل إلى الخلية المستهدفة ، فسيحصل على مكافأة قدرها 1.0 وتنتهي الحلقة.
لجعل الحياة أكثر تعقيدًا ، يكون العالم زلقًا (إنها بحيرة متجمدة بعد كل شيء) ، لذا فإن تصرفات الوكيل لا تسير دائمًا كما هو متوقع – هناك احتمال بنسبة 33٪ أن ينزلق إلى اليمين أو اليسار. إذا كنت تريد أن يتحرك الوكيل إلى اليسار ، على سبيل المثال ، هناك احتمال بنسبة 33٪ أنه سيتحرك بالفعل إلى اليسار ، وفرصة بنسبة 33٪ أن ينتهي به الأمر في الخلية أعلاه ، وفرصة بنسبة 33٪ في أن ينتهي حتى في الخلية أدناه. كما سترى في نهاية القسم ، فإن هذا يجعل التقدم صعبًا.
لنلق نظرة على كيفية تمثيل هذه البيئة في GYM:
>>> e = gym.make("FrozenLake-v0") [2017-10-05 12:39:35,827] Making new env: FrozenLake-v0 >>> e.observation_space Discrete(16) >>> e.action_space Discrete(4) >>> e.reset() 0 >>> e.render() SFFF FHFH FFFH HFFG
فضاء الملاحظة لدينا منفصل ، مما يعني أنه مجرد عدد من صفر إلى 15. من الواضح أن هذا الرقم هو موضعنا الحالي في الشبكة. فضاء الإجراء منفصل أيضًا ، ولكن يمكن أن يتراوح من صفر إلى ثلاثة. تتوقع الشبكة العصبية من مثال CartPole متجهًا للأرقام.
للحصول على هذا ، يمكننا إستخدام خط الترميز الأحادي (one-hot encoding) للمدخلات المنفصلة ، مما يعني أن الإدخال إلى شبكتنا سيكون له 16 رقمًا عائمًا وصفرًا في كل مكان باستثناء الفهرس الذي سنقوم بترميزه. لتقليل التغييرات في الكود الخاص بنا ، يمكننا استخدام فئة ObservationWrapper من Gym وتنفيذ فئة DiscreteOneHotWrapper الخاصة بنا:
class DiscreteOneHotWrapper(gym.ObservationWrapper): def __init__(self, env): super(DiscreteOneHotWrapper, self).__init__(env) assert isinstance(env.observation_space, gym.spaces.Discrete) shape = (env.observation_space.n, ) self.observation_space = gym.spaces.Box( 0.0, 1.0, shape, dtype=np.float32) def observation(self, observation): res = np.copy(self.observation_space.low) res[observation] = 1.0 return res
مع تطبيق هذا الغلاف على البيئة ، فإن فضاء المراقبة و فضاء العمل متوافقة بنسبة 100٪ مع حل CartPole . ومع ذلك ، من خلال إطلاقه ، يمكننا أن نرى أن هذا لا يحسن النتيجة بمرور الوقت.
لفهم ما يحدث ، نحتاج إلى النظر بشكل أعمق في هيكل المكافأة في كلتا البيئتين. في CartPole ، تمنحنا كل خطوة من خطوات البيئة المكافأة 1.0 ، حتى لحظة سقوط القطب.
لذلك ، كلما طالت مدة موازنة وكيلنا للعمود ، زادت المكافأة التي حصل عليها. نظرًا للعشوائية في سلوك وكيلنا ، كانت الحلقات المختلفة ذات أطوال مختلفة ، مما أعطانا توزيعًا طبيعيًا جدًا لمكافآت الحلقات. بعد اختيار حد المكافأة ، رفضنا الحلقات الأقل نجاحًا وتعلمنا كيفية تكرار حلقات أفضل (من خلال التدريب على بيانات الحلقات الناجحة).
هذا موضح في الرسم البياني التالي:
في بيئة FrozenLake ، تبدو الحلقات ومكافآتها مختلفة. نحصل على مكافأة 1.0 فقط عندما نصل إلى الهدف ، وهذه المكافأة لا تقول شيئًا عن مدى جودة كل حلقة. هل كانت سريعة وفعالة أم أننا قمنا بأربع جولات على البحيرة قبل أن ندخل بشكل عشوائي إلى الخلية النهائية؟ لا نعلم.
إنها مجرد مكافأة 1.0 وهذا كل شيء. يعتبر توزيع المكافآت على حلقاتنا مشكلة أيضًا. هناك نوعان فقط من الحلقات الممكنة ، مع عدم وجود مكافأة (فاشلة) ومكافأة واحدة (ناجحة) ، ومن الواضح أن الحلقات الفاشلة ستهيمن في بداية التدريب. لذا ، فإن اختيارنا المئوي من حلقات “النخبة” هو تمامًا خطأ ويعطينا أمثلة سيئة للتدرب عليها. هذا هو سبب فشل تدريبنا.
يوضح لنا هذا المثال قيود طريقة الانتروبيا المتقاطعة:
- للتدريب ، يجب أن تكون حلقاتنا محدودة ، ويفضل أن تكون قصيرة
- يجب أن تحتوي المكافأة الإجمالية للحلقات على تنوع كافٍ لفصل الحلقات الجيدة عن الحلقات السيئة
- لا يوجد مؤشر وسيط حول ما إذا كان الوكيل قد نجح أو فشل
إإليك قائمة بتعديلات الكود التي تحتاج إلى إجراؤها باستخدام طريقة الانتروبيا المتقاطعة :
- مجموعات أكبر من الحلقات التي تم تشغيلها: في CartPole ، كان يكفي وجود 16 حلقة في كل تكرار ، لكن FrozenLake يتطلب 100 حلقة على الأقل فقط للحصول على بعض الحلقات الناجحة.
- عامل الخصم مطبق على المكافأة: لجعل المكافأة الإجمالية للحلقة تعتمد على طولها ، وإضافة التنوع في الحلقات ، يمكننا استخدام مكافأة إجمالية مخفضة مع عامل الخصم 0.9 أو 0.95. في هذه الحالة ، ستكون مكافأة الحلقات الأقصر أعلى من مكافأة الحلقات الأطول. يؤدي هذا إلى زيادة التباين في توزيع المكافآت ، مما يساعد على تجنب مواقف مثل تلك الموضحة في الشكل أعلاه.
- الاحتفاظ بحلقات “النخبة” لفترة أطول: في تدريب CartPole ، أخذنا عينات من الحلقات من البيئة ، ودربنا على أفضلها ، ثم تخلصنا منها. في FrozenLake ، تعتبر الحلقة الناجحة شيئاً نادرًا جدًا ، لذلك نحتاج إلى الاحتفاظ بها لعدة مرات للتدريب عليها.
- خفض معدل التعلم: سيعطي هذا مزيد من الوقت للشبكة العصبية لدينا لتوسيط المزيد من عينات التدريب.
- وقت تدريب أطول بكثير: و ذلك نظرًا لتناثر الحلقات الناجحة ، والنتيجة العشوائية لإجرائتنا ، يصعب على الشبكة الحصول على فكرة عن أفضل سلوك يمكن أداؤه في أي موقف معين. للوصول إلى 50٪ من الحلقات الناجحة ، يتطلب الأمر حوالي 5 آلاف مرة تدريب.
لدمج كل هذه العناصر في الكود الخاص بنا ، نحتاج إلى تغيير وظيفة filter_batch لحساب المكافأة المخصومة وإرجاع حلقات “النخبة” لكي نحتفظ بها:
def filter_batch(batch, percentile): filter_fun = lambda s: s.reward * (GAMMA ** len(s.steps)) disc_rewards = list(map(filter_fun, batch)) reward_bound = np.percentile(disc_rewards, percentile) train_obs = [] train_act = [] elite_batch = [] for example, discounted_reward in zip(batch, disc_rewards): if discounted_reward > reward_bound: train_obs.extend(map(lambda step: step.observation, example.steps)) train_act.extend(map(lambda step: step.action, example.steps)) elite_batch.append(example) return elite_batch, train_obs, train_act, reward_bound
بعد ذلك ، في حلقة التدريب ، سنخزن حلقات “النخبة” السابقة لتمريرها إلى الوظيفة السابقة في تكرار التدريب التالي.
full_batch = [] for iter_no, batch in enumerate(iterate_batches( env, net, BATCH_SIZE)): reward_mean = float(np.mean(list(map( lambda s: s.reward, batch)))) full_batch, obs, acts, reward_bound = \ filter_batch(full_batch + batch, PERCENTILE) if not full_batch: continue obs_v = torch.FloatTensor(obs) acts_v = torch.LongTensor(acts) full_batch = full_batch[-500:]
باقي الكود هو نفسه ، باستثناء أن معدل التعلم انخفض 10 مرات وتم ضبط BATCH_SIZE على 100. بعد فترة انتظار (يستغرق الإصدار الجديد حوالي ساعة ونصف الساعة لإنهاء 10 آلاف تكرار) ، يمكنك أن تلاحظ أن تدريب النموذج توقف عن التحسن في حوالي 55٪ من الحلقات التي تم حلها. هناك طرق لمعالجة هذا (من خلال تطبيق تنظيم خسارة الانتروبيا ، على سبيل المثال) .
النقطة الأخيرة التي يجب ملاحظتها هنا هي تأثير الانزلاق في بيئة FrozenLAke . يتم استبدال كل إجراء من أفعالنا باحتمالية 33٪ بإجراء مستدير بزاوية 90 درجة (على سبيل المثال ، سينجح الإجراء “لأعلى” باحتمال 0.33 وستكون هناك فرصة 0.33 لاستبداله بالإجراء “الأيسر” و 0.33 مع الإجراء “الصحيح”).
يمكننا تعديل ذلك و جعل البيئة غير منزلقة والفرق الوحيد هو في إنشاء البيئة :
env = gym.envs.toy_text.frozen_lake.FrozenLakeEnv( is_slippery=False) env.spec = gym.spec("FrozenLake-v0") env = gym.wrappers.TimeLimit(env, max_episode_steps=100) env = DiscreteOneHotWrapper(env)
التأثير مثير! يمكن حل النسخة غير المنزلقة من البيئة في 120-140 حزمة تكرارية ، وهي 100 مرة أسرع من البيئة الصاخبة:
الخلفية النظرية لطريقة الانتروبيا
هذا القسم اختياري ويتم تضمينه للقراء المهتمين بسبب عمل الطريقة. إذا كنت ترغب في ذلك ، يمكنك الرجوع إلى الورقة الأصلية على طريقة الانتروبيا المتقاطعة.
يكمن أساس طريقة الانتروبيا في نظرية أهمية أخذ العينات ، والتي تنص على ما يلي:
في حالة التعلم العميق (H (x هي قيمة مكافأة تم الحصول عليها من خلال بعض السياسات ، و (x ، و p (x هي توزيع لجميع السياسات الممكنة. لا نريد تعظيم مكافأتنا من خلال البحث في جميع السياسات الممكنة ؛ بدلاً من ذلك ، نريد إيجاد طريقة لتقريب
(p(x)H(x بواسطة (q(x ، وتقليل المسافة بينهما بشكل متكرر. يتم حساب المسافة بين توزيعين احتماليين بواسطة اختلاف Kullback-Leibler (KL) ، وهو كالتالي:
يُطلق على المصطلح الأول في KL اسم إنتروبيا ولا يعتمد على (p2(x ، لذلك يمكن حذفه أثناء التصغير. يُطلق على المصطلح الثاني الانتروبيا ، وهو هدف تحسين شائع جدًا في التعلم العميق.
بدمج كلتا الصيغتين ، يمكننا الحصول على خوارزمية تكرارية تبدأ بـ (q0 (x) = p (x وفي كل خطوة تتحسن. هذا تقريب لـ p (x)H(x مع تحديث:
هذه طريقة إنتروبيا عامة يمكن تبسيطها بشكل كبير في حالة التعلم العميق. أولاً ، نستبدل (H(x بدالة مؤشر(indicator function) ، وهي 1 عندما تكون مكافأة الحلقة أعلى من الحد الأدنى و 0 عندما تكون المكافأة أقل. سيبدو تحديث سياستنا على النحو التالي:
بالمعنى الدقيق للكلمة ، فإن الصيغة السابقة تتجاوز مصطلح التسوية، لكنها لا تزال تعمل في الممارسة بدونها. لذا ، فإن الطريقة واضحة تمامًا: فنحن نقوم بأخذ عينات من الحلقات باستخدام سياستنا الحالية (بدءًا من بعض السياسات الأولية العشوائية) وتقليل احتمالية السجل السلبي لأنجح العينات وسياستنا.
إضافة تعليق