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. Тем не менее такое обучение скорее делало хуже результат, чем помогало.
Симуляция 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 показал себя очень хорошо, следовательно его тоже было бы неплохо использовать.
Сейчас совсем неочевиден прогресс обучения. Нужно ввести какую-то систему для отслеживания уровня игры бота, например, относительно какого-то другого бота. Можно периодически во время обучения устраивать тестовые игры.
Комментарии (4)
mordoorg
20.11.2023 17:12Очень крутая статья!
Как думаете: возможно ли научить такого бота полетам, воздушному контролю мяча и отправки мяча в ворота в воздухе?
На сколько я знаю, игровые боты умеют делать всякие AirRoll'ы, AirDrop'ы и всякие другие классные штуки :)
Ладно лет 7 назад, боты в RL были глуповатыми, но сейчас, со сложными ботами иногда так и вовсе тяжело было бороться, потому что те освоили неплохой контроль мяча в воздухе.MarkWatney Автор
20.11.2023 17:12+1Спасибо!
Вот тут можно посмотреть как Nexto играет против типового игрока и его контроль мяча.
И я уверен, что можно научить бота намеренно стараться держать мяч в воздухе. Вопрос просто в подборе верных наград, гиперпараметров обучения и времени.
mostodont32
Очень интересно!
Странно, что нигде нет награды за близость мяча к воротам соперника. Кажется, что такая награда была бы лучше, чем за гол :)
Еще, если вам не трудно, хотя бы при первом появлении вставляйте ссылки на методы, которые в статье используются. Так ни разу и не увидел раскрытия аббревиатуры PPO за весь текст.
MarkWatney Автор
Спасибо!
PPO - proximal policy optimization, метод, предложенный OpenAI.
Я использовал похожие типы наград в первоначальном варианте, но, как я писал, лучше сработали более простые варианты. Если придумать правильные более хитрые типы наград, то бот быстрее будет обучаться, но с другой стороны это может его ограничивать для более хитрых тактик. Например, иногда выгоднее подвести мяч к своим воротам, но зато получить контроль мяча. В целом при достаточном времени обучения, бот все равно найдёт правильную тактику, и в данный момент мне кажется более перспективным мотивировать бота изучать больше вариантов стратегии. Например, я вижу, что он не пытается использовать задний ход, хотя моментами это было бы очень полезно. Для решения таких проблем как раз существует entropy loss, этот лосс заставляет сеть быть менее уверенной в своих решениях, и она в процессе обучение пробует больше других вариантов.