После многих лет корпоративной карьеры (17 лет), отклоняющейся от информатики, я решил изучить машинное обучение и в процессе вернуться к кодированию (то, что я всегда любил!).

Чтобы полностью понять суть ML, я решил начать с самого написания библиотеки ML, чтобы я мог полностью понять внутреннюю работу, линейную алгебру и исчисление, связанные со стохастическим градиентным спуском. И вдобавок выучить Python (я писал код на C++ 20 лет назад).

Я создал базовую библиотеку ML общего назначения, которая создает нейронную сеть (только слои DENSE), сохраняет и загружает веса в файл, выполняет прямое распространение и обучение (оптимизация весов и смещений) с использованием SGD. Я протестировал библиотеку ML с проблемой XOR, чтобы убедиться, что она работает нормально. Вы можете прочитать сообщение в блоге об этом здесь.

Для следующего задания меня интересует обучение с подкреплением, вдохновленное удивительными подвигами Deep Mind, когда их программы Alpha Go, Alpha Zero и Alpha Star изучают (и преуспевают в этом) игры в го, шахматы, игры Atari и, в последнее время, Starcraft; Я поставил перед собой задачу запрограммировать нейронную сеть, которая сама научится играть в древнюю игру крестики-нолики (или крестики-нолики).

Как трудно это может быть?

Конечно, первым делом нужно было запрограммировать саму игру, поэтому я выбрал Python, потому что я его изучаю, так что это дает мне хорошую возможность попрактиковаться, и PyGame для интерфейса. Написание кода для игры было довольно простым, несмотря на то, что это была моя первая PyGame и почти первая программа на Python. Я создал игру совершенно открыто, таким образом, чтобы в нее могли играть два человека, человек против алгоритмического ИИ и человек против нейронной сети. И, конечно же, нейронная сеть на выбор из 3 ИИ-движков: случайный, минимаксный (закодированный с помощью минимаксного алгоритма) или хардкодированный (упражнение, которое я давно хотел сделать).

Во время тренировки визуальные эффекты игры можно отключить, чтобы сделать тренировку намного быстрее. Теперь самое интересное — обучение сети. Я следовал собственным рекомендациям Deep Mind по DQN:

  • Сеть будет аппроксимацией функции значений Q или уравнения Беллмана, а это означает, что сеть будет обучена предсказывать «ценность» каждого хода, доступного в данном игровом состоянии.
  • Была реализована память повторного воспроизведения. Это означало, что нейронная сеть не будет обучаться после каждого хода. Каждый ход будет записываться в специальную «память» вместе с состоянием доски и полученной наградой за такое действие (ход).
  • После того, как объем памяти станет достаточно большим, для каждого тренировочного раунда используются партии случайных событий, выбранных из воспроизводимой памяти.
  • Вторичная нейронная сеть (идентичная основной) используется для расчета части функции значений Q (уравнение Беллмана), в частности будущих значений Q. Затем он обновляется весами основной сети каждые n игр. Это сделано для того, чтобы мы не гнались за движущейся целью.

Проектирование нейронной сети

Выбранная нейронная сеть принимает 9 входных данных (текущее состояние игры) и выводит 9 значений Q для каждого из 9 квадратов на игровом поле (возможные действия). Очевидно, что некоторые квадраты являются недопустимыми ходами, поэтому во время обучения за недопустимые ходы давали отрицательное вознаграждение в надежде, что модель научится не делать недопустимые ходы в данной позиции.

Я начал с двух скрытых слоев по 36 нейронов в каждом, полностью связанных и активированных через ReLu. Выходной слой изначально был активирован с помощью сигмоида, чтобы убедиться, что мы получаем хорошее значение от 0 до 1, которое представляет QValue данной пары действий состояния.

Модель 1 — первая попытка

