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/pl/8-Reinforcement/2-Gym/README.md

20 KiB

CartPole Skating

Problem, który rozwiązywaliśmy w poprzedniej lekcji, może wydawać się zabawkowy i mało przydatny w rzeczywistych scenariuszach. Jednak tak nie jest, ponieważ wiele problemów w prawdziwym świecie również ma podobny charakter na przykład gra w szachy czy Go. Są one podobne, ponieważ również mamy planszę z określonymi zasadami i dyskretny stan.

Quiz przed wykładem

Wprowadzenie

W tej lekcji zastosujemy te same zasady Q-Learningu do problemu z ciągłym stanem, czyli stanem opisanym przez jedną lub więcej liczb rzeczywistych. Zajmiemy się następującym problemem:

Problem: Jeśli Piotr chce uciec przed wilkiem, musi nauczyć się poruszać szybciej. Zobaczymy, jak Piotr może nauczyć się jeździć na łyżwach, a w szczególności utrzymywać równowagę, korzystając z Q-Learningu.

Wielka ucieczka!

Piotr i jego przyjaciele wykazują się kreatywnością, aby uciec przed wilkiem! Obraz autorstwa Jen Looper

Użyjemy uproszczonej wersji problemu utrzymywania równowagi, znanej jako problem CartPole. W świecie CartPole mamy poziomy suwak, który może poruszać się w lewo lub w prawo, a celem jest utrzymanie pionowego słupka na górze suwaka.

Wymagania wstępne

W tej lekcji będziemy korzystać z biblioteki OpenAI Gym, aby symulować różne środowiska. Możesz uruchomić kod z tej lekcji lokalnie (np. w Visual Studio Code), w takim przypadku symulacja otworzy się w nowym oknie. Jeśli uruchamiasz kod online, może być konieczne wprowadzenie pewnych zmian w kodzie, jak opisano tutaj.

OpenAI Gym

W poprzedniej lekcji zasady gry i stan były określone przez klasę Board, którą sami zdefiniowaliśmy. Tutaj użyjemy specjalnego środowiska symulacyjnego, które zasymuluje fizykę stojącą za balansującym słupkiem. Jednym z najpopularniejszych środowisk symulacyjnych do trenowania algorytmów uczenia ze wzmocnieniem jest Gym, utrzymywany przez OpenAI. Dzięki Gym możemy tworzyć różne środowiska, od symulacji CartPole po gry Atari.

Uwaga: Inne środowiska dostępne w OpenAI Gym możesz zobaczyć tutaj.

Najpierw zainstalujmy Gym i zaimportujmy wymagane biblioteki (blok kodu 1):

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

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

Ćwiczenie inicjalizacja środowiska CartPole

Aby pracować z problemem balansowania CartPole, musimy zainicjalizować odpowiednie środowisko. Każde środowisko jest związane z:

  • Przestrzenią obserwacji, która definiuje strukturę informacji, jakie otrzymujemy ze środowiska. W przypadku problemu CartPole otrzymujemy pozycję słupka, prędkość i inne wartości.

  • Przestrzenią akcji, która definiuje możliwe działania. W naszym przypadku przestrzeń akcji jest dyskretna i składa się z dwóch działań lewo i prawo. (blok kodu 2)

  1. Aby zainicjalizować środowisko, wpisz następujący kod:

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

