You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ML-For-Beginners/translations/uk/8-Reinforcement/2-Gym/README.md

29 KiB

CartPole Катання

Задача, яку ми вирішували в попередньому уроці, може здатися іграшковою, не дуже застосовною до реальних сценаріїв. Але це не так, адже багато реальних проблем мають схожі характеристики — наприклад, гра в шахи чи Ґо. Вони схожі тим, що також мають дошку з певними правилами та дискретний стан.

Передлекційна вікторина

Вступ

У цьому уроці ми застосуємо ті ж принципи Q-навчання до задачі з неперервним станом, тобто станом, який визначається одним або кількома дійсними числами. Ми розглянемо наступну задачу:

Задача: Якщо Петро хоче втекти від вовка, йому потрібно навчитися рухатися швидше. Ми побачимо, як Петро може навчитися кататися на ковзанах, зокрема тримати рівновагу, використовуючи Q-навчання.

Велика втеча!

Петро та його друзі проявляють креативність, щоб втекти від вовка! Зображення від Jen Looper

Ми використаємо спрощену версію задачі збереження рівноваги, відому як задача CartPole. У світі CartPole є горизонтальний слайдер, який може рухатися вліво або вправо, і мета — утримати вертикальний стовп на вершині слайдера.

Передумови

У цьому уроці ми будемо використовувати бібліотеку OpenAI Gym для симуляції різних середовищ. Ви можете запускати код цього уроку локально (наприклад, у Visual Studio Code), у такому випадку симуляція відкриється у новому вікні. Якщо ви запускаєте код онлайн, можливо, доведеться внести деякі зміни в код, як описано тут.

OpenAI Gym

У попередньому уроці правила гри та стан були задані класом Board, який ми визначили самостійно. Тут ми використаємо спеціальне середовище симуляції, яке моделюватиме фізику стовпа, що балансує. Одним із найпопулярніших середовищ симуляції для навчання алгоритмів з підкріпленням є Gym, який підтримується OpenAI. Використовуючи Gym, ми можемо створювати різні середовища — від симуляції CartPole до ігор Atari.

Примітка: Інші середовища, доступні в OpenAI Gym, можна переглянути тут.

Спочатку встановимо Gym і імпортуємо необхідні бібліотеки (блок коду 1):

import sys
!{sys.executable} -m pip install gym 

import gym
import matplotlib.pyplot as plt
import numpy as np
import random

Вправа — ініціалізація середовища CartPole

Щоб працювати із задачею балансування CartPole, потрібно ініціалізувати відповідне середовище. Кожне середовище має:

  • Observation space (простір спостережень), який визначає структуру інформації, яку ми отримуємо від середовища. Для задачі CartPole ми отримуємо положення стовпа, швидкість та інші значення.

  • Action space (простір дій), який визначає можливі дії. У нашому випадку простір дій дискретний і складається з двох дій — вліво та вправо. (блок коду 2)

  1. Для ініціалізації введіть наступний код:

    env = gym.make("CartPole-v1")
    print(env.action_space)
    print(env.observation_space)
    print(env.action_space.sample())
    

