Напишете AI, за да спечелите в Pong от нулата с Reinforcement Learning

Има огромна разлика между четенето за Укрепване на обучението и действителното му прилагане.

В тази публикация ще внедрите Невронна мрежа за усилване на обучението и ще я видите да научава все повече и повече, тъй като най-накрая става достатъчно добър, за да победи компютъра в Pong! Можете да играете с други такива игри Atari в OpenAI фитнес.

До края на тази публикация ще можете да направите следното:

  • Напишете невронна мрежа от нулата.
  • Прилагане на градиент на политиката с усилване на обучението.
  • Създайте AI за Pong, който може да победи компютъра в по-малко от 250 реда Python.
  • Използвайте OpenAI фитнес.

Източници

Кодът и идеята са плътно базирани на публикацията в блога на Андрей Карпати. Кодът в me_pong.py е предназначен да бъде по-лесна за следване версия на pong.py, която е написана от д-р Карпати.

Предпоставки и фоново четене

За да следвате, ще трябва да знаете следното:

  • Основен Python
  • Дизайн на невронни мрежи и заден ход
  • Изчисление и линейна алгебра

Ако искате по-дълбоко гмуркане на материала под ръка, прочетете публикацията в блога, на която се основава всичко това. Тази публикация е предназначена да бъде по-просто въведение в този материал.

Страхотен! Да започваме.

Настройвам

  1. Отворете кода, който да следвате.
  2. Следвайте инструкциите за инсталиране на OpenAI Gym. Може да се наложи първо да инсталирате cmake
  3. Изпълнете pip install -e. [Atari]
  4. Върнете се в основата на това хранилище и отворете нов файл, наречен my_pong.py в любимия си редактор.
  5. Нека да стигнем до решаване на проблеми. Ето проблема:

проблем

Получават ни следното:

  • Поредица от изображения (рамки), представящи всеки кадър от играта Pong.
  • Индикация кога сме спечелили или загубили играта.
  • Противник агент, който е традиционният компютърен играч Pong.
  • Агент, който контролираме, който можем да кажем да се движи нагоре или надолу във всеки кадър.

Можем ли да използваме тези парчета, за да обучим нашия агент да победи компютъра? Освен това можем ли да направим нашето решение достатъчно общо, така че да може да се използва отново за победа в игри, които не са понг?

Решение

Наистина можем! Андрей прави това, като изгражда невронна мрежа, която взема всяко изображение и извежда команда на нашия AI да се движи нагоре или надолу.

Архитектурата на решението на Андрей от публикацията му в блога.

Можем да разбием това малко повече на следните стъпки:

Нашата Невронна мрежа, базирана предимно на решението на Андрей, ще направи следното:

  1. Вземете изображения от играта и ги обработете предварително (премахнете цвят, фон, образец и т.н.).
  2. Използвайте Невронната мрежа, за да изчислите вероятност от движение нагоре.
  3. Вземете проба от това разпределение на вероятността и кажете на агента да се движи нагоре или надолу.
  4. Ако рундът приключи (пропуснал сте топката или противникът изпусна топката), намерете дали сте спечелили или загубили.
  5. Когато епизодът приключи (някой стигна до 21 точки), прекарайте резултата през алгоритъма за размножаване, за да изчислите градиента за нашите тегла.
  6. След приключване на 10 епизода обобщете градиента и преместете тежестите в посока на градиента.
  7. Повтаряйте този процес, докато теглото ни не бъде настроено до точката, в която можем да победим компютъра. Това е основно! Нека започнем да разглеждаме как нашия код постига това.

Добре, че сега описахме проблема и неговото решение, нека да напишем някакъв код!

код

Сега ще следваме кода в me_pong.py. Моля, дръжте го отворено и прочетете заедно! Кодът започва тук:

def main ():

Инициализация

Първо, нека използваме OpenAI Gym, за да направим игрова среда и да получим първото си изображение на играта.

