Такой простой, но такой мощный

Если вы недавно экспериментировали с алгоритмами обучения с подкреплением (RL), то наверняка заметили, насколько сложно их правильно реализовать: вычисление градиентов, добавление целевых сетей, воспроизведение опыта…

Конечно, вы всегда можете использовать сторонние реализации, например Базовые показатели OpenAI, но это противоречит цели обучения. Вы никогда не сможете полностью освоить алгоритм, если не реализуете его самостоятельно с нуля.

Итак, после некоторого изучения деталей DQN, DDPG и A3C, я был невероятно взволнован, когда обнаружил Evolution Strategies (ES) в качестве действительной альтернативы: https://arxiv.org/abs/1703.03864

Я попытаюсь объяснить ES в простых терминах и покажу реализацию этого метода для решения стандартной среды CartPole в Gym.

ES очень похож на другие алгоритмы поиска политик:

1. у вас есть нейронная сеть, которая представляет политику, то есть функцию, которая решает, какое действие предпринять с учетом текущего состояния.

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

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

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

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

Видите ли, этот алгоритм невероятно прост, всего три действия в цикле, но при этом он настолько мощный.

Некоторые преимущества:

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

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

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

4. Никакой функции значения не требуется, только определите одну сеть для политики и все.

5. Лучшее поведение при исследовании, чем другие методы поиска по политике. Очевидно, изменяя веса напрямую, вы можете создать «более случайное поведение», чем настраивая действия.

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

7. Инвариантен к времени выборки наблюдений, то есть не имеет значения, как часто выполняются действия и рассчитываются вознаграждения.

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

Теперь давайте посмотрим, как вставить ES в код:

  • Для нашей политики нам нужна сеть с функцией прямого распространения. Мы выбрали небольшую сеть для нашего простого примера (1 скрытый слой с 20 узлами), но вы можете быстро изменить пропускную способность сети в соответствии со сложностью политики.
import numpy as np
import gym
#neural network to store state to action policy
#add hidden layers or nodes according to needs
IL = 4 #input layer nodes
HL = 20 #hidden layer nodes
OL = 2 #output layer nodes
w1 = np.random.randn(HL,IL) / np.sqrt(IL)
w2 = np.random.randn(OL,HL) / np.sqrt(HL)
NumWeights1 = len(w1.flatten())
NumWeights2 = len(w2.flatten())
#forward propagation
def predict(s,w1,w2):
    h = np.dot(w1,s) #input to hidden layer
    h[h<0]=0 #relu
    out = np.dot(w2,h) #hidden layer to output
    out = 1.0 / (1.0 + np.exp(-out)) #sigmoid 
    return out
  • Затем мы загружаем среду и определяем некоторые гиперпараметры. Количество политик определяет, сколько случайных вариантов исходной политики генерируется и оценивается. При слишком высоком значении запуск займет слишком много времени, при слишком низком - недостаточно исследовать окружающую среду. Sigma определяет, насколько далеко от исходной политики мы будем исследовать (см. Объяснение ниже).
#load environment
env = gym.make('CartPole-v0')
#parameters
NumEpisodes = 50
NumPolicies = 10
sigma = 0.1
learning_rate = 0.001
Reward = np.zeros(NumPolicies)
  • Теперь мы запускаем основной цикл и генерируем новую группу политик, случайным образом распределенных вокруг исходной:
#start learning
for episode in range(NumEpisodes):
    #generate random variations around original policy
    eps = np.random.randn(NumPolicies,NumWeights1+NumWeights2)
    #evaluate each policy over one episode
    for policy in range(NumPolicies):
        w1_try = w1 + sigma * eps[policy,:NumWeights1].reshape(w1.shape)
        w2_try = w2 + sigma * eps[policy,NumWeights1:].reshape(w2.shape)
  • Каждую из этих новых политик необходимо оценить, т.е. мы запускаем агент в среде и получаем вознаграждение:
    #initial state
    observation = env.reset() #observe initial state
    Reward[policy] = 0
    while True:
        Action = predict(observation,w1_try,w2_try)
        Action = np.argmax(Action)
        #execute action
        observation_new, reward, done, _ = env.step(Action)
 
        #collect reward
        Reward[policy] += reward
        #update state
        observation = observation_new
        #end episode
        if done:
            break
#calculate incremental rewards
F = (Reward - np.mean(Reward))
  • Наконец, мы обновляем исходную политику, двигаясь в направлении, которое дало наилучшие результаты. Это своего рода градиентный подъем, за исключением того, что градиент не рассчитывается точно, а оценивается как среднее значение, а веса не обновляются посредством обратного распространения, а только увеличиваются.
#update weights of original policy according to rewards of all variations
weights_update = learning_rate/(NumPolicies*sigma) * np.dot(eps.T, F)
w1 += weights_update[:NumWeights1].reshape(w1.shape)
w2 += weights_update[NumWeights1:].reshape(w2.shape)

Примечание 1. Какое отношение имеет этот метод к эволюции? Что ж, вы можете думать о своей политике как о живом существе, а колебания его параметров - как о случайных мутациях в его ДНК с течением времени. Каждая мутация порождает новое существо, которое будет либо «лучше», либо «хуже», чем исходное, где быть лучше или хуже означает получать от окружающей среды большую или меньшую награду в течение всей жизни. Алгоритм выделяет мутации, за которые накапливаются большие награды, и перемещает следующее поколение существ в этом направлении. Это закон природы: чем сильнее выживет, тем слабее погибнет.

Примечание 2. Важна ли сигма? Да, все гиперпараметры играют роль в определении того, как алгоритм обучается. Мы генерируем новых людей, случайным образом распределяемых вокруг оригинала. Sigma решает, насколько сильны мутации, другими словами, насколько мы исследуем мир параметров. При очень низком значении сигмы поведение новой политики практически такое же, как и предыдущая, что означает, что многому нельзя научиться вообще. С очень большой Sigma новые политики слишком разнесены друг от друга, и может быть трудно решить, куда двигаться дальше, что также приводит к медленным улучшениям. На следующих графиках показаны кривые обучения, усредненные по 10 различным прогонам для Sigma = 0,01 (почти без обучения); 0,1 (очень быстрое обучение); 1 (очень медленное обучение).

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

                #normalize inputs
                observation[0] /= 2.5
                observation[1] /= 2.5
                observation[2] /= 0.2
                observation[3] /= 2.5

Эта история опубликована в The Startup, крупнейшем предпринимательском издании Medium, за которым следят более 295 232 человека.

Подпишитесь, чтобы получать наши главные новости здесь.