Щоб побачити, як працює середовище, запустимо коротку симуляцію на 100 кроків. На кожному кроці ми надаємо одну з дій для виконання — у цій симуляції ми просто випадково вибираємо дію з action_space.

  1. Запустіть код нижче і подивіться, до чого це призведе.

    Пам’ятайте, що бажано запускати цей код на локальній установці Python! (блок коду 3)

    env.reset()
    
    for i in range(100):
       env.render()
       env.step(env.action_space.sample())
    env.close()
    

    Ви повинні побачити щось подібне до цього зображення:

    CartPole без балансу

  2. Під час симуляції нам потрібно отримувати спостереження, щоб вирішити, як діяти. Насправді функція step повертає поточні спостереження, функцію винагороди та прапорець done, який вказує, чи має сенс продовжувати симуляцію: (блок коду 4)

    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()
    

    Ви побачите щось подібне до цього у виводі блокнота:

    [ 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
    

    Вектор спостережень, який повертається на кожному кроці симуляції, містить наступні значення:

    • Положення візка
    • Швидкість візка
    • Кут нахилу стовпа
    • Швидкість обертання стовпа
  3. Отримайте мінімальне та максимальне значення цих чисел: (блок коду 5)

    print(env.observation_space.low)
    print(env.observation_space.high)
    

    Ви також можете помітити, що значення винагороди на кожному кроці симуляції завжди дорівнює 1. Це тому, що наша мета — вижити якомога довше, тобто утримувати стовп у відносно вертикальному положенні максимально довгий час.

    Насправді симуляція CartPole вважається вирішеною, якщо ми зможемо отримати середню винагороду 195 за 100 послідовних спроб.

Дискретизація стану

У Q-навчанні нам потрібно створити Q-таблицю, яка визначає, що робити в кожному стані. Щоб це зробити, стан має бути дискретним, точніше, він повинен містити кінцеву кількість дискретних значень. Таким чином, нам потрібно якось дискретизувати наші спостереження, відображаючи їх у кінцевий набір станів.

Є кілька способів це зробити:

  • Розділення на інтервали. Якщо ми знаємо діапазон певного значення, ми можемо розділити цей діапазон на кілька інтервалів і замінити значення номером інтервалу, до якого воно належить. Це можна зробити за допомогою методу numpy digitize. У цьому випадку ми точно знатимемо розмір стану, оскільки він залежатиме від кількості інтервалів, які ми виберемо для дискретизації.

Ми можемо використовувати лінійну інтерполяцію, щоб привести значення до певного кінцевого діапазону (наприклад, від -20 до 20), а потім перетворити числа на цілі, округливши їх. Це дає нам трохи менше контролю над розміром стану, особливо якщо ми не знаємо точних діапазонів вхідних значень. Наприклад, у нашому випадку 2 із 4 значень не мають верхніх/нижніх меж, що може призвести до нескінченної кількості станів.

У нашому прикладі ми використаємо другий підхід. Як ви помітите пізніше, незважаючи на невизначені верхні/нижні межі, ці значення рідко виходять за певні кінцеві інтервали, тому стани з екстремальними значеннями будуть дуже рідкісними.

  1. Ось функція, яка бере спостереження з нашої моделі та створює кортеж із 4 цілих значень: (блок коду 6)

    def discretize(x):
        return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))
    
  2. Давайте також дослідимо інший метод дискретизації за допомогою інтервалів: (блок коду 7)

    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))
    
  3. Тепер запустимо коротку симуляцію та спостерігатимемо ці дискретні значення середовища. Спробуйте обидва методи discretize та discretize_bins і подивіться, чи є різниця.

    discretize_bins повертає номер інтервалу, який починається з 0. Таким чином, для значень вхідної змінної близько 0 він повертає число з середини інтервалу (10). У discretize ми не враховували діапазон вихідних значень, дозволяючи їм бути від’ємними, тому значення стану не зміщені, і 0 відповідає 0. (блок коду 8)

    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-навчання.

Структура Q-таблиці

У попередньому уроці стан був простою парою чисел від 0 до 8, тому було зручно представляти Q-таблицю за допомогою тензора numpy з формою 8x8x2. Якщо ми використовуємо дискретизацію за інтервалами, розмір нашого вектора стану також відомий, тому ми можемо використовувати той самий підхід і представляти стан у вигляді масиву форми 20x20x10x10x2 (тут 2 — це розмірність простору дій, а перші розміри відповідають кількості інтервалів, які ми вибрали для кожного з параметрів у просторі спостережень).

Однак іноді точні розміри простору спостережень невідомі. У випадку функції discretize ми ніколи не можемо бути впевнені, що наш стан залишається в певних межах, оскільки деякі з початкових значень не обмежені. Тому ми використаємо трохи інший підхід і представимо Q-таблицю у вигляді словника.

  1. Використовуйте пару (state, action) як ключ словника, а значення відповідатиме значенню запису в Q-таблиці. (блок коду 9)

    Q = {}
    actions = (0,1)
    
    def qvalues(state):
        return [Q.get((state,a),0) for a in actions]
    

    Тут ми також визначаємо функцію qvalues(), яка повертає список значень Q-таблиці для заданого стану, що відповідає всім можливим діям. Якщо запис відсутній у Q-таблиці, ми повернемо 0 за замовчуванням.

