31 KiB
Картофелно пързаляне
Проблемът, който решавахме в предишния урок, може да изглежда като играчка, която няма реално приложение в живота. Това не е така, защото много реални проблеми също споделят този сценарий - включително играта на шах или го. Те са подобни, защото също имаме дъска с определени правила и дискретно състояние.
Тест преди урока
Въведение
В този урок ще приложим същите принципи на Q-Learning към проблем с непрекъснато състояние, т.е. състояние, което се определя от едно или повече реални числа. Ще разгледаме следния проблем:
Проблем: Ако Петър иска да избяга от вълка, трябва да се научи да се движи по-бързо. Ще видим как Петър може да се научи да се пързаля, по-специално да запазва баланс, използвайки Q-Learning.
Петър и приятелите му стават креативни, за да избягат от вълка! Изображение от 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, трябва да инициализираме съответната среда. Всяка среда е свързана с:
-
Пространство на наблюденията, което определя структурата на информацията, която получаваме от средата. За проблема с CartPole получаваме позицията на пръта, скоростта и някои други стойности.
-
Пространство на действията, което определя възможните действия. В нашия случай пространството на действията е дискретно и се състои от две действия - наляво и надясно. (код блок 2)
-
За да инициализирате, въведете следния код:
env = gym.make("CartPole-v1") print(env.action_space) print(env.observation_space) print(env.action_space.sample())
За да видим как работи средата, нека изпълним кратка симулация за 100 стъпки. На всяка стъпка предоставяме едно от действията, които трябва да се изпълнят - в тази симулация просто случайно избираме действие от action_space
.
-
Изпълнете кода по-долу и вижте какво води до това.
✅ Запомнете, че е препоръчително да изпълнявате този код на локална Python инсталация! (код блок 3)
env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close()
Трябва да виждате нещо подобно на това изображение:
-
По време на симулацията трябва да получаваме наблюдения, за да решим как да действаме. Всъщност функцията
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()
Ще видите нещо подобно на това в изхода на notebook-а:
[ 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
Векторът на наблюденията, който се връща на всяка стъпка от симулацията, съдържа следните стойности:
- Позиция на количката
- Скорост на количката
- Ъгъл на пръта
- Скорост на въртене на пръта
-
Получете минималната и максималната стойност на тези числа: (код блок 5)
print(env.observation_space.low) print(env.observation_space.high)
Може също да забележите, че стойността на наградата на всяка стъпка от симулацията винаги е 1. Това е така, защото нашата цел е да оцелеем възможно най-дълго, т.е. да запазим пръта в разумно вертикално положение за най-дълъг период от време.
✅ Всъщност симулацията на CartPole се счита за решена, ако успеем да постигнем средна награда от 195 за 100 последователни опита.
Дискретизация на състоянието
В Q-Learning трябва да изградим Q-таблица, която определя какво да правим във всяко състояние. За да можем да направим това, състоянието трябва да бъде дискретно, по-точно, трябва да съдържа краен брой дискретни стойности. Следователно трябва по някакъв начин да дискретизираме нашите наблюдения, като ги картографираме към краен набор от състояния.
Има няколко начина да направим това:
- Разделяне на интервали. Ако знаем интервала на дадена стойност, можем да разделим този интервал на определен брой интервали и след това да заменим стойността с номера на интервала, към който принадлежи. Това може да се направи с помощта на метода
digitize
на numpy. В този случай ще знаем точно размера на състоянието, защото той ще зависи от броя на интервалите, които изберем за дигитализация.
✅ Можем да използваме линейна интерполация, за да приведем стойностите към някакъв краен интервал (например от -20 до 20), и след това да конвертираме числата в цели числа чрез закръгляване. Това ни дава малко по-малък контрол върху размера на състоянието, особено ако не знаем точните граници на входните стойности. Например, в нашия случай 2 от 4 стойности нямат горна/долна граница, което може да доведе до безкраен брой състояния.
В нашия пример ще използваме втория подход. Както може да забележите по-късно, въпреки неопределените горни/долни граници, тези стойности рядко приемат стойности извън определени крайни интервали, така че състоянията с екстремни стойности ще бъдат много редки.
-
Ето функцията, която ще вземе наблюдението от нашия модел и ще произведе кортеж от 4 цели числа: (код блок 6)
def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))
-
Нека също така изследваме друг метод за дискретизация, използвайки интервали: (код блок 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))
-
Сега нека изпълним кратка симулация и наблюдаваме тези дискретни стойности на средата. Чувствайте се свободни да опитате както
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-Learning.
Структура на Q-таблицата
В предишния урок състоянието беше проста двойка числа от 0 до 8, и затова беше удобно да представим Q-таблицата като numpy тензор с размери 8x8x2. Ако използваме дискретизация с интервали, размерът на нашия вектор на състоянието също е известен, така че можем да използваме същия подход и да представим състоянието като масив с размери 20x20x10x10x2 (тук 2 е размерът на пространството на действията, а първите размери съответстват на броя на интервалите, които сме избрали за всяка от параметрите в пространството на наблюденията).
Въпреки това, понякога точните размери на пространството на наблюденията не са известни. В случай на функцията discretize
, никога не можем да бъдем сигурни, че нашето състояние остава в определени граници, защото някои от оригиналните стойности не са ограничени. Затова ще използваме малко по-различен подход и ще представим Q-таблицата като речник.
-
Използвайте двойката (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-Learning
Сега сме готови да научим Петър да балансира!
-
Първо, нека зададем някои хиперпараметри: (код блок 10)
# hyperparameters alpha = 0.3 gamma = 0.9 epsilon = 0.90
Тук
alpha
е скоростта на учене, която определя до каква степен трябва да коригираме текущите стойности на Q-таблицата на всяка стъпка. В предишния урок започнахме с 1 и след това намалихмеalpha
до по-ниски стойности по време на обучението. В този пример ще го запазим константно за простота, а вие можете да експериментирате с настройката на стойностите наalpha
по-късно.gamma
е факторът на дисконтиране, който показва до каква степен трябва да приоритизираме бъдещата награда пред текущата награда.epsilon
е факторът на изследване/експлоатация, който определя дали трябва да предпочитаме изследването пред експлоатацията или обратното. В нашия алгоритъм ще избираме следващото действие според стойностите на Q-таблицата вepsilon
процента от случаите, а в останалите случаи ще изпълняваме случайно действие. Това ще ни позволи да изследваме области от пространството за търсене, които никога не сме виждали преди.✅ В контекста на балансирането - изборът на случайно действие (изследване) би действал като случайно избутване в грешна посока, и прътът ще трябва да се научи как да възстанови баланса от тези "грешки".
Подобряване на алгоритъма
Можем също така да направим две подобрения на нашия алгоритъм от предишния урок:
-
Изчисляване на средна кумулативна награда за определен брой симулации. Ще отпечатваме напредъка на всеки 5000 итерации и ще усредняваме кумулативната награда за този период от време. Това означава, че ако постигнем повече от 195 точки, можем да считаме проблема за решен, и то с по-високо качество от изискваното.
-
Изчисляване на максимален среден кумулативен резултат,
Qmax
, и ще съхраняваме Q-таблицата, съответстваща на този резултат. Когато изпълнявате обучението, ще забележите, че понякога средният кумулативен резултат започва да намалява, и искаме да запазим стойностите на Q-таблицата, които съответстват на най-добрия модел, наблюдаван по време на обучението.
-
Съберете всички кумулативни награди на всяка симулация в вектор
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()
Трябва да видите нещо подобно:
🚀Предизвикателство
Задача 3: Тук използвахме финалното копие на Q-таблицата, което може да не е най-доброто. Помнете, че сме запазили най-добре представящата се Q-таблица в променливата
Qbest
! Опитайте същия пример с най-добре представящата се Q-таблица, като копиратеQbest
върхуQ
и вижте дали забелязвате разлика.
Задача 4: Тук не избирахме най-доброто действие на всяка стъпка, а по-скоро избирахме според съответното разпределение на вероятностите. Би ли имало повече смисъл винаги да избираме най-доброто действие, с най-високата стойност в Q-таблицата? Това може да се направи с помощта на функцията
np.argmax
, за да се намери номерът на действието, съответстващ на най-високата стойност в Q-таблицата. Реализирайте тази стратегия и вижте дали подобрява балансирането.
Тест след лекцията
Задание
Заключение
Сега научихме как да обучаваме агенти да постигат добри резултати, само като им предоставим функция за награда, която дефинира желаното състояние на играта, и като им дадем възможност интелигентно да изследват пространството за търсене. Успешно приложихме алгоритъма Q-Learning в случаи на дискретни и непрекъснати среди, но с дискретни действия.
Важно е също така да изучим ситуации, в които състоянието на действията също е непрекъснато, и когато пространството за наблюдение е много по-сложно, като например изображението от екрана на играта Atari. В тези проблеми често се налага да използваме по-мощни техники за машинно обучение, като невронни мрежи, за да постигнем добри резултати. Тези по-напреднали теми са предмет на нашия предстоящ курс за по-напреднал изкуствен интелект.
Отказ от отговорност:
Този документ е преведен с помощта на AI услуга за превод Co-op Translator. Въпреки че се стремим към точност, моля, имайте предвид, че автоматизираните преводи може да съдържат грешки или неточности. Оригиналният документ на неговия роден език трябва да се счита за авторитетен източник. За критична информация се препоръчва професионален човешки превод. Ние не носим отговорност за недоразумения или погрешни интерпретации, произтичащи от използването на този превод.