Aby zobaczyć, jak działa środowisko, uruchommy krótką symulację na 100 kroków. Na każdym kroku podajemy jedną z akcji do wykonania w tej symulacji losowo wybieramy akcję z action_space.

  1. Uruchom poniższy kod i zobacz, co się stanie.

    Pamiętaj, że preferowane jest uruchamianie tego kodu na lokalnej instalacji Pythona! (blok kodu 3)

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

    Powinieneś zobaczyć coś podobnego do tego obrazu:

    CartPole bez równowagi

  2. Podczas symulacji musimy uzyskać obserwacje, aby zdecydować, jak działać. W rzeczywistości funkcja step zwraca bieżące obserwacje, funkcję nagrody oraz flagę done, która wskazuje, czy symulacja powinna być kontynuowana, czy nie: (blok kodu 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()
    

    W notatniku powinieneś zobaczyć coś takiego:

    [ 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
    

    Wektor obserwacji zwracany na każdym kroku symulacji zawiera następujące wartości:

    • Pozycja wózka
    • Prędkość wózka
    • Kąt słupka
    • Prędkość obrotowa słupka
  3. Uzyskaj minimalne i maksymalne wartości tych liczb: (blok kodu 5)

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

    Możesz również zauważyć, że wartość nagrody na każdym kroku symulacji wynosi zawsze 1. Dzieje się tak, ponieważ naszym celem jest przetrwanie jak najdłużej, tj. utrzymanie słupka w możliwie pionowej pozycji przez najdłuższy czas.

    W rzeczywistości symulacja CartPole jest uznawana za rozwiązaną, jeśli uda nam się uzyskać średnią nagrodę 195 w 100 kolejnych próbach.

Dyskretyzacja stanu

W Q-Learningu musimy zbudować Q-Table, która określa, co robić w każdym stanie. Aby to zrobić, stan musi być dyskretny, a dokładniej, powinien zawierać skończoną liczbę wartości dyskretnych. Dlatego musimy w jakiś sposób zdyskretyzować nasze obserwacje, mapując je na skończony zbiór stanów.

Istnieje kilka sposobów, aby to zrobić:

  • Podział na przedziały. Jeśli znamy zakres danej wartości, możemy podzielić ten zakres na liczbę przedziałów, a następnie zastąpić wartość numerem przedziału, do którego należy. Można to zrobić za pomocą metody numpy digitize. W tym przypadku dokładnie znamy rozmiar stanu, ponieważ zależy on od liczby przedziałów, które wybierzemy do digitalizacji.

Możemy użyć interpolacji liniowej, aby sprowadzić wartości do pewnego skończonego zakresu (np. od -20 do 20), a następnie przekonwertować liczby na liczby całkowite przez zaokrąglenie. Daje nam to nieco mniej kontroli nad rozmiarem stanu, szczególnie jeśli nie znamy dokładnych zakresów wartości wejściowych. Na przykład w naszym przypadku 2 z 4 wartości nie mają górnych/dolnych ograniczeń, co może skutkować nieskończoną liczbą stanów.

W naszym przykładzie wybierzemy drugie podejście. Jak zauważysz później, mimo nieokreślonych górnych/dolnych ograniczeń, te wartości rzadko przyjmują wartości poza pewnymi skończonymi zakresami, więc stany z ekstremalnymi wartościami będą bardzo rzadkie.

  1. Oto funkcja, która pobiera obserwację z naszego modelu i zwraca krotkę 4 wartości całkowitych: (blok kodu 6)

    def discretize(x):
        return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))
    
  2. Przyjrzyjmy się również innej metodzie dyskretyzacji za pomocą przedziałów: (blok kodu 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. Uruchommy teraz krótką symulację i zaobserwujmy te zdyskretyzowane wartości środowiska. Możesz wypróbować zarówno discretize, jak i discretize_bins, aby zobaczyć, czy istnieje różnica.

    discretize_bins zwraca numer przedziału, który zaczyna się od 0. Dlatego dla wartości zmiennej wejściowej w okolicach 0 zwraca numer ze środka zakresu (10). W discretize nie przejmowaliśmy się zakresem wartości wyjściowych, pozwalając im być ujemnymi, więc wartości stanu nie są przesunięte, a 0 odpowiada 0. (blok kodu 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()
    

    Odkomentuj linię zaczynającą się od env.render, jeśli chcesz zobaczyć, jak środowisko działa. W przeciwnym razie możesz uruchomić je w tle, co jest szybsze. Tę "niewidzialną" egzekucję wykorzystamy podczas procesu Q-Learningu.

Struktura Q-Table

W poprzedniej lekcji stan był prostą parą liczb od 0 do 8, więc wygodnie było reprezentować Q-Table jako tensor numpy o kształcie 8x8x2. Jeśli używamy dyskretyzacji za pomocą przedziałów, rozmiar naszego wektora stanu jest również znany, więc możemy użyć tego samego podejścia i reprezentować stan jako tablicę o kształcie 20x20x10x10x2 (gdzie 2 to wymiar przestrzeni akcji, a pierwsze wymiary odpowiadają liczbie przedziałów, które wybraliśmy dla każdej z wartości w przestrzeni obserwacji).

Jednak czasami dokładne wymiary przestrzeni obserwacji nie są znane. W przypadku funkcji discretize nigdy nie możemy być pewni, że nasz stan pozostaje w określonych granicach, ponieważ niektóre z oryginalnych wartości nie są ograniczone. Dlatego użyjemy nieco innego podejścia i przedstawimy Q-Table jako słownik.

  1. Użyj pary (stan, akcja) jako klucza słownika, a wartość będzie odpowiadać wartości wpisu w Q-Table. (blok kodu 9)

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

    Tutaj definiujemy również funkcję qvalues(), która zwraca listę wartości Q-Table dla danego stanu, odpowiadającą wszystkim możliwym akcjom. Jeśli wpis nie jest obecny w Q-Table, zwrócimy 0 jako wartość domyślną.

Zaczynamy Q-Learning

Teraz jesteśmy gotowi, aby nauczyć Piotra balansowania!

  1. Najpierw ustawmy kilka hiperparametrów: (blok kodu 10)

    # hyperparameters
    alpha = 0.3
    gamma = 0.9
    epsilon = 0.90
    

    Tutaj alpha to współczynnik uczenia się, który określa, w jakim stopniu powinniśmy dostosowywać bieżące wartości Q-Table na każdym kroku. W poprzedniej lekcji zaczynaliśmy od 1, a następnie zmniejszaliśmy alpha do niższych wartości podczas treningu. W tym przykładzie utrzymamy go na stałym poziomie dla uproszczenia, ale możesz eksperymentować z dostosowywaniem wartości alpha później.

    gamma to współczynnik dyskontowy, który pokazuje, w jakim stopniu powinniśmy priorytetyzować przyszłą nagrodę nad bieżącą.

    epsilon to współczynnik eksploracji/eksploatacji, który określa, czy powinniśmy preferować eksplorację czy eksploatację. W naszym algorytmie w epsilon procentach przypadków wybierzemy następną akcję zgodnie z wartościami Q-Table, a w pozostałych przypadkach wykonamy losową akcję. Pozwoli nam to eksplorować obszary przestrzeni poszukiwań, których wcześniej nie widzieliśmy.

    W kontekście balansowania wybór losowej akcji (eksploracja) działałby jak przypadkowe "pchnięcie" w złą stronę, a słupek musiałby nauczyć się, jak odzyskać równowagę po tych "błędach".

Ulepszanie algorytmu

Możemy również wprowadzić dwa ulepszenia do naszego algorytmu z poprzedniej lekcji:

  • Obliczanie średniej skumulowanej nagrody w serii symulacji. Będziemy drukować postęp co 5000 iteracji i uśredniać naszą skumulowaną nagrodę w tym okresie. Oznacza to, że jeśli uzyskamy więcej niż 195 punktów możemy uznać problem za rozwiązany, i to z jeszcze wyższą jakością niż wymagana.

  • Obliczanie maksymalnego średniego wyniku skumulowanego, Qmax, i przechowywanie Q-Table odpowiadającej temu wynikowi. Podczas treningu zauważysz, że czasami średni wynik skumulowany zaczyna spadać, a my chcemy zachować wartości Q-Table odpowiadające najlepszemu modelowi zaobserwowanemu podczas treningu.

  1. Zbierz wszystkie skumulowane nagrody z każdej symulacji w wektorze rewards do dalszego wykreślania. (blok kodu 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=[]
    

Co możesz zauważyć z tych wyników:

  • Blisko naszego celu. Jesteśmy bardzo blisko osiągnięcia celu uzyskania 195 skumulowanych nagród w ponad 100 kolejnych uruchomieniach symulacji, lub możemy już go osiągnęliśmy! Nawet jeśli uzyskamy mniejsze liczby, nadal nie wiemy, ponieważ uśredniamy wyniki z 5000 uruchomień, a formalne kryterium wymaga tylko 100 uruchomień.

  • Nagroda zaczyna spadać. Czasami nagroda zaczyna spadać, co oznacza, że możemy "zniszczyć" już wyuczone wartości w Q-Table, zastępując je tymi, które pogarszają sytuację.

To zjawisko jest bardziej widoczne, jeśli wykreślimy postęp treningu.

Wykres postępu treningu

Podczas treningu zbieraliśmy wartość skumulowanej nagrody na każdej iteracji w wektorze rewards. Oto jak wygląda, gdy wykreślimy ją względem numeru iteracji:

plt.plot(rewards)

surowy postęp

Z tego wykresu trudno coś wywnioskować, ponieważ ze względu na charakter stochastycznego procesu treningowego długość sesji treningowych znacznie się różni. Aby lepiej zrozumieć ten wykres, możemy obliczyć średnią kroczącą dla serii eksperymentów, na przykład 100. Można to wygodnie zrobić za pomocą np.convolve: (blok kodu 12)

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

plt.plot(running_average(rewards,100))

postęp treningu

Zmienianie hiperparametrów

Aby uczynić proces uczenia bardziej stabilnym, warto dostosować niektóre z naszych hiperparametrów podczas treningu. W szczególności:

  • Dla współczynnika uczenia się, alpha, możemy zacząć od wartości bliskich 1, a następnie stopniowo zmniejszać ten parametr. Z czasem będziemy uzyskiwać dobre wartości prawdopodobieństwa w Q-Table, więc powinniśmy je dostosowywać delikatnie, a nie całkowicie nadpisywać nowymi wartościami.

  • Zwiększanie epsilon. Możemy chcieć stopniowo zwiększać epsilon, aby mniej eksplorować, a bardziej eksploatować. Prawdopodobnie warto zacząć od niższej wartości epsilon i stopniowo zwiększać ją do prawie 1.

Zadanie 1: Pobaw się wartościami hiperparametrów i sprawdź, czy możesz osiągnąć wyższą skumulowaną nagrodę. Czy udaje Ci się przekroczyć 195? Zadanie 2: Aby formalnie rozwiązać problem, musisz osiągnąć średnią nagrodę na poziomie 195 w 100 kolejnych próbach. Mierz to podczas treningu i upewnij się, że formalnie rozwiązałeś problem!

Zobaczenie wyników w praktyce

Ciekawie byłoby zobaczyć, jak zachowuje się wytrenowany model. Uruchommy symulację i zastosujmy tę samą strategię wyboru akcji, co podczas treningu, próbkując zgodnie z rozkładem prawdopodobieństwa w Q-Table: (blok kodu 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()

Powinieneś zobaczyć coś takiego:

a balancing cartpole


🚀Wyzwanie

Zadanie 3: Tutaj korzystaliśmy z ostatecznej wersji Q-Table, która może nie być najlepsza. Pamiętaj, że zapisaliśmy najlepiej działającą Q-Table w zmiennej Qbest! Wypróbuj ten sam przykład, używając najlepiej działającej Q-Table, kopiując Qbest do Q, i sprawdź, czy zauważysz różnicę.

Zadanie 4: W tym przypadku nie wybieraliśmy najlepszej akcji na każdym kroku, lecz próbkowaliśmy zgodnie z odpowiadającym rozkładem prawdopodobieństwa. Czy miałoby więcej sensu zawsze wybierać najlepszą akcję, z najwyższą wartością w Q-Table? Można to zrobić, używając funkcji np.argmax, aby znaleźć numer akcji odpowiadający najwyższej wartości w Q-Table. Zaimplementuj tę strategię i sprawdź, czy poprawia to balansowanie.

Quiz po wykładzie

Zadanie

Wytrenuj Mountain Car

Podsumowanie

Nauczyliśmy się, jak trenować agentów, aby osiągali dobre wyniki, dostarczając im jedynie funkcję nagrody, która definiuje pożądany stan gry, oraz dając im możliwość inteligentnego eksplorowania przestrzeni poszukiwań. Z powodzeniem zastosowaliśmy algorytm Q-Learning w przypadkach środowisk dyskretnych i ciągłych, ale z dyskretnymi akcjami.

Ważne jest również badanie sytuacji, w których przestrzeń akcji jest również ciągła, a przestrzeń obserwacji jest znacznie bardziej złożona, na przykład obraz z ekranu gry Atari. W takich problemach często musimy korzystać z bardziej zaawansowanych technik uczenia maszynowego, takich jak sieci neuronowe, aby osiągnąć dobre wyniki. Te bardziej zaawansowane tematy będą przedmiotem naszego kolejnego, bardziej zaawansowanego kursu AI.


Zastrzeżenie:
Ten dokument został przetłumaczony za pomocą usługi tłumaczenia AI Co-op Translator. Chociaż dokładamy wszelkich starań, aby tłumaczenie było precyzyjne, prosimy pamiętać, że automatyczne tłumaczenia mogą zawierać błędy lub nieścisłości. Oryginalny dokument w jego języku źródłowym powinien być uznawany za wiarygodne źródło. W przypadku informacji o kluczowym znaczeniu zaleca się skorzystanie z profesjonalnego tłumaczenia przez człowieka. Nie ponosimy odpowiedzialności za jakiekolwiek nieporozumienia lub błędne interpretacje wynikające z użycia tego tłumaczenia.