Починаємо Q-навчання

Тепер ми готові навчити Петра балансувати!

  1. Спочатку встановимо деякі гіперпараметри: (блок коду 10)

    # hyperparameters
    alpha = 0.3
    gamma = 0.9
    epsilon = 0.90
    

    Тут alpha — це швидкість навчання, яка визначає, наскільки ми повинні коригувати поточні значення Q-таблиці на кожному кроці. У попередньому уроці ми починали з 1, а потім зменшували alpha до нижчих значень під час навчання. У цьому прикладі ми залишимо її сталою для простоти, і ви можете експериментувати з налаштуванням значень alpha пізніше.

    gamma — це коефіцієнт дисконтування, який показує, наскільки ми повинні віддавати перевагу майбутній винагороді над поточною.

    epsilon — це фактор дослідження/експлуатації, який визначає, чи повинні ми віддавати перевагу дослідженню чи експлуатації. У нашому алгоритмі ми в epsilon відсотках випадків вибиратимемо наступну дію відповідно до значень Q-таблиці, а в решті випадків виконуватимемо випадкову дію. Це дозволить нам досліджувати області простору пошуку, які ми раніше не бачили.

    У контексті балансування — вибір випадкової дії (дослідження) діятиме як випадковий поштовх у неправильному напрямку, і стовп повинен буде навчитися відновлювати рівновагу після цих "помилок".

Покращення алгоритму

Ми також можемо зробити два покращення до нашого алгоритму з попереднього уроку:

  • Обчислення середньої кумулятивної винагороди за кілька симуляцій. Ми будемо виводити прогрес кожні 5000 ітерацій і усереднювати нашу кумулятивну винагороду за цей період часу. Це означає, що якщо ми отримаємо більше 195 балів — ми можемо вважати задачу вирішеною, навіть із вищою якістю, ніж потрібно.

  • Обчислення максимальної середньої кумулятивної винагороди, Qmax, і ми збережемо Q-таблицю, що відповідає цьому результату. Коли ви запустите навчання, ви помітите, що іноді середня кумулятивна винагорода починає знижуватися, і ми хочемо зберегти значення Q-таблиці, які відповідають найкращій моделі, спостереженій під час навчання.

  1. Збирайте всі кумулятивні винагороди на кожній симуляції у вектор rewards для подальшого побудови графіків. (блок коду 11)

    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()<epsilon:
                # exploitation - chose the action according to Q-Table probabilities
                v = probs(np.array(qvalues(s)))
                a = random.choices(actions,weights=v)[0]
            else:
                # exploration - randomly chose the action
                a = np.random.randint(env.action_space.n)
    
            obs, rew, done, info = env.step(a)
            cum_reward+=rew
            ns = discretize(obs)
            Q[(s,a)] = (1 - alpha) * Q.get((s,a),0) + alpha * (rew + gamma * max(qvalues(ns)))
        cum_rewards.append(cum_reward)
        rewards.append(cum_reward)
        # == Periodically print results and calculate average reward ==
        if epoch%5000==0:
            print(f"{epoch}: {np.average(cum_rewards)}, alpha={alpha}, epsilon={epsilon}")
            if np.average(cum_rewards) > Qmax:
                Qmax = np.average(cum_rewards)
                Qbest = Q
            cum_rewards=[]
    

Що ви можете помітити з цих результатів:

  • Близько до нашої мети. Ми дуже близькі до досягнення мети отримання 195 кумулятивних винагород за 100+ послідовних запусків симуляції, або ми вже досягли її! Навіть якщо ми отримуємо менші числа, ми все одно не знаємо, тому що усереднюємо за 5000 запусків, а формальний критерій вимагає лише 100 запусків.

  • Винагорода починає знижуватися. Іноді винагорода починає знижуватися, що означає, що ми можемо "зруйнувати" вже вивчені значення в Q-таблиці, замінивши їх тими, які погіршують ситуацію.

Це спостереження стає більш очевидним, якщо ми побудуємо графік прогресу навчання.

Побудова графіка прогресу навчання

Під час навчання ми збирали значення кумулятивної винагороди на кожній ітерації у вектор rewards. Ось як це виглядає, якщо побудувати графік залежності від номера ітерації:

