المحتوى عن 'ألعاب'.



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

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

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

نوع المُحتوى


التصنيفات

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

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • لغة TypeScript
    • Node.js
    • React
    • AngularJS
    • Vue.js
    • jQuery
    • Cordova
  • HTML
    • HTML5
    • إطار عمل Bootstrap
  • CSS
    • Sass
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
    • Unity3D
  • سير العمل
    • 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

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

  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. نبذة مختصرة عن صناعة الألعاب وتطويرها لعل صناعة الألعاب هي إحدى أصعب الصناعات في هذا العصر، وذلك من عدة نواح تبدأ بالتحديات التقنية، مرورا بجمهور صعب الإرضاء ولا يرحم حتى كبريات الشركات إن لم تكن منتجاتها بالمستوى المطلوب، وليس انتهاءا بالمنافسة الشرسة ونسب الفشل العالية وصعوبة تحقيق أرباح تغطي تكاليف الإنتاج المرتفعة. على الجانب الآخر يوجد ميزات لهذه الصناعة تجعل من النجاة فيها أمرا ممكنا، فعلى الجانب التقني مثلا، لا تخلوا الغالبية العظمى من الألعاب من وظائف متشابهة وأنماط متكررة من معالجة البيانات، مما يجعل إعادة استخدام الوحدات البرمجية للألعاب السابقة من أجل إنشاء ألعاب جديدة أمرا ممكنا، وهذا بدوره يساهم في تذليل العقبات التقنية واختصار الوقت والجهد. عندما تتحدث عن صناعة لعبة، فأنت هنا تذكر العملية الكبرى والتي تنطوي على عشرات وربما مئات المهام التي يجب أن تنجزها في مجالات عدة. فصناعة لعبة تعني إنتاجها وتسويقها ونشرها وكل ما يتعلق بهذه العمليات من إجراءات وخطوات إدارية وتقنية وفنية ومالية وقانونية. على أية حال فإن ما يهمنا في سلسلة الدروس هذه هو الجانب التقني وهو تطوير اللعبة، وهي عملية بناء المنتج البرمجي النهائي بكافة مكوّناته. هذه العملية لا تشمل بالضرورة تصميم اللعبة، حيث أن عملية التصميم ذات منظور أوسع وتركز على أمور مثل القصة والسمة العامة للعبة وأشكال المراحل وطبيعة الخصوم، بالإضافة إلى قواعد اللعبة وأهدافها وشروط الفوز والخسارة. بالعودة لعملية تطوير اللعبة، نجد أن العديد من الاختصاصات والمهارات تساهم في هذه العملية، فهناك الرسامون ومصممو النماذج وفنيو التحريك ومهندسو الصوت والمخرج، إضافة – بالطبع – للمبرمجين. هذه النظرة الشاملة مهمة لنعرف أن دور المبرمج في إنتاج اللعبة ليس سوى دورا مكمّلا لأدوار غيره من أعضاء الفريق، ولو أن هذه الصورة بدأت تتغير بظهور المطورين المستقلين Indie Developers والذين يقومون بالعديد من المهام إلى جانب البرمجة. لماذا نستخدم محركات الألعاب؟ لو أردنا الحديث بتفصيل أكبر عن دور المبرمجين في صناعة الألعاب، سنجد أنه حتى على مستوى البرمجة نفسها هناك أدوار عديدة يجب القيام بها: فهناك برمجة الرسومات وهناك أنظم الإدخال وأنظمة استيراد الموارد والذكاء الاصطناعي ومحاكاة الفيزياء وغيرها مثل مكتبات الصوت والأدوات المساعدة. كل هذه المهام يمكن إنجازها على شكل وحدات برمجية قابلة لإعادة الاستخدام كما سبق وذكرت، وبالتالي فهذه الوحدات تشكل معا ما يعرف بمحرك الألعاب Game Engine. باستخدامك للمحرك والمكتبات البرمجية التي يتكون منها، فأنت تختصر على نفسك الجهد اللازم لبناء نظام الإدخال والإخراج والاستيراد ومحاكاة الفيزياء، وحتى جزء من الذكاء الاصطناعي. وما يتبقى عليك هو كتابة منطق لعبتك الخاصة وإبداع ما يميزها عن غيرها من الألعاب. هذه النقطة الأخيرة هي ما ستدور حوله سلسلة الدروس القادمة، وبالرغم من أن المهمة تبدو صغيرة جدا مقارنة بتطوير اللعبة كاملة، إلا أنها على صغرها تحتاج لمجهود معتبر في التصميم والتنفيذ كما سنرى. خطوات سريعة لتبدأ مع محرك Unity إن لم تكن ذا خبرة سابقة بهذا المحرك يمكنك قراءة هذه المقدمة السريعة، كما يمكنك تخطيها إن كنت تعاملت مع هذا المحرك سابقا. لن أطيل شرح هذه الخطوات حيث هناك الكثير من الدروس سواء بالعربية أو الإنجليزية تتناولها، لكنها هنا لنتأكد من أن كل قارئ للسلسلة على نفس الدرجة من المعرفة الأولية قبل البدء. الخطوة الأولى: تحميل وتنصيب المحرك لتنزيل الإصدار الأحدث من المحرك وهو 5 ادخل مباشرة إلى الموقع http://unity3d.com ومن ثم قم بتحميل النسخة المناسبة لنظام التشغيل الذي تستخدمه، علما بأن النسخة المجانية من المحرك ذات إمكانات كبيرة وهي تفي بالغرض بالنسبة لمشروعنا في سلسلة الدروس هذه. الخطوة الثانية: إنشاء المشروع بمجرد تشغيل المحرك بعد تنصيبه ستظهر لك شاشة البداية، قم بالضغط على New Project لتظهر لك شاشة كالتي تراها في الصورة أدناه. كل ما عليك هو اختيار النوع 2D ومن ثم اختيار اسم وموقع المشروع الجديد الذي ستقوم بإنشائه، ومن ثم الضغط على Create Project. الخطوة الثالثة: التعرف على نوافذ البرنامج الرئيسية تهمنا في البداية 4 نوافذ رئيسية في محرك Unity، وفيما يلي ملخص لوظائفها: نافذة المشهد Scene: وهي التي تستخدمها لبناء مشهد اللعبة وإضافة الكائنات المختلفة إليه وتوزيعها في الفضاء ثنائي الأبعاد. تحتوي هذه النافذة مبدئيا على كائن واحد وهو الكاميرا. هرمية المشهد Hierarchy: تحتوي على ترتيب شجري يحوي كافة الكائنات التي تمت إضافتها للمشهد ويساعدك في تنظيم العلاقات بينها، حيث أنه من الممكن أن تضيف كائنات كأبناء لكائنات أخرى بحيث يتأثر الكائن الابن بالكائن الأب كما سنرى. تحتوي هذه النافذة مبدئيا على كائن واحد وهو الكاميرا. مستعرض المشروع Project: يقوم بعرض جميع الملفات الموجودة داخل مجلد المشروع، سواء تلك التي تمت إضافتها للمشهد أم التي لم تُضف. يحتوي المشروع مبدئيا على مجلد واحد يسمى Assets، وسنضيف داخله كافة الملفات والمجلدات الأخرى. نافذة الخصائص Inspector: عند اختيار أي كائن من هرمية المشهد أو نافذة المشهد أو مستعرض المشروع، فإن خصائصه ستظهر في هذه النافذة ويمكنك تغييرها من هناك. استعرضنا في هذا الدرس ما يظهر من واجهة Unity3D للوهلة الأولى، مع مقدمة بسيطة حول صناعة الألعاب، سنشرع في الدروس القادمة في مشروع عملي نتعلم من خلاله كيفية صناعة لعبة كاملة حقيقية. فترقبوا!
  3. سنحاول هنا أن نحيط قدر الإمكان بجوانب هذا الفن الرائع ورسوماته المذهلة عبر معرفة ماهيته، أنواعه، تطورّه، تاريخه وأهم البرامج المستخدمة في رسمه. وسنتعلّم لاحقًا كيفية رسم رسومات البكسل المتنوعة. ما هو فن البِكسل؟ فن البِكسل أو Pixel Art هو فن رسومات قديم يشبه من حيث المبدأ الحياكة الصوفية كالرسومات على الملابس الصوفية إلا أن بداية هذا النوع من الفنون بدأ مع بدء ظهور أجهزة اللعب والحواسيب حيث لم يكن بمقدور تلك الأجهزة القديمة عرض رسوميات معقدّة وكثيرة الألوان وثلاثية الأبعاد كما في يومنا هذا لذلك اعتمد مطوّرو الألعاب في ذلك القوت على هذا النوع الوحيد المتوفّر لرسم خلفيات وشخصيات الألعاب بألوانها البسيطة، وعلى الرغم من بساطتها في ذلك الوقت إلا أنها كانت رائعة وممتعة للغاية ولعّل ألعاب Pacman و Mario وStreet Fighters وKingKong وSonic أبرز ألعاب تلك المرحلة شاهد ومثال رائع على روعة تلك الرسومات في تلك الحقبة من الزمن. ويعتمد الرسم هنا على رسم النقاط الصغيرة بجانب بعضها بحيث تكون النقطة الواحدة بحجم بكسل واحد فقط ما يجعل هذه الرسومات دقيقة وصغيرة نسبيًّا بالنسبة إلى المقاسات المعتمدة في الأجهزة المتطوّرة في أيامنا هذه بينما كانت أكثر من كافية لأجهزة تلك الحقبة القديمة. لذلك عند الرسم بأي برنامج للرسم عليك تكبير منظور العمل إلى أكبر درجة ممكنة تستطيع معها التعامل مع نقاط البكسل بسهولة وعند العودة للحجم الطبيعي ستشاهد نتيجة العمل الذي قمت به. هذا مثال رسمته باستخدام برنامج الرسام Paint الموجود ضمن نظام الويندوز Windows حيث تظهر نقاط البكسل بوضوح عند التكبير وفي شكل الرأس الصغير تظهر النتيجة بالحجم الطبيعي. وكانت رسومات البكسل بسيطة جدًّا في البداية حيث اعتمدت نظام ألوان 8bit لتتطور إلى 16bit ومنها إلى أكثر وأكثر حتى أصبحت متطورة جدًّا لدرجة أن الرسومات الحديثة منها أصبحت تتضمن تدرجات لونية عادية بعد زيادة حجم العمل وعدد الألوان المستخدم. على الرغم من انتشار الرسومات عالية الدقة والرسومات ثلاثية الأبعاد إلا أن رسومات البكسل مازالت موجودة بقوة وبخاصة في ألعاب الأجهزة المحمولة كالهواتف الذكية وغيرها. ومن الممكن رسم هذه الرسومات باستخدام تقنيات وأدوات اعتيادية كأدوات برنامج الرسّام Paint والفوتوشوب Photoshop أو يمكن رسمها بدون أدوات وبشكل يدوي وهو ما ينتج إبداعًا مميزًا قد لا تستطيع الأدوات تقديمه. أنواعه: تم تقسيم رسومات البِكسل إلى نوعين: الإيزومِترِك Isometric: ويُطلق عليها اسم ثلاثي البعاد أو متساوي القياس أيضًا وهي رسومات بكسل تبدو بثلاثة أبعاد وتظهر فيها ثلاث جوانب للأشكال المرسومة ما يعطي انطباعًا بأنها ثلاثية الأبعاد وعادة ما تكون بمنظور جانبي بزاوية معينة وتكون غالبًا 30 درجة وتستخدم لرسم مناظير معينة لمواقع شهيرة أو افتراضية وفي بعض الأحيان لرسم خلفيات لعبة من هذا النمط وقد ظهرت بعض الألعاب التي تعتمد على هذا النمط من الرسوميات خصوصًا للأجهزة المحمولة كالهواتف الذكية فيما يتباهى الآن المصممون برسوماتهم المعقّدة والرائعة باستخدام هذا النمط. وهذه صور لبعض الألعاب القديمة التي تستخدم هذا النمط سلسلة ألعاب Age of Empires Diablo Transport Tycoon وهذه صور لبعض ألعاب الهواتف المحمولة تستخدم هذا النمط من الرسومات Pocket Harvest Zombie Commando وهذه بعض الرسومات لهذا النمط لبعض المصممين للمصممة Sylvia Flores Espinoza للمصمم Robert Podgórski للمصمم Sergey Kostik غير الإيزومِترِك Non-Isometric: وهو نمط رسومات بِكسل عادي والأكثر انتشارًا وهو الصورة بشكل مباشرة من جهة واحدة من الأمام أو الجانب أو حتى من الأعلى بدون زوايا وهو معروف في عالم الألعاب القديمة وأيضًا الحديثة الخاصة بمنصات الأجهزة المحمولة عادة. ولعل أشهر الصور والألعاب المعروفة التي تستخدم هذا النمط هو ألعاب ماريو وسونيك وغيرها بالإضافة إلى الألعاب الجديدة على منصات الهواتف المحمولة وهذه بعض الأمثلة Random Heroes 2 Sword Of Xolan وقد اشتهرت مؤخرًا هذه الرسمات كثيرًا في رسم الوجوه التعبيرية (سمايلات) وخصوصًا في رسم الأيقونات التي تعتمد بشكل كبير على هذا النوع من الرسومات وبخاصة أيقونات مواقع الإنترنت. ولتوضيح الفرق بين نوعي فن البيكسل إليك هذا المثال Non-Isometric Isometric وسنتعلّم في الدرس التالي كيفية تصميم رسومات بفن Pixel Art بنوعية Non-Isometric وكذلك في الدرس الذي يليه سنتعلّم كيفية تصميم نوعية Isometric. مصادر الصور: صور المصممين من صفحاتهم على Behance. صورة التلفاز التوضيحية من موقع ويكيبيديا تحت رخصة CC BY-SA 3.0.
  4. نتوقّع أنا وزوجتي أن نُرزَق بفتاة في الأشهر القليلة المقبلة، ولذلك عملنا على تعمير منزلنا بكل ماهو ذو لون وردي ولطيف، حتَّى نرى تلك اللّمسة الرّقيقة في كلّ ما اقتنيناه للحضانة، هذا ما ألهمني لكتابة درس للمبتدئين في برنامج (Illustrator) في هذا الموضوع، الذي أفصل فيه كيف يمكننا إنشاء تصميم خاص بالأطفال، معتمدين على أشكال وأنماط بسيطة. تابع هذا الدّرس خطوة خطوة لإنشاء مجموعة من التصاميم البسيطة، مع توظيف بعض الطّرق المتقدّمة شيئا ما لتكوين مجموعة قابلة للاستعمال والتصرف في الحوصلة. سوف يكون درسنا مقسما إلى أربعة أقسام للإحاطة بعمليّة تصميم كل واحد من تلك الألعاب أو الدُمى المتحرّكة، سنبتدئ في هذا بأيسر الطّرق ثمّ بعد ذلك في كلّ مرّةٍ نضيف بعض الطّرق المتقدّمة خلال العمل. أساسا، سنعمل بأداة (Shape tool)، وكذلك (Pathfinder) ثمّ ننظر كيفيّة عمل (Swatches) والخطوط لتحقيق هذا العمل الفنّيّ. تشكيل سحابةسنبدأ عملنا هذا من أبسط الأشكال مقارنة مع بقيّة التّشكيلات، ألا وهو السّحاب، ابدأ برسم ثلاثة دوائر بأحجام مختلفة على لوحة الرّسم ولا تنس الضّغط على (shift) خلال الرّسم للحفاظ على هيئة الدّائرة الصّحيحة، ثمّ قم بالضّغط على جميعها واحدة واحدة ثمّ قم باستعمال (alignment panel) لترصيف الدّوائر أفقيّا إلى الأسفل. أضف شكل مستطيل لملء الفراغات ولتشكيل قاعدة متّصلة، ولا تجعل المستطيل يتجاوز نهايات الدّوائر وإلّا فإنّك ستجد نقاط حادّة خارج حدود السّحابة. اضغط على خاصّيّة (Unite) من (Pathfinder) وذلك من أجل لمّ الأجزاء ودمجها كلّيّة. غيّر خطّ السّحابة حتّى يكون مثل غرز الإبر وذلك عن طريق تفعيل (Dash Line) واملأ الفراغين (Dash/Gap) بعددين لتعيين المسافة بين أجزاء الخطّ المقسّم ثمّ اضغط على زرّ (Round Cap) حتَّى تجعل نهاية أجزاء الخطوط انسابيّة وجميلة لا مستطيلة حادّة. غيّر البياض الطبيعي الذي يملأ الشّكل، واضغط مرّتين على لون الخطّ ثم اختر لونا أرجوانيا فاتحا وللتّسهيل فهذا رمز اللّون الذي عملنا عليه في درسنا هذا (#C093C6). اجعل شكل السّحابة مضغوطا عليه ثم اذهب إلى: Object > Path > Offset Path ثمّ اجعله ثلاثة مليمتر (3mm). حدّد الأخير المعدّل للتو ثمّ حسّنه عبر زيادة حجمه بستّ نقاط (6pt) وعدّل (Dash/Gap) إلى صفر وخمسة نقاط (0pt , 5pt). هذا سيجعل الخطّ المتقطّع يبدو كدائرات متداخلة ممّا يجعلها تبدو جميلة من جهة أحرف الشّكل الذي نصممّه. غيّر لون الخطّ المنقّط الخارجيّ للّون الورديّ الطّفوليّ، وفي درسنا هذا اخترنا (#F7B5D3). اختر أداة رسم المستطيل (Rectangle tool) ثمّ قم بنقرة واحدة في مكان ما على لوحة الرّسم، ثمّ أدخل بيانات حجم هذا المستطيل ثلاثة على ستة مليمتر (3x6mm) ليكون لديك مستطيل مثل الذي نستعمله في درسنا هذا بالضّبط. لوّن المستطيل بنفس اللّون الورديّ ثم انسخه والصقه أمام الأوّل وذلك عبر ضغط الأزرار بالتّوالي (Ctrl+C ثم Ctrl+F). أدر المستطيل المنسوخ لتسعين درجة ليبدو كالقاطع والمقطوع (90°) ثمّ زد في الشّفافيّة عبر تخفيض (Opacity) لكلا الشّكلين بخمسين بالمائة (50%). اسحب الشّكلين نحو (Swatches palette)، فإنّ هذا سيولّد (Swatch) تمكنك من صبغِ كلّ الأجزاء بنفس اللّون وهذا ما سيمكّننا من تكوين (Pattern) منقوش. استعمل هذه (Swatch) الجديدة لصبغ حافية شكل السحابة، ثمَّ تأكّد من أنّها قد فعّلت حتى نحافظ على سلامة الخطّ المنقّط. تشكيل القلبالآن تعالوا بنا ننتقل إلى أمر أكثر تعقيدا. ارسم مستطيلا مدوّرا على لوحة الرّسم ولكن قبل نزع اصبعك من على الفأرة وسّع الزاوية قدر المستطاع بالضغط على الرز الأيمن للفأرة ثمّ حرّك الفأرة خمسا وأربعين درجة (45%). انسخ هذا الشّكل ثمّ أدره حتى يصبح على شكل قاطع ومقطوع، (Shape builder) بديل جيّد لـ (Pathfinder) استعمله لقطع الزائدين في الأسفل ضاغطا على (Alt). (Shape builder) يمكنه أيضا مزجها معا مثل (Pathfinder)، قم برسم خطّ يقطع الأجزاء الثلاثة لجعلهما شيئا واحدا. يمكننا الآن اتّباع نفس الطريقة لعمل النموذج الورديّ مبتدئين مع الخط الخارجيّ بثلاثة مليمتر (3mm) . بدلا من إعادة كلّ الخطوات السابقة بالإمكان استعمال (eyedroppper) وهي ستنسخ لك النموذج على الشّكل الجديد بلا عناء. تشكيل الشجرةارسم دائرة على لوحة الرّسم ثمّ استعمل القلم لرسم شكل قريب ممّا تراه في الصّورة ليكون جذع الشّجرة. استعمل (Pathfinder) أو (Shape builder) لدمج الشّكليْن معا. اجعل الخطّ مقطّعا متّبعا نفس الخطوات المشروحة سابقا باستخدام (Eyedroppper) من أجل تزيين الشّكل بالنّموذج الورديّ. هذه المرّة سنستعمل القلم لإضافة بعض الميزات لرسومنا، اجعل الخطّ بحجم ثلاثة نقاط (3pt)، ثمّ اضغط على (Shift) واللّون الأرجوانيّ فقط وليس كلّ النموذج كما هو الحال بالضّغط بزرّ الفأرة الأيسر مباشرة، ثمّ ارسم خطّا لرسم فرع الشجرة. سيقوم القلم بإعادة رسم ذلك الخطّ، ولذلك قم بالضغط مرّتين على القلم ثمّ عطّل خاصّيّة (keep selected) ثمّ أكمل رسم فروع الشّجرة. تذكّر أنّه يمكنك دائما تعيين الخطّ وإعادة رسمه بالقلم إذا لم يكن رسمك الأوّل سليما أو إذا أردت تعديل بعض النّقاط في رسمك فإنّه يمكنك تحريكها بـ (Direct selection tool). إضافة بعض الخطوط والرسّوم يدويّا تزيد من جمال الرّسم، والآن فتعالوا نتقدّم خطوة أخرى إلى الأمام. تشكيل الفراشةالآن باستخدام أداة (Ellipse tool) و(Pathfinder) فقط، سنرسم ثمّ ندمج مجموعة من الرّسومات حتى نكوّن الجسم الأساسيّ للفراشة الطّائرة. لتحقيق التطابق بين الطّرف الأيمن و الأيسر اذهب إلى: Object > Transform > Reflect ثمّ اختر (Vertical) ثمّ اضغط على (Copy)، ثمّ ضع الجناح الجديد في مكانه. انسخ الجسم الأساسيّ إلى ذاكرة الحاسوب (Ctrl+c) لأنّنا سنلصقه فيما بعد. ادمج كلّ الأشكال مستعملا (Pathfinder) أو (Shape Builder) ثمّ انجز النموذج الورديّ الذي أعددناه. ليس من اللّائق ترك فراشتنا مكوّنة فقط من هذا النموذج البسيط معدومة من المزيد من التفاصيل، من أجل ذلك سنقوم بلصق ما نسخناه في ذاكرة الحاسوب (Ctrl+v) داخل جسم الفراشة ثمّ غيّر التصميم إلى نموذج الخطّ المتقطع باستخدام (Eyedroppper). الآن سنرسم ابتسامة جميلة على وجه فراشتنا. يمكننا تزيين الأجنحة أيضا باستعمال القلم نستخدمه في ذلك مع خاصّيّة (Keep selected option) لأنّه من الصّعب أن نرسم دائرة أو دوائر بالقلم دون أن نعدّل آخر جزء منها والخاصّيّة (Keep selected) معطّلة في حين أنّ تفعيلها يجمع أواخر الرّسم بالقلم معا فيكون حينئذ الشّكل أفضل بكثير. الآن ارسم دوائر مركزيّة بالقلم ولا تكترث للزّلات لأنّه يمكنك بسهولة إعادة رسم الأجزاء لتنظيف الرّسم أكثر. اذهب إلى: Object > Transform > Reflect لقلب الرّسم مع التطابق للجهة الأخرى. تتمة الرسمارسم دائرة صغيرة ومستطيلا في لوحة الرّسم ثمّ قم باستعمال (Align panel) لتصفيفهما من الجهتين كما يظهر في الصّورة: أعط الدّائرة لونا أرجوانيّا وشفافيّة بـعشرين بالمائة (20% Opacity) ثمّ أزل اللّون الذي يملأ المستطيل وكذلك لون المحيط حتّى يكون غير مرئيّ، ثمّ اسحب الشكليْن نحو (Swatches panel) لصناعة نموذج جديد منهما. طبّق نموذج الّنقاط على مستطيل كبير حتى يستوعب الرّسوم التي هي أصغر منه، واضغط على (Crtl+Shit) ورمز "]" أو اضغط الزّر الأيمن للفأرة واختر: select Arrange > Send to Back استعمل القلم لرسم خطوط على الخلفيّة. أخف هذه الخطوط خلف الرسوم عن طريق إرجاعها للخلف مرّات عديدة بالضّغط على(Crtl+Shit) ورمز "]" أوبالضّغط على الزّر الأيمن للفأرة واختر: select Arrange > Send to Back الصّورة النهائيّة تمتاز بمجموعة من الأشكال التي رسمناها في هذا الدّرس، قمنا بذلك باستعمال (Pathfinder) و(Shape Builder) وكذلك عن طريق العمل على الخطوط وخصائصها و(Swatch palette) وأخيرا استعمال القلم من أجل إضافة بعض الإمتيازات لرسومنا. ترجمة -وبتصرف- للدرس Beginner Illustrator Tutorial: Cute Baby Style Artwork.