Автор: Эйб Хаскинс (Twitter, Github)

В этой статье мы углубимся в использование Unity3D и TensorFlow, чтобы научить ИИ выполнять простую внутриигровую задачу: бросать мячи в обруч. Полный исходный код доступен на Github, если у вас есть какие-либо вопросы, напишите мне в Twitter .

Введение в нашу игру

Есть игра, в которой у игроков одна главная цель: забросить мяч в корзину. Звучит не так уж сложно, но когда ваша кровь колотится, ваше сердце колотится, толпа аплодирует - что ж, сделать этот выстрел становится довольно сложно. Я говорю о классической американской игре Баскетбол? Нет, никогда об этом не слышал. Я говорю о классической аркадной игре Midway NBA Jam.

Если вы когда-либо играли в NBA Jam или в любую из игр, на которые он вдохновил (включая реальную лигу NBA, которая, как мне кажется, возникла после NBA Jam), то вы знаете, что с точки зрения игрока механика удара по мячу довольно проста. Вы удерживаете и отпускаете кнопку снимать как раз вовремя. Вы когда-нибудь задумывались, как выглядит этот кадр с точки зрения игры? Как выбирается дуга мяча? Насколько сильно брошен мяч? Как компьютер узнает, под каким углом стрелять?

Если бы вы были умным человеком, склонным к математике, вы могли бы найти эти ответы ручкой и бумагой, однако автор этого сообщения в блоге не справился с алгеброй 8-го класса, так что… об ответах «умного человека» не может быть и речи. Мне нужно будет сделать это по-другому.

Вместо того, чтобы выбрать более простой, быстрый и эффективный способ выполнения математических расчетов, необходимых для выполнения выстрела, мы посмотрим, насколько глубока кроличья нора, изучим простой TensorFlow и попробуем прострелить несколько чертовых обручей.

Начиная

Нам понадобится несколько вещей, чтобы пройти через этот проект.

Если вы не эксперт ни в одной из этих технологий, ничего страшного! (Я определенно не эксперт во всем этом!) Я постараюсь объяснить, как все эти части сочетаются друг с другом. Одним из недостатков использования стольких разнообразных технологий является то, что я не смогу объяснить все во всех подробностях, но я постараюсь как можно больше ссылаться на образовательные ресурсы!

Скачать проект

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

Примечание. Вам нужно будет загрузить импортированный пакет ресурсов Unity ML-Agents, чтобы Tensorflow можно было использовать на C #. Если вы получаете какие-либо ошибки, касающиеся того, что Tensorflow не найден в Unity, убедитесь, что вы выполнили документацию по настройке Unity для TensorflowSharp.

Какова наша цель?

Чтобы не усложнять задачу, желаемый результат для этого проекта будет невероятно простым. Мы хотим решить: если стрелок находится на расстоянии X от кольца, стрелять по мячу с силой Y. Вот и все! Мы не будем пытаться целиться в мяч или что-нибудь необычное. Мы только пытаемся понять, насколько сложно забросить мяч, чтобы сделать бросок.

Если вас интересует, как создавать более сложные ИИ в Unity, вам следует ознакомиться с гораздо более полным проектом ML-Agents от Unity. Методы, о которых я здесь расскажу, должны быть простыми, доступными и не обязательно отражать передовой опыт (я тоже учусь!)

Мои ограниченные знания TensorFlow, машинного обучения и математики не будут тонкими. Так что отнеситесь к этому с недоверием и поймите, что это все для развлечения.

Корзина и мяч

Мы уже обсуждали суть нашей цели: забить корзину. Чтобы забить мяч в корзину, нужна корзина и… ну, мяч. Здесь на помощь приходит Unity.

Если вы не знакомы с Unity, просто знайте, что это игровой движок, который позволяет создавать 2D- и 3D-игры для всех платформ. Он имеет встроенную физику, базовое 3D-моделирование и отличную среду выполнения сценариев (Mono), которая позволяет нам писать нашу игру на C #.