plt.plot(rewards)

Сирий прогрес

З цього графіка неможливо нічого сказати, оскільки через природу стохастичного процесу навчання тривалість навчальних сесій сильно варіюється. Щоб надати більше сенсу цьому графіку, ми можемо обчислити ковзне середнє за серією експериментів, наприклад, 100. Це можна зручно зробити за допомогою np.convolve: (блок коду 12)

def running_average(x,window):
    return np.convolve(x,np.ones(window)/window,mode='valid')

plt.plot(running_average(rewards,100))

Прогрес навчання

Зміна гіперпараметрів

Щоб зробити навчання більш стабільним, має сенс налаштувати деякі з наших гіперпараметрів під час навчання. Зокрема:

  • Для швидкості навчання, alpha, ми можемо почати зі значень, близьких до 1, а потім поступово зменшувати цей параметр. З часом ми будемо отримувати хороші ймовірнісні значення в Q-таблиці, і тому слід коригувати їх незначно, а не повністю перезаписувати новими значеннями.

  • Збільшення epsilon. Ми можемо поступово збільшувати epsilon, щоб менше досліджувати і більше експлуатувати. Ймовірно, має сенс почати з нижчого значення epsilon і поступово збільшувати його до майже 1.

Завдання 1: Спробуйте змінити значення гіперпараметрів і подивіться, чи зможете ви досягти більшої сукупної винагороди. Чи отримуєте ви більше 195? Завдання 2: Щоб формально вирішити проблему, необхідно досягти середньої винагороди 195 протягом 100 послідовних запусків. Виміряйте це під час навчання і переконайтеся, що ви формально вирішили проблему!

Перегляд результату в дії

Було б цікаво побачити, як поводиться навчена модель. Давайте запустимо симуляцію і будемо дотримуватися тієї ж стратегії вибору дій, що й під час навчання, вибираючи дії відповідно до розподілу ймовірностей у Q-таблиці: (блок коду 13)

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()

Ви повинні побачити щось подібне:

балансуючий CartPole


🚀Виклик

Завдання 3: Тут ми використовували фінальну копію Q-таблиці, яка може бути не найкращою. Пам’ятайте, що ми зберегли найкращу Q-таблицю у змінній Qbest! Спробуйте той самий приклад із найкращою Q-таблицею, скопіювавши Qbest у Q, і подивіться, чи помітите різницю.

Завдання 4: Тут ми не вибирали найкращу дію на кожному кроці, а натомість вибирали дії відповідно до розподілу ймовірностей. Чи було б логічніше завжди вибирати найкращу дію з найвищим значенням у Q-таблиці? Це можна зробити за допомогою функції np.argmax, щоб визначити номер дії, що відповідає найвищому значенню Q-таблиці. Реалізуйте цю стратегію і подивіться, чи покращиться балансування.

Тест після лекції

Завдання

Навчіть Mountain Car

Висновок

Ми тепер знаємо, як навчати агентів досягати хороших результатів, просто надаючи їм функцію винагороди, яка визначає бажаний стан гри, і даючи їм можливість розумно досліджувати простір пошуку. Ми успішно застосували алгоритм Q-Learning у випадках дискретних і безперервних середовищ, але з дискретними діями.

Важливо також вивчати ситуації, коли стан дій також є безперервним, а простір спостережень набагато складніший, наприклад, зображення з екрану гри Atari. У таких задачах часто потрібно використовувати більш потужні методи машинного навчання, такі як нейронні мережі, щоб досягти хороших результатів. Ці більш складні теми є предметом нашого майбутнього курсу з просунутого штучного інтелекту.


Відмова від відповідальності:
Цей документ було перекладено за допомогою сервісу автоматичного перекладу Co-op Translator. Хоча ми прагнемо до точності, зверніть увагу, що автоматичні переклади можуть містити помилки або неточності. Оригінальний документ мовою оригіналу слід вважати авторитетним джерелом. Для критично важливої інформації рекомендується професійний людський переклад. Ми не несемо відповідальності за будь-які непорозуміння або неправильні тлумачення, що виникли внаслідок використання цього перекладу.