المحتوى عن 'تعلم الآلة'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
    • HTML5
    • إطار عمل Bootstrap
  • CSS
    • Sass
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • خوادم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)
  • مقالات DevOps عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عمل حر عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • أساسيات استعمال الحاسوب
  • مقالات عامة

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 2 نتائج

  1. يُعَد التعلم المعزز مجالًا فرعيًا من فروع نظرية التحكم، والتي تُعنى بالتحكم في الأنظمة التي تتغير بمرور الوقت؛ وتشمل عِدّة تطبيقاتٍ من بينها: السيارات ذاتية القيادة، وبعض أنواع الروبوتات مثل الروبوتات المُخصصة للألعاب. وسنستخدم في هذا الدرس التعلم المعزز لبناء روبوتٍ لألعاب الفيديو المُخصصة لأجهزة آتاري Atari، حيث لن يُمنح هذا الروبوت إمكانية الوصول إلى المعلومات الداخلية للعبة، وإنما سُيمنح إمكانية الوصول لنتائج اللعبة المعروضة على الشاشة، وكذا المكافآت الناتجة عن هذه اللعبة فقط؛ أي أنه سيرى ما يراه أي شخصٍ سيلعبُ هذه اللعبة. في مجال تعلّم الآلة، يُعرَّف الروبوت Bot بأنه الوكيل Agent، وسيكون الوكيل بمثابة لاعبٍ في النظام، بحيث يعمل وفقًا لدالة اتخاذ القرار، ويُطلق عليها اسم خطة Policy، وهدفنا الأساسي هو تطوير روبوتاتٍ ذكيةٍ من خلال تعليمهم وإمدادهم بقدراتٍ قويةٍ تقدر على اتخاذ القرار. وسنبدأ من خلال تدريب روبوتٍ بسيطٍ يعتمد على التعلم المعزز، إذ يقوم بحركاتٍ عشوائيةٍ عند ممارسته للعبة غزاة الفضاء Space Invaders، وهي لعبةٌ مخصصةٌ لجهاز أتاري أركايد Atari Arcade الكلاسيكي، وهذه اللعبة ستكون بمثابة عنصر الموازنة الرئيسي. سنكتشف لاحقًا العديد من التقنيات الأخ_رى مثل التعلم المعزز وفق النموذج الحُر Q-learning، والتعلم المعزز العميق وفق النموذج الحُر Deep Q-learning، وكذا منهجية المربعات الصغرى، كما سنتعرف على بيئة الألعاب البسيطة Gym، وهي تحتوي على مجموعة أدواتٍ للتعلم المعزز التي طُورت من قِبل OpenAI، وذلك أثناء بنائنا للروبوتات التي ستلعبُ لعبة غزاة الفضاء Space Invaders ولعبة البحيرة المتجمدة Frozen Lake. باتباعك لهذا الدرس، ستفهمُ الأفكار الأساسية التي تَحكمُ طريقة مفاضلة الشخص أثناء اختياره درجة تعقيد نموذج تعلم الآلة الذي يستخدمه. المتطلبات الرئيسية لإكمال هذا الدرس بنجاحٍ، ستحتاج إلى تجهيز بيئة العمل المكونة من: خادم يعمل بنظام أوبونتو 18.04، وبذاكرة وصولٍ عشوائيٍ RAM لا تقِل عن 1 غيغابايت، ويجب أن يحتوي الخادم على مستخدمٍ غير مسؤولٍ non-root user وبصلاحيات sudo المُهيّأة مسبقًا، وجدار حمايةٍ مهيئٍ باستخدام UFW. وللمزيد، يمكنك الاطلاع على كيفية التهيئة الأولية لخادم أوبونتو 18.04. تثبيت بايثون 3 وإعداد بيئته البرمجية، وبإمكانك معرفة كيفية تثبيت بايثون 3 وإعداد بيئة برمجية على أوبنتو 18.04. وإذا كنت تستخدم جهازًا محليًا، فيمكنك تثبيت بايثون 3 محليًا خطوةً بخطوة، باطلاعك على كيفية تثبيت بايثون 3 وإعداد بيئته البرمجية على ويندوز 10. 1. إنشاء المشروع وتثبيت التبعيات أعِدَّ بيئة التطوير الخاصة بك لبرمجة الروبوتات، وذلك من خلال تحميل اللعبة ذاتها والمكتبات اللازمة للعمليات الحسابية الخاصة بها. وسنبدأ بإنشاء مساحة عملٍ لهذا المشروع، ولتكن AtariBot، هكذا: mkdir ~/AtariBot وسننتقل بعدها إلى دليل AtariBot الجديد عن طريق إنشاءه، هكذا: cd ~/AtariBot ثم سنُنشئ بيئةً افتراضيةً جديدةً للمشروع، ويمكنك تسمية هذه البيئة الافتراضية بأي اسمٍ تريده، وسنطلق عليها اسم ataribot: python3 -m venv ataribot وسنُنشط هذه البيئة الافتراضية هكذا: source ataribot/bin/activate في نظام أوبونتو بدءًا من الإصدار 16.04 فما فوق، ستتطلبُ حزمة OpenCV تثبيت بعض الحِزم الإضافية الأخرى لتعمل جيدَا، ومن ضمنها CMake وهو تطبيقٌ يدير عملية البناء البرمجية، بالإضافة لحزمة مدير الجلسة session manager، وحزمة امتداداتٍ متنوعةٍ، وحزمة مؤلف الصور الرقمية digital image composition. وسنُنفذ الأمر التالي لتثبيت هذه الحزم: sudo apt-get install -y cmake libsm6 libxext6 libxrender-dev libz-dev سيكون تثبيت هذه الحزمة من خلال كتابة هذا الأمر: brew install cmake ومن ثَمّ سيُستخدم مدير الحزم pip الخاص بلغة بايثون لتثبيت الحزمة wheel، وهي المعيار الجديد المعتمد لنشر برمجيات وشيفرات لغة بايثون، وتتضمن أداة سطر أوامرٍ للعمل مع ملفات ‎.whl: python -m pip install wheel بالإضافة إلى الحزمة wheel، ستحتاج لتثبيت الحزم التالية: Gym: وهي مكتبة بايثون تجعل الألعاب المختلفة متاحةً للأبحاث، وكذا جميع التبعيات الخاصة بالألعاب المخصصة لأجهزة أتاري، والتي طورتها شركة OpenAI. حيث تقدم مكتبة Gym معاييرًا عامة لكلّ لعبةٍ، وذلك لتمكننا من تقييم أداء الروبوتات والخوارزميات المختلفة بطريقةٍ موحدةٍ. Tensorflow: وهي مكتبة التعلم العميق المقدمة من غوغل، تتيح لنا إجراء العمليات الحسابية بطريقةٍ أكثر كفاءةً عن ذي قبل، حيث تؤدي ذلك من خلال بناء دوال رياضيةً باستخدام التجريدات الخاصة بمكتبة Tensorflow تحديدًا ، كما تعمل حصريًا على وِحدة المعالجة الرسومية GPU الخاصة بك. OpenCV: وهي مكتبةٌ خاصةٌ بالرؤية الحاسوبية التي ذكرناها سابقًا. SciPy: وهي مكتبة للحوسبة العلمية، وتوفر خوارزميات تحسين الكفاءة. NumPy: وهي مكتبة الجبر الخطي. وسنُثبت كلّا من هذه الحزم من خلال الأمر التالي، لاحظ أن هذا الأمر يحدد إصدار كلّ حزمة: python -m pip install gym==0.9.5 tensorflow==1.5.0 tensorpack==0.8.0 numpy==1.14.0 scipy==1.1.0 opencv-python==3.4.1.15 بعد ذلك، سنستخدم مدير الحزم pip مرةً أخرى، وذلك لتثبيت البيئات الخاصة بأجهزة أتاري، والتي تتضمن مجموعةً متنوعةً من ألعاب الفيديو المُخصصة لهذا الجهاز، والتي من ضمنها لعبة غزاة الفضاء Space Invaders، هكذا: python -m pip install gym[atari] وفي حال نجحت عملية تثبيت الحزمة gym[atari]‎، فستظهر لك النتيجة التالية: Installing collected packages: atari-py, Pillow, PyOpenGL Successfully installed Pillow-5.4.1 PyOpenGL-3.1.0 atari-py-0.1.7 وبتثبيت هذه التبعيات، سنستعد للمضي قُدمًا في بناء روبوتٍ يلعب عشوائيًا ليكون بمثابة عنصر الموازنة الرئيسي. 2. إنشاء روبوت عشوائي أولي من خلال مكتبة Gym الآن بعد أن أَوجدْت البيئة البرمجية المطلوبة على خادمك، سنهيّئ روبوتًا للعب نسخة مبسطة من ألعاب جهاز أتاري الكلاسيكية، وسنستخدم لعبة غزاة الفضاء بالنسبة لهذه التجربة، ومن الضروري الحصول على روبوتٍ أوليٍ لمساعدتك على فهم مدى أداء نموذجك. ونظرًا لأن هذا الروبوت سينفّذ أفعالًا عشوائيةً في كلّ إطارٍ؛ فسوف نشير إليه بالروبوت الأولي العشوائي، وفي هذه الحالة سنوازن هذا الروبوت الأساسي مع الروبوتات الأخرى، وذلك لفهم مدى جودة أداء الروبوتات الخاصة بك في الخطوات اللاحقة. ويمكنك من خلال مكتبة Gym الحفاظ على حلقة اللعبة الخاصة بك، وهذا يعني أنك ستتحكم في كلّ خطوةٍ من خطوات تنفيذ اللعبة؛ ففي كلّ خطوةٍ زمنيةٍ ستَمنح مكتبة Gym فعلًا جديدًا، كما ستطلب من مكتبة Gym تحديد حالة اللعبة. وفي هذا الدرس، ستكون حالة اللعبة هي مظهر اللعبة في خطوةٍ زمنيةٍ معينة، وهي ما ستراه بالضبط إذا كنت تلعب اللعبة. سنُنشئ ملف بايثون وليكن bot_2_random.py من خلال محرر النصوص المفضل لديك، وسنستخدم المحرر nano في هذا الدرس: nano bot_2_random.py سنبدأ هذا السكربت بإضافة الأسطر المميزة التالية، بحيث تتضمن تعليقًا كتليًا لشرح ما ستفعله هذه الشيفرة البرمجية، وكذا تعليمتيْ الاستيراد import، واللتان تستوردان الحزم التي سيحتاجها هذا السكربت لكي يعمل بكفاءةٍ وفق المطلوب: /AtariBot/bot_2_random.py """ الروبوت 2 - أنشئ روبوت عشوائي للعبة غزاة الفضاء """ import gym import random وسنُضيف الدالة main التي ستُنشئ بيئة لعبة غزاة الفضاء SpaceInvaders-v0 المناسبة، ومن ثَمّ نُعدّها من خلال env.reset: /AtariBot/bot_2_random.py . . . import gym import random def main(): env = gym.make('SpaceInvaders-v0') env.reset() بعد ذلك، سنُضيف الدالة env.step، والتي يمكن أن تُعيد أي قيمةٍ من القيم التالية: الحالة state: وهي الحالة الجديدة للعبة بعد تطبيق فعلٍ معينٍ من جهةٍ ما. المكافأة reward: وهي الزيادة في الدرجة المترتبة عن فعلٍ معينٍ، مثل إطلاق رصاصةٍ أو تدمير رصاص العدو، وستزداد النتيجة بمقدار 50 نقطة، بحيث ستكون قيمة المكافأة reward=50. وفي نمط الألعاب المعتمدة على النقاط، يكون هدف اللاعب هو تعظيم المكافأة الإجمالية، بزيادة مجموع النقاط لأقصى درجةٍ ممكنةٍ. حالة انتهاء اللعبة done: سواءٌ إنتهت الحلقة أم لا، وتحدث عادةً عندما يخسر اللاعب ويفقد جميع الفرص المتاحة له. المعلومات info: وهي المعلومات الجانبية، وسنتجاوز شرحها حاليًا. وستُستخدم قيمة المكافأة لحساب إجمالي مكافأتك، كما ستستخدم حالة انتهاء اللعبة done لمعرفة كيفية تحديد متى يخسر اللاعب بموته، وهذا يحدث عندما تُعيد done القيمة True. سنُضيف الحلقة التالية إلى اللعبة، والتي ستُبقي اللعبة مستمرة إلى أن يخسر اللاعب بموته: /AtariBot/bot_2_random.py . . . def main(): env = gym.make('SpaceInvaders-v0') env.reset() episode_reward = 0 while True: action = env.action_space.sample() _, reward, done, _ = env.step(action) episode_reward += reward if done: print('Reward: %s' % episode_reward) break وأخيرًا سنُشغّل الدالة main، وذلك لنضمن عملية فحص __name__ للتأكد من أن الدالة main ستعمل فقط عند استدعائها مباشرةً، وذلك من خلال الأمر python bot_2_random.py. لاحظ: إذا لم تُضف الجملة الشرطية if، فستُشغّل الدالة main دائمًا عند كلّ تنفيذٍ لملف البايثون، حتى عند استيرادِك للملف، ولذا فإن من بين الممارسات الجيدة، وضع الشيفرة البرمجية في الدالة main، وتنفيذها فقط عندما يتحقق الشرط ‎__name__ =='__main__'‎، هكذا: /AtariBot/bot_2_random.py . . . def main(): . . . if done: print('Reward %s' % episode_reward) break if **name** == '**main**': main() ستحفظ التغييرات المُطبقة على الملف، وستخرج من المحرّر nano بالضغط على CTRL+X+Y، ثم تضغط على ENTER. وبعد ذلك سنُنفذ السكربت بكتابة الأمر التالي، هكذا: python bot_2_random.py وستظهر النتيجة في صورة رقمٍ مشابهٍ للرقم التالي، ولاحظ أنه في كلّ مرة نشغّل الملف، سنحصل على نتيجةٍ مختلفةٍ: Making new env: SpaceInvaders-v0 Reward: 210.0 وهذه النتائج العشوائية تُمثل مشكلة، ولإنتاج عملٍ يستفيد منه الباحثون والممارسون الآخرين في هذا المجال، لا بدّ من أن تكون نتائجك وتجاربك قابلةُ للتكرار. ولتصحيح ذلك،سنُعيد فتح السكربت: nano bot_2_random.py سنضيف random.seed(0)‎ بعد تعليمة الاستيراد import random، كما سنضيف env.seed(0)‎ بعد التعليمة env=gym.make('SpaceInvaders-v0')‎، إذ تعمل هذه البذور seeds معًا على تزويد البيئة بنقطة بدايةٍ متساويةٍ، مما يضمن أن النتائج ستكون دائمًا قابلةً للتكرار. وسيتطابق ملفك النهائي مع هذا الملف تمامًا: /AtariBot/bot_2_random.py """ Bot 2 -- Make a random, baseline agent for the SpaceInvaders game. """ import gym import random random.seed(0) def main(): env = gym.make('SpaceInvaders-v0') env.seed(0) env.reset() episode_reward = 0 while True: action = env.action_space.sample() _, reward, done, _ = env.step(action) episode_reward += reward if done: print('Reward: %s' % episode_reward) break if **name** == '**main**': main() وسنحفظَ التغييرات التي أجريناها على الملف، كما سنُغلق المحرّر، ومن ثم سنشغّل السكربت بكتابة الأمر التالي: python bot_2_random.py وسيؤدي ذلك لإظهار نتائج مطابقةً تمامًا للنتيجة التالية: Making new env: SpaceInvaders-v0 Reward: 555.0 تهانينا بهذا تكون قد برمجت روبوتك الأول، ومع ذلك فهو غير ذكيٍ إلى حدٍ ما، وذلك لأنه لا يُراعي البيئة المحيطة عند اتخاذه للقرارات، لذا فللحصول على تقييمٍ أكثر موثوقية لأداء الروبوت الخاص بنا، يمكننا تشغيل الروبوت لمراحل متعددةً في المرة الواحدة، وسنحسب المتوسط الحسابي للمكافآت عبر تلك المراحل المتعددة. ولتهيئة هذا الأمر، سنُعيد فتح الملف أولًا: nano bot_2_random.py كما سنضيف بعد التعليمة ‫random.seed(0)‎، السطر التالي، وهو الذي سيطلب من الروبوت لعب اللعبة لمدة 10 مراحل: /AtariBot/bot_2_random.py . . . random.seed(0) num_episodes = 10 . . . أنشئ قائمةً جديدةً للمكافآت مباشرة بعد التعليمة env.seed(0)‎، هكذا: /AtariBot/bot_2_random.py . . . env.seed(0) rewards = [] . . . ثُم شعّب الشيفرة البرمجية ابتداءً من التعليمة env.reset()‎ وصولًا إلى نهاية التابع main()‎ في حلقة for، مع تكرار مرات اللعب بحسب num_episodes. تأكد من وضع مسافةٍ بادئةٍ بحيث تتكون من أربع مسافاتٍ لكلّ سطرٍ، انطلاقًا من عند التعليمة env.reset()‎ وصولًا إلى التعليمة break، هكذا: /AtariBot/bot_2_random.py . . . def main(): env = gym.make('SpaceInvaders-v0') env.seed(0) rewards = [] for _ in range(num_episodes): env.reset() episode_reward = 0 while True: ... سنضيف مكافأة المرحلة الحالية إلى القائمة التي تحتوي جميع المكافآت، وذلك في السطر الأخير من حلقة اللعبة الموجودة في التابع main قبل التعليمة break مباشرةً، هكذا: /AtariBot/bot_2_random.py . . . if done: print('Reward: %s' % episode_reward) rewards.append(episode_reward) break . . . وفي نهاية الدالة main، سنطبع متوسط المكافآت: /AtariBot/bot_2_random.py . . . def main(): ... print('Reward: %s' % episode_reward) break print('Average reward: %.2f' % (sum(rewards) / len(rewards))) . . . والآن سيتماشى ملفك مع هذه الشيفرة البرمجية. لاحظ أن الشيفرة البرمجية تتضمن بعض التعليقات، وذلك لتوضيح الأجزاء الرئيسية من السكربت: /AtariBot/bot_2_random.py """ Bot 2 -- Make a random, baseline agent for the SpaceInvaders game. """ import gym import random random.seed(0) # make results reproducible num_episodes = 10 def main(): env = gym.make('SpaceInvaders-v0') # create the game env.seed(0) # make results reproducible rewards = [] for _ in range(num_episodes): env.reset() episode_reward = 0 while True: action = env.action_space.sample() _, reward, done, _ = env.step(action) # فعل عشوائي episode_reward += reward if done: print('Reward: %d' % episode_reward) rewards.append(episode_reward) break print('Average reward: %.2f' % (sum(rewards) / len(rewards))) if __name__ == '__main__': main() وسنحفظ التغييرات التي أجريناها على الملف، وسنخرج من المحرّر ونُنفّذ السكربت، هكذا: python bot_2_random.py سيؤدي هذا إلى طباعة متوسط المكافآت، وستكون النتيجة مشابهةً تمامًا للنتيجة التالية: Making new env: SpaceInvaders-v0 . . . Average reward: 163.50 أصبح الآن لدينا تقييمٌ أكثر موثوقيةً للنتيجة الأولية. ولإنشاء روبوتٍ أفضل، سنحتاج لفهم إطار عمل التعلم المعزز، وكيف يمكن للمرء أن يجعل الفكرة المجردة (القدرة اتخاذ القرار) أكثر واقعية؟ فهم التعلم المعزز يكون هدف اللاعب في أي لعبة هو زيادة درجاته، وسنشير في هذا الدرس لنتيجة اللاعب على أنها مكافأته، ولتعظيم المكافأة يجب على اللاعب أن يكون قادرًا على تحسين قدراته في اتخاذ القرارات، وبصيغةٍ أكثر رسميةٍ، فإن القرار هو عملية النظر إلى اللعبة، أو مراقبة حالة اللعبة، واختيار فعلٍ معينٍ مناسبٍ لهذه الحالة، تُسمى دالة اتخاذ القرار لدينا بالسياسة policy، وهي تقبل الحالات state مثل مدخلاتٍ لها، وتُقرر بدورها فعلًا معينًا لفعله: policy: state -> action ولبناء مثل هذه الدالة، سنبدأ بمجموعةٍ محددةٍ من خوارزميات التعلم المعزز، وهي خوارزميات Q-learning. ولتوضيح ذلك، ضع في حُسبانك الحالة الأولية للعبة، والتي سوف نُسميها الحالة الصفرية state0، ومعناها أن جميع السفن الفضائية والأعداء في مواقع انطلاقهم، ثم سنفترض أن لدينا إمكانية الوصول إلى الجدول السحري Q-table، والذي سيخبرنا بمقدار المكافأة التي سنحصل عليها بعد كلّ فعلٍ معينٍ، كما يظهر بالجدول التالي: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المكافأة الفعل الحالة 10 اطلاق نار الحالة "state0" 3 الاتجاه لليمين الحالة "state0" 3 الاتجاه لليسار الحالة "state0" وسنلاحظ أن فعل إطلاق النار shoot سيُعظّم مكافأتك، إذ ستنتجُ عنه المكافأةً الأعلى القيمة، وهي 10. وكما ترى، يوفر لنا الجدول السحري Q-table، طريقةً بسيطةً لاتخاذ القرارات بناءً على الحالة المرصودة، هكذا: policy: state -> look at Q-table, pick action with greatest reward غير أن معظم الألعاب الحالية تحتوي على حالاتٍ كثيرةٍ جدًا لإدراجها في جدول، ففي مثل هذه الحالات يتعلم الروبوت من الدالة Q-function، بدلًا من تعلمه من الجدول السحري Q-table. وتُستخدم الدالة Q-function بطريقةٍ مماثلةٍ لكيفية استخدامنا للجدول السحري Q-table. سنعيد كتابة المدخلات مثل دوال إلى الجدول، وهذا سيعطينا النتيجة التالية: Q(state0, shoot) = 10 Q(state0, right) = 3 Q(state0, left) = 3 يبدو من السهل علينا اتخاذ قرارٍ عند إعطائنا حالةً معينةً، حيث سننظر ببساطةٍ لكلّ فعلٍ ممكنٍ والمكافأة المقابلة له، ومن ثَم سنتخذ الفعل الذي قد يتوافق مع أعلى مكافأةٍ مُتوقعة. سنعيد صياغة الخطة السابقة بطريقةٍ أكثر كفاءةً، وسيكون لدينا: policy: state -> argmax_{action} Q(state, action) هذا يفي بمتطلبات دالة اتخاذ القرار، إذ يمكنها اتخاذ قرار المناسب بالنظر فقط لحالة اللعبة، غير أن هذا الحل سيعتمد على معرفة قيم الحالة والفعل Q(state, action)‎ لكلّ حالةٍ وفعلٍ، ومن أجل تقدير قيم Q(state, action)‎، سنضع في حُسباننا ما يلي: عند مشاهدة العديد من الملاحظات حول حالات الروبوت وأفعاله ومكافآته، يمكن للمرء تقدير المكافأة المناسبة لكلّ حالةٍ وفعل معينٍ من خلال أخذ المتوسط الحسابي للمكافأة الحالية. تُعَد لعبة غزاة الفضاء Space Invaders من الألعاب ذات المكافأة المتأخرة؛ أي أن اللاعب يكافَأ عندما يفجّر أحد الأعداء وليس بمجرد إطلاقه للنار فقط، ومع ذلك فإن قرار اللاعب بإطلاق النار هو الدافع الصحيح للحصول على المكافأة، لذا بطريقةٍ أو بأخرى، يجب على الدالة Q-function أن تمنح الحالة المرتبطة بفعل الإطلاق (state0، shoot) مكافأةً إيجابيةً. وهاتين الملاحظتين ستُشكّلان المعادلات التالية: Q(state, action) = (1 - learning_rate) * Q(state, action) + learning_rate * Q_target Q_target = reward + discount_factor * max_{action'} Q(state', action') وتستخدم المعادلات السابقة التعريفات التالية: الحالة state: وهي وضع اللعبة في الخطوة الزمنية الحالية، وقد تعبّر عن وهو الوضع الجديد للخطوة التالية، وذلك عند اتخاذنا لفعلٍ معين. الفعل action: وهو الفعل المُتخذّ في الخطوة الزمنية الحالية، كما يعبّر عن جميع الإجراءات الممكنة. المكافأة reward: وهي مكافأة الخطوة الزمنية الحالية. معدل التعلم learning_rate: وهو معدل تعلم الروبوت في الخطوة الحالية. معامل النقص discount_factor: وهو كمية تناقص المكافأة عند نشرها. وللمزيد من الشرح المفصل حول المعادلتين السابقتين، سنحيلك إلى آلية التعلم المعزز وفق النموذج الحر. بفهمنا لآلية التعلم المعزز وفق النموذج الحر فهمًا صحيحًا، سيكون كلّ ما بقي لدينا الآن تشغيل اللعبة والحصول على تقديرات القيم Q-value لهذه الخطة الجديدة. 3. إنشاء روبوت بسيط باستخدام التعلم المعزز وفق النموذج الحر للعبة البحيرة المتجمدة الآن وبما أن لدينا روبوتًا أوليًا، يمكننا إنشاء روبوتاتٍ جديدةٍ وموازنة كلّ منهم بالروبوت المبدئي، وسنُنشئ في هذه الخطوة روبوتًا يعتمد على التعلم المعزز وفق النموذج الحرّ Q-learning، وهي طريقة من طُرق التعلم المعزز، حيث تُستخدم لتعليم الروبوت أيُّ الأفعال سيتخذها في حالةٍ معينةٍ، وسيلعب هذا الروبوت لعبةً جديدةً هي لعبة البحيرة المتجمدة Frozen Lake، وطريقة الإعداد لهذه اللعبة مشروحةٌ في موقع Gym كما يلي: سيوصف سطح البحيرة باستخدام شبكةٍ كما يلي: SFFF (S: starting point, safe) FHFH (F: frozen surface, safe) FFFH (H: hole, fall to your doom) HFFG (G: goal, where the frisbee is located) سيبدأ اللاعب في الجهة العلوية من ناحية اليسار، وسيُشار إليه بالرمز S، ثم سيشقُ طريقه للهدف الموجود في الجهة السفلية من ناحية اليمين، وسيُشار إليه بالرمز G. الحركة المتاحة هي الاتجاه لليمين أو لليسار أو للأعلى أو للأسفل، وذلك للوصول إلى الهدف وتحقيق النتيجة 1. وهناك عددٌ من الثقوب التي سيُشار إليها بالرمز H، حيث أن سقوط اللاعب في إحدى الثقوب سيؤدي لتحقيقه للنتيجة 0. في هذا القسم، سننفذ روبوتًا بسيطًا يعتمد على التعلم المعزز وفق النموذج الحر Q-learning، ومن خلال ما تعلمناه سابقًا، سنُنشئ روبوتًا يبدل بين عمليتيْ الاستكشاف والاستغلال Exploration and Exploitation لحالة اللعبة، وتُعني عملية الاستكشاف في هذا السياق بأن الروبوت سينفذّ فعلًا عشوائيًا، بينما تُعني عملية الاستغلال بأنه يستخدم جدول القيم Q-values الخاص به، وذلك لاختيار ما يعتقد أنه الإجراء الأمثل. سنُنشئ كذلك جدولًا للاحتفاظ بالقيم Q-values، وسنحدّثه دوريًا كلما نفّذ الروبوت فعلًا معينًا أوتعلم شيئًا جديدًا. سنأخُذَ نُسخةً من السكربت الذي عملنا عليه من الخطوة 2، هكذا: cp bot_2_random.py bot_3_q_table.py وسنفتحَ هذا الملف الجديد لتحريره: nano bot_3_q_table.py سنبدأ تعديل الملف بتعديل التعليق الموجود في أعلى الملف، والذي يصف الغرض الأساسي من السكربت، ونظرًا لأن هذا تعديلًا لتعليق، فإنه ليس ضروريًا لعمل السكربت بصورةٍ صحيحةٍ، إلا أنه سيكون مفيدًا لتتبع ما يفعله هذا السكربت: /AtariBot/bot_3_q_table.py """ Bot 3 -- Build simple q-learning agent for FrozenLake """ . . . وقبل إجراء تعديلاتٍ وظيفيةٍ على السكربت، سنحتاج لاستيراد مكتبة numpy الخاصة بأدوات الجبر الخطية، وذلك بإضافة السطر التالي أسفل عملية استيراد مكتبة Gym مباشرةً، هكذا: /AtariBot/bot_3_q_table.py """ Bot 3 -- Build simple q-learning agent for FrozenLake """ import gym import numpy as np import random random.seed(0) # make results reproducible . . . كما سنُضيف المولّد العشوائي random.seed(0)‎ الخاص بمكتبة numpy، هكذا: /AtariBot/bot_3_q_table.py . . . import random random.seed(0) # make results reproducible np.random.seed(0) . . . بعد ذلك، لتسهيل عملية الوصول لحالات اللعبة، سنعدِّل السطر الخاص بالتعليمة env.reset()‎ ليصبح كما سنراه، وسيُخزن المتغيّر state الحالة الأولية للعبة: /AtariBot/bot_3_q_table.py . . . for \_ in range(num_episodes): state = env.reset() . . . كما سنعدِّل السطر الخاص بالتعليمة env.step(...)‎ ليصبح كما سنراه، وسيخزن المتغيّر state2 الحالة التالية، إذ سنحتاج كلًا من المتغيّر state لأجل الحالة الحالية، إلى جانب المتغيّر state2 لأجل الحالة التالية، وذلك لتعديل Q-function، هكذا: /AtariBot/bot_3_q_table.py . . . while True: action = env.action_space.sample() state2, reward, done, _ = env.step(action) . . . سنُضيف سطرًا جديدًا بعد التعليمة episode_reward += reward لتعديل المتغيّر state، وهذا السطر سيجعل المتغيّر state مُحدِّثًا للتكرار التالي، إذ نتوقع دائمًا من المتغيّر state أن يخبرنا بالحالة الحالية: /AtariBot/bot_3_q_table.py . . . while True: . . . episode_reward += reward state = state2 if done: . . . سنحذف عملية الطباعة print من الكتلة if done، والتي ستطبعُ المكافأة الخاصة بكلّ حلقة، وبدلًا من ذلك سنطبعُ متوسط المكافآت للعديد من الحلقات، وبعد تعديلنا للكتلة if done؛ ستبدو على النحو التالي: /AtariBot/bot_3_q_table.py . . . if done: rewards.append(episode_reward) break . . . وبعد هذه التعديلات، ستكون حلقة اللعبة هكذا: /AtariBot/bot_3_q_table.py . . . for _ in range(num_episodes): state = env.reset() episode_reward = 0 while True: action = env.action_space.sample() state2, reward, done, _ = env.step(action) episode_reward += reward state = state2 if done: rewards.append(episode_reward)) break . . . بعد ذلك سنُضيف ميزةً إضافيةً للروبوت تمكنّه من التبديل بين عمليتيْ الاستكشاف والاستغلال، حيث سنُنشئ جدول القيم Q-value مباشرةً قبل حلقة اللعبة الرئيسية، وهي التي تبدأ بالكلمة المفتاحية for...‎.، هكذا: /AtariBot/bot_3_q_table.py . . . Q = np.zeros((env.observation_space.n, env.action_space.n)) for _ in range(num_episodes): . . . ثم سنُعيد كتابة الحلقة for، وذلك لكشف رقم المرحلة: /AtariBot/bot_3_q_table.py . . . Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): . . . تُنشئ الحلقة الداخلية while True:‎ للعبة تشويشًا، كما تُصدِر أحيانًا بياناتٍ عشوائيةٍ بلا معنى، ويحدث ذلك عند تدريب الشبكات العصبية العميقة بسبب قدرتها على تحسين كل من أداء النموذج ودقته. لاحظ أنه كلما زادت الضوضاء، قلت القيم في Q[state, :]‎، ونتيجةً لذلك، فكلما زاد التشويش زاد احتمال تصرف الروبوت بطريقةٍ مستقلةٍ عن معرفته باللعبة. وبعبارة أخرى، يشجع التشويش العالي الروبوت على استكشاف الأفعال العشوائية: /AtariBot/bot_3_q_table.py . . . while True: noise = np.random.random((1, env.action_space.n)) / (episode**2.) action = env.action_space.sample() . . . سنلاحظ أنه مع زيادة المراحل episodes، فإن كمية التشويش تنخفض تربيعيًا بمرور الوقت، كما ستنخفض عملية استكشاف الروبوت لأنه سيثق في تقييمه الخاص لمكافأة اللعبة، وسيبدأ في استغلال معرفته بها. سنُحدّث السطر الذي يحتوي على الفعل action، إذ سيختار الروبوت الفعل المناسب وفقًا لجدول القيم Q-value، مع بعض الاستكشافات المضمّنة: /AtariBot/bot_3_q_table.py . . . noise = np.random.random((1, env.action_space.n)) / (episode**2.) action = np.argmax(Q[state, :] + noise) state2, reward, done, _ = env.step(action) . . . وستكون حلقة اللعبة الرئيسية مشابهةٌ لما يلي: /AtariBot/bot_3_q_table.py . . . Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): state = env.reset() episode_reward = 0 while True: noise = np.random.random((1, env.action_space.n)) / (episode**2.) action = np.argmax(Q[state, :] + noise) state2, reward, done, _ = env.step(action) episode_reward += reward state = state2 if done: rewards.append(episode_reward) break . . . سنُحدث جدول القيم Q-value الخاص بك، باستخدام معادلة بيلمان Bellman، وهي معادلةٌ مستخدمةٌ على نطاقٍ واسعٍ في مجال تعلّم الآلة، وذلك للعثور على الخطة المُثلى في بيئةٍ معينةٍ. تتضمن معادلة بيلمان فكرتين ترتبطان بهذا المشروع وهما: سيؤدي اتخاذ فعلٍ معينٍ في حالةٍ معينةٍ عدة مراتٍ إلى تقديرٍ جيدٍ للقيمة Q-value المرتبطة بهذه الحالة والفعل، ولنُحقّق هذه النتيجة، سنزيد من عدد المراحل التي يجب أن يلعبها هذا الروبوت، وذلك من أجل إعادة تقديرٍ أقوى وأدق للقيمة Q-value. يجب نشر المكافآت بمرور الوقت، وذلك لئلا تُسند للفعل الأصليّ مكافأةً صفرية، وهذه الفكرة تظهر جليًا في الألعاب ذات المكافآت المتأخرة؛ مثلما يحدث في لعبة غزاة الفضاء، ففيها يُكافأ اللاعب عندما يفجر العدو، وليس بمجرد إطلاق النار عليه فقط، وبالرغم من ذلك فإن إطلاق النار هو الدافع الحقيقي للمكافأة. وبالمثل، يجب تخصيص مكافأةٌ إيجابيةٌ للدالة state0, shoot في Q-function. ففي البداية سنحدّث قيمة المتغيّر num_episodes لتصبح 4000، هكذا: /AtariBot/bot_3_q_table.py . . . np.random.seed(0) num_episodes = 4000 . . . ثم سنُضيف الوسطاء الفائقة hyperparameters الضرورية إلى أعلى الملف على شكل متغيّرين جديدين: /AtariBot/bot_3_q_table.py . . . num_episodes = 4000 discount_factor = 0.8 learning_rate = 0.9 . . . وسنحسبُ قيمة Q-value الخاصة بالهدف، مباشرةً بعد السطر الذي يحتوي على التعليمة env.step(...)‎، هكذا: /AtariBot/bot_3_q_table.py . . . state2, reward, done, _ = env.step(action) Qtarget = reward + discount_factor * np.max(Q[state2, :]) episode_reward += reward . . . كما سنحدّث جدول القيم Q-value باستخدام متوسط مرجح للقيم Q-value بالاعتماد على القيم القديمة والجديدة، وذلك بعد السطر الذي يحتوي على المتغيّر Qtarget، هكذا: /AtariBot/bot_3_q_table.py . . . Qtarget = reward + discount_factor * np.max(Q[state2, :]) Q[state, action] = ( 1-learning_rate ) * Q[state, action] + learning_rate * Qtarget episode_reward += reward . . . والآن سنتحقق من أن حلقة اللعبة الرئيسية مشابهةٌ للشيفرة التالية: /AtariBot/bot_3_q_table.py . . . Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): state = env.reset() episode_reward = 0 while True: noise = np.random.random((1, env.action_space.n)) / (episode**2.) action = np.argmax(Q[state, :] + noise) state2, reward, done, _ = env.step(action) Qtarget = reward + discount_factor * np.max(Q[state2, :]) Q[state, action] = ( 1-learning_rate ) * Q[state, action] + learning_rate * Qtarget episode_reward += reward state = state2 if done: rewards.append(episode_reward) break . . . وبذلك نكون قد أكملنا بنجاح المسار الذي نريده لتدريب الروبوت، وكلّ ما تبقى هو إضافة آليات إعداد التقارير. على الرغم من أن بايثون لا تفرض فحصًا صارمًا لنوع المتغيّر، إلا أننا سنُضيف أنواعًا محددةً لتصريحات الدوال الخاصة بنا، وذلك للمحافظة على نظافة الشيفرة البرمجية، ففي أعلى الملف وقبل السطر الأول لقراءة مكتبة Gym، سنستورد النوع قائمة List، هكذا: /AtariBot/bot_3_q_table.py . . . from typing import List import gym . . . وبعد المتغيّر learning_rate = 0.9 مباشرةً، سنُصرح عن شكل التقارير ومعدل إصدارها الزمني، وذلك خارج الدالة main، هكذا: /AtariBot/bot_3_q_table.py . . . learning_rate = 0.9 report_interval = 500 report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' def main(): . . . سنُضيف دالةً جديدةً قبل الدالة main، وبذلك ستُملأ السلسلة النصية للمتغير report باستخدام قائمةٍ مكونةٍ من جميع المكافآت: /AtariBot/bot_3_q_table.py . . . report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' def print_report(rewards: List, episode: int): """Print rewards report for current episode - Average for last 100 episodes - Best 100-episode average across all time - Average for all episodes across time """ print(report % ( np.mean(rewards[-100:]), max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]), np.mean(rewards), episode)) def main(): . . . والآن سنغيّر اسم اللعبة من SpaceInvaders إلى FrozenLake، هكذا: /AtariBot/bot_3_q_table.py . . . def main(): env = gym.make('FrozenLake-v0') # إنشاء اللعبة . . . وسنطبع متوسط المكافأة بعد التعليمة rewards.append(...)‎ من خلال آخر 100 مرحلة، كما سنطبع متوسط المكافآت لجميع المراحل، هكذا: /AtariBot/bot_3_q_table.py . . . if done: rewards.append(episode_reward) if episode % report_interval == 0: print_report(rewards, episode) . . . وفي نهاية الدالة main()‎، سنطبع كِلا المتوسطين مرةً أخرى باستبدال السطر print('Average reward:%.2f' % (sum(rewards) / len(rewards)))‎ بالسطر التالي: /AtariBot/bot_3_q_table.py . . . def main(): ... break print_report(rewards, -1) . . . بهذا سنكون قد أكملنا الروبوت المعتمد على التعلم وفق النموذج الحر Q-learning الخاص بنا، وتبقى لنا أن نتحقق من توافق شيفرتنا البرمجية مع الشيفرة التالية: /AtariBot/bot_3_q_table.py """ Bot 3 -- Build simple q-learning agent for FrozenLake """ from typing import List import gym import numpy as np import random random.seed(0) # make results reproducible np.random.seed(0) # make results reproducible num_episodes = 4000 discount_factor = 0.8 learning_rate = 0.9 report_interval = 500 report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' def print_report(rewards: List, episode: int): """Print rewards report for current episode - Average for last 100 episodes - Best 100-episode average across all time - Average for all episodes across time """ print(report % ( np.mean(rewards[-100:]), max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]), np.mean(rewards), episode)) def main(): env = gym.make('FrozenLake-v0') # create the game env.seed(0) # make results reproducible rewards = [] Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): state = env.reset() episode_reward = 0 while True: noise = np.random.random((1, env.action_space.n)) / (episode**2.) action = np.argmax(Q[state, :] + noise) state2, reward, done, _ = env.step(action) Qtarget = reward + discount_factor * np.max(Q[state2, :]) Q[state, action] = ( 1-learning_rate ) * Q[state, action] + learning_rate * Qtarget episode_reward += reward state = state2 if done: rewards.append(episode_reward) if episode % report_interval == 0: print_report(rewards, episode) break print_report(rewards, -1) if __name__ == '__main__': main() سنحفظ التغييرات في الملف، وسنخرج من المحرّر، ثم سنشغّل السكربت: python bot_3_q_table.py وستتطابقُ النتيجة مع ما يلي: 100-ep Average: 0.11 . Best 100-ep Average: 0.12 . Average: 0.03 (Episode 500) 100-ep Average: 0.25 . Best 100-ep Average: 0.24 . Average: 0.09 (Episode 1000) 100-ep Average: 0.39 . Best 100-ep Average: 0.48 . Average: 0.19 (Episode 1500) 100-ep Average: 0.43 . Best 100-ep Average: 0.55 . Average: 0.25 (Episode 2000) 100-ep Average: 0.44 . Best 100-ep Average: 0.55 . Average: 0.29 (Episode 2500) 100-ep Average: 0.64 . Best 100-ep Average: 0.68 . Average: 0.32 (Episode 3000) 100-ep Average: 0.63 . Best 100-ep Average: 0.71 . Average: 0.36 (Episode 3500) 100-ep Average: 0.56 . Best 100-ep Average: 0.78 . Average: 0.40 (Episode 4000) 100-ep Average: 0.56 . Best 100-ep Average: 0.78 . Average: 0.40 (Episode -1) لديك الآن أول روبوتٍ غير بسيطٍ للألعاب، لكن دعنا نضع قيمة متوسط المكافأة 0.78 في إطارها الصحيح، فوفقًا لصفحة اللعبة Frozen Lake في مكتبة Gym، فإن حل اللعبة يعني بلوغ المتوسط القيمة 0.78 في 100 مرحلة، وبشكلٍ غير رسميٍ. إن كلمة حل تعني إنهاء اللعبة بطريقةٍ جيدةٍ للغاية وليس في وقتٍ قياسيٍ، إذ إن روبوت Q-table قادرٌ على حل لعبة البحيرة المتجمدة في 4000 مرحلة. وعلى أيةٍ حال قد تكون هنا اللعبة أكثر تعقيدًا إذا استخدمنا جدولًا لتخزين جميع الحالات المحتملة، والتي يبلغ عددها 144 حالة، ولكن عندما ننظر للعبة إكس-أو tic tac toe، فسنلاحظ أنها تحتوي على 19683 حالةٍ ممكنةٍ. والأمر مماثل عند التفكير في لعبة غزاة الفضاء Space Invaders، إذ يوجد بها عددٌ كبيرٌ جدًا من الحالات المحتملة والتي لا يمكن حسابها. إن جدول Q-table ليس حلًا مستدامًا لأن الألعاب تزداد تعقيدًا، ولهذا السبب سنحتاج طريقةً من أجل تقريب جدول Q-table، وبمتابعتك لهذا الدرس ستُشاهد كيف سنُصمم في الخطوة التالية دالةً يمكنها قبول الحالات والأفعال مثل مدخلاتٍ، وكذا إخراج قيمة Q-value المناسبة. 4. بناء روبوت يعتمد على التعلم المعزز العميق وفق النموذج الحر للعبة البحيرة المتجمدة تتوقع الشبكة العصبية في التعلم المعزز القيم بطريقةٍ فعالةٍ بناءً على البيانات المدخلة، وهي متغيّر الفعل action ومتغيّر الحالة state، وذلك باستخدام جدولٍ خاصٍ لتخزين جميع القيم الممكنة، ولكن هذا يصبح غير مستقرٍ في حالة الألعاب المعقّدة. وبدلًا من ذلك يستخدم التعلم المعزز العميق شبكةً عصبيةً لتقريب قيم Q-function، وللمزيد يمكنك الاطلاع على Understanding Deep Q-Learning لفهمٍ أعمق لماهيّة التعلم المعزز العميق وفق النموذج الحر. سنستخدم مكتبة Tensorflow، وهي مكتبة للتعلم العميق التي سبق وأن ثبتناها في الخطوة 1، وسنُعيد تطبيق كلّ المنطق المُستخدم حتى الآن مع تجريدات مكتبة Tensorflow، كما سنستخدمُ الشبكة العصبية لتقريب قيم Q-function الخاصة بك، ومع ذلك ستكون شبكتنا العصبية بسيطةً للغاية، إذ سيكون ناتج Q(s)‎ هو حاصل ضرب المصفوفة W والمتغيّر المدخل s، ويُعرف هذا باسم الشبكة العصبية ذات الطبقة الواحدة متصلة بالكامل، هكذا: Q(s) = Ws الهدف هنا هو إعادة تطبيق المنطق أو المسار المستخدَم في الروبوتات التي سبق وبنيناها بالفعل، ولكن هذه المرة باستخدام تجريدات مكتبة Tensorflow، والتي ستجعل عملياتك أكثر كفاءةً، إذ يمكن لمكتبة Tensorflow بعد ذلك إجراء جميع العمليات الحسابية على وِحدة المعالجة الرسومية GPU. سنبدأ بتكرار السكربت الخاص بجدول القيم Q-table، ومن الخطوة 3: cp bot_3_q_table.py bot_4_q_network.py ثم سنفتح الملف الجديد باستخدام المحرّر nano، أو بأي محرر نصوص تفضله: nano bot_4_q_network.py سنُعدّل التعليق الموجود في أعلى الملف في البداية، هكذا: /AtariBot/bot_4_q_network.py """ Bot 4 -- Use Q-learning network to train bot """ . . . وبعد ذلك سنستورد المكتبة البرمجية Tensorflow من خلال إضافة التعليمة import، والتي ستكون موجودةً تحت التعليمة import random، كما سنُضيف التعليمة tf.set_radon_seed(0)‎ تحت التعليمة np.random.seed(0)‎ مباشرةً، وسيضمنُ هذا أن نتائج هذا السكربت ستكون قابلةً للتكرار عبر جميع الجلسات: /AtariBot/bot_4_q_network.py . . . import random import tensorflow as tf random.seed(0) np.random.seed(0) tf.set_random_seed(0) . . . سنُعيد تعريف الوسطاء الفائقة في أعلى الملف لتصبح كما سنراه، كما سنُضيف دالةً جديدةً تُسمى exploration_probability، وهي التي ستُعيد احتمالية الاستكشاف في كلّ خطوة. لاحظ أنه في هذا السياق، تعني كلمة الاستكشاف اتخاذ أي فعلٍ عشوائيٍ، بدلًا من اتخاذ الفعل الذي أوصت به التقديرات المحسوبة في قيم Q-value: /AtariBot/bot_4_q_network.py . . . num_episodes = 4000 discount_factor = 0.99 learning_rate = 0.15 report_interval = 500 exploration_probability = lambda episode: 50. / (episode + 10) report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' . . . بعد ذلك، سنضيف دالة الترميز الأحادي النشط one-hot encoding، وهي باختصار عملية تحويل المتغيّرات إلى نموذجٍ لمساعدة خوارزميات تعلّم الآلة في استنتاج تنبؤاتٍ أفضل، ولمزيد من المعلومات حول هذا الموضوع، يمكنك الاطلاع على دالة الترميز الأحادي النشط. سنضيف الدالة one_hot، أسفل السطر الخاص بالمتغيّر report=...‎، هكذا: /AtariBot/bot_4_q_network.py . . . report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' def one_hot(i: int, n: int) -> np.array: """Implements one-hot encoding by selecting the ith standard basis vector""" return np.identity(n)[i].reshape((1, -1)) def print_report(rewards: List, episode: int): . . . وبعد ذلك سنُعيد كتابة المسار الخاص بالخوارزمية باستخدام تجريدات المكتبة Tensorflow، ولكن قبل ذلك ينبغي علينا أن نُنشئ متغيراتٍ من نوع placeholders لبياناتك، ويمكننا تعريف متغيرات من نوع placeholders، لملاحظتنا عند الزمن t هكذا obs_t_ph، وللزمن t + 1 هكذا obs_tp1_ph، كما يمكننا تعريف متغيراتٍ من نوع placeholders، وذلك للأفعال والمكافآت والأهداف Q-target. ففي الدالة main وتحت السطر الخاص بالمتغيّر rewards=[]‎ مباشرةً، سنضيف الأسطر البرمجية التالية: /AtariBot/bot_4_q_network.py . . . def main(): env = gym.make('FrozenLake-v0') # create the game env.seed(0) # make results reproducible rewards = [] # 1. Setup placeholders n_obs, n_actions = env.observation_space.n, env.action_space.n obs_t_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32) obs_tp1_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32) act_ph = tf.placeholder(tf.int32, shape=()) rew_ph = tf.placeholder(shape=(), dtype=tf.float32) q_target_ph = tf.placeholder(shape=[1, n_actions], dtype=tf.float32) Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): . . . كما سنضيف الشيفرة البرمجية التالية أسفل السطر الذي يحتوي على المتغيّر q_target_ph=‎، وهي التي ستبدأ عملية الحوسبة الخاصة بك عن طريق حساب Q(s, a)‎ لكلّ a لإنشاء q_current وQ(s’, a’)‎، وذلك لجميع قيم a’‎ من أجل إنشاء q_target، هكذا: /AtariBot/bot_4_q_network.py . . . rew_ph = tf.placeholder(shape=(), dtype=tf.float32) q_target_ph = tf.placeholder(shape=[1, n_actions], dtype=tf.float32) # 2. Setup computation graph W = tf.Variable(tf.random_uniform([n_obs, n_actions], 0, 0.01)) q_current = tf.matmul(obs_t_ph, W) q_target = tf.matmul(obs_tp1_ph, W) Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): . . . كما سنضيف الشيفرة التالية أسفل السطر الأخير الذي أضفناه سابقًا، والتي تكافئ أول سطرين من شيفرة السطر المضاف في الخطوة 3، والذي يحسب Qtarget، إذ يكون Qtarget = reward + discount_factor * np.max(Q[state2, :])‎، أما السطران التاليان فسوف يجهّزان الخسارة، بينما السطر الأخير سيحسبُ الفعل الذي سيعظّم قيمة Q-value الخاصة بك: /AtariBot/bot_4_q_network.py . . . q_current = tf.matmul(obs_t_ph, W) q_target = tf.matmul(obs_tp1_ph, W) q_target_max = tf.reduce_max(q_target_ph, axis=1) q_target_sa = rew_ph + discount_factor * q_target_max q_current_sa = q_current[0, act_ph] error = tf.reduce_sum(tf.square(q_target_sa - q_current_sa)) pred_act_ph = tf.argmax(q_current, 1) Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): . . . وبعد إعدادنا للخوارزمية ودالة الخسارة بطريقة صحيحة، سنعرّف المُحسِّن optimizer، هكذا: /AtariBot/bot_4_q_network.py . . . error = tf.reduce_sum(tf.square(q_target_sa - q_current_sa)) pred_act_ph = tf.argmax(q_current, 1) # 3. Setup optimization trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate) update_model = trainer.minimize(error) Q = np.zeros((env.observation_space.n, env.action_space.n)) for episode in range(1, num_episodes + 1): . . . بعد ذلك سنجهّز جسم حلقة اللعبة، ولفعل ذلك يجب أن نمرر البيانات إلى متغيراتٍ من نوع placeholders لمكتبة Tensorflow، وستُعالِج تجريدات مكتبة Tensorflow عملية الحوسبة على وحدة المعالجة الرسومية GPU، كما ستعيد نتيجة الخوارزمية. سنبدأ بحذف جدول القيم Q-table القديم والمسار الخاص به، إذ سنحذف الأسطر التي تعرّف المتغيّر Q تحديدًا، وهي التي تكون قبل حلقة for مباشرةً، إلى جانب الضجيج noise الذي يكون بداخل حلقة while، والفعل action، والهدف Qtarget وزوجي الفعل والحالة المقابلة Q[state, action]‎. سنُعيد تسمية state إلىobs_t وstate2 إلىobs_tp1، لتتوافق مع المتغيّرات من نوع placeholders الخاصة بمكتبة Tensorflow التي ثبّتناها سابقًا. وعند انتهائنا ستتطابقُ حلقة for مع ما يلي: /AtariBot/bot_4_q_network.py . . . # 3. إعداد المُحسّن trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate) update_model = trainer.minimize(error) for episode in range(1, num_episodes + 1): obs_t = env.reset() episode_reward = 0 while True: obs_tp1, reward, done, _ = env.step(action) episode_reward += reward obs_t = obs_tp1 if done: ... سنضيف السطرين التاليين مباشرةً فوق حلقة for، وسيهيئ السطر الأول جلسة Tensorflow، وهي التي تدير الموارد اللازمة لتشغيل العمليات على وِحدة المعالجة الرسومية GPU؛ أما السطر الثاني فيهيئ جميع المتغيّرات الحسابية في الرسم البياني الخاص بك، إذ سيهيئ مثلًا الأوزان بالقيمة 0 قبل تحديثها، وبالإضافة إلى ذلك سنشعّب حلقة for في عبارة with، لذا سنُحاذِ جميع التعليمات البرمجية في الحلقة for لتصبح أربع مسافات: /AtariBot/bot_4_q_network.py . . . trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate) update_model = trainer.minimize(error) with tf.Session() as session: session.run(tf.global_variables_initializer()) for episode in range(1, num_episodes + 1): obs_t = env.reset() ... سنضيف الأسطر التالية لحساب قيمة متغيّر الفعل action، وذلك قبل السطر الذي يقرأ المتغيّرات obs_tp1 والمتغيّر reward والمتغيّر done و_=env.step(action)‎، وستقيّم هذه الشيفرة المتغيّرات من نوع placeholder المقابلة، كما ستستبدل الأفعال بأفعالٍ عشوائيةٍ مع بعض الاحتمالات: /AtariBot/bot_4_q_network.py . . . while True: # 4. Take step using best action or random action obs_t_oh = one_hot(obs_t, n_obs) action = session.run(pred_act_ph, feed_dict={obs_t_ph: obs_t_oh})[0] if np.random.rand(1) < exploration_probability(episode): action = env.action_space.sample() . . . سنضيف الأسطر البرمجية التالية، بعد السطر الذي يحتوي على env.step (action)‎، وذلك لتدريب الشبكة العصبية على تقدير دالة Q-value، هكذا: /AtariBot/bot_4_q_network.py . . . obs_tp1, reward, done, _ = env.step(action) # 5. Train model obs_tp1_oh = one_hot(obs_tp1, n_obs) q_target_val = session.run(q_target, feed_dict={ obs_tp1_ph: obs_tp1_oh }) session.run(update_model, feed_dict={ obs_t_ph: obs_t_oh, rew_ph: reward, q_target_ph: q_target_val, act_ph: action }) episode_reward += reward . . . ينبغي أن يتطابقُ ملفك النهائي مع الشيفرة البرمجية التالية: /AtariBot/bot_4_q_network.py """ Bot 4 -- Use Q-learning network to train bot """ from typing import List import gym import numpy as np import random import tensorflow as tf random.seed(0) np.random.seed(0) tf.set_random_seed(0) num_episodes = 4000 discount_factor = 0.99 learning_rate = 0.15 report_interval = 500 exploration_probability = lambda episode: 50. / (episode + 10) report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' def one_hot(i: int, n: int) -> np.array: """Implements one-hot encoding by selecting the ith standard basis vector""" return np.identity(n)[i].reshape((1, -1)) def print_report(rewards: List, episode: int): """Print rewards report for current episode - Average for last 100 episodes - Best 100-episode average across all time - Average for all episodes across time """ print(report % ( np.mean(rewards[-100:]), max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]), np.mean(rewards), episode)) def main(): env = gym.make('FrozenLake-v0') # create the game env.seed(0) # make results reproducible rewards = [] # 1. Setup placeholders n_obs, n_actions = env.observation_space.n, env.action_space.n obs_t_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32) obs_tp1_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32) act_ph = tf.placeholder(tf.int32, shape=()) rew_ph = tf.placeholder(shape=(), dtype=tf.float32) q_target_ph = tf.placeholder(shape=[1, n_actions], dtype=tf.float32) # 2. Setup computation graph W = tf.Variable(tf.random_uniform([n_obs, n_actions], 0, 0.01)) q_current = tf.matmul(obs_t_ph, W) q_target = tf.matmul(obs_tp1_ph, W) q_target_max = tf.reduce_max(q_target_ph, axis=1) q_target_sa = rew_ph + discount_factor * q_target_max q_current_sa = q_current[0, act_ph] error = tf.reduce_sum(tf.square(q_target_sa - q_current_sa)) pred_act_ph = tf.argmax(q_current, 1) # 3. Setup optimization trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate) update_model = trainer.minimize(error) with tf.Session() as session: session.run(tf.global_variables_initializer()) for episode in range(1, num_episodes + 1): obs_t = env.reset() episode_reward = 0 while True: # 4. Take step using best action or random action obs_t_oh = one_hot(obs_t, n_obs) action = session.run(pred_act_ph, feed_dict={obs_t_ph: obs_t_oh})[0] if np.random.rand(1) < exploration_probability(episode): action = env.action_space.sample() obs_tp1, reward, done, _ = env.step(action) # 5. Train model obs_tp1_oh = one_hot(obs_tp1, n_obs) q_target_val = session.run(q_target, feed_dict={ obs_tp1_ph: obs_tp1_oh }) session.run(update_model, feed_dict={ obs_t_ph: obs_t_oh, rew_ph: reward, q_target_ph: q_target_val, act_ph: action }) episode_reward += reward obs_t = obs_tp1 if done: rewards.append(episode_reward) if episode % report_interval == 0: print_report(rewards, episode) break print_report(rewards, -1) if __name__ == '__main__': main() ثم سنحفظ التغييرات التي أجريناها على الملف، وسنخرج من المحرّر، وسنُنفذّ السكربت: python bot_4_q_network.py وستظهر النتيجة الخاصة بك تمامًا كما يلي: 100-ep Average: 0.11 . Best 100-ep Average: 0.11 . Average: 0.05 (Episode 500) 100-ep Average: 0.41 . Best 100-ep Average: 0.54 . Average: 0.19 (Episode 1000) 100-ep Average: 0.56 . Best 100-ep Average: 0.73 . Average: 0.31 (Episode 1500) 100-ep Average: 0.57 . Best 100-ep Average: 0.73 . Average: 0.36 (Episode 2000) 100-ep Average: 0.65 . Best 100-ep Average: 0.73 . Average: 0.41 (Episode 2500) 100-ep Average: 0.65 . Best 100-ep Average: 0.73 . Average: 0.43 (Episode 3000) 100-ep Average: 0.69 . Best 100-ep Average: 0.73 . Average: 0.46 (Episode 3500) 100-ep Average: 0.77 . Best 100-ep Average: 0.79 . Average: 0.48 (Episode 4000) 100-ep Average: 0.77 . Best 100-ep Average: 0.79 . Average: 0.48 (Episode -1) بذلك نكون انتهينا من تدريب أول روبوتٍ لنا يعتمد على تعلّم الآلة العميق وفق النموذج الحر Q-learning، وذلك من أجل لعبةِ بسيطةِ مثل لعبة البحيرة المتجمدة Frozen Lake، وسيتطلب هذا الروبوت 4000 حلقةً للتدريب. ولك تتخيل إذا كانت اللعبة أكثر تعقيدًا، فكم ستكون عدد العينات المطلوبة للتدريب؟ من الواضح أنه سيتطلب الملايين من العينات، وسيُشار إلى عدد العينات المطلوبة بمدى تعقيد العينة، وهو مفهوم سنكتشفهُ بمزيدٍ من التفصيل في القسم التالي. فهم المقايضة بين التباين والانحياز يختلف معنى تعقيد العينة عن تعقيد النموذج في تعلم الآلة، بل ويتضادان كذلك، إذ نعرّف المصطلحين كما يلي: تعقيد النموذج: يريد المطوّرون نموذجًا معقّدًا بما فيه الكفاية لحل مشكلتهم، فالنموذج البسيط مثل الخط، لا يُعَد معقدًا كفاية للتمكن من التنبؤ بمسار السيارة. تعقيد العينة: وفيه يحتاج المطوّر لنموذجٍ لا يتطلب العديد من العينات، مثل محدودية الوصول لبياناتٍ مصنّفةٍ، والكمية غير الكافية من القدرة الحاسوبية، والذاكرة المحدودة، وما إلى ذلك. سنفترض أن لدينا نموذجان، أحدهما بسيطٌ والآخر معقّدٌ جدًا، ولكي يحقق النموذجين نفس الأداء، سيبيّن مفهوم التباين أو الانحياز bias-variance أن النموذج المعقّد سيحتاج لكميةٍ هائلةٍ من العينات للتدريب، مثل احتياج روبوت التعلم المعزز العميق وفق النموذج الحر المعتمد على الشبكة العصبية إلى 4000 مرحلةٍ للوصول لحلّ لعبة البحيرة المتجمدة Frozen Lake، لذا فإن إضافة طبقةٍ ثانيةٍ إلى الشبكة العصبية لهذا الروبوت، سيُضاعف عدد مراحل التدريب لأربع مرات! ومع زيادة تعقيد الشبكات العصبية سيزداد هذا النموذج في التعقيد، وذلك للحفاظ على معدل الخطأ نفسه، كما سيزيد تعقيد النموذج من تعقيد العينة نفسها، بحيث تصبح أكثر تعقيدًا. وبالمثل، فإن تقليل تعقيد العينة سيقلل من تعقيد النموذج، وبناءً على ذلك لا يمكننا زيادة تعقيد النموذج وتقليل تعقيد العينة للحد الأدنى الذي نرغب به. ومع ذلك، يمكننا الاستفادة من معرفتنا بهذه المقايضة، يمكنك الاطلاع على التفسير الواقعي للعمليات الرياضية الكامنة وراء المقايضة بين التباين والانحياز وعلى مستوىً عالٍ، إذ يعني تحليل فكرة المقايضة بين التباين والانحياز، تفصيل خطأٍ حقيقيٍ إلى مكونين هما الانحياز والتباين، ونشير إلى الخطأ الحقيقي بأنه الخطأ التربيعي المتوسط MSE، وهو الفرق المتوقع بين التصنيفات المُتوقِعة والتصنيفات الحقيقية، ويوضح الشكل التالي تغيير الخطأ الحقيقي مع زيادة تعقيد النموذج: 5. بناء روبوت بالاعتماد على المربعات الصغرى للعب لعبة البحيرة المتجمدة سنستخدم طريقة المربعات الصغرى، وهي المعروفة باسم الانحدار الخطي Linear Regression، وهي وسيلةٌ لتحليل الانحدار. حيث تُستخدم على نطاقٍ واسعٍ في مجالاٍت عدةٍ، من بينها الرياضيات وعلوم البيانات، كما تُستخدم في تعلّم الآلة وذلك للعثور على النموذج الخطي الأمثل لوسيطين أو مجموعة بيانات. في الخطوة 4، بنينا شبكةً عصبيةً لحساب قيم Q-values، وسنستخدم في هذه الخطوة طريقة انحدار الحافة Ridge Regression بدلًا من طريقة الشبكة العصبية، وهو شكلٌ من أشكال المربعات الصغرى، والذي سنستخدمه لحساب المتجه الناتج عن قيم Q-values، على أمل أن يتطلب حل اللعبة عددًا أقل من مراحل التدريب، وذلك عند استخدامنا لنموذج غير معقّد مثل المربعات الصغرى. سنبدأ بنسخ السكربت الناتج من الخطوة 3: cp bot_3_q_table.py bot_5_ls.py ثم سنفتح الملف الجديد: nano bot_5_ls.py وسنُعدّل التعليق في أعلى الملف، إذ سيصف ما سيفعله هذا السكربت: /AtariBot/bot_4_q_network.py """ Bot 5 -- Build least squares q-learning agent for FrozenLake """ . . . وسنضيف تعليمتيْ استيرادٍ للتحقق من النوع، وذلك قبل كتلة التعليمات البرمجية الخاصة باستيراد المكتبات الموجودة بأعلى الملف، هكذا: /AtariBot/bot_5_ls.py . . . from typing import Tuple from typing import Callable from typing import List import gym . . . وسنضيف وسيطًا فائقًا جديدًا في قائمة الوسطاء الفائقة وهو w_lr، وذلك للتحكم في معدل تعلم الدوال الأخرى Q-function’s، كما سنُعدّل عدد المراحل لتصبح 5000، ونُسنِد معامل النقص ليصبح 0.85، وذلك من خلال تغيير قيم الوسطاء الفائقة في num_episodes وdiscount_factor إلى قيمٍ أكبر، ليتمكن الروبوت من إصدار أداء أقوى، هكذا: /AtariBot/bot_5_ls.py . . . num_episodes = 5000 discount_factor = 0.85 learning_rate = 0.9 w_lr = 0.5 report_interval = 500 . . . سنضيف الدالة higher-order قبل الدالة print_report، وهي التي ستُعيد لامبدا lambda مثل دالةٍ مجهولةٍ anonymous function، إذ تُجرّد النموذج، هكذا: /AtariBot/bot_5_ls.py . . . report_interval = 500 report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' def makeQ(model: np.array) -> Callable[[np.array], np.array]: """Returns a Q-function, which takes state -> distribution over actions""" return lambda X: X.dot(model) def print_report(rewards: List, episode: int): . . . سنضيف الدالة initialize بعد الدالة makeQ، وهي التي ستُهيّئ النموذج باستخدام القيم ذات التوزيع الاحتمالي الطبيعي normally-distributed: /AtariBot/bot_5_ls.py . . . def makeQ(model: np.array) -> Callable[[np.array], np.array]: """Returns a Q-function, which takes state -> distribution over actions""" return lambda X: X.dot(model) def initialize(shape: Tuple): """Initialize model""" W = np.random.normal(0.0, 0.1, shape) Q = makeQ(W) return W, Q def print_report(rewards: List, episode: int): . . . سنضيف الدالة train بعد الكتلة الخاصة بدالة initialize، وهي التي ستحسب الحل للشكل المغلق لانحدار الحافة Ridge Regression، ثم ستزنُ النموذج القديم بالنموذج الجديد، وتعيد النموذج والدوال المجردة Q-function، هكذا: /AtariBot/bot_5_ls.py . . . def initialize(shape: Tuple): ... return W, Q def train(X: np.array, y: np.array, W: np.array) -> Tuple[np.array, Callable]: """Train the model, using solution to ridge regression""" I = np.eye(X.shape[1]) newW = np.linalg.inv(X.T.dot(X) + 10e-4 _ I).dot(X.T.dot(y)) W = w_lr _ newW + (1 - w_lr) \* W Q = makeQ(W) return W, Q def print_report(rewards: List, episode: int): . . . ثبعد ذلك سنضيف دالةً أخيرةً one_hot بعد دالة train، وذلك لأداء الترميز الأحادي النشط one-hot encoding للحالات والأفعال الخاصة بالسكربت: /AtariBot/bot_5_ls.py . . . def train(X: np.array, y: np.array, W: np.array) -> Tuple[np.array, Callable]: ... return W, Q def one_hot(i: int, n: int) -> np.array: """Implements one-hot encoding by selecting the ith standard basis vector""" return np.identity(n)[i] def print_report(rewards: List, episode: int): . . . بعدها سنحتاج لتعديل المسار الخاص بالتدريب، ففي السكربت الذي كتبناه سابقًا، كانت القيم بداخل الجدول Q-table تُعدَّل عند كلّ تكرار، في حين سيجمع هذا السكربت العينات والتصنيفات في كلّ خطوةٍ زمنيةٍ ويدرب نموذجًا جديدًا عند كلّ 10 خطوات بدلًا من الاحتفاظ بجدول القيم Q-table أو بالشبكة العصبية، إذ أنه سيستخدم نموذج المربعات الصغرى للتنبؤ بالقيم Q-values. وسننتقلُ إلى الدالة main ونستبدلُ تعريف جدول القيم Q =np.zeros(...) بما يلي: /AtariBot/bot_5_ls.py . . . def main(): ... rewards = [] n_obs, n_actions = env.observation_space.n, env.action_space.n W, Q = initialize((n_obs, n_actions)) states, labels = [], [] for episode in range(1, num_episodes + 1): . . . فإذا كان هناك الكثير من المعلومات المخزنة، فسننزل قليلًا في الشيفرة البرمجية إلى قبل حلقة for، ونضيفُ بعدها مباشرةً الأسطر البرمجية التالية، وهي التي تُعيد إسناد القوائم الخاصة بالحالات states والتصنيفات labels: /AtariBot/bot_5_ls.py . . . def main(): ... for episode in range(1, num_episodes + 1): if len(states) >= 10000: states, labels = [], [] . . . بعدها سنضيف سطرًا برمجيًا جديدًا يحتوي على طريقةٍ جديدةٍ لتعريف الحالة state =env.reset()‎، وهو الذي سيرمز ترميزًا نشطًا للحالة على الفور، إذ أن جميع استخداماتها تتطلب ترميزًا نشطًا،وستكون الشيفرة كما يلي: /AtariBot/bot_5_ls.py . . . for episode in range(1, num_episodes + 1): if len(states) >= 10000: states, labels = [], [] state = one_hot(env.reset(), n_obs) . . . ثم سنعدّل قائمة الحالات states قبل السطر الأول في الحلقة while الرئيسية للعبة، هكذا: /AtariBot/bot_5_ls.py . . . for episode in range(1, num_episodes + 1): ... episode_reward = 0 while True: states.append(state) noise = np.random.random((1, env.action_space.n)) / (episode\*\*2.) . . . وسنعدل طريقة حساب الأفعال action، مما سيقلل احتمالية الضوضاء، وإمكانية تعديل تقييم الدوال Q-function: /AtariBot/bot_5_ls.py . . . while True: states.append(state) noise = np.random.random((1, n*actions)) / episode action = np.argmax(Q(state) + noise) state2, reward, done, * = env.step(action) . . . سنضيف ترميزًا نشطًا من الحالة state2، وسنعدّل استدعاء الدوال Q-function، في تعريف Qtarget، لتصبح على النحو التالي: /AtariBot/bot_5_ls.py . . . while True: ... state2, reward, done, \_ = env.step(action) state2 = one_hot(state2, n_obs) Qtarget = reward + discount_factor * np.max(Q(state2)) . . . سنحذف السطر الذي يعدِّل Q[state,action] = ...‎، وسنستبدله بالأسطر التالية، إذ تأخذ هذه الشيفرة النتيجة من النموذج الحالي، كما تعدّل القيمة في هذه النتيجة فقط، وهي التي تتوافق مع الفعل الحالي المُتخذ، وكنتيجةٍ لذلك، لن تتكبد قيم Q-values للأفعال الأخرى أي خسارة: /AtariBot/bot_5_ls.py . . . state2 = one_hot(state2, n_obs) Qtarget = reward + discount_factor _ np.max(Q(state2)) label = Q(state) label[action] = (1 - learning_rate) _ label[action] + learning_rate \* Qtarget labels.append(label) episode_reward += reward . . . سنضيف تعديلًا دوريًا للنموذج بعد التعليمة state = state2 مباشرة، حيث سيؤدي هذا إلى تدريب النموذج مرةً واحدةً لكلّ 10 خطوات: /AtariBot/bot_5_ls.py . . . state = state2 if len(states) % 10 == 0: W, Q = train(np.array(states), np.array(labels), W) if done: . . . والآن، تأكد من تطابق شيفرتنا البرمجية مع الشيفرة التالية: /AtariBot_5_ls.py """ Bot 5 -- Build least squares q-learning agent for FrozenLake """ from typing import Tuple from typing import Callable from typing import List import gym import numpy as np import random random.seed(0) # make results reproducible np.random.seed(0) # make results reproducible num_episodes = 5000 discount_factor = 0.85 learning_rate = 0.9 w_lr = 0.5 report_interval = 500 report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \ '(Episode %d)' def makeQ(model: np.array) -> Callable[[np.array], np.array]: """Returns a Q-function, which takes state -> distribution over actions""" return lambda X: X.dot(model) def initialize(shape: Tuple): """Initialize model""" W = np.random.normal(0.0, 0.1, shape) Q = makeQ(W) return W, Q def train(X: np.array, y: np.array, W: np.array) -> Tuple[np.array, Callable]: """Train the model, using solution to ridge regression""" I = np.eye(X.shape[1]) newW = np.linalg.inv(X.T.dot(X) + 10e-4 * I).dot(X.T.dot(y)) W = w_lr * newW + (1 - w_lr) * W Q = makeQ(W) return W, Q def one_hot(i: int, n: int) -> np.array: """Implements one-hot encoding by selecting the ith standard basis vector""" return np.identity(n)[i] def print_report(rewards: List, episode: int): """Print rewards report for current episode - Average for last 100 episodes - Best 100-episode average across all time - Average for all episodes across time """ print(report % ( np.mean(rewards[-100:]), max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]), np.mean(rewards), episode)) def main(): env = gym.make('FrozenLake-v0') # create the game env.seed(0) # make results reproducible rewards = [] n_obs, n_actions = env.observation_space.n, env.action_space.n W, Q = initialize((n_obs, n_actions)) states, labels = [], [] for episode in range(1, num_episodes + 1): if len(states) >= 10000: states, labels = [], [] state = one_hot(env.reset(), n_obs) episode_reward = 0 while True: states.append(state) noise = np.random.random((1, n_actions)) / episode action = np.argmax(Q(state) + noise) state2, reward, done, _ = env.step(action) state2 = one_hot(state2, n_obs) Qtarget = reward + discount_factor * np.max(Q(state2)) label = Q(state) label[action] = (1 - learning_rate) * label[action] + \ learning_rate * Qtarget labels.append(label) episode_reward += reward state = state2 if len(states) % 10 == 0: W, Q = train(np.array(states), np.array(labels), W) if done: rewards.append(episode_reward) if episode % report_interval == 0: print_report(rewards, episode) break print_report(rewards, -1) if __name__ == '__main__': main() سنحفظ التغييرات التي أجريناها على الملف وسنخرج من المحرر، ليُنَفذ السكربت: python bot_5_ls.py وستظهر النتيجة التالية: 100-ep Average: 0.17 . Best 100-ep Average: 0.17 . Average: 0.09 (Episode 500) 100-ep Average: 0.11 . Best 100-ep Average: 0.24 . Average: 0.10 (Episode 1000) 100-ep Average: 0.08 . Best 100-ep Average: 0.24 . Average: 0.10 (Episode 1500) 100-ep Average: 0.24 . Best 100-ep Average: 0.25 . Average: 0.11 (Episode 2000) 100-ep Average: 0.32 . Best 100-ep Average: 0.31 . Average: 0.14 (Episode 2500) 100-ep Average: 0.35 . Best 100-ep Average: 0.38 . Average: 0.16 (Episode 3000) 100-ep Average: 0.59 . Best 100-ep Average: 0.62 . Average: 0.22 (Episode 3500) 100-ep Average: 0.66 . Best 100-ep Average: 0.66 . Average: 0.26 (Episode 4000) 100-ep Average: 0.60 . Best 100-ep Average: 0.72 . Average: 0.30 (Episode 4500) 100-ep Average: 0.75 . Best 100-ep Average: 0.82 . Average: 0.34 (Episode 5000) 100-ep Average: 0.75 . Best 100-ep Average: 0.82 . Average: 0.34 (Episode -1) تذكر أنه وفقًا لصفحة لعبة Frozen Lake الرسمية، فإن حل اللعبة يعني بلوغ متوسط 100 مرحلة بقيمة 0.78، فأعلى، وهنا لما يحقق الوكيل متوسط 0.82، فهذا يعن أنه كان قادرًا على حل اللعبة في 5000 مرحلة، وبالرغم من أن هذا لا يحل اللعبة في عدد مراحل أقل، إلا أن طريقة المربعات الصغرى الأساسية تقدر على حل لعبةٍ بسيطةٍ، وبنفس العدد تقريبًا من مراحل التدريب. ومع أن الشبكات العصبية الخاصة بك قد يزداد تعقيدها، إلا أننا رأينا أن النماذج البسيطة كانت كافيةً لحل لعبة البحيرة المتجمدة Frozen Lake. بهذا نكون قد اطلعنا على ثلاثة طرقٍ لبناء روبوتاتٍ تعتمد على التعلم المعزز وفق النموذج الحر Q-learning، حيث يستخدم أولهم طريقة جداول القيم Q-table، بينما يستخدم الثاني طريقة الشبكة العصبية، في حين أن الثالث يستخدم طريقة المربعات الصغرى least squares. وسنبني لاحقًا روبوتًا يعتمد على التعلم المعزز العميق للعب لعبة غزاة الفضاء Space Invaders، بحيث تصير أكثر تعقيدًا من سابقتها. 6. إنشاء روبوت يعتمد على التعلم المعزز العميق وفق النموذج الحر للعبة غزاة الفضاء سنفترض أنك قد ضبطت كلًا من تعقيد النموذج الخاص بخوارزمية التعلم السابقة، وتعقيد العينة بطريقة مثالية، وذلك بغض النظر عن الطريقة التي تستخدمها سواءً كانت شبكةً عصبيةً أو طريقة المربعات الصغرى، فقد اتضح لنا أن أداء الروبوت المعتمد على التعلم المعزز وفق النموذج الحر، سيبقى سيئًا وغير ذكيٍ في الألعاب المعقّدة جدًا، حتى مع وجود عددٍ كبيرٍ من مراحل التدريب. وسنُغطي في هذا القسم تقنيتين مختلفتين يمكنهما تحسين الأداء، ومن ثَم سنختبر الروبوت المدرب باستخدام هذه التقنيات. طُوّر أول روبوتٍ للأغراض العامة، بحيث يقدر على تكييف سلوكه باستمرار دون أي تدخلٍ بشري، من قِبل باحثين في شركة ديب مايند DeepMind، وهم الذين دربوا كذلك روبوتهم على ممارسة مجموعةٍ متنوعةٍ من الألعاب المُخصصة لأجهزة أتاري Atari، ثم نشروا بعدها ورقةً بحثيةً أقرّت بمسألتين مهمتين وهما: الحالات المترابطة: وسنأخذ حالةً من اللعبة التي عملنا عليها في الزمن 0، ونُطلق عليه s0، كما سنفترض أننا عدَّلنا أو حدَّثنا قيمة Q(s0)‎، بحيث تتفق مع القواعد التي اشتققناها سابقًا، والآن سنأخذ الحالة في الزمن 1، والذي سنسميه s1، كما عدَّلنا قيمة Q(s1)‎ وفقًا لنفس القواعد. سنلاحظ أن حالة اللعبة في الزمن 0 تشبه إلى حدٍ كبيرٍ حالتها في الزمن 1، ففي لعبة غزاة الفضاء Space Invaders مثلًا، يكون الأعداء قد تحركوا بمقدار بكسلٍ واحدٍ لكلّ منهم، وباختصار فإنّ قيم s0 وs1 متشابهةٌ جدًا. وبالمثل، نتوقع أيضًا أن تكون القيم في Q(s0)‎ وQ(s1)‎ متشابهةٌ جدًا، لذا فإن تحديث أحدهما سيؤثر على الآخر، وهذا يؤدي إلى تقلّب قيم Q-values، إذ أّن تعديل Q(s0)‎ قد يتعارض في الواقع مع التعديل في Q(s1)‎. وبطريقةٍ أكثر تنظيمًا، يترابط كلًّا من s0 وs1 مع بعضهما البعض، وبما أن الدالة Q-function هي دالةٌ حتميةٌ، فإن Q(s1)‎ وQ(s0)‎ ترتبطان كذلك مع بعضهما البعض. عدم استقرار الدالة Q-function: لا تنسَ أن الدالة Q-function هي النموذج الذي سندربه، وهو كذلك مصدر التصنيفات الخاص بنا، إذ سنفترض أن تصنيفاتنا والتي اُختيرت عشوائيًا، تُمثّل توزيعًا حقيقيًا وليكن L، ففي كلّ مرةٍ سنعدل Q، سنعدل L أيضًا، مما يعني أن نموذجنا يحاول التعلم من هدفٍ متحركٍ، وهذه مشكلةٌ لأننا افتراضنا منذ البداية أن النماذج التي سنستخدمها لديها توزيعًا ثابتًا. ولمكافحة الحالات المرتبطة والدوال Q-function غير المستقرة: يمكن الاحتفاظ بقائمةٍ من الحالات التي يُطلق عليها المخزن المؤقت للردود، إذ سنُضيف في كلّ خطوةٍ زمنيةٍ حالة اللعبة التي سنلاحظها إلى المخزن المؤقت، كما يمكننا اختيار مجموعةٍ فرعيةٍ من الحالات بطريقةٍ عشوائيةٍ من هذه القائمة، وهي التي سندّرب النموذج عليها. نسخ فريق ديب مايند DeepMind الدالة Q(s, a)‎، وذلك لبناء دالةٍ جديدةٍ وسُمّيت Q_current (s، a)‎، وهي دالة Q-function التي سنعدلها، كما سنحتاج لدالة Q-function أخرى للحالات اللاحقة Q_target(s’, a’)‎، وهي التي لن نُعدلها. تذكر أن الدالة Q_target(s’, a’)‎ تُستخدم لإنشاء التصنيفات الخاصة بك، من خلال فصل Q_current عن Q_target؛ أما الإصلاح الأخير فهو إصلاح التوزيع الاحتمالي للتصنيفات الذي قد تؤخذ العينات منه. بعد ذلك يمكن أن يمضي نموذج التعلم العميق فترةً قصيرةً في تعلم هذا التوزيع،وبعد فترةٍ من الوقت يمكنك إعادة نسخ Q_current إلى Q_target جديد. لن تنفذ هذه الأمور بنفسك، وإنما ستحمل النماذج المدربة مسبقًا على هذه الحلول، ولفعل ذلك سنُنشئ مجلدًا جديدًا لتخزين وسطاء هذه النماذج: mkdir models ثم سنستخدم wget، وذلك لتحميل وسطاء النموذج المدرب مسبقًا على لعبة غزاة الفضاء: wget http://models.tensorpack.com/OpenAIGym/SpaceInvaders-v0.tfmodel -P models بعد ذلك، سنحمّل سكربت خاص ليُحدد النموذج المرتبط بالوسطاء التي حملناها للتو. لاحظ أن هذا النموذج المدرب مسبقًا له محدداتٌ على المدخلات، إذ يجب أن تبقى نصب أعيننا وهي: يجب اختزال الحالات أو تقليص حجمها إلى حجم 84×84 بكسل. يتكون الدخل من أربع حالاتٍ، ويجب أن تكون مُكدسة. سنوضح هذه المحددات بمزيد من التفصيل في وقت لاحق، والآن سنحمل السكربت بكتابة الأمر التالي: wget https://github.com/alvinwan/bots-for-atari- games/raw/master/src/bot_6_a3c.py بعد ذلك سنشغّل الروبوت المدرب مسبقًا على لعبة غزاة الفضاء Space Invaders، وذلك لمعرفة أدائه. وعلى عكس الروبوتات السابقة التي استخدمناها، سنكتب هذا السكربت من الصفر. سنُنشئ ملفًا جديدًا لكتابة سكربت: nano bot_6_dqn.py وسنبدأ هذا السكربت بإضافة تعليقٍ افتتاحيّ، واستيراد الأدوات المساعدة اللازمة، وبدء الحلقة الرئيسية للعبة: /AtariBot/bot_6_dqn.py """ Bot 6 - Fully featured deep q-learning network. """ import cv2 import gym import numpy as np import random import tensorflow as tf from bot_6_a3c import a3c_model def main(): if **name** == '**main**': main() سنُسند قيم المولدات العشوائية بعد عمليات الاستيراد مباشرةً، وذلك لجعل النتائج قابلةً للتكرار، كما سنُعرف الوسيط الفائق num_episodes، وهو الذي سيخبر السكربت بعدد المراحل الواجب تشغيل الروبوت من أجلها: /AtariBot/bot_6_dqn.py . . . import tensorflow as tf from bot_6_a3c import a3c_model random.seed(0) # make results reproducible tf.set_random_seed(0) num_episodes = 10 def main(): . . . سنُعرف الدالة downsample بعد سطرين من تعريف num_episodes، وهي التي ستصغّر جميع الصور لحجم 84×84 بكسل، وسننفذ عملية التصغير قبل تمريرها داخل الشبكة العصبية المدربة مسبقًا، وذلك لأن النموذج دُرّب مسبقًا على صورٍ ذات حجم 84×84 بكسل: /AtariBot/bot_6_dqn.py . . . num_episodes = 10 def downsample(state): return cv2.resize(state, (84, 84), interpolation=cv2.INTER_LINEAR)[None] def main(): . . . بعد ذلك سنُنشئ بيئة اللعبة في بداية الدالة main الخاصة بنا، ونعمل على تهيئتها لتكون نتائجها قابلةً للتكرار: /AtariBot/bot_6_dqn.py . . . def main(): env = gym.make('SpaceInvaders-v0') # create the game env.seed(0) # make results reproducible . . . وسنهيئ قائمةً فارغةً بعد إنشاء البيئة مباشرةً، وذلك للاحتفاظ بالمكافآت: /AtariBot/bot_6_dqn.py . . . def main(): env = gym.make('SpaceInvaders-v0') # create the game env.seed(0) # make results reproducible rewards = [] . . . ثم سنهيئ النموذج المدرب مسبقًا بوسطاء النموذج الذي سبق وأن حمّلناه في بداية هذه الخطوة: /AtariBot/bot_6_dqn.py . . . def main(): env = gym.make('SpaceInvaders-v0') # create the game env.seed(0) # make results reproducible rewards = [] model = a3c_model(load='models/SpaceInvaders-v0.tfmodel') . . . بعد ذلك سنضيف بعض الأسطر التي ستُخبر السكربت بالتكرار طبقًا للعدد الموجود في المتغيّر num_episodes، وذلك لحساب متوسط الأداء وتهيئة المكافأة لكلّ حلقةٍ بالقيمة 0، وبالإضافة إلى ذلك سنضيف سطرًا لإعادة اِسناد البيئة env.reset()‎، وسنجمع الحالة الجديدة في العملية، ثم سنصغّر هذه الحالة الأولية باستخدام الدالة downsample()‎، كما سنبدأ حلقة اللعبة من خلال الحلقة while. هكذا: /AtariBot/bot_6_dqn.py . . . def main(): env = gym.make('SpaceInvaders-v0') # create the game env.seed(0) # make results reproducible rewards = [] model = a3c*model(load='models/SpaceInvaders-v0.tfmodel') for * in range(num_episodes): episode_reward = 0 states = [downsample(env.reset())] while True: . . . وبدلًا من قبول حالةٍ واحدةٍ في كلّ مرةٍ، ستقبل الشبكة العصبية الجديدة أربع حالاتٍ في وقتٍ واحد. وبذلك يجب الانتظار حتى تحتوي قائمة الحالات states على أربع حالاتٍ على الأقل قبل تطبيق النموذج المدرب مسبقًا. أضِف الأسطر البرمجية التالية أسفل السطر الخاص بالتعليمة while True:‎، وهذه الأسطر ستخبر الروبوت باتخاذ فعلٍ عشوائيٍ إذا كان هناك أقل من أربع حالاتٍ، كما ستستخدم لربط الحالات وتمريرها للنموذج المدرب مسبقًا، وذلك في حال وجود أربع حالاتٍ على الأقل: /AtariBot/bot_6_dqn.py . . . while True: if len(states) < 4: action = env.action_space.sample() else: frames = np.concatenate(states[-4:], axis=3) action = np.argmax(model([frames])) . . . بعد ذلك سنأخذ الفعل المناسب ونُعدل البيانات ذات الصلة، وسنُضيف نسخةً مصغرةً من الحالة المرصودة، ثم سنُعدّل مكافأة هذه المرحلة: /AtariBot/bot_6_dqn.py . . . while True: ... action = np.argmax(model([frames])) state, reward, done, _ = env.step(action) states.append(downsample(state)) episode_reward += reward . . . بعد ذلك سنُضيف الأسطر التالية، وهي التي تتحقق مما إذا ما كانت الحلقة منتهيةً done أم لا، فإذا كانت منتهيةً، فسنطبع المكافأة الإجمالية للمراحل، ثم سنعدّل القائمة الخاصة بجميع النتائج، وسنخرج من حلقة while مبكرًا: /AtariBot/bot_6_dqn.py . . . while True: ... episode_reward += reward if done: print('Reward: %d' % episode_reward) rewards.append(episode_reward) break . . . سنُظهر متوسط المكافأة خارج الحلقتين while وfor، حيث سنضعها في نهاية الدالة main، هكذا: /AtariBot/bot_6_dqn.py def main(): ... break print('Average reward: %.2f' % (sum(rewards) / len(rewards))) يجب أن تحقق من أن ملفك يطابق الشيفرة التالية: /AtariBot/bot_6_dqn.py """ Bot 6 - Fully featured deep q-learning network. """ import cv2 import gym import numpy as np import random import tensorflow as tf from bot_6_a3c import a3c_model random.seed(0) # make results reproducible tf.set_random_seed(0) num_episodes = 10 def downsample(state): return cv2.resize(state, (84, 84), interpolation=cv2.INTER_LINEAR) [None] def main(): env = gym.make('SpaceInvaders-v0') # create the game env.seed(0) # make results reproducible rewards = [] model = a3c_model(load='models/SpaceInvaders-v0.tfmodel') for _ in range(num_episodes): episode_reward = 0 states = [downsample(env.reset())] while True: if len(states) < 4: action = env.action_space.sample() else: frames = np.concatenate(states[-4:], axis=3) action = np.argmax(model([frames])) state, reward, done, _ = env.step(action) states.append(downsample(state)) episode_reward += reward if done: print('Reward: %d' % episode_reward) rewards.append(episode_reward) break print('Average reward: %.2f' % (sum(rewards) / len(rewards))) if __name__ == '__main__': main() سنحفظ التغييرات التي أجريناها على الملف، ثم سنخرج من المحرّر، كما سنُنفذّ السكربت: python bot_6_dqn.py ستظهر النتيجة كما يلي: . . . Reward: 1230 Reward: 4510 Reward: 1860 Reward: 2555 Reward: 515 Reward: 1830 Reward: 4100 Reward: 4350 Reward: 1705 Reward: 4905 Average reward: 2756.00 وازن هذه النتيجة بنتيجة السكربت الأول، الذي نفذنا فيه روبوتًا عشوائيًا للعبة غزاة الفضاء، إذ كان متوسط المكافأة في تلك الحالة حوالي 150 فقط، مما يعني أن هذه النتيجة أفضل بعشرين مرةً. على أية حال، لقد نفذنا الشيفرة البرمجية لثلاث مراحل فقط، لأنها بطيئةٌ إلى حدٍ ما، ومتوسط المراحل الثلاث ليس مقياسًا موثوقًا به، وبذلك فإن التنفيذ لأكثر من 10 مراحلٍ، سيعطي متوسطًا مقداره 2756؛ بينما التنفيذ لأكثر من 100 مرحلة، سيعطي متوسطًا مقداره 2500، ومع هذه المتوسطات، يمكنك أن تستنتج استنتاجًا مريحًا بأن روبوتك يؤدي بالفعل بطريقة أفضل من حيث الحجم، كما أن لديك الآن روبوتًا يلعب لعبة غزاة الفضاء بطريقةٍ معقولةٍ. مع ذلك، تذكر المشكلة التي طرحناها في القسم السابق فيما يتعلق بتعقيد العينة، فقد اتضح لنا أن هذا الروبوت الخاص بلعبة غزاة الفضاء سيتطلب ملايين العينات للتدريب، بحيث يتطلب هذا الروبوت في الواقع 24 ساعة على أربع معالجاتٍ من نوع GPUs Titan X لتدريبه حتى يصل لهذا المستوى الحالي؛ بعبارة أخرى، لقد احتاج لقدرٍ كبيرٍ من الحوسبة لتدريبه تدريبًا كافيًا، فهل يمكنك تدريب روبوتٍ مماثل الأداء ولكن مع عيناتٍ أقل بكثير؟ يجب أن تمدك الخطوات السابقة بالمعرفة الكافية للبدء باستكشاف إجابةٍ مناسبةٍ لهذا السؤال، فقد يكون ذلك ممكنًا في حال استخدام نماذج أبسط بكثيرٍ في عملية المقايضة بين التباين والانحياز. الخلاصة بنينا في هذا الدرس العديد من الروبوتات المخصصة للألعاب، كما استكشفنا المفهوم الأساسي في تعلّم الآلة والمُسمى بعملية المقايضة بين التباين والانحياز، والسؤال الطبيعي الذي سيخطر ببالنا هو، هل يمكننا بناء روبوتاتٍ لألعابٍ أكثر تعقيدًا مثل لعبة StarCraft 2؟ فكما اتضح لنا، إن هذا سؤالٌ بحثيٌ معلقٌ ومدعومٌ بأدواتٍ مفتوحة المصدر من شركاتٍ كبيرةٍ مثل غوغل Google وديب مايند DeepMind وشركة بليزارد Blizzard. فإذا كانت هذه المشاكل تهمك فعلًا، فيمكنك الاطلاع على المشاكل الحالية التي يواجهونها. يُعَد الهدف الرئيسي من هذا الدرس، هو التعرف على عملية المقايضة بين التباين والانحياز، والأمر متروكٌ لممارسي تعلّم الآلة للنظر في آثار تعقيد النموذج، في حين أنه يمكن الاستفادة من النماذج والطبقات المعقّدة جدًا لتقليل الكميات الكبيرة من القدرة الحاسوبية وعدد العينات والوقت الزمني؛ وذلك لنحقق تقدمًا كبيرًا في طريقة إدارتنا للموارد المطلوبة. ترجمة -وبتصرف- للفصل Bias-Variance for Deep Reinforcement Learning: How To Build a Bot for Atari with OpenAI Gym من كتاب Python Machine Learning Projects لكاتبه Alvin Wan. اقرأ أيضًا بناء شبكة عصبية للتعرف على الأرقام المكتوبة بخط اليد باستخدام مكتبة TensorFlow بناء مصنف بالاعتماد على طرق تعلم الآلة بلغة بايثون باستخدام مكتبة Scikit-Learn النسخة الكاملة من كتاب مدخل إلى الذكاء الاصطناعي وتعلم الآلة
  2. سننفذ في هذا المقال خوارزميةً بسيطةً لتعلم الآلة بلغة بايثون Python باستخدام مكتبة Scikit-learn، وهذه المكتبة ما هي إلا أداةٌ لتطبيق تعلّم الآلة بلغة البايثون، كما سنستخدم المُصنّف Naive Bayes (NB) مع قاعدة بياناتٍ حقيقية لمعلومات ورم سرطان الثدي، والذي سيتنبأ إذا ما كان الورم خبيثًا أم حميدًا. وفي نهاية هذا المقال ستعرف خطوات وكيفية إنشاء نموذج تنبؤي خاص بك لتَعَلّم الآلة بلغة بايثون. المتطلبات الرئيسية قبل البدء بهذا المقال لا بد من تجهيز البيئة المناسبة، وسنستخدم محرر الشيفرات البرمجية Jupyter Notebooks، وهو مفيد جدًا لتجربة وتشغيل الأمثلة الخاصة بتَعَلّم الآلة بطريقةٍ تفاعليةٍ، حيث تستطيع من خلاله تشغيل كتلًا صغيرةً من الشيفرات البرمجية ورؤية النتائج بسرعة، مما سيسهل علينا اختبار الشيفرات البرمجية وتصحيحها. يُمكنك فتح متصفح الويب والذهاب لموقع المحرر الرسمي على الوِيب لبدء العمل بسرعة، ومن ثمّ انقر فوق "جرّب المحرر التقليدي Try Classic Notebook"، وستنتقل بعدها لملفٍ جديدٍ بداخل محرر Jupyter Notebooks التفاعلي، وبذلك تجهّز نفسك لكتابة الشيفرة البرمجية بلغة البايثون. إذا رغبت بمزيدٍ من المعلومات حول محرر الشيفرات البرمجية Jupyter Notebooks وكيفيّة إعداد بيئته الخاصة لكتابة شيفرة بايثون، فيمكنك الاطلاع على: كيفية تهيئة تطبيق المفكرة كيفية jupyter notebook للعمل مع لغة البرمجة python. ولتجهيز البيئة المناسبة سنتبع الخطوات الآتية: 1. استيراد مكتبة Scikit-Learn لنبدأ أولًا بتثبيت مكتبة Scikit-Learn، والتي تُعدّ واحدةً من أفضل مكتبات تعلّم الآلة الموثقة توثيقًا جيدًا في لغة البايثون، ولنفعّل البيئة البرمجية للغة بايثون الإصدار 3 وذلك لبدء مشروعنا. تأكد من أنك بنفس المجلد الذي توجد فيه بيئتك البرمجية، وبعدها نفذّ الأمر التالي: . my_env/bin/activate تحقق مما إذا كانت الوِحدة (أو المكتبة) Sckikit-Learn مُثَبتةً بالفعل في البيئة البرمجيّة الخاصة بك: (my_env) $ python -c "import sklearn" في حال كانت المكتبة مُثَبتةً فعلًا، فسيُنفذُ هذا الأمر بدون أخطاءٍ. وإلا فسترى رسالة الخطأ التالية: Traceback (most recent call last): File "<string>", line 1, in <module> ImportError: No module named 'sklearn' تُشير رسالة الخطأ السابقة إلى أن مكتبة scikit-learn غير مُثَبتةٍ، لذلك نَزّل المكتبة باستخدام الأمر التالي: (my_env) $ pip install scikit-learn[alldeps] شغّل محرر الشيفرات البرمجية Jupyter Notebook بمجرد اكتمال عملية التثبيت. هكذا: (my_env) $ jupyter notebook أنشئ ملفًا جديدًا في داخل المحرر وليكن ML Tutorial، حيث ستكون في الخلية الأولى للملف عملية استيراد الوِحدة (أو المكتبة) البرمجية scikit-learn (لمزيد من المعلومات حول طريقة استيراد وحدة برمجية في لغة بايثون يمكنك الاطلاع على كيفية استيراد الوحدات في بايثون 3 سبق وأن ناقشنا فيه هذه الفكرة بالتفصيل): ML Tutorial import sklearn يجب أن يبدو الملف الخاص بك شبيهًا بالملف التالي: والآن بعد استيرادنا للمكتبة بنجاح، سنبدأ العمل مع مجموعة البيانات لبناء نموذج تعلّم الآلة الخاص بنا. 2. استيراد مجموعة بيانات Scikit-Learn’s مجموعة البيانات التي سنتعامل معها في هذا الدرس هي قاعدة بيانات الخاصة بتشخيص مرض سرطان الثدي في ولاية ويسكونسن الأمريكية. تتضمن هذه المجموعة من البيانات معلوماتٍ مختلفةٍ حول أورام سرطان الثدي، بالإضافة إلى تصنيفات الأورام سواءً كانت خبيثةً أم حميدةً. كما تحتوي على 569 حالة (أو للدقة بيانات 569 ورمًا)، كما تتضمن معلومات عن 30 ميزة لكلّ ورم، مثل: نصف قطر الورم ونسيجه ونعومته ومساحته. سنبني نموذجًا لتعلّم الآلة من مجموعة البيانات السابقة باستخدام معلومات الورم فقط للتنبؤ فيما إذا كان الورم خبيثًا أم حميدًا. يُثَبت مع مكتبة Scikit-learn مجموعات بياناتٍ مختلفةٍ افتراضيًا، ويُمكننا استيرادها لتُصبح متاحةً للاستخدام في بيئتنا مباشرةً، لنفعل ذلك: ML Tutorial ... from sklearn.datasets import load_breast_cancer # Load dataset data = load_breast_cancer() سيُمثَل المتغير data ككائنٍ في البايثون، والذي سيعمل مثل عمل القاموس الذي هو نوعٌ مُضمَّنٌ في بايثون، بحيث يربط مفاتيحًا بقيمٍ على هيئة أزواجٍ، وستُؤخذ بالحسبان مفاتيح القاموس، وهي أسماء الحقول المُصنّفة target_names، والقيم الفعلية لها target، وأسماء الميّزات feature_names، والقيم الفعلية لهذه الميزات data. تُعَد الميّزات جزءًا مهمًا من أي مصنّف، إذ تُمثّل هذه الميزات خصائص مهمةً تصف طبيعة البيانات، كما ستساعدنا في عملية التنبؤ بحالة الورم (ورم الخبيث malignant tumor أو ورم حميد benign tumor)، ومن الميّزات المُفيدة المحتملة في مجموعة بياناتنا هذه، هي حجم الورم ونصف قطره ونسيجه. أنشئ متغيرات جديدةً لكلّ مجموعةٍ مهمةٍ من هذه المعلومات وأسند لها البيانات: ML Tutorial ... # تنظيم بياناتنا label_names = data['target_names'] labels = data['target'] feature_names = data['feature_names'] features = data['data'] والآن أصبحت لدينا قوائم لكلّ مجموعةٍ من المعلومات، ولفَهم مجموعة البيانات الخاصة بنا فهمًا صحيحًا ودقيقًا، سنُلقي نظرةً عليها من خلال طباعة حقول الصنف مثل طباعة أول عينةٍ من البيانات، وأسماء ميّزاتها، وقيمها. هكذا: ML Tutorial ... # الاطلاع على بياناتنا print(label_names) print(labels[0]) print(feature_names[0]) print(features[0]) إن نفذّت هذه الشيفرة بطريقةٍ صحيحةٍ فسترى النتائج التالية: نُلاحظ من الصورة أن أسماء الأصناف الخاصة بنا ستكون خبيث malignant وحميد benign (أي أن الورم سيكون إما خبيثًا أو حميدًا)، والمرتبطة بقيم ثنائية وهي إما 0 أو 1، إذ يُمثّل الرقم 0 أورامًا خبيثة ويُمثّل الرقم 1 أورامًا حميدة، لذا فإن أول مثالٍ للبيانات الموجودة لدينا هو ورمٌ خبيثٌ نصف قطره 1.79900000e+01. والآن بعد تأكدنا من تحميل بياناتنا تحميلًا صحيحًا في بيئة التنفيذ، سنبدأ العمل مع بياناتنا لبناء مصنّف باستخدام طُرق تعلّم الآلة. 3. تنظيم البيانات في مجموعات ينبغي عليك دائمًا اختبار النموذج على البيانات غير المرئية، وذلك لتقييم مدى جودة أداء المُصنّف، لهذا قسّم البيانات الخاصة بك إلى جزئين قبل بناء النموذج، بحيث تكون هناك مجموعةٌ للتدريب ومجموعةٌ للاختبار. تستطيع استخدام المجموعة المخصصة للتدريب من أجل تدريب وتقييم النموذج أثناء مرحلة التطوير. حيث ستمنحك منهجية تنبؤات هذا النموذج المُدرّب على المجموعة المخصصة للاختبار غير المرئية، فكرةً دقيقةً عن أداء النموذج وقوته. لحسن الحظ، لدى المكتبة Scikit-learn دالة تُدعى train_test_split()‎، والتي ستقسمُ بياناتك لهذه المجموعات. ولكن يجب أن تستورد هذه الدالة أولًا ومن ثَمّ تستخدمها لتقسيم البيانات: ML Tutorial ... from sklearn.model_selection import train_test_split # تقسيم بياناتنا train, test, train_labels, test_labels = train_test_split(features, labels, test_size=0.33, random_state=42) ستُقسّمُ هذه الدّالة البيانات بطريقةٍ عشوائيةٍ باستخدام الوسيط test_size. في مثالنا لدينا الآن مجموعةً مخصصةً للاختبار test تُمثّل 33٪ من مجموعة البيانات الأصلية، وسيُشكّل الجزء المتبقي من البيانات المجموعة المخصصة للتدريب train. كما لدينا حقولٌ مخصصةٌ لكلٍ من المتغيرات، سواء أكانت مُخصّصةً للاختبار أو للتدريب، أي train_labels وtest_labels. لنُدرّب الآن نموذجنا الأول. 4. بناء النموذج وتقييمه هناك العديد من النماذج المُخَصصة لتعلّم الآلة، ولكلّ نموذجٍ منها نقاط قوةٍ وضعفٍ. في هذا الدرس، سنُركّز على خوارزمية بسيطةٍ تؤدي عادةً أداءً جيدًا في مهام التصنيف الثنائية، وهي Naive Bayes (NB). أولًا، سنستورد الوِحدة البرمجية GaussianNB ثم نُهَيّئ النموذج باستخدام الدالة GaussianNB()‎، بعدها سنُدرّب النموذج من خلال مُلاءمته مع البيانات باستخدام الدالة gnb.fit()‎: ML Tutorial ... from sklearn.naive_bayes import GaussianNB # تهيئة المصنّف خاصتنا gnb = GaussianNB() # تدريب المصنّف model = gnb.fit(train, train_labels) بعد أن نُدّرب النموذج سنستخدمه للتنبؤ على المجموعة المخصصة للاختبار، وسننفذ ذلك من خلال الدّالة predict()‎، والتي ستُعيد مجموعةً من التنبؤات لكلّ نسخة بياناتٍ في المجموعة المخصصة للاختبار، ثم نطبع تنبؤاتنا لِفَهم ما حدده هذا النموذج. استخدِم الدالة predict()‎ مع مجموعة البيانات المخصصة للاختبار test واطبع النتائج: ML Tutorial ... # بناء التوقعات preds = gnb.predict(test) print(preds) عند تنفيذك للشيفرة البرمجية تنفيذًا صحيحًا سترى النتائج التالية: فكما ترى، أعادت الدالة predict()‎ مصفوفةً ثُنائية القيم إما 0 أو 1، حيث تُمثل القيم المتوقعة لصنف الورم (خبيث أم حميد). والآن بعد أن حصلنا على توقعاتنا، لِنُقيِّم مدى جودة أداء هذا المُصنّف. 5. تقييم دقة النموذج نُقّيم دقة القيم المتوقَّعة لنموذجنا باستخدام مصفوفة التصنيفات الناتجة للأصناف الحقيقية التي لدينا، وذلك من خلال موازنة المصفوفتين test_labels وpreds باستخدام الدالة accuracy_score()‎ التابعة للمكتبة Scikit-learn، وذلك لتحديد دِقة المُصنّف. ML Tutorial ... from sklearn.metrics import accuracy_score # تقييم الدقة print(accuracy_score(test_labels, preds)) سترى النتائج التالية: كما ترى في النتيجة، فإن المُصنّف NB دقيقٌ بنسبة 94.15٪. وهذا يعني أن المُصنِّف قادرٌ على التنبؤ الصحيح فيما إذا كان الورم خبيثًا أو حميدًا بنسبة 94.15٪ من الحالات الكُليّة. كما تُشير هذه النتائج إلى أن مجموعة الميّزات المُكونة من 30 ميزة هي مؤشراتٍ جيدةٍ لصنف الورم. بهذا تكون قد نجحت في إنشاء مصنِّفك الأول الذي يعتمد في عمله على طرق تعلّم الآلة، والآن لنعد تنظيم الشيفرة البرمجية بوضع جميع عمليات الاستيراد في أعلى الملف، إذ يجب أن تبدو النسخة النهائية من الشيفرة البرمجية خاصتك شبيةً بهذه الشيفرة: ML Tutorial from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.naive_bayes import GaussianNB from sklearn.metrics import accuracy_score # تحميل البيانات data = load_breast_cancer() # تنظيم البيانات label_names = data['target_names'] labels = data['target'] feature_names = data['feature_names'] features = data['data'] # الاطلاع على البيانات print(label_names) print('Class label = ', labels[0]) print(feature_names) print(features[0]) # تقسيم البيانات train, test, train_labels, test_labels = train_test_split(features, labels, test_size=0.33, random_state=42) # تهيئة المصنّف gnb = GaussianNB() # تدريب المصنّف model = gnb.fit(train, train_labels) # بناء التوقعات preds = gnb.predict(test) print(preds) # تقييم الدقة print(accuracy_score(test_labels, preds)) والآن بإمكانك إكمال العمل على الشيفرة البرمجية، وتحسين عمل المُصنّف وتوسيعه، وكذا تجربة هذا المصنّف مع مجموعاتٍ فرعيةٍ مختلفةٍ من الميزات، أو حتى تجربة خوارزمياتٍ مختلفةٍ تمامًا. تستطيع الاطلاع على الموقع الرسمي لمكتبة Scikit-Learn لمزيدٍ من الأفكار حول تطبيق تعلّم الآلة مع البيانات لبناء شيءٍ مفيدٍ. الخلاصة لقد تعلمنا في هذا المقال كيفية إنشاء مُصنّف بالاعتماد على تعلّم الآلة بلغة بايثون باستخدام المكتبة Scikit-learn، والآن بإمكانك تحميل البيانات في بيئةٍ برمجيةٍ وتنظيمها وتدريبها، وكذا التنبؤ بأشياء بناءًا عليها، وتقييم دِقّة المُصنّفات الناتجة. نتمنى أن تُساعدك هذه الخطوات في تسهيل طريقة العمل مع بياناتك الخاصة بلغة بايثون. ترجمة -وبتصرف- للفصل How To Build a Machine Learning Classifier in Python with Scikit-learn من كتاب Python Machine Learning Projects لكاتبه Michelle Morales. اقرأ أيضًا النسخة الكاملة لكتاب مدخل إلى الذكاء الاصطناعي وتعلم الآلة المرجع الشامل إلى تعلم لغة بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون المفاهيم الأساسية لتعلم الآلة