На следващо място, зададохме куп параметри въз основа на публикацията на блога на Андрей. Няма да се притесняваме за настройката им, но имайте предвид, че вероятно можете да постигнете по-добри резултати, като го направите. Параметрите, които ще използваме са:

  • batch_size: колко кръга играем, преди да актуализираме тежестите на нашата мрежа.
  • гама: Коефициентът на отстъпка, който използваме, за да намалим ефекта от старите действия върху крайния резултат. (Не се тревожете за това все още)
  • decay_rate: Параметър, използван в алгоритъма RMSProp. (Не се тревожете за това все още)
  • num_hidden_layer_neurons: Колко неврона са в нашия скрит слой.
  • learning_rate: Скоростта, с която се учим от нашите резултати, за да изчислим новите тегла. По-високата скорост означава, че реагираме повече на резултатите, а по-ниската скорост означава, че не реагираме толкова силно на всеки резултат.

След това задаваме броячи, начални стойности и начални тегла в нашата Невронна мрежа.

Теглата се съхраняват в матрици. Слой 1 на нашата Невронна мрежа е матрица с размери 200 x 6400, представляваща тежестите за нашия скрит слой. За слой 1 елемент w1_ij представлява теглото на неврона i за входен пиксел j в слой 1.

Слой 2 е матрица с размери 200 x 1, представляваща теглата на изхода на скрития слой на крайния ни изход. За слой 2 елемент w2_i представлява тежестите, които поставяме върху активирането на неврона i в скрития слой.

За сега инициализираме тежестите на всеки слой с произволни числа. Разделяме на квадратния корен на броя на размера на размерите, за да нормализираме теглата си.

След това задаваме първоначалните параметри за RMSProp (метод за актуализиране на теглата, който ще обсъдим по-нататък). Не се притеснявайте твърде много от разбирането на това, което виждате по-долу. Аз го довеждам до тук, за да можем да продължим да следваме основния код.

Ще трябва да съберем куп наблюдения и междинни стойности в целия епизод и да ги използваме, за да изчислим градиента в края въз основа на резултата. По-долу задава масивите, в които ще събираме цялата тази информация.

Добре, че сме готови с настройката! Ако сте следвали, трябва да изглежда така:

Пфу. Сега за забавната част!

Измисляме как да се движим

Същността на нашия алгоритъм ще живее в цикъл, където непрекъснато правим ход и след това учим въз основа на резултатите от хода. За сега ще поставим всичко в блок, но в действителност може да зададете условие за почивка, за да спрете процеса.

Първата стъпка към нашия алгоритъм е обработката на образа на играта, през която ни подмина OpenAI Gym. Наистина не ни интересува цялото изображение - само определени подробности. Правим това по-долу:

Нека се потопим в preprocess_observation, за да видим как преобразуваме изображението OpenAI Gym ни дава в нещо, което можем да използваме за обучение на нашата Невронна мрежа. Основните стъпки са:

  1. Изрязвайте изображението (просто ни интересуват частите с информация, която ни интересува).
  2. Направете пример на изображението.
  3. Преобразувайте изображението в черно и бяло (цветът не е особено важен за нас).
  4. Премахнете фона.
  5. Преобразувайте от 80 x 80 матрица от стойности в 6400 x 1 матрица (изравнете матрицата, така че да е по-лесна за използване).
  6. Съхранявайте само разликата между текущия и предишния кадър, ако знаем предишния кадър (интересуваме се само какво е променено).

Сега, когато предварително сме обработили наблюденията, нека да преминем към действително изпращане на наблюденията през нашата невронна мрежа, за да генерираме вероятността да кажем на AI да се движи нагоре. Ето стъпките, които ще предприемем:

Как точно apply_neural_nets взема наблюдения и тежести и генерира вероятност от повишаване? Това е само предния пропуск на Невронната мрежа. Нека разгледаме кода по-долу за повече информация:

