Иногда простые идеи дают отличные результаты!

Недавно некоммерческая организация OpenAI под руководством Илона Маска продемонстрировала, что стратегия эволюции, которая ранее использовалась как метод оптимизации, может быть использована для создания агентов, которые учатся взаимодействовать со средой на основе обучения с подкреплением. Он изучал среды намного быстрее, чем современный A3C от Deepmind, и обладает гораздо большей масштабируемостью, потому что между потоками требуется гораздо меньше взаимодействия.

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

Чтобы понять, что будет дальше, вам необходимо знать следующее:

  1. Обучение с подкреплением:
    Посмотрите это видео, чтобы увидеть реальное применение обучения с подкреплением, гораздо лучше, чем чтение статей.
  2. Немного C ++, потому что это то, что я использовал для кода. Он по-прежнему будет в стиле питона, но фигурные скобки останутся!

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

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

Теперь приступим к поиску решения!

#include <iostream>
#include <vector>
#include <string>
#include <armadillo>
#include "../environment.hpp"
// Convenience for eyes
using namespace std;
using namespace arma;
using namespace gym;

Просто набор заголовков, библиотека armadillo дает нам математические функции для матриц, которые уменьшают размер кода. environment.hpp - еще одна вспомогательная библиотека от gym_tcp_api. Это хороший интерфейс для взаимодействия с тренажерным залом OpenAI с любого языка, здесь я использовал C ++, поскольку API уже содержит большую часть кода для C ++.

Теперь давайте определим класс и его конструктор,