Сначала модель обучали, играя с идеальным ИИ, то есть с жестко запрограммированным алгоритмом, который никогда не проигрывает и выиграет, если ему дадут шанс. После нескольких тысяч тренировочных раундов я заметил, что нейронная сеть мало учится; поэтому я переключился на обучение против совершенно случайного игрока, чтобы он тоже научился побеждать. После тренировки со случайным игроком нейронная сеть, кажется, добилась прогресса и со временем неуклонно уменьшает функцию потерь.

Однако модель по-прежнему генерировала много недопустимых ходов, поэтому я решил изменить алгоритм обучения с подкреплением, чтобы больше наказывать за незаконные ходы. Изменение заключалось в заполнении нулями всех соответствующих недопустимых ходов для данной позиции при целевых значениях для обучения сети. Это, казалось, очень хорошо работало для уменьшения количества незаконных ходов:

Тем не менее, модель по-прежнему работала довольно плохо, выигрывая только около 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 миллионов изученных игр), скорость обучения действительно уменьшилась (а немного), но явно все еще находится на плато; интересно, что нижняя граница графика функции потерь, по-видимому, продолжает уменьшаться, тогда как верхняя граница и скользящее среднее остаются постоянными. Это привело меня к мысли, что я, возможно, достиг локального минимума.

Модель 3 — новая топология сети

После всех неудач я решил, что мне нужно переосмыслить топологию сети и поэкспериментировать с комбинациями разных сетей и скоростей обучения.

Победы: 76,83% Проигрыши: 17,35% Ничья: 5,82%

Я увеличил до 200 нейронов в каждом скрытом слое. Несмотря на это значительное улучшение, функция потерь все еще находилась на плато около 0,1 (среднеквадратичная ошибка). Который, хоть и сильно урезанный по сравнению с тем, что у нас был, все же выдавал только 77% винрейта против случайного игрока, сеть играла в крестики-нолики как малыш!

Победы: 82,25 % Проигрыши: 13,28 % Ничья: 4,46 %

Наконец-то мы преодолели отметку в 80%! Это большое достижение, кажется, что изменение топологии сети работает, хотя также похоже, что функция потерь застряла на отметке 0,15.

После нескольких тренировочных раундов и некоторых экспериментов со скоростью обучения и другими параметрами я не смог улучшить показатель выигрыша выше 82,25%.

Это были результаты до сих пор:

Довольно интересно узнать, как многие параметры (гиперпараметры, как их называет большинство авторов) модели нейронной сети влияют на эффективность ее обучения, с которыми я играл:

  • скорость обучения
  • топология сети и функции активации
  • циклические и затухающие параметры скорости обучения
  • размер партии
  • целевой цикл обновления (когда целевая сеть обновляется весами из сети политик)
  • политика вознаграждения
  • эпсилон жадная стратегия
  • следует ли тренироваться против случайного игрока или «умного» ИИ.

И до сих пор самым эффективным изменением была топология сети, но так как я так близок, но еще не совсем достиг своей цели в 90% винрейта против случайного игрока, я все же попытаюсь оптимизировать дальше.

Модель 4 — реализация импульса

Я связался с сообществом Reddit, и добрая душа указала, что, возможно, мне нужно придать импульс алгоритму оптимизации. Поэтому я провел небольшое исследование и в итоге решил применить различные методы оптимизации для экспериментов:

  • Стохастический градиентный спуск с импульсом
  • RMSProp: среднеквадратический моментум
  • NAG: Ускоренный импульс Незтерова
  • Адам: адаптивная оценка момента
  • и сохрани мой старый ванильный градиентный спуск (vGD) ☺

Нажмите здесь для подробного объяснения и кода всех реализованных алгоритмов оптимизации.

До сих пор мне не удавалось добиться лучших результатов с Моделью 4, я перепробовал все алгоритмы оптимизации импульса без особого успеха.

Модель 5 — реализация одноразового кодирования и изменение топологии (снова)

Я наткнулся на интересный проект на Github, который занимается именно Deep Q Learning, и я заметил, что он использовал горячее кодирование для ввода вместо прямого ввода значений игрока в 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 г.