Както можете да видите, изобщо не са много стъпките! Нека вървим стъпка по стъпка:

  1. Изчислете необработените стойности на скрития слой, като просто намерите точков произход на теглата [1] (тегла на слой 1) и матрицата за наблюдение. Ако си спомняте, теглата [1] е матрица с размери 200 x 6400, а матрицата за наблюдение е матрица 6400 x 1. Така точков продукт ще ни даде матрица с размери 200 x 1. Имаме 200 неврона, така че всеки ред представлява изхода на един неврон.
  2. На следващо място, ние прилагаме нелинейна функция за праг на тези стойности на скрития слой - в този случай просто обикновен ReLU. На високо ниво това въвежда нелинейностите, които правят нашата мрежа способна да изчислява нелинейни функции, а не просто прости линейни.
  3. Използваме тези стойности на активиране на скрития слой, за да изчислим стойностите на изходния слой. Това се прави от обикновен точков продукт от скрити_слоени_ценности (200 х 1) и тегла ['2'] (1 х 200), което дава една стойност (1 х 1).
  4. И накрая, ние прилагаме сигмоидна функция върху тази стойност на изхода, така че да е между 0 и 1 и следователно е валидна вероятност (вероятност от повишаване).

Да се ​​върнем към основния алгоритъм и да продължим нататък. Сега, след като получихме вероятност да се качим нагоре, сега трябва да запишем резултатите за по-късно обучение и да изберем действие, което да каже на нашия ИИ да приложи:

Избираме действие, като обърнем въображаема монета, която се приземява с „нагоре“ с вероятност нагоре-вероятност и надолу с 1 - нагоре-вероятност. Ако се приземи, избираме да кажем на AI да се изкачи нагоре, а ако не, казваме му да слиза надолу. Ние също

След като направим това, предаваме действието на OpenAI Gym чрез env.step (действие).

Добре, покрихме първата половина на решението! Знаем какви действия да кажем на нашия AI да предприеме. Ако следвате заедно, вашият код трябва да изглежда така:

Сега, когато направихме своя ход, е време да започнем да учим, така че да разберем правилните тежести в нашата Невронна мрежа!

Изучаване на

Ученето се състои в това да видим резултата от действието (т.е. дали сме спечелили или не кръга) и съответно да променим тежестта си. Първата стъпка към обучението е задаването на следния въпрос:

  • Как промяната на вероятността на изхода (на качването нагоре) влияе върху резултата ми от спечелването на рунда?

Математически това е просто производното на нашия резултат по отношение на резултатите от крайния ни слой. Ако L е стойността на нашия резултат за нас и f е функцията, която ни дава активациите на крайния ни слой, това производно е просто ∂L / ∂f.

В контекста на бинарна класификация (т.е. просто трябва да кажем на AI едно от две действия, нагоре или надолу), това производно се оказва

Обърнете внимание, че σ в горното уравнение представлява сигмоидната функция. Прочетете раздела за класификация на атрибути тук за повече информация за това как получаваме горната производна. Опростяваме това по-долу:

∂L / ∂f = true_label (0 или 1) - predvited_label (0 или 1)

След едно действие (преместване на греблото нагоре или надолу) всъщност нямаме представа дали това е правилното действие или не. Така че ние ще изневеряваме и ще третираме действието, което в крайна сметка вземаме от извадката от нашата вероятност като правилното действие.

Нашата прогноза за този кръг ще бъде вероятността да се качим нагоре, която изчислихме. Използвайки това, имаме, че ∂L / ∂f може да бъде изчислено чрез

Страхотно! Имаме градиент на действие.

Следващата стъпка е да разберем как научаваме след края на епизод (т.е. когато ние или противникът ни пропускаме топката и някой получава точка). Правим това, като изчисляваме градиента на политиката на мрежата в края на всеки епизод. Интуицията тук е, че ако спечелим рунда, бихме искали нашата мрежа да генерира повече от действията, довели до победата ни. Освен това, ако загубим, ще се опитаме да генерираме по-малко от тези действия.

OpenAI Gym ни предоставя удобната променлива, за да ни каже кога епизод приключи (т.е. пропуснахме топката или противникът ни пропусна топката). Когато забележим, че сме готови, първото нещо, което правим, е да съставим всички наши наблюдения и градиентни изчисления за епизода. Това ни позволява да приложим нашите знания върху всички действия в епизода.