class EvolutionStrategyAgent{
 public:
  mat model;
  size_t frameW, frameH, num_actions;
  EvolutionStrategyAgent(size_t frameW,
                         size_t frameH,
                         size_t num_actions)
  :
  frameW(frameW),  // These make variables local
  frameH(frameH),
  num_actions(num_actions)
  {
    model = randu(num_actions, frameW * frameH * 3);
  }

Поскольку наш агент будет использовать игровой фрейм для принятия решения, мы получаем от пользователя ширину фрейма, высоту фрейма и количество действий.
Для простоты наша модель будет представлять собой однослойную полностью подключенную сеть. Итак, здесь мы просто создаем случайную матрицу размеров [num_actions, frameW * frameH * 3]. Здесь 3 означает, что у нас есть значение RGB в каждом пикселе кадра.
Обратите внимание, что я использовал случайную однородную randu, так как я хочу, чтобы значения были равномерно распределены между 0 and 1.

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

ActionMatrix[6, 1] = Model[6, 10000] * Frame[10000, 1]

Теперь займемся функцией Play!

void Play(string environment,
            string host,
            string port)
  {
    size_t num_workers = 5;
    double sigma = 1; // Noise Multiplier
    double alpha = 0.005; // Learning Rate
    size_t input = frameW * frameH * 3;
    
    mat workerRewards(num_workers, 1);
    vector<mat> epsilons(num_workers);

Мы определили некоторые константы для алгоритма обучения, мы будем использовать 5 воркеров в каждом поколении. В каждом поколении мы будем создавать некоторый шум и умножать его на сигму, это будет делать шум больше (сигма ›1) или меньше (сигма‹ 1) или оставаться неизменным (сигма == 1). Альфа - это скорость обучения, которую мы будем использовать позже. WorkerRewards - это матрица, которая будет содержать вознаграждения, которые мы получили от каждого рабочего, epsilons - это вектор матриц, в которых будет храниться шум, который мы использовали для каждого рабочего.

while(1) //Until I am bored!
{
   // Run the for loop in different threads
   #pragma omp parallel for
   for(size_t i = 0; i < num_workers; i++)
   {
     mat epsilon = randn(num_actions, input); // Random Noise
     mat innerModel = model + (sigma * epsilon); // Add it
     epsilons[i] = epsilon;

Здесь мы создаем 5 воркеров, каждый со своим собственным потоком, благодаря OpenMP это заняло всего одну строку! #pragma omp parallel for

Теперь мы создаем эпсилон, используя случайное нормальное распределение randn тех же размеров, что и веса, потому что нам нужно добавить их позже.

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

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

Environment env(host, port, environment);
        
env.compression(9);
        
// Create a folder for Agent Files
string folder("./dummy/");
folder += i + '/';
// Monitor its moves
env.monitor.start(folder, true, false);
env.reset();
env.render();

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

size_t totalReward = 0;
//Until the episode is complete
while(1)
{
  mat maxAction;
  mat action =  innerModel * vectorise(env.observation);
  maxAction = action.index_max();
  env.step(maxAction);
  totalReward += env.reward;
  if (env.done)
  {
    break;
  }
}
        
env.close();
workerRewards[i] = totalReward;

Это мыслящая часть модели, тоже довольно приятная для глаз!

  1. Получите матрицу действий, выполнив Model * Frame
  2. Получите максимальную отдачу
  3. Сделайте действие
  4. Получите награду и добавьте к общей награде.
  5. Если окружение готово, прекратите воспроизведение, в противном случае вернитесь к 1.
  6. Закройте среду и отправьте общее вознаграждение в матрицу вознаграждений.

Ух ты! Это было легко: P

Теперь приступим к оптимизации!

Давайте сложим все награды и эпсилоны вместе

mat sumRxEpsilon = zeros(num_actions, input);
for(size_t i = 0; i < num_workers; i++){
    mat stdReward = (workerRewards[i] -
                    mean(workerRewards)) /
                    stddev(workerRewards);
        
    sumRxEpsilon += epsilons[i] * as_scalar(stdReward);
}

Здесь есть одна загвоздка, мы до нее доберемся.

  1. Сначала создайте матрицу той же размерности, что и модель, но на этот раз заполненную нулями.
  2. Мы вычисляем стандартное вознаграждение, что означает, что для [2, 4, 6] мы берем среднее значение (здесь 4) и стандартное отклонение, то есть среднее значение того, насколько каждый элемент отличается от среднего (здесь 2), как показано ниже.

Теперь вычитаем среднее значение и делим на стандартное отклонение, чтобы получить стандартизированное вознаграждение [-1, 0, +1]. Это гарантирует, что значения не будут слишком большими, то есть 500 наград, когда все получают 100 наград, и 5 наград, когда все получают 1 награду, являются аналогичными ситуациями. Большая награда не означает большего прыжка, больший прыжок должен произойти, если все награды низкие и одна из них светится, то есть Единая!

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

4. Наконец, мы складываем их все в одну основную матрицу, которая теперь содержит путь к победе, путь к свету!

Почему мы это сделали? вы можете спросить ... вот формула,

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

Давайте разберемся! Не надо этого бояться!

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

Альфа (этот символ), если вы помните, - это скорость обучения, это очень маленькое число ... поэтому оно уменьшает количество умножаемых данных. Это сделано для того, чтобы наши шаги были маленькими, иначе мы можем перепрыгивать с холма на холм, но никогда не добраться до подножия холмов, где находится настоящее сокровище.

Этот большой E означает добавление того, что находится справа, путем заполнения j как 1, 2, 3… .n, здесь n означает num_workers! F[j] - это Награда для рабочего, e[j] - это эпсилон / шум для этого рабочего. Мы сделали этот шаг ранее, и матрица сумм ожидает использования.

У нас есть сумма из n чисел, если мы разделим ее на n, мы получим ... да ... да-а! Значение! Кроме того, вы помните, что мы умножили эпсилон на сигму? это было по ошибке, мы должны убрать это сейчас, поэтому мы снова разделим то же самое с сигмой! следовательно sum / (n * sigma)

Проще говоря,
Добавьте к модели среднее произведение (вознаграждение, шум) для каждого работника ... да ... вы испугались такой простой вещи.

В коде это выглядит так,

model = model + alpha * (sumRxEpsilon / (num_workers * sigma));

А теперь, ради здравого смысла, давайте распечатаем награды, чтобы мы знали, что обучение происходит.

cout << "Worker Rewards: ";
for(double reward : workerRewards){
  cout << reward << " ";
}
cout << "\n";

Выполнено!

Полученные результаты:

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

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

Ваши выводы!

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

  1. Сделайте аналогичный агент на языке, который вам нравится :-) Я не использовал библиотеки машинного обучения более высокого уровня, поэтому его легко переделать на любом языке. Для взаимодействия есть ссылка на API в gym_tcp_api's README.md
    Так что вам просто нужно отправить JSON в этом формате, и сервер выполнит взаимодействие за вас. Затем сервер возвращает состояние тренажерного зала, которое вы можете использовать для размышлений на следующем шаге.
  2. Добавьте модель побольше! Если вам комфортно работать с библиотеками машинного обучения, вы можете использовать более крупную модель и получить потрясающие результаты, обратите внимание, что вам понадобится шум для веса каждого слоя.
  3. Попробуйте больше окружений! В спортзале их много, забери яд здесь.
  4. Начните читать научные статьи по arXiv. Каждый алгоритм можно разбить на более мелкие части, и вы увидите истинную красоту математики, используемой в информатике, как только ваш мозг начнет замечать закономерности в математических формулах.

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