## پیش‌نیازها در این درس، از کتابخانه‌ای به نام **OpenAI Gym** برای شبیه‌سازی محیط‌های مختلف استفاده خواهیم کرد. شما می‌توانید کد این درس را به صورت محلی اجرا کنید (مثلاً از طریق Visual Studio Code)، که در این صورت شبیه‌سازی در یک پنجره جدید باز خواهد شد. اگر کد را به صورت آنلاین اجرا می‌کنید، ممکن است نیاز باشد تغییراتی در کد ایجاد کنید، همانطور که [اینجا](https://towardsdatascience.com/rendering-openai-gym-envs-on-binder-and-google-colab-536f99391cc7) توضیح داده شده است. ## OpenAI Gym در درس قبلی، قوانین بازی و وضعیت توسط کلاس `Board` که خودمان تعریف کرده بودیم مشخص می‌شد. در اینجا از یک **محیط شبیه‌سازی خاص** استفاده خواهیم کرد که فیزیک پشت میله تعادل را شبیه‌سازی می‌کند. یکی از محبوب‌ترین محیط‌های شبیه‌سازی برای آموزش الگوریتم‌های یادگیری تقویتی، [Gym](https://gym.openai.com/) است که توسط [OpenAI](https://openai.com/) نگهداری می‌شود. با استفاده از این Gym می‌توانیم محیط‌های مختلفی از شبیه‌سازی CartPole تا بازی‌های Atari ایجاد کنیم. > **توجه**: می‌توانید سایر محیط‌های موجود در OpenAI Gym را [اینجا](https://gym.openai.com/envs/#classic_control) مشاهده کنید. ابتدا Gym را نصب کرده و کتابخانه‌های مورد نیاز را وارد کنید (کد بلاک 1): ```python import sys !{sys.executable} -m pip install gym import gym import matplotlib.pyplot as plt import numpy as np import random ``` ## تمرین - مقداردهی اولیه به محیط CartPole برای کار با مسئله تعادل CartPole، باید محیط مربوطه را مقداردهی اولیه کنیم. هر محیط دارای موارد زیر است: - **فضای مشاهده** که ساختار اطلاعاتی را که از محیط دریافت می‌کنیم تعریف می‌کند. برای مسئله CartPole، موقعیت میله، سرعت و برخی مقادیر دیگر را دریافت می‌کنیم. - **فضای عمل** که اقدامات ممکن را تعریف می‌کند. در مورد ما، فضای عمل گسسته است و شامل دو عمل - **چپ** و **راست** می‌شود. (کد بلاک 2) 1. برای مقداردهی اولیه، کد زیر را تایپ کنید: ```python env = gym.make("CartPole-v1") print(env.action_space) print(env.observation_space) print(env.action_space.sample()) ``` برای مشاهده نحوه عملکرد محیط، یک شبیه‌سازی کوتاه برای 100 مرحله اجرا کنید. در هر مرحله، یکی از اقدامات را ارائه می‌دهیم - در این شبیه‌سازی، به صورت تصادفی یک اقدام از `action_space` انتخاب می‌شود. 1. کد زیر را اجرا کنید و ببینید چه نتیجه‌ای حاصل می‌شود. ✅ به یاد داشته باشید که ترجیحاً این کد را روی نصب محلی Python اجرا کنید! (کد بلاک 3) ```python env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close() ``` باید چیزی مشابه این تصویر مشاهده کنید: ![CartPole بدون تعادل](../../../../8-Reinforcement/2-Gym/images/cartpole-nobalance.gif) 1. در طول شبیه‌سازی، باید مشاهداتی دریافت کنیم تا تصمیم بگیریم چگونه عمل کنیم. در واقع، تابع step مشاهدات فعلی، یک تابع پاداش و یک پرچم done را که نشان می‌دهد آیا ادامه شبیه‌سازی منطقی است یا خیر، بازمی‌گرداند: (کد بلاک 4) ```python env.reset() done = False while not done: env.render() obs, rew, done, info = env.step(env.action_space.sample()) print(f"{obs} -> {rew}") env.close() ``` در خروجی نوت‌بوک باید چیزی شبیه به این مشاهده کنید: ```text [ 0.03403272 -0.24301182 0.02669811 0.2895829 ] -> 1.0 [ 0.02917248 -0.04828055 0.03248977 0.00543839] -> 1.0 [ 0.02820687 0.14636075 0.03259854 -0.27681916] -> 1.0 [ 0.03113408 0.34100283 0.02706215 -0.55904489] -> 1.0 [ 0.03795414 0.53573468 0.01588125 -0.84308041] -> 1.0 ... [ 0.17299878 0.15868546 -0.20754175 -0.55975453] -> 1.0 [ 0.17617249 0.35602306 -0.21873684 -0.90998894] -> 1.0 ``` بردار مشاهده‌ای که در هر مرحله از شبیه‌سازی بازگردانده می‌شود شامل مقادیر زیر است: - موقعیت چرخ دستی - سرعت چرخ دستی - زاویه میله - نرخ چرخش میله 1. حداقل و حداکثر مقدار این اعداد را دریافت کنید: (کد بلاک 5) ```python print(env.observation_space.low) print(env.observation_space.high) ``` همچنین ممکن است متوجه شوید که مقدار پاداش در هر مرحله شبیه‌سازی همیشه 1 است. این به این دلیل است که هدف ما زنده ماندن تا حد ممکن است، یعنی نگه داشتن میله در موقعیت عمودی معقول برای طولانی‌ترین مدت زمان ممکن. ✅ در واقع، شبیه‌سازی CartPole زمانی حل شده در نظر گرفته می‌شود که بتوانیم میانگین پاداش 195 را در 100 آزمایش متوالی کسب کنیم. ## گسسته‌سازی وضعیت در Q-Learning، باید یک Q-Table بسازیم که مشخص کند در هر وضعیت چه کاری انجام دهیم. برای انجام این کار، وضعیت باید **گسسته** باشد، به طور دقیق‌تر، باید شامل تعداد محدودی از مقادیر گسسته باشد. بنابراین، باید به نوعی مشاهدات خود را **گسسته‌سازی** کنیم و آنها را به مجموعه‌ای محدود از وضعیت‌ها نگاشت کنیم. چند روش برای انجام این کار وجود دارد: - **تقسیم به بازه‌ها**. اگر بازه یک مقدار خاص را بدانیم، می‌توانیم این بازه را به تعدادی **بازه** تقسیم کنیم و سپس مقدار را با شماره بازه‌ای که به آن تعلق دارد جایگزین کنیم. این کار را می‌توان با استفاده از روش [`digitize`](https://numpy.org/doc/stable/reference/generated/numpy.digitize.html) در numpy انجام داد. در این حالت، اندازه وضعیت را دقیقاً می‌دانیم، زیرا به تعداد بازه‌هایی که برای دیجیتالی‌سازی انتخاب می‌کنیم بستگی دارد. ✅ می‌توانیم از درون‌یابی خطی برای آوردن مقادیر به یک بازه محدود (مثلاً از -20 تا 20) استفاده کنیم و سپس اعداد را با گرد کردن به اعداد صحیح تبدیل کنیم. این روش کنترل کمتری بر اندازه وضعیت به ما می‌دهد، به خصوص اگر بازه‌های دقیق مقادیر ورودی را ندانیم. به عنوان مثال، در مورد ما 2 مورد از 4 مقدار هیچ حد بالا/پایینی ندارند، که ممکن است منجر به تعداد بی‌نهایت وضعیت شود. در مثال ما، از روش دوم استفاده خواهیم کرد. همانطور که بعداً متوجه خواهید شد، با وجود عدم تعریف حد بالا/پایین، این مقادیر به ندرت مقادیر خارج از بازه‌های محدود خاصی می‌گیرند، بنابراین وضعیت‌هایی با مقادیر شدید بسیار نادر خواهند بود. 1. در اینجا تابعی است که مشاهده را از مدل ما می‌گیرد و یک تاپل از 4 مقدار صحیح تولید می‌کند: (کد بلاک 6) ```python def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int)) ``` 1. بیایید روش دیگری برای گسسته‌سازی با استفاده از بازه‌ها بررسی کنیم: (کد بلاک 7) ```python def create_bins(i,num): return np.arange(num+1)*(i[1]-i[0])/num+i[0] print("Sample bins for interval (-5,5) with 10 bins\n",create_bins((-5,5),10)) ints = [(-5,5),(-2,2),(-0.5,0.5),(-2,2)] # intervals of values for each parameter nbins = [20,20,10,10] # number of bins for each parameter bins = [create_bins(ints[i],nbins[i]) for i in range(4)] def discretize_bins(x): return tuple(np.digitize(x[i],bins[i]) for i in range(4)) ``` 1. حالا یک شبیه‌سازی کوتاه اجرا کنید و این مقادیر گسسته محیط را مشاهده کنید. می‌توانید هر دو `discretize` و `discretize_bins` را امتحان کنید و ببینید آیا تفاوتی وجود دارد. ✅ `discretize_bins` شماره بازه را بازمی‌گرداند که از 0 شروع می‌شود. بنابراین برای مقادیر متغیر ورودی در حدود 0، عددی از وسط بازه (10) بازمی‌گرداند. در `discretize`، به بازه مقادیر خروجی توجه نکردیم و اجازه دادیم مقادیر منفی باشند، بنابراین مقادیر وضعیت جابجا نشده‌اند و 0 متناظر با 0 است. (کد بلاک 8) ```python env.reset() done = False while not done: #env.render() obs, rew, done, info = env.step(env.action_space.sample()) #print(discretize_bins(obs)) print(discretize(obs)) env.close() ``` ✅ خطی که با `env.render` شروع می‌شود را اگر می‌خواهید ببینید محیط چگونه اجرا می‌شود، از حالت کامنت خارج کنید. در غیر این صورت می‌توانید آن را در پس‌زمینه اجرا کنید که سریع‌تر است. ما از این اجرای "نامرئی" در طول فرآیند Q-Learning استفاده خواهیم کرد. ## ساختار Q-Table در درس قبلی، وضعیت یک جفت ساده از اعداد از 0 تا 8 بود و بنابراین راحت بود که Q-Table را با یک آرایه numpy با شکل 8x8x2 نمایش دهیم. اگر از گسسته‌سازی بازه‌ها استفاده کنیم، اندازه بردار وضعیت ما نیز مشخص است، بنابراین می‌توانیم از همان روش استفاده کنیم و وضعیت را با یک آرایه با شکل 20x20x10x10x2 نمایش دهیم (اینجا 2 بعد فضای عمل است و ابعاد اول متناظر با تعداد بازه‌هایی است که برای هر یک از پارامترهای فضای مشاهده انتخاب کرده‌ایم). با این حال، گاهی اوقات ابعاد دقیق فضای مشاهده مشخص نیست. در مورد تابع `discretize`، ممکن است هرگز مطمئن نباشیم که وضعیت ما در محدوده خاصی باقی می‌ماند، زیرا برخی از مقادیر اصلی محدود نیستند. بنابراین، از رویکرد کمی متفاوت استفاده خواهیم کرد و Q-Table را با یک دیکشنری نمایش می‌دهیم. 1. از زوج *(state,action)* به عنوان کلید دیکشنری استفاده کنید و مقدار متناظر با مقدار ورودی Q-Table باشد. (کد بلاک 9) ```python Q = {} actions = (0,1) def qvalues(state): return [Q.get((state,a),0) for a in actions] ``` در اینجا همچنین تابعی به نام `qvalues()` تعریف می‌کنیم که لیستی از مقادیر Q-Table را برای یک وضعیت مشخص که متناظر با تمام اقدامات ممکن است بازمی‌گرداند. اگر ورودی در Q-Table وجود نداشته باشد، مقدار پیش‌فرض 0 را بازمی‌گردانیم. ## شروع Q-Learning حالا آماده‌ایم که به پیتر آموزش دهیم چگونه تعادل را حفظ کند! 1. ابتدا برخی از هایپرپارامترها را تنظیم کنیم: (کد بلاک 10) ```python # hyperparameters alpha = 0.3 gamma = 0.9 epsilon = 0.90 ``` در اینجا، `alpha` **نرخ یادگیری** است که مشخص می‌کند تا چه حد باید مقادیر فعلی Q-Table را در هر مرحله تنظیم کنیم. در درس قبلی با مقدار 1 شروع کردیم و سپس `alpha` را در طول آموزش به مقادیر پایین‌تر کاهش دادیم. در این مثال برای سادگی مقدار آن را ثابت نگه می‌داریم و شما می‌توانید بعداً با تنظیم مقادیر `alpha` آزمایش کنید. `gamma` **عامل تخفیف** است که نشان می‌دهد تا چه حد باید پاداش آینده را نسبت به پاداش فعلی اولویت دهیم. `epsilon` **عامل اکتشاف/بهره‌برداری** است که تعیین می‌کند آیا باید اکتشاف را به بهره‌برداری ترجیح دهیم یا بالعکس. در الگوریتم ما، در درصد `epsilon` موارد، اقدام بعدی را بر اساس مقادیر Q-Table انتخاب می‌کنیم و در باقی موارد یک اقدام تصادفی اجرا می‌کنیم. این به ما امکان می‌دهد مناطقی از فضای جستجو را که قبلاً ندیده‌ایم کشف کنیم. ✅ در مورد تعادل - انتخاب اقدام تصادفی (اکتشاف) مانند یک ضربه تصادفی در جهت اشتباه عمل می‌کند و میله باید یاد بگیرد چگونه از این "اشتباهات" تعادل خود را بازیابی کند. ### بهبود الگوریتم می‌توانیم دو بهبود در الگوریتم خود نسبت به درس قبلی ایجاد کنیم: - **محاسبه میانگین پاداش تجمعی**، در طول تعدادی شبیه‌سازی. پیشرفت را هر 5000 تکرار چاپ می‌کنیم و میانگین پاداش تجمعی را در آن دوره زمانی محاسبه می‌کنیم. این بدان معناست که اگر بیش از 195 امتیاز کسب کنیم - می‌توانیم مسئله را حل شده در نظر بگیریم، حتی با کیفیتی بالاتر از حد مورد نیاز. - **محاسبه حداکثر نتیجه تجمعی میانگین**، `Qmax`، و Q-Table متناظر با آن نتیجه را ذخیره می‌کنیم. وقتی آموزش را اجرا می‌کنید، متوجه می‌شوید که گاهی میانگین نتیجه تجمعی شروع به کاهش می‌کند و می‌خواهیم مقادیر Q-Table را که متناظر با بهترین مدل مشاهده شده در طول آموزش است حفظ کنیم. 1. تمام پاداش‌های تجمعی را در هر شبیه‌سازی در بردار `rewards` برای رسم نمودار بعدی جمع‌آوری کنید. (کد بلاک 11) ```python def probs(v,eps=1e-4): v = v-v.min()+eps v = v/v.sum() return v Qmax = 0 cum_rewards = [] rewards = [] for epoch in range(100000): obs = env.reset() done = False cum_reward=0 # == do the simulation == while not done: s = discretize(obs) if random.random() Qmax: Qmax = np.average(cum_rewards) Qbest = Q cum_rewards=[] ``` آنچه ممکن است از این نتایج متوجه شوید: - **نزدیک به هدف ما**. ما بسیار نزدیک به دستیابی به هدف کسب 195 پاداش تجمعی در بیش از 100 اجرای متوالی شبیه‌سازی هستیم، یا ممکن است واقعاً به آن دست یافته باشیم! حتی اگر اعداد کوچکتری کسب کنیم، هنوز نمی‌دانیم، زیرا میانگین را در طول 5000 اجرا محاسبه می‌کنیم و فقط 100 اجرا در معیار رسمی مورد نیاز است. - **پاداش شروع به کاهش می‌کند**. گاهی پاداش شروع به کاهش می‌کند، که به این معناست که ممکن است مقادیر یاد گرفته شده در Q-Table را با مقادیری که وضعیت را بدتر می‌کنند "خراب" کنیم. این مشاهده در صورتی که پیشرفت آموزش را رسم کنیم واضح‌تر است. ## رسم پیشرفت آموزش در طول آموزش، مقدار پاداش تجمعی را در هر یک از تکرارها در بردار `rewards` جمع‌آوری کرده‌ایم. اینجا نحوه نمایش آن در برابر شماره تکرار آمده است: ```python plt.plot(rewards) ``` ![پیشرفت خام](../../../../8-Reinforcement/2-Gym/images/train_progress_raw.png) از این نمودار نمی‌توان چیزی گفت، زیرا به دلیل ماهیت فرآیند آموزش تصادفی، طول جلسات آموزشی بسیار متفاوت است. برای درک بهتر این نمودار، می‌توان میانگین متحرک را در طول یک سری آزمایش‌ها، مثلاً 100، محاسبه کرد. این کار را می‌توان به راحتی با استفاده از `np.convolve` انجام داد: (کد بلاک 12) ```python def running_average(x,window): return np.convolve(x,np.ones(window)/window,mode='valid') plt.plot(running_average(rewards,100)) ``` ![پیشرفت آموزش](../../../../8-Reinforcement/2-Gym/images/train_progress_runav.png) ## تغییر هایپرپارامترها برای پایدارتر کردن یادگیری، منطقی است که برخی از هایپرپارامترهای خود را در طول آموزش تنظیم کنیم. به طور خاص: - **برای نرخ یادگیری**، `alpha`، می‌توانیم با مقادیر نزدیک به 1 شروع کنیم و سپس این پارامتر را کاهش دهیم. با گذشت زمان، احتمال‌های خوبی در Q-Table خواهیم داشت و بنابراین باید آنها را کمی تنظیم کنیم و نه اینکه کاملاً با مقادیر جدید بازنویسی کنیم. - **افزایش epsilon**. ممکن است بخواهیم `epsilon` را به آرامی افزایش دهیم تا کمتر اکتشاف کنیم و بیشتر بهره‌برداری کنیم. احتمالاً منطقی است که با مقدار پایین‌تر `epsilon` شروع کنیم و به تدریج به تقریباً 1 برسیم. > **وظیفه ۱**: با مقادیر هایپرپارامترها بازی کنید و ببینید آیا می‌توانید پاداش تجمعی بیشتری کسب کنید. آیا به بالای ۱۹۵ می‌رسید؟ > **وظیفه ۲**: برای حل رسمی مسئله، باید میانگین پاداش ۱۹۵ را در طول ۱۰۰ اجرای متوالی به دست آورید. این مقدار را در طول آموزش اندازه‌گیری کنید و مطمئن شوید که مسئله را به طور رسمی حل کرده‌اید! ## مشاهده نتیجه در عمل جالب است که ببینیم مدل آموزش‌دیده چگونه رفتار می‌کند. بیایید شبیه‌سازی را اجرا کنیم و همان استراتژی انتخاب عمل را که در طول آموزش استفاده کردیم دنبال کنیم، یعنی نمونه‌گیری بر اساس توزیع احتمالی در Q-Table: (بلوک کد ۱۳) ```python obs = env.reset() done = False while not done: s = discretize(obs) env.render() v = probs(np.array(qvalues(s))) a = random.choices(actions,weights=v)[0] obs,_,done,_ = env.step(a) env.close() ``` شما باید چیزی شبیه به این ببینید: ![یک چرخ دستی متعادل](../../../../8-Reinforcement/2-Gym/images/cartpole-balance.gif) --- ## 🚀چالش > **وظیفه ۳**: در اینجا، ما از نسخه نهایی Q-Table استفاده کردیم که ممکن است بهترین نسخه نباشد. به یاد داشته باشید که ما بهترین Q-Table را در متغیر `Qbest` ذخیره کرده‌ایم! همین مثال را با بهترین Q-Table امتحان کنید، با کپی کردن `Qbest` به `Q` و ببینید آیا تفاوتی مشاهده می‌کنید. > **وظیفه ۴**: در اینجا ما بهترین عمل را در هر مرحله انتخاب نمی‌کردیم، بلکه بر اساس توزیع احتمالی مربوطه نمونه‌گیری می‌کردیم. آیا منطقی‌تر است که همیشه بهترین عمل را با بالاترین مقدار Q-Table انتخاب کنیم؟ این کار را می‌توان با استفاده از تابع `np.argmax` انجام داد تا شماره عمل مربوط به بالاترین مقدار Q-Table را پیدا کنید. این استراتژی را پیاده‌سازی کنید و ببینید آیا تعادل بهبود می‌یابد. ## [آزمون پس از درس](https://ff-quizzes.netlify.app/en/ml/) ## تکلیف [آموزش یک ماشین کوهستانی](assignment.md) ## نتیجه‌گیری ما اکنون یاد گرفته‌ایم که چگونه عوامل را آموزش دهیم تا فقط با ارائه یک تابع پاداش که حالت مطلوب بازی را تعریف می‌کند و با دادن فرصت به آن‌ها برای جستجوی هوشمندانه فضای جستجو، به نتایج خوبی دست یابند. ما الگوریتم Q-Learning را در موارد محیط‌های گسسته و پیوسته، اما با اعمال گسسته، با موفقیت اعمال کرده‌ایم. مهم است که همچنین شرایطی را مطالعه کنیم که در آن حالت عمل نیز پیوسته باشد و فضای مشاهده بسیار پیچیده‌تر باشد، مانند تصویر صفحه بازی آتاری. در این مسائل، اغلب نیاز داریم از تکنیک‌های قدرتمندتر یادگیری ماشین، مانند شبکه‌های عصبی، برای دستیابی به نتایج خوب استفاده کنیم. این موضوعات پیشرفته‌تر، موضوع دوره پیشرفته‌تر هوش مصنوعی ما خواهد بود. --- **سلب مسئولیت**: این سند با استفاده از سرویس ترجمه هوش مصنوعی [Co-op Translator](https://github.com/Azure/co-op-translator) ترجمه شده است. در حالی که ما تلاش می‌کنیم دقت را حفظ کنیم، لطفاً توجه داشته باشید که ترجمه‌های خودکار ممکن است شامل خطاها یا نادرستی‌ها باشند. سند اصلی به زبان اصلی آن باید به عنوان منبع معتبر در نظر گرفته شود. برای اطلاعات حساس، توصیه می‌شود از ترجمه حرفه‌ای انسانی استفاده کنید. ما مسئولیتی در قبال سوءتفاهم‌ها یا تفسیرهای نادرست ناشی از استفاده از این ترجمه نداریم.