На следващо място, искаме да научим по такъв начин, че действията, предприети към края на епизод, влияят по-силно на нашето обучение, отколкото действията, предприети в началото. Това се нарича отстъпка.

Помислете за това по този начин - ако сте се преместили в първия кадър на епизода, това вероятно е имало много малко влияние върху това дали печелите или не. Въпреки това, по-близо до края на епизода, действията ви вероятно имат много по-голям ефект, тъй като определят дали греблото ви достига до топката или как вашето гребло удря топката.

Ще вземем предвид това претегляне, като намалим наградите си, така че наградите от по-ранни кадри да се отстъпят много повече от наградите за по-късни кадри. След това най-накрая ще използваме обратното размножаване, за да изчислим градиента (т.е. посоката, от която се нуждаем, за да преместим тежестите си, за да се подобрим).

Нека да разгледаме малко как се изчислява градиентът на правилата за епизода. Това е една от най-важните части на Укрепването на обучението, тъй като нашият агент измисля как да се подобри във времето.

Като начало, ако още не сте го прочетете този откъс за размножаване от отличната безплатна книга на Майкъл Нилсен за Deep Learning.

Както ще видите в този откъс, има четири основни уравнения на обратната пропорция, техника за изчисляване на градиента за нашите тегла.

Четири основни уравнения на размножаването. Източник: Майкъл Нилсен

Нашата цел е да намерим ∂C / ∂w1 (BP4), производната на функцията на разходите по отношение на теглата на първия слой, и ∂C / ∂w2, производната на функцията на разходите по отношение на теглата на втория слой. Тези градиенти ще ни помогнат да разберем в каква посока да движим тежестите си за най-голямо подобрение.

Като начало, нека започнем с ∂C / ∂w2. Ако a ^ l2 е активирането на скрития слой (слой 2), виждаме, че формулата е:

Всъщност точно тук правим:

След това трябва да изчислим ∂C / ∂w1. Формулата за това е:

и също така знаем, че ^ l1 е просто нашите наблюдения-стойности.

Така че всичко, от което се нуждаем сега, е δ ^ l2. След като имаме това, можем да изчислим ∂C / ∂w1 и да се върнем. Правим точно това по-долу:

Ако следвате заедно, функцията ви трябва да изглежда така:

С това приключихме с обратно размножаване и изчислихме градиентите си!

След като приключим с епизодите на batch_size, най-накрая актуализираме тежестта си за нашата Невронна мрежа и прилагаме нашите знания.

За да актуализираме теглата, ние просто прилагаме RMSProp, алгоритъм за актуализиране на теглата, описан от Sebastian Reuder тук.

Ние изпълняваме това по-долу:

Това е стъпката, която ощипва тежестта ни и ни позволява да се подобряваме с времето.

Това всъщност е това! Общо казано, трябва да изглежда така.

Току-що сте кодирали пълна Невронна мрежа за игра на Pong! Декомментирайте env.render () и го стартирайте за 3-4 дни, за да видите как най-накрая бие компютъра! Ще трябва да направите някои окисвания, както е направено в решението на Андрей Карпати, за да можете да визуализирате резултатите си, когато спечелите.

производителност

Според публикацията в блога, този алгоритъм трябва да отнеме около 3 дни обучение на Macbook, за да започне да бие компютъра.

Помислете за настройване на параметрите или използване на Convolutional Neural Nets за допълнително повишаване на производителността.

Допълнителна информация

Ако искате допълнителен грунд в Neural Networks и Reinforcement Learning, има някои големи ресурси, за да научите повече (аз работя в Udacity като директор на програмите за машинно обучение):

  • Оригиналната публикация в блога на Андрей Карпати
  • Безплатен курс за дълбоко обучение на Udacity от Google
  • Безплатен обучен курс за управление на Udacity от Georgia Tech
  • Книга за дълбокото обучение на Майкъл Нийлсен
  • Учебната книга за подсилване на Сатън и Барто