20 KiB
CartPole Schaatsen
Het probleem dat we in de vorige les hebben opgelost, lijkt misschien een speelgoedprobleem, niet echt toepasbaar in echte scenario's. Dit is echter niet het geval, omdat veel echte problemen ook dit scenario delen - zoals het spelen van schaken of Go. Ze zijn vergelijkbaar omdat we ook een bord hebben met gegeven regels en een discrete toestand.
Quiz voorafgaand aan de les
Introductie
In deze les passen we dezelfde principes van Q-Learning toe op een probleem met een continue toestand, d.w.z. een toestand die wordt weergegeven door een of meer reële getallen. We gaan het volgende probleem aanpakken:
Probleem: Als Peter wil ontsnappen aan de wolf, moet hij sneller kunnen bewegen. We zullen zien hoe Peter kan leren schaatsen, en in het bijzonder hoe hij balans kan houden, met behulp van Q-Learning.
Peter en zijn vrienden worden creatief om aan de wolf te ontsnappen! Afbeelding door Jen Looper
We gebruiken een vereenvoudigde versie van balanceren, bekend als het CartPole-probleem. In de CartPole-wereld hebben we een horizontale slider die naar links of rechts kan bewegen, en het doel is om een verticale paal bovenop de slider in balans te houden.
Vereisten
In deze les gebruiken we een bibliotheek genaamd OpenAI Gym om verschillende omgevingen te simuleren. Je kunt de code van deze les lokaal uitvoeren (bijvoorbeeld vanuit Visual Studio Code), in welk geval de simulatie in een nieuw venster wordt geopend. Bij het online uitvoeren van de code moet je mogelijk enkele aanpassingen maken, zoals beschreven hier.
OpenAI Gym
In de vorige les werden de spelregels en de toestand gegeven door de Board
-klasse die we zelf hebben gedefinieerd. Hier gebruiken we een speciale simulatieomgeving, die de fysica achter de balancerende paal simuleert. Een van de meest populaire simulatieomgevingen voor het trainen van reinforcement learning-algoritmen heet een Gym, die wordt onderhouden door OpenAI. Met deze gym kunnen we verschillende omgevingen creëren, van een CartPole-simulatie tot Atari-spellen.
Let op: Je kunt andere beschikbare omgevingen van OpenAI Gym bekijken hier.
Laten we eerst de gym installeren en de benodigde bibliotheken importeren (codeblok 1):
import sys
!{sys.executable} -m pip install gym
import gym
import matplotlib.pyplot as plt
import numpy as np
import random
Oefening - een CartPole-omgeving initialiseren
Om te werken met een CartPole-balanceringsprobleem, moeten we de bijbehorende omgeving initialiseren. Elke omgeving is gekoppeld aan een:
-
Observatieruimte die de structuur definieert van de informatie die we van de omgeving ontvangen. Voor het CartPole-probleem ontvangen we de positie van de paal, snelheid en enkele andere waarden.
-
Actieruimte die mogelijke acties definieert. In ons geval is de actieruimte discreet en bestaat uit twee acties - links en rechts. (codeblok 2)
-
Om te initialiseren, typ de volgende code:
env = gym.make("CartPole-v1") print(env.action_space) print(env.observation_space) print(env.action_space.sample())
Om te zien hoe de omgeving werkt, laten we een korte simulatie uitvoeren van 100 stappen. Bij elke stap geven we een van de acties op die moet worden uitgevoerd - in deze simulatie selecteren we willekeurig een actie uit action_space
.
-
Voer de onderstaande code uit en kijk wat het oplevert.
✅ Onthoud dat het aanbevolen is om deze code uit te voeren op een lokale Python-installatie! (codeblok 3)
env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close()
Je zou iets moeten zien dat lijkt op deze afbeelding:
-
Tijdens de simulatie moeten we observaties verkrijgen om te beslissen hoe te handelen. De stapfunctie retourneert namelijk de huidige observaties, een beloningsfunctie en de vlag
done
die aangeeft of het zinvol is om de simulatie voort te zetten of niet: (codeblok 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()
Je zult iets zien zoals dit in de notebook-uitvoer:
[ 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
De observatievector die bij elke stap van de simulatie wordt geretourneerd, bevat de volgende waarden:
- Positie van de kar
- Snelheid van de kar
- Hoek van de paal
- Rotatiesnelheid van de paal
-
Verkrijg de minimale en maximale waarde van deze getallen: (codeblok 5)
print(env.observation_space.low) print(env.observation_space.high)
Je zult ook merken dat de beloningswaarde bij elke simulatiestap altijd 1 is. Dit komt omdat ons doel is om zo lang mogelijk te overleven, d.w.z. de paal zo lang mogelijk in een redelijk verticale positie te houden.
✅ In feite wordt de CartPole-simulatie als opgelost beschouwd als we erin slagen een gemiddelde beloning van 195 te behalen over 100 opeenvolgende pogingen.
Toestand discretiseren
Bij Q-Learning moeten we een Q-Tabel bouwen die definieert wat te doen in elke toestand. Om dit te kunnen doen, moet de toestand discreet zijn, meer precies, het moet een eindig aantal discrete waarden bevatten. Daarom moeten we onze observaties op de een of andere manier discretiseren, en ze koppelen aan een eindige set toestanden.
Er zijn een paar manieren waarop we dit kunnen doen:
- Verdelen in intervallen. Als we het interval van een bepaalde waarde kennen, kunnen we dit interval verdelen in een aantal intervallen, en vervolgens de waarde vervangen door het intervalnummer waartoe het behoort. Dit kan worden gedaan met behulp van de numpy-methode
digitize
. In dit geval kennen we de grootte van de toestand precies, omdat deze afhankelijk is van het aantal intervallen dat we selecteren voor digitalisering.
✅ We kunnen lineaire interpolatie gebruiken om waarden naar een eindig interval te brengen (bijvoorbeeld van -20 tot 20), en vervolgens getallen naar gehele getallen converteren door ze af te ronden. Dit geeft ons iets minder controle over de grootte van de toestand, vooral als we de exacte bereiken van invoerwaarden niet kennen. In ons geval hebben bijvoorbeeld 2 van de 4 waarden geen boven-/ondergrenzen, wat kan resulteren in een oneindig aantal toestanden.
In ons voorbeeld gaan we met de tweede aanpak. Zoals je later zult merken, nemen deze waarden ondanks de ongedefinieerde boven-/ondergrenzen zelden extreme waarden buiten bepaalde eindige intervallen aan, waardoor toestanden met extreme waarden zeer zeldzaam zullen zijn.
-
Hier is de functie die de observatie van ons model neemt en een tuple van 4 gehele waarden produceert: (codeblok 6)
def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))
-
Laten we ook een andere discretisatiemethode verkennen met behulp van intervallen: (codeblok 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))
-
Laten we nu een korte simulatie uitvoeren en die discrete omgevingswaarden observeren. Voel je vrij om zowel
discretize
alsdiscretize_bins
te proberen en te kijken of er een verschil is.✅
discretize_bins
retourneert het intervalnummer, dat 0-gebaseerd is. Voor waarden van de invoervariabele rond 0 retourneert het het nummer uit het midden van het interval (10). Bijdiscretize
hebben we ons niet bekommerd om het bereik van uitvoerwaarden, waardoor ze negatief konden zijn, en 0 komt overeen met 0. (codeblok 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()
✅ Haal de regel die begint met
env.render
uit commentaar als je wilt zien hoe de omgeving wordt uitgevoerd. Anders kun je het op de achtergrond uitvoeren, wat sneller is. We zullen deze "onzichtbare" uitvoering gebruiken tijdens ons Q-Learning-proces.
De structuur van de Q-Tabel
In onze vorige les was de toestand een eenvoudige paar getallen van 0 tot 8, en daarom was het handig om de Q-Tabel te representeren met een numpy-tensor met een vorm van 8x8x2. Als we intervallen-discretisatie gebruiken, is de grootte van onze toestandsvector ook bekend, dus we kunnen dezelfde aanpak gebruiken en de toestand representeren door een array met een vorm van 20x20x10x10x2 (hier is 2 de dimensie van de actieruimte, en de eerste dimensies komen overeen met het aantal intervallen dat we hebben geselecteerd voor elk van de parameters in de observatieruimte).
Soms zijn de exacte dimensies van de observatieruimte echter niet bekend. In het geval van de discretize
-functie kunnen we nooit zeker weten dat onze toestand binnen bepaalde grenzen blijft, omdat sommige van de oorspronkelijke waarden niet begrensd zijn. Daarom gebruiken we een iets andere aanpak en representeren we de Q-Tabel met een dictionary.
-
Gebruik het paar (toestand, actie) als de sleutel van de dictionary, en de waarde zou overeenkomen met de waarde van de Q-Tabel. (codeblok 9)
Q = {} actions = (0,1) def qvalues(state): return [Q.get((state,a),0) for a in actions]
Hier definiëren we ook een functie
qvalues()
, die een lijst retourneert van Q-Tabelwaarden voor een gegeven toestand die overeenkomt met alle mogelijke acties. Als de invoer niet aanwezig is in de Q-Tabel, retourneren we standaard 0.
Laten we beginnen met Q-Learning
Nu zijn we klaar om Peter te leren balanceren!
-
Laten we eerst enkele hyperparameters instellen: (codeblok 10)
# hyperparameters alpha = 0.3 gamma = 0.9 epsilon = 0.90
Hier is
alpha
de leersnelheid die bepaalt in welke mate we de huidige waarden van de Q-Tabel bij elke stap moeten aanpassen. In de vorige les begonnen we met 1 en verlaagden we vervolgensalpha
naar lagere waarden tijdens de training. In dit voorbeeld houden we het constant voor de eenvoud, en je kunt later experimenteren met het aanpassen vanalpha
-waarden.gamma
is de kortingsfactor die aangeeft in welke mate we toekomstige beloning boven huidige beloning moeten prioriteren.epsilon
is de exploratie/exploitatie-factor die bepaalt of we exploratie boven exploitatie moeten verkiezen of andersom. In ons algoritme selecteren we inepsilon
procent van de gevallen de volgende actie op basis van Q-Tabelwaarden, en in de resterende gevallen voeren we een willekeurige actie uit. Dit stelt ons in staat om gebieden van de zoekruimte te verkennen die we nog nooit eerder hebben gezien.✅ In termen van balanceren - het kiezen van een willekeurige actie (exploratie) zou werken als een willekeurige duw in de verkeerde richting, en de paal zou moeten leren hoe de balans te herstellen van die "fouten".
Verbeter het algoritme
We kunnen ook twee verbeteringen aanbrengen in ons algoritme van de vorige les:
-
Gemiddelde cumulatieve beloning berekenen, over een aantal simulaties. We printen de voortgang elke 5000 iteraties en middelen onze cumulatieve beloning over die periode. Dit betekent dat als we meer dan 195 punten behalen, we het probleem als opgelost kunnen beschouwen, met een nog hogere kwaliteit dan vereist.
-
Maximale gemiddelde cumulatieve beloning berekenen,
Qmax
, en we slaan de Q-Tabel op die overeenkomt met dat resultaat. Wanneer je de training uitvoert, zul je merken dat soms de gemiddelde cumulatieve beloning begint te dalen, en we willen de waarden van de Q-Tabel behouden die overeenkomen met het beste model dat tijdens de training is waargenomen.
-
Verzamel alle cumulatieve beloningen bij elke simulatie in de vector
rewards
voor verdere plotting. (codeblok 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=[]
Wat je kunt opmerken uit die resultaten:
-
Dicht bij ons doel. We zijn heel dicht bij het bereiken van het doel van 195 cumulatieve beloningen over 100+ opeenvolgende runs van de simulatie, of we hebben het misschien zelfs bereikt! Zelfs als we kleinere aantallen behalen, weten we het nog niet, omdat we middelen over 5000 runs, en slechts 100 runs zijn vereist volgens de formele criteria.
-
Beloning begint te dalen. Soms begint de beloning te dalen, wat betekent dat we de al geleerde waarden in de Q-Tabel kunnen "vernietigen" met de waarden die de situatie verslechteren.
Deze observatie is duidelijker zichtbaar als we de trainingsvoortgang plotten.
Trainingsvoortgang plotten
Tijdens de training hebben we de cumulatieve beloningswaarde bij elke iteratie verzameld in de vector rewards
. Hier is hoe het eruit ziet wanneer we het plotten tegen het iteratienummer:
plt.plot(rewards)
Van deze grafiek is het niet mogelijk om iets te zeggen, omdat door de aard van het stochastische trainingsproces de lengte van trainingssessies sterk varieert. Om meer betekenis te geven aan deze grafiek, kunnen we het lopende gemiddelde berekenen over een reeks experimenten, laten we zeggen 100. Dit kan handig worden gedaan met np.convolve
: (codeblok 12)
def running_average(x,window):
return np.convolve(x,np.ones(window)/window,mode='valid')
plt.plot(running_average(rewards,100))
Hyperparameters variëren
Om het leren stabieler te maken, is het zinvol om enkele van onze hyperparameters tijdens de training aan te passen. In het bijzonder:
-
Voor leersnelheid,
alpha
, kunnen we beginnen met waarden dicht bij 1 en vervolgens de parameter blijven verlagen. Na verloop van tijd krijgen we goede waarschijnlijkheidswaarden in de Q-Tabel, en dus moeten we ze licht aanpassen en niet volledig overschrijven met nieuwe waarden. -
Epsilon verhogen. We kunnen
epsilon
langzaam verhogen, zodat we minder verkennen en meer exploiteren. Het is waarschijnlijk zinvol om te beginnen met een lagere waarde vanepsilon
en deze op te voeren tot bijna 1.
Taak 1: Speel met de waarden van de hyperparameters en kijk of je een hogere cumulatieve beloning kunt behalen. Kom je boven de 195? Taak 2: Om het probleem formeel op te lossen, moet je een gemiddelde beloning van 195 behalen over 100 opeenvolgende runs. Meet dit tijdens de training en zorg ervoor dat je het probleem formeel hebt opgelost!
Het resultaat in actie zien
Het zou interessant zijn om daadwerkelijk te zien hoe het getrainde model zich gedraagt. Laten we de simulatie uitvoeren en dezelfde strategie voor actie-selectie volgen als tijdens de training, waarbij we sampelen volgens de kansverdeling in de Q-Table: (code block 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()
Je zou iets moeten zien zoals dit:
🚀Uitdaging
Taak 3: Hier gebruikten we de uiteindelijke versie van de Q-Table, die mogelijk niet de beste is. Onthoud dat we de best presterende Q-Table hebben opgeslagen in de variabele
Qbest
! Probeer hetzelfde voorbeeld met de best presterende Q-Table doorQbest
over te kopiëren naarQ
en kijk of je verschil merkt.
Taak 4: Hier selecteerden we niet de beste actie bij elke stap, maar sampelden we met de bijbehorende kansverdeling. Zou het logischer zijn om altijd de beste actie te kiezen, met de hoogste waarde in de Q-Table? Dit kan worden gedaan door de functie
np.argmax
te gebruiken om het actienummer te vinden dat overeenkomt met de hoogste waarde in de Q-Table. Implementeer deze strategie en kijk of het de balans verbetert.
Quiz na de les
Opdracht
Conclusie
We hebben nu geleerd hoe we agenten kunnen trainen om goede resultaten te behalen door hen simpelweg een beloningsfunctie te geven die de gewenste toestand van het spel definieert, en door hen de kans te geven om intelligent de zoekruimte te verkennen. We hebben het Q-Learning-algoritme succesvol toegepast in gevallen van discrete en continue omgevingen, maar met discrete acties.
Het is ook belangrijk om situaties te bestuderen waarin de actiestatus ook continu is, en wanneer de observatieruimte veel complexer is, zoals het beeld van het scherm van een Atari-spel. Bij die problemen moeten we vaak krachtigere machine learning-technieken gebruiken, zoals neurale netwerken, om goede resultaten te behalen. Die meer geavanceerde onderwerpen zijn het onderwerp van onze komende meer gevorderde AI-cursus.
Disclaimer:
Dit document is vertaald met behulp van de AI-vertalingsservice Co-op Translator. Hoewel we ons best doen voor nauwkeurigheid, dient u zich ervan bewust te zijn dat geautomatiseerde vertalingen fouten of onnauwkeurigheden kunnen bevatten. Het originele document in zijn oorspronkelijke taal moet worden beschouwd als de gezaghebbende bron. Voor cruciale informatie wordt professionele menselijke vertaling aanbevolen. Wij zijn niet aansprakelijk voor misverstanden of verkeerde interpretaties die voortvloeien uit het gebruik van deze vertaling.