След много години на корпоративна кариера (17), отклоняваща се от компютърните науки, сега реших да науча машинно обучение и в процеса да се върна към кодирането (нещо, което винаги съм обичал!).
За да разбера напълно същността на ML, реших да започна с „само кодиране на ML библиотека“, така че да мога да разбера напълно вътрешната работа, линейната алгебра и смятането, включени в Stochastic Gradient Descent. И отгоре научете Python (преди 20 години кодирах на C++).
Създадох базова ML библиотека с общо предназначение, която създава невронна мрежа (само ПЛЪТНИ слоеве), записва и зарежда теглата във файл, прави разпространение и обучение (оптимизиране на тегла и отклонения) с помощта на SGD. Тествах ML библиотеката с проблема XOR, за да се уверя, че работи добре. Можете да прочетете публикацията в блога за него тук.
За следващото предизвикателство се интересувам от обучение за подсилване, силно вдъхновено от удивителните постижения на Deep Mind, за да накарат техните програми Alpha Go, Alpha Zero и Alpha Star да учат (и да бъдат невероятни в това) Go, Chess, Atari игри и напоследък Starcraft; Поставих си задачата да програмирам невронна мрежа, която сама ще се научи как да играе древната игра на тик так (или кръстчета нула).
Колко трудно може да бъде?
Разбира се, първото нещо, което трябваше да направя, беше да програмирам самата игра, така че избрах Python, защото го уча, така че ми дава възможност за добра практика, и PyGame за интерфейса. Кодирането на играта беше доста лесно, макар и заради хълцането, че беше първата ми PyGame и почти първата ми програма на Python. Създадох играта съвсем открито, по такъв начин, че да може да се играе от двама души, от човек срещу алгоритмичен AI и човек срещу невронна мрежа. И, разбира се, невронната мрежа срещу избор от 3 AI машини: произволни, минимаксни (кодирани с помощта на минимаксния алгоритъм) или твърдо кодирани (упражнение, което исках да направя от много време).
Докато тренирате, визуалните елементи на играта могат да бъдат деактивирани, за да бъде обучението много по-бързо. Сега, за забавната част, обучение на мрежата, последвах собствените DQN препоръки на Deep Mind:
- Мрежата ще бъде приближение за функцията на стойността Q или уравнението на Белман, което означава, че мрежата ще бъде обучена да предсказва „стойността“ на всеки ход, наличен в дадено състояние на играта.
- Внедрена е памет за опит за повторение. Това означаваше, че невронната мрежа няма да се обучава след всяко движение. Всеки ход ще бъде записан в специална „памет“ заедно със състоянието на дъската и наградата, която е получила за предприемане на такова действие (ход).
- След като паметта е достатъчно голяма, партиди от произволни преживявания, взети от паметта за повторение, се използват за всеки рунд на обучение
- Вторична невронна мрежа (идентична на основната) се използва за изчисляване на част от функцията на стойността на Q (уравнение на Белман), по-специално на бъдещите стойности на Q. И след това се актуализира с теглата на основната мрежа на всеки n игри. Това се прави, за да не преследваме движеща се цел.
Проектиране на невронна мрежа
Избраната невронна мрежа приема 9 входа (текущото състояние на играта) и извежда 9 Q стойности за всяко от 9-те квадрата в дъската на играта (възможни действия). Очевидно някои квадрати са незаконни ходове, следователно по време на обучение имаше отрицателна награда, дадена на незаконни ходове, надявайки се, че моделът ще се научи да не играе незаконни ходове в дадена позиция.
Започнах с два скрити слоя от по 36 неврона всеки, всички напълно свързани и активирани чрез ReLu. Изходният слой първоначално беше активиран с помощта на sigmoid, за да се гарантира, че получаваме добра стойност между 0 и 1, която представлява QValue на дадена двойка действия на състоянието.
Модел 1 — първи опит
Първоначално моделът беше обучен чрез игра срещу „перфектен“ AI, което означава „твърдо кодиран алгоритъм“, който никога не губи и който ще спечели, ако му се даде възможност. След няколко хиляди кръга на обучение забелязах, че невронната мрежа не учи много; така че преминах към обучение срещу напълно случаен играч, така че и той да се научи как да печели. След тренировка срещу случаен играч, невронната мрежа изглежда е постигнала напредък и постоянно намалява функцията за загуба с течение на времето.
Въпреки това, моделът все още генерираше много незаконни ходове, така че реших да модифицирам алгоритъма за обучение за подсилване, за да накажа повече незаконните ходове. Промяната се състоеше в попълване с нули на всички съответни незаконни ходове за дадена позиция при целевите стойности за обучение на мрежата. Това изглежда работи много добре за намаляване на незаконните ходове:
Въпреки това, моделът все още се представяше доста слабо, печелейки само около 50% от игрите срещу напълно случаен играч (очаквах да спечели над 90% от времето). Това беше след само 100 000 тренировки, така че реших да продължа да тренирам и да видя резултатите:
Обърнете внимание, че когато обучението се рестартира, загубата и незаконните ходове са все още високи в началото на тренировъчния рунд и това е причинено от епсилон алчната стратегия, която предпочита изследването (напълно случаен ход) пред експлоатацията, това предпочитание намалява с времето.
След още един кръг от 100 000 игри, виждам, че функцията за загуба всъщност започна да намалява и процентът на печалба завърши на 65%, така че с малка надежда реших да продължа и да направя още един кръг от 100 000 игри (около 2 часа в i7 MacBook Pro):
Както можете да видите на диаграмата, изчислената загуба дори не достигна плато, но изглежда се увеличи малко с течение на времето, което ми казва, че моделът вече не се учи. Това беше потвърдено от намаляването на процента на победа по отношение на предишния кръг до скромните 46,4%, което не изглежда по-добре от случаен играч.
Модел 2 — Линейно активиране за изхода
След като не получих резултатите, които исках, реших да променя функцията за активиране на изхода на линейна, тъй като изходът трябва да бъде Q стойност, а не вероятност за действие.
Първоначално тествах само с 1000 игри, за да видя дали новата функция за активиране работи, функцията за загуба изглежда намалява, но достигна плато около стойност 1, следователно все още не се учи според очакванията. Попаднах на техника от Брад Кенстлър, Карл Том и Джеръми Джордан, наречена Cyclical Learning Rate, която изглежда разрешава някои случаи на стагниращи функции на загуба в този тип мрежи. Така че пробвах с техния модел Triangle 1.
Със скоростта на усвояване на колоезденето, все още няма късмет след бърз рунд от 1000 игри; затова реших да внедря отгоре затихваща скорост на учене съгласно следната формула:
Получената скорост на обучение, комбинираща циклите и затихването за епоха, е:
С тези много промени реших да рестартирам с нов набор от произволни тегла и отклонения и да опитам да тренирам повече (много повече) игри.
Победи: 52.66% Загуби: 36.02% Равенства: 11.32%
След 24 часа! компютърът ми успя да изпълни 1 000 000 епизода (изиграни игри), което представляваше 7,5 милиона тренировъчни епохи на партиди от 64 пускания (480 милиона научени пускания), скоростта на обучение наистина намаля (a малко), но очевидно все още е в плато; интересно е, че долната граница на графиката на функцията на загубата изглежда продължава да намалява, тъй като горната граница и подвижната средна остават постоянни. Това ме накара да повярвам, че може да съм достигнал локален минимум.
Модел 3 — нова мрежова топология
След всички неуспехи реших, че трябва да преосмисля топологията на мрежата и да си поиграя с комбинации от различни мрежи и скорости на обучение.
Победи: 76,83% Загуби: 17,35% Равенства: 5,82%
Увеличих до 200 неврона всеки скрит слой. Въпреки това голямо подобрение функцията на загубите все още беше в плато около 0,1 (средна квадратна грешка). Което, въпреки че е значително намалено от това, което имахме, все още даваше само 77% процент на победа срещу произволен играч, мрежата играеше tic tac toe като малко дете!
Победи: 82,25% Загуби: 13,28% Равенства: 4,46%
Най-накрая преминахме границата от 80%! Това е голямо постижение, изглежда, че промяната в топологията на мрежата работи, въпреки че също така изглежда, че функцията на загубите стагнира на около 0,15.
След повече обучителни рундове и някои експерименти със скоростта на обучение и други параметри, не можах да се подобря от 82,25% победа.
Това са резултатите до момента:
Доста интересно е да научите как многото параметри (хиперпараметри, както ги наричат повечето автори) на модел на невронна мрежа влияят върху ефективността на обучението, с което съм играл:
- скоростта на обучение
- топологията на мрежата и функциите за активиране
- параметрите на цикъла и затихването на скоростта на обучение
- размера на партидата
- целевият цикъл на актуализиране (когато целевата мрежа се актуализира с теглата от мрежата на правилата)
- политиката на наградите
- алчната стратегия епсилон
- дали да тренирате срещу произволен играч или „интелигентен“ AI.
И досега най-ефективната промяна беше топологията на мрежата, но тъй като съм толкова близо, но все още не съвсем, до целта си от 90% процент на победа срещу произволен играч, все пак ще се опитам да оптимизирам допълнително.
Модел 4 — внедряване на инерция
„Свързах се с общността на reddit“ и една добра душа посочи, че може би това, от което се нуждая, е да приложа инерция към алгоритъма за оптимизация. Така че направих някои проучвания и в крайна сметка реших да внедря различни методи за оптимизация, с които да експериментирам:
- Стохастично градиентно спускане с инерция
- RMSProp: Средноквадратичен обикновен импулс
- NAG: Ускореният импулс на Незтеров
- Адам: Оценка на адаптивния момент
- и запазя стария си ванилов градиентен спускане (vGD) ☺
Щракнете тук за подробно обяснение и код на всички внедрени алгоритми за оптимизация.
Досега не успях да постигна по-добри резултати с Model 4, опитах всички алгоритми за оптимизиране на инерцията с малък или никакъв успех.
Модел 5 — прилагане на едно горещо кодиране и промяна на топологията (отново)
Попаднах на „интересен проект в Github“, който се занимава точно с Deep Q Learning, и забелязах, че той използва „one-hot“ кодиране за входа, за разлика от директното въвеждане на стойностите на плейъра в 9-те входни слота. Затова реших да опитам и в същото време да променя моята топология, за да съответства на неговата:
И така, „едно горещо“ кодиране основно променя входа на едно квадратче в таблото с тик так пръсти на три числа, така че всяко състояние да бъде представено с различни входове, като по този начин мрежата може ясно да разграничи трите от тях. Както го казва оригиналният автор, начинът, по който кодирах, като имах 0 за празно, 1 за X и 2 за O, мрежата не можеше лесно да разбере, че например O и X означават заети състояния, защото едно е две пъти по-далеч от 0, отколкото другия. С новото кодиране празното състояние ще бъде 3 входа: (1,0,0), X ще бъде (0,1,0) и O (0,0,1), както е на диаграмата.
Все пак няма късмет дори с модел 5, така че започвам да мисля, че може да има грешка в кода ми.
За да тествам тази хипотеза, реших да внедря същия модел с помощта на Tensorflow / Keras.
Модел 6 — Tensorflow / Keras
Използвам повторно целия си стар код и просто заменям библиотеката си с Neural Net с Tensorflow/Keras, като запазвам дори моите константи на хиперпараметри.
С внедряването на Tensorflow първото нещо, което забелязах, беше, че имах грешка в изчисляването на загубата, въпреки че това засегна само отчитането и не промени нищо в обучението на мрежата, така че резултатите останаха същите, Функцията на загубите все още беше в застой! Моят код не беше проблемът.
Модел 7 — промяна на тренировъчния график
След това се опитах да променя начина, по който мрежата се обучаваше според u/elBarto015 ме посъветва в reddit.
Начинът, по който тренирах първоначално беше:
- Игрите започват да се симулират и резултатът се записва в паметта за повторение
- След като се запише достатъчно количество преживявания (поне равно на размера на партидата), мрежата ще тренира с произволна извадка от преживявания от паметта за повторение. Количеството преживявания за извадка е размерът на партидата.
- Игрите продължават да се играят между произволния играч и мрежата.
- Всяко движение от който и да е играч генерира нов тренировъчен кръг, отново с произволна извадка от паметта за повторение.
- Това продължава, докато броят на настроените игри приключи.
Първата промяна беше да се тренира само след като всяка игра приключи с едно и също количество данни (партида). Това все още не дава добри резултати.
Втората промяна беше по-драстична, тя въведе концепцията за епохи за всеки тренировъчен кръг, тя основно взе проби от паметта за повторение за епохи * преживявания с размер на партида, например ако избраните епохи бяха 10, а размерът на партидата беше 81, тогава бяха взети проби от 810 преживявания извън паметта за повторение. С тази проба мрежата след това беше обучена за 10 епохи на случаен принцип, като се използва размерът на партидата.
Това означаваше, че сега тренирах ефективно 10 (или броя на избраните епохи) пъти повече на игра, но в партиди със същия размер и произволно разместване на преживяванията всяка епоха.
След като все още си играх с някои хипер-параметри, успях да постигна подобна производителност, както преди, достигайки 83,15% процент на победа спрямо случаен играч, така че реших да продължа да тренирам в кръгове от по 2000 игри всеки, за да оценя ефективността. С почти всеки кръг виждах подобрение:
Към днешна дата най-добрият ми резултат досега е 87,5%, ще го оставя да почине известно време и ще продължа да проучвам, за да намеря причина да не мога да достигна поне 90%. Четох за „самостоятелна игра“ и изглежда като жизнеспособна опция за тестване и забавно предизвикателство за програмиране. Въпреки това, преди да се впусна в още една голяма промяна, искам да се уверя, че съм бил задълбочен с модела и съм тествал всяка опция правилно.
Чувствам, че краят е близо… трябва ли да продължа да актуализирам тази публикация, когато се развият нови събития, или да я направя нишка с няколко публикации?
Първоначално публикувано на адрес https://the-mvm.github.io на 18 март 2021 г.