Rocket League — это соревновательная игра, в которой, управляя машинкой на футбольном поле, нужно забить мяч в ворота противника. Своеобразная интерпретация футбола на машинках. Звучит просто, но на деле игра требует определенных навыков и не так проста, как может показаться на первый взгляд, и даже была признана киберспортивной дисциплиной. Тем интереснее было попробовать обучить своего бота играть в эту игру, используя нейросети и обучение с подкреплением.
Оказалось, что существует множество возможностей для создания ботов в этой игре. Взаимодействие стороннего кода и игры происходит через отдельный плагин. Но использовать эти возможности и запускать ботов в сетевом режиме нельзя, по понятным причинам. Создать бота можно используя разные языки программирования, но так как речь зашла про нейросети, то понятно, что это будет python. В игре доступны разные режимы игры (1 vs 1, 2 vs 2, хоккей и т.д.). Чтобы упростить себе задачу, я сосредоточился на режиме 1 vs 1.
Перед тем как приступить непосредственно к теме я хочу выделить два бота, которые можно было найти в open-source:
-
Nexto. Самый продвинутый бот на данный момент. Был момент, когда игру взломали и добавили возможность запускать бота в соревновательный режим. В итоге этот бот в онлайне показал себя лучше, чем 99% игроков. Для обучения использовался алгоритм PPO с дискретным пространством действий, т.е. бот как бы играет на клавиатуре, а также работает в 12 фпс. Ещё интересный момент, что на входе в сеть есть backbone, основанный на attention-like архитектуре, который собирает всю информацию о текущем состоянии игры в вектор фиксированного размера. Благодаря этому бот может играть с разным количеством игроков, что также позволяет ему играть в разных режимах игры. Только код этого backbon’а выглядит сыровато, и не очень понятно, как конкретно это работает. Размер policy-модели — 444032 параметров.
Также в его репозитории есть предыдущая версия бота — Necto. -
Seer — похоже чья-то магистерская работа. Судя по оценкам автора, он почти добрался до уровня Necto (не Nexto!). Тоже использовал PPO. В его документе можно найти конкретную архитектуру, которую он использовал, систему наград и гиперпараметры. Он использовал общую LSTM для Actor и Critic модели. Actor и Critic модели вместе содержат 2 миллиона параметров. Автор пробовал выполнять претренировку модели на данных с повторов обычных игроков, но это не дало результатов. А ещё он пишет, что обучение заняло 1.5 месяца 😱..
Попытка № 0. DQN на записях Nexto.
У меня был план, надёжный как швейцарские часы — собрать записи игр Nexto, а затем использовать DQN для обучения уже своей модели. DQN — это off-policy метод, что означает, что мы по идее можем отдельно собрать данные, а затем использовать их для обучения. Если бы сеть в итоге научилась просто играть как Nexto, то это было очень неплохо. В принципе, можно было бы свести задачу к обучению с учителем, использовать его действия в качестве таргетов. Кто-то даже делал что-то подобное. Однако, это уже будет не обучение с подкреплением и скорее читерством — я бы просто научил модель повторять действия за Nexto, и в этом бы не было смысла.
Я не пытался сразу обучить бота полноценно играть, и решил, что пусть он сначала просто научиться ездить за мячом. Чем ближе агент к мячу, тем больше награды он получает — использовал эту награду.
Итак, я запустил играть двух ботов Nexto играть между собой, собрал данные за два дня игры, и запустил обучение. По графикам все начало сходится, после чего я попробовал полученного бота в игре — и получил просто набор бессмысленных движений от него. Похоже, что полученная Q-функция ничего не знала о других действиях, за пределами действий бота Nexto.
Я выбрал DQN по двум причинам: 1. он прост в реализации; 2. можно собирать данные и переиспользовать их. Тем не менее, в процессе моих экспериментов с ним в gym gymnasium, оказалось, что это очень нестабильный метод. В 2013 году DeepMind успешно использовали для обучение игре в Atari-игры, используя одни и те же гиперпараметры, и это выглядит удивительным для меня. Получить хороший результат с PPO было намного проще (но на момент появления DQN алгоритм PPO ещё не придумали).
Чуть позже я попробовал использовать библиотеку rlgym, и оказалось, что собрать данные для обучения не так уж и сложно.
RLGym
Это библиотека, которая создает интерфейс взаимодействия между игрой и python, превращая ее в аналог библиотеки gym. Также там есть готовые схемы систем наград, а интерфейс совестим с библиотекой для обучения stable-baselines3. В результате запустить обучение становиться довольно просто. Запустить уже всё готовое — это не очень интересно, поэтому я продолжил использовать свои реализации, но в rlgym есть пара функций, которые сильно упрощают весь процесс, и без которых всё становиться на порядок сложнее — можно ускорить игру, например, в 100 раз, а также запустить несколько экземпляров игры одновременно. И процесс обучения начинает выглядеть как-то так:
Без записи экрана работает побыстрее. Совсем отключить рендеринг нельзя, но можно все просто свернуть, и тогда рендеринг выключиться сам.
Эта библиотека также позволяет задавать то, как будет начинаться раунд — где будут находиться машины и мяч. Если начинать с плюс / минус одинаковых положений вначале раунда, то бот стратегия бота может быть слишком детерминистической. Чтобы этого избежать, я скачал повторы игр других с сайта https://ballchasing.com/, распарсил их при помощи carball, а затем брал случайный кадр из случайного повтора и начинал раунд с него. Сама библиотека carball уже не поддерживается, и пришлось немного позаниматься некромантией, чтобы её запустить. Дальше я пробовал ставить мяч и машины в случайные места, что на данном этапе обучения скорее всего не будет иметь разницы.
Режиме 1 vs 1 предполагает, что в игре учувствуют два бота — синий и оранжевый. Значит с обоих мы можем снимать данные для обучения. Чтобы бот понимал, в какие ворота он должен забивать, ему нужно передавать информацию о том, в какой команде он находится. Или все-таки не нужно? RLGym предлагает более хитрый способ обойти эту ситуацию — поле у нас симметричное, а значит мы можем сделать отражение всех координат для оранжевого бота. Тогда оранжевый бот будет воспринимать игру так, как будто он играет за синюю команду. В итоге имеем следующее: есть возможность запустить 6 экземпляров игры, в каждом по два бота, т.е. делаем записи одновременно с 12 ботов. Также можно ускорить игру в 100 раз, но по факту скорость будет ограничиваться скоростью процессора. При таком раскладе данные будут собираться довольно быстро.
Попытка № 1. DQN + LSTM.
Отладив код DQN на примерах из gym, пробую запустить обучение с использованием жадной стратегии. Среднее количество награды начинает возрастать, но затем упираются в «потолок».
Seer использовал LSTM в архитектуре сети, что само по себе не выглядит очень хорошей идеей. Всё-таки обучение с подкреплением само себе довольно нестабильное, так если добавить рекуррентные сети, обучение в которых также не очень стабильное, то получаем в два раза более нестабильное обучение. Вероятно поэтому Seer использовал большой размер батча — 1728. При этом каждый элемент в батче в его случае — это последовательность из 16 кадров для lstm.
Но возможно, что мы можем предобучить lstm отдельно, затем заморозить её и использовать в качестве backbone, как это примерно это было сделано у бота Nexto. Можно было бы использовать не lstm, а трансформер, но трансформер работает преимущественно с дискретными значениями, так что непонятно как его в данном случае можно применить.
Идея была такая — мы обучаем сеть с основой в виде lstm предсказывать следующее состояние окружения, а потом использовать эту lstm в качестве backbone. Т.е. реализуем transferring learning. Это немного противоречит сути model-free методов обучения с подкреплением, но если это сработает, то почему бы и нет.
Я успешно обучил модель предсказывающую следующее состояние системы с приемлемым качеством. Тут пригодились данные, которые я собрал, гоняя двух Nexto-ботов. На видео показаны результаты предсказания траекторий машин и мяча на 10 кадров вперед (1 секунду). Линии тонкие, поэтому приходится всматриваться, но это лучше, чем ничего:
Результаты выглядят неплохо, но использовать их не получилось. Итоговая модель была на 10 миллионов параметров. Вектора в LSTM были размером 1024, а сама lstm состояла из двух слоев. Если использовать все данные — выход + (hidden_state + cell_state) x 2, то получается вектор размером более 5000. Что несравнимо больше, чем размер исходного вектора состояния — 43 (это координаты и скорость машин, мяча и т.д.). Проще тогда взять каскад векторов состояний с предыдущих кадров, размер тогда будет существенно меньше, чем после lstm.
Я, конечно, попробовал потренировать DQN вместе с этой lstm, но успехов не достиг. Моделировать среду сложно, особенно если это такая сложна среда, как Rocket League. В этом и плюс model-free методов.
Попытка № 2. PPO.
Собирать данные на лету оказалось не такой уж и большой проблемой, значит можно спокойно переключить на on-policy методы. С PPO у меня сразу всё получалось быстрее и проще, поэтому я не стал зацикливаться на методе DQN.
Как и с DQN, я попробовал для начала обучить агента ездить просто за мячом. Использовал эту награду + некоторые количество награды за касание мяча. После обучения агент успешно подъезжал к мячу, но несмотря на дополнительную награду за касание, он останавливался у мяча и не трогал его. Ведь если он его тронет, то он покатиться дальше. Проще остановиться у мяча и получать награду.
Самой очевидной и важной наградой для бота будет награда гол, остальное уже не так важно. Однако, с такой наградой бот будет обучаться слишком долго. Поэтому придётся сделать более комплексную награду, которая поможет боту намного быстрее прийти к результаты. Поэтому следующая система наград, которую я использовал, была довольно сложной. Эта система исходила из особенностей успешной стратегии в игре. Была, например, такая награда, которая дает больше очков, если агент находится между своими воротами и мячом (значит бот может успешно защищать свои ворота), и если мяч находится между агентом и воротами противника (значит бот может успешно атаковать). Но так как какого-то понятного результата я не получил, то нет никакого смысла разбирать её дальше.
Далее я начал пробовать использовать более простые награды, и добавлять их поэтапно. И это уже привело к результату. Я использовал следующие награды:
-
Агент получает +1, если он ближе к мячу, чем противник, получает -1, если дальше. Это награда сработала ощутимо лучше, чем предыдущая награда, которая давала разную награду, в зависимости от расстояния до мяча. До этого бот вальяжно подъезжал к мячу и осторожно останавливался, здесь же он начала гнаться за мячом как угорелый, не боясь его ударить. Когда добавляем награду, где один агент выигрывает. а другой проигрывает, это делает сбор статистики и её понимание более сложным. Постоянно даём +1 одному агенту, -1 другому — в итоге получаем что-то нуля, и не получается проследить прогресс. Однако работают такие награды похоже лучше.
-
Агент получает +1, если он последним касался мяча. Эта награда привела к неожиданным результатам — бот стал более агрессивным: в какие-то моменты, доезжая до мяча, он вместо того, чтобы продолжать к нему ехать, разворачивался и пинал противника. Интересный эффект, это мы оставляем.
-
Наконец добавим награду, которая будет мотивировать забивать. Во-первых мы будем давать штраф -1 каждый кадр, чтобы мотивировать агента закончить игру как можно быстрее. Все штрафы мы аккумулируем с учетом обесценивающего коэффициента (discount factor). Если агент забил гол, то мы компенсируем ему все штрафы, т.е. даём ему то, что мы аккумулировали, плюс даём столько же, т.е. суммарно x2, и плюс ещё 10 поинтов. Если агент пропустил мяч, то даём ему все тоже самое, но со знаком минус.
С этой наградой началась уже настоящая игра. Боты сами разобрались, что такое позиционирование в игре, активно использовали бусты, но особо не подбирали их. Бусты — это бонусы, разбросанные по полю, которые позволяют машине ускориться. Также очень слабо использовали прыжки. Они могли вести мяч, но часто теряли его. -
Так как бот не торопился собирать бусты, то я добавил ещё одну награду — — даю награду пропорционально изменению количеству буста, которое бот собрал, но не меньше нуля. С этой наградой бот собирал бусты, но опять же не очень активно. Возможно стоит умножать не на 10, а на 100.
-
Награда за касание мяча в воздухе: . Это наградой хотелось обучить бота поднимать мяч повыше и прыгать. С этой наградой бот стал закидывать мяч на стенку. Возможно, опять же, нужно умножать не на 10, а на 100.
Как видно из статистики, наибольший вклад вносит награда за гол — goal_reward. При этом она отрицательная, так как бот в ней постоянно штрафуется и получает компенсацию только когда забил гол. А забивает агент не часто, поэтому средняя награда отрицательная. Награда за близость к мячу — соревновательная, поэтому она варьируется вокруг нуля. А награды за взятие буста и касание мяча в воздухе даёт, и правда, небольшой вклад.
Кроме того, я пробовал добавлять бота Nexto в обучения. Периодически, вместо одного из ботов появлялся Nexto. Ставить необученного бота против бота такого уровня — это не очень разумная идея, но у Nexto есть специальный параметр, который определяет насколько случайными будут его действия, этим параметром можно регулировать сложность бота. Я пробовал выставлять это параметр случайным числом от 0 до 0.1. Тем не менее обучение с Nexto скорее делало хуже результат, чем помогало.
Симуляция Rocket League.
Сбор данных оказался быстрее, чем я ожидал с самого начала, но он всё ещё занимал значительную часть времени. Решить эту проблему попытался автор этого репозитория. Автор сделал свою симуляции игры на плюсах (с учётом того, что Rocket League — это уже симуляция футбола, то выходит симуляция симуляции). По утверждением автора, с этим проектом уже можно бы за 1 секунду собрать данных за 10 минут реальной игры. Что выглядело очень многообещающе. Кто-то уже сделал интерфейс под питон, который совместим с gym. Отдельно есть rlviser-py для визуализации процесса. Выглядело всё неплохо, поэтому я сел интегрировать.
Запуск самой симуляции не очень user-friendly — кое-что пришлось скомпилировать из rust-проекта, отдельной тулзой нужно было сделать дамп 3д-моделей в Rocket League. Для запуска визуализации пришлось даже сделать свой форк питоновского интерфейса с правками на скорую руку. Выглядит всё не так красиво, как в самой игре, но работает. И работает быстрее, хотя не так быстро, как хотелось бы — получил ускорение в полтора раза. Но при этом все игра вела себя как-то нестабильно. В некоторые моменты в начале матча мяч зависал в воздухе. Вероятно это происходило из-за физического движка. Объекты с нулевой скоростью замораживаются и не общипываются, в начале матча скорость мяча равна нуля — мяч замораживался. Это лечилось путём того, что я добавлял маленькую скорость мячу в начале матча. Также в процессе обучения, агенты от адекватной игры переходили в каким-то странным поворотам вокруг себя. Посмотрев смотреть статистику обучения, ничего странного я не заметил, но что конкретно не так происходило в симуляции, я не нашёл. Ускорение в полтора раза того не стоило.
Сбор данных действительно можно ускорить, процессор был загружен только на процентов 20-35. Можно улучшить распараллеливание (stable baseline3-like подход — не самый удачный). Можно больше вычислений перевести на плюсы. Но это уже требует намного большей работы.
Итоги
Бот научился контролировать мяч, понял как атаковать, как защищать, но с другой стороны, слабо использовал прыжки, не пытался перебрасывать мяч, периодически промахивался мимо мяча. Стандартный заскриптованный бот в Rocket League играет лучше него из-за этих проблем. Тем не менее есть ряд моментов, которые позволят улучшить качество данного бота:
-
Банально дольше обучать. Каждая моя попытка обучения не превышала двух суток. Seer и Nexto потратили на порядки больше времени для обучения.
-
Выглядит так, что бот нашёл неоптимальную стратегию и перестал рассматривать какие-то необходимые механики игры. Чтобы заставить бота больше исследовать другие варианты, можно использовать entropy loss, который можно уменьшать во время обучения. Понятно, что это также замедлит обучение.
-
Можно модифицировать систему наград, чтобы мотивировать бота использовать больше возможностей. Как минимум, можно изменить веса существующих наград.
-
Использовать симуляцию игры для обучения. Потом придётся дообучать на реальной игре в любом случае, но если оптимизировать процесс сбора данных, то можно ускорить обучение раз 10, я полагаю. Но нужно иметь ввиду наличие возможных багов.
-
Attention-механизм в боте Nexto показал себя очень хорошо, следовательно его тоже было бы неплохо использовать.
-
Сейчас совсем неочевиден прогресс обучения. Нужно ввести какую-то систему для отслеживания уровня игры бота, например, относительно какого-то другого бота. Можно периодически во время обучения устраивать тестовые игры.