Я не художник, но я перетащил несколько блоков и собрал эту сцену.

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

В редакторе Unity вы можете увидеть невидимые триггеры, обведенные зеленым. Вы заметите, что есть два триггера. Это сделано для того, чтобы мы могли рассчитывать только те корзины, в которых мяч падает сверху вниз.

Если мы посмотрим на метод OnTriggerEnter в /Assets/BallController.cs (сценарий, который будет иметь каждый экземпляр нашего баскетбольного мяча), вы увидите, как эти два триггера используются вместе.

Эта функция делает несколько вещей. Во-первых, он обеспечивает срабатывание как верхнего, так и нижнего триггеров, затем изменяет материал мяча, чтобы мы могли визуально видеть, что мяч совершил бросок, и, наконец, он регистрирует две ключевые переменные, которые нам важны distance и force.y.

Съемка снимков

Откройте /Assets/BallSpawnerController.cs. Это сценарий, который живет в нашем шутере и выполняет работу по созданию баскетбольных мячей и попыткам сделать выстрелы. Посмотрите этот фрагмент в конце метода DoShoot().

Этот код Instantiates создает новый экземпляр мяча, а затем устанавливает силу, с которой мы будем стрелять, и расстояние от цели (чтобы нам было проще выйти из нее позже, как мы показали в последнем фрагменте).

Если у вас все еще открыто /Assets/BallController.cs, вы можете взглянуть на наш Start() метод. Этот код вызывается, когда мы создаем новый баскетбольный мяч.

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

Давайте попробуем запустить все это и посмотрим, как поживает наш звездный стрелок. Вы можете нажать кнопку ▶ ️ (Воспроизвести) в редакторе Unity, и мы увидим…

Наш игрок, ласково известный как «Красный», почти готов сразиться со Стефом Карри.

Так почему же Красный так ужасен? Ответ кроется в одной строке в Assets/BallController.cs, в которой написано float force = 0.2f. В этой строке делается смелое заявление о том, что все кадры должны быть одинаковыми. Вы заметите, что Unity понимает это «в точности то же самое» буквально. Один и тот же объект с теми же силами, повторяющийся снова и снова, всегда будет отскакивать одним и тем же образом. Аккуратный.

Это, конечно, не то, чего мы хотим. Мы никогда не научимся стрелять, как Леброн, если не попробуем ничего нового, так что давайте приправим это.

Случайный выбор снимков, сбор данных

Мы можем ввести некоторый случайный шум, просто изменив силу на что-то случайное.

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

Красный очень тупой, изредка делает выстрел, но это чистая удача. Хотя это нормально. На этом этапе любой сделанный снимок является точкой данных, которую мы можем использовать. Мы скоро вернемся к этому.

А пока мы не хотим делать снимки только из одной точки. Мы хотим, чтобы Рэд успешно стрелял (когда ему повезет) с любого расстояния. В Assets/BallSpawnController.cs найдите эти строки и раскомментируйте MoveToRandomDistance().

Если мы запустим это, то увидим, как Рыжий с энтузиазмом прыгает по площадке после каждого удара.

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

Каждый успешный выстрел фиксирует количество успешных выстрелов на данный момент, расстояние от обруча и силу, необходимую для выполнения выстрела. Однако это довольно медленно, давайте увеличим его. Вернитесь туда, где мы добавили вызов MoveToRandomDistance(), и измените 0,3f (задержка в 300 миллисекунд на выстрел) на 0.05f (задержка в 50 миллисекунд).

Теперь нажмите кнопку воспроизведения и наблюдайте, как вливаются наши успешные выстрелы.

Теперь это хороший режим тренировок! По стойке сзади видно, что мы успешно забиваем около 6,4% бросков. Стеф Карри, а он нет. Говоря об обучении, действительно ли мы учимся чему-нибудь из этого? Где TensorFlow? Почему это интересно? Что ж, это следующий шаг. Теперь мы готовы извлечь эти данные из Unity и построить модель для прогнозирования необходимой силы.

Прогнозы, модели и регрессия

Проверка наших данных в Google Таблицах

Прежде чем мы углубимся в TensorFlow, я хотел взглянуть на данные, поэтому я позволил Unity работать, пока Red не выполнит около 50 снимков. Если вы посмотрите в корневой каталог проекта Unity, вы должны увидеть новый файл successful_shots.csv. Это необработанный дамп каждого сделанного нами удачного кадра от Unity! У меня есть экспорт в Unity, чтобы я мог легко анализировать его в электронной таблице.

В файле .csv всего три строки index, distance и force. Я импортировал этот файл в Google Таблицы и создал Диаграмму рассеяния с линией тренда, которая позволит нам получить представление о распределении наших данных.

Ух ты! Посмотри на это. Я имею в виду, посмотри на это. То есть, вау ... Хорошо, признаю, я сначала тоже не понял, что это значит. Позвольте мне рассказать о том, что мы видим

На этом графике показан ряд точек, расположенных по оси Y в зависимости от силы выстрела, а по оси X в зависимости от расстояния, с которого был произведен выстрел. Мы видим очень четкую корреляцию между требуемой силой и расстоянием, с которого производится выстрел (за некоторыми случайными исключениями, которые имели сумасшедшие отскоки).

Практически вы можете прочитать это как «TensorFlow будет очень в этом деле».

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

Создание нашей модели TensorFlow.js

Откройте файл tsjs/index.js в своем любимом редакторе. Этот файл не имеет отношения к Unity и представляет собой просто сценарий для обучения нашей модели на основе данных в successful_shots.csv.

Вот весь метод, который обучает и сохраняет нашу модель ...

Как видите, в этом нет ничего особенного. Мы загружаем наши данные из файла .csv и создаем серию точек X и Y (очень похоже на нашу таблицу Google выше!). Затем мы просим модель «подогнать» под эти данные. После этого сохраняем нашу модель для дальнейшего использования!

К сожалению, TensorFlowSharp не ожидает модели в формате, в который Tensorflow.js может сохранять. Итак, нам нужно сделать какой-то волшебный перевод, чтобы мы могли перенести нашу модель в Unity. Я добавил несколько утилит, чтобы помочь с этим. Общий процесс состоит в том, что мы переводим нашу модель с TensorFlow.js Format на Keras Format, где мы можем создать контрольную точку, которую мы объединяем с нашим Protobuf Graph Definition, чтобы получить Frozen Graph Definition, который мы можем перенести в Unity.

К счастью, если вы хотите подыграть, вы можете пропустить все это и просто запустить tsjs/build.sh, и если все пойдет хорошо, он автоматически выполнит все шаги и поместит замороженную модель в Unity.

Внутри Unity мы можем посмотреть GetForceFromTensorFlow() в Assets/BallSpawnController.cs, чтобы увидеть, как выглядит взаимодействие с нашей моделью.

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

Когда вы используете model.predict в TensorFlow.js, он автоматически передает ваши входные данные в правильный узел входного графа и предоставляет вам выходные данные из правильного узла после завершения расчета. Однако TensorFlowSharp работает иначе и требует, чтобы мы напрямую взаимодействовали с узлами графа через их имена.

Имея это в виду, нужно преобразовать наши входные данные в формат, который ожидает наш график, и отправить выходные данные обратно в Red.

Настал игровой день!

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

Мы видим, что количество корзин увеличилось примерно в 10 раз! Что произойдет, если мы потренируем Рэда пару часов и соберем 10 000 или 100 000 успешных выстрелов? Несомненно, это еще больше улучшит его игру! Что ж, оставлю это на ваше усмотрение.

Я настоятельно рекомендую вам проверить исходный код на Github и написать мне в Твиттере, если вы можете превзойти показатель успеха 60% (спойлер: превышение 60% возможно на 100%, вернитесь и посмотрите на первую гифку, чтобы увидеть, насколько хорошо Красного можно тренировать!)