Непрерывные пространства состояний

Непрерывные пространства состояний

Здравствуйте и вновь добро пожаловать на занятия по теме «Глубокое обучение с подкреплением: глубокое обучение на языке Python, часть 7».

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

К счастью, существует среда MountainCarContinuous, которая даёт нам непрерывные пространства состояний, так что у нас есть, на чём себя проверить.

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

Формально мы можем выбрать любое непрерывное распределение, однако для начала кажется разумным выбрать гауссово. Не забывайте, что оно имеет два параметра – среднее значение и дисперсию, поэтому для создания параметризованной стратегии мы должны параметризовать среднее значение и дисперсию:

Для целей данной лекции допустим, что и среднее значение, и дисперсия являются по своим параметрам линейными, то есть, другими словами, μ равно θμ, умноженному на вектор признаков, а дисперсия равна экспоненте от θ, умноженного на вектор признаков:

Как альтернативный вариант вместо экспоненциальной функции можно использовать функцию softplus:

Как альтернативный вариант вместо экспоненциальной функции можно использовать функцию softplus:

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

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

Определив модель, мы должны выяснить, как её обновлять. К счастью, принципы метода градиента стратегии неизменны – именно потому, что мы переопределили стратегию. У нас всё ещё остаются прежние функция затрат и обновления, и, разумеется, мы можем подставить обычный градиентный спуск или какие-либо его модификации вроде AdaGrad или RMSprop:

Это важно, поэтому повторим: метод градиента стратегии не изменяется именно потому, что мы переопределили стратегию. Стратегия может быть любой функцией, какую вы установите, и всё равно метод градиента стратегии остаётся применимым. Таким образом, у нас может быть любой вид выходного распределения и любой вид параметризации – метод остаётся прежним.

И последнее, о чём стоит задуматься. Всё это было бы невозможно, если бы у нас не было метода градиента стратегии. Рассмотрим обычное Q-обучение с приближением функции. Приближение функции действия Q позволяет нам обрабатывать бесконечные пространства состояний, но не бесконечные пространства действий. Вспомните нашу предыдущую стратегию: мы создавали различные линейные модели для каждого действия, а затем брали аргумент максимизации, чтобы определить, какое действие выполнить на каком-либо из этапов. Глубокое Q-обучение такое же, только теперь у нас есть нейронная сеть с рядом исходящих узлов – по одному на каждое отдельное действие. Поскольку нам нужен отдельный выход или модель для каждого из действий, становится ясным, что их нельзя применить к бесконечному количеству действий. Таким образом, в Q-обучении непрерывное пространство действий невозможно, а в методе градиента стратегии – возможно.

Особенности непрерывной задачи о машине на склоне

Давайте более подробно рассмотрим применение параметризованной стратегии в непрерывной версии задачи о машине на склоне.

Стоит пройтись по различиям между дискретной и непрерывной версиями этой задачи, поскольку они неочевидны. В частности, в непрерывной версии отличается структура вознаграждений. Эту информацию можно также получить на странице Википедии по адресу

https://github.com/openai/gym/wiki/MountainCarContinuous-v0

Итак, в непрерывной версии задачи о машине на склоне если мы достигаем цели, то автоматически получаем вознаграждение 100, то есть, в отличие от дискретной задачи, наше вознаграждение может быть положительным. Впрочем, из этой сотни вычитается сумма квадратов предпринятых действий, то есть чем больше у нас действий, тем больше штраф к каждому последующему действию. Таким образом наш агент получает стимул выполнить как можно меньше действий. Задача считается решённой, если удаётся получить среднее общее вознаграждение 90 на протяжении 100 последовательно идущих эпизодов. Обратите внимание, что хотя пространство действий непрерывно, наши действия должны находиться в диапазоне между -1 и 1.

Следующее. Хотя мы могли бы напрямую использовать теорию градиента стратегии, однако вместо этого мы пойдём другим путём. Проблема градиентного спуска (и подъёма) в том, что он, судя по всему, в этой ситуации не очень хорошо сходится. Поэтому вместо него мы используем специальный тип алгоритма случайного поиска, который называется «восхождением по склону».

Работает он следующим образом. Предположим, у нас есть только два параметра, так что их можно показать на двухмерном графике. Вначале мы случайным образом инициируем параметры – это зелёная точка (см. слайд). Затем мы отыгрываем один или несколько эпизодов с этими параметрами. Если отыграть меньше эпизодов, то это увеличит дисперсию наших оценок, а если больше – то уменьшит её и даст лучшие оценки, но займёт больше времени. Причина, по которой нужно сыграть с несколькими оценками, состоит в том, чтобы понять, хороши ли данные параметры или нет. Если новые параметры лучше, чем уже обнаруженные, мы указываем их как лучшие, в противном случае придерживаемся первоначальных.

Затем мы генерируем новый набор параметров путём привнесения случайных возмущений в текущие лучшие параметры. Таким образом, в основном этот новый набор параметров будет находиться поблизости от текущих лучших параметров. После этого мы проверяем эти новые параметры на предмет того, дадут ли они лучшую среднюю отдачу. Если да, то они становятся текущими лучшими параметрами и так далее. В конце концов мы проверим множество наборов параметров, как бы взбираясь на склон – отсюда и название. Обратите внимание, что путь всегда будет лежать в направлении увеличения средней отдачи.

Из курса «Глубокое обучение, часть 2» вы можете припомнить, что это один из методов, который предлагался для нахождения хорошего набора значений гиперпараметров для обучения нейронной сети. То есть на самом деле это просто ещё один метод оптимизации, вовсе не уникальный для обучения с подкреплением, но в некоторых ситуациях определённо полезный. Я обнаружил, что данный метод в целом даёт более последовательные результаты, чем градиентный спуск, и куда лучше попыток бессистемного поиска гиперпараметров. Впрочем, поскольку у вас уже есть все необходимые навыки для создания параметризованной стратегии и выполнения градиентного подъёма, я предлагаю вам попробовать самим и посмотреть, удастся ли вам отыскать хорошие значения гиперпараметров. Попробуйте использовать как нейронные сети, так и линейные модели с ядрами радиальных базисных функций.

Непрерывная задача о машине на склоне в Theano

Воплотим параметризованную стратегию в Theano для непрерывной версии задачи о машине на склоне. Если вы не хотите писать код самостоятельно, хотя я настоятельно рекомендую сделать именно так, соответствующий файл для этой лекции называется pg_theano_random.py и находится в папке mountaincar.

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

# https://deeplearningcourses.com/c/deep-reinforcement-learning-in-python

# https://www.udemy.com/deep-reinforcement-learning-in-python

from __future__ import print_function, division

from builtins import range

# Note: you may need to update your version of future

# sudo pip install -U future

import gym

import os

import sys

import numpy as np

import theano

import theano.tensor as T

import matplotlib.pyplot as plt

from gym import wrappers

from datetime import datetime

from q_learning import plot_running_avg, FeatureTransformer

# so you can test different architectures

class HiddenLayer:

  def __init__(self, M1, M2, f=T.nnet.relu, use_bias=True, zeros=False):

    if zeros:

      W = np.zeros((M1, M2))

    else:

      W = np.random.randn(M1, M2) * np.sqrt(2 / M1)

    self.W = theano.shared(W)

    self.params = [self.W]

    self.use_bias = use_bias

    if use_bias:

      self.b = theano.shared(np.zeros(M2))

      self.params += [self.b]

    self.f = f

  def forward(self, X):

    if self.use_bias:

      a = X.dot(self.W) + self.b

    else:

      a = X.dot(self.W)

    return self.f(a)

Следующим идёт класс PolicyModel. Он похож предыдущие его версии, но с некоторыми дополнительными функциями, связанными с копированием. Вначале мы объявляем ряд гиперпараметров, а затем копируем все аргументы из конструктора, чтобы потом можно было сделать их копии.

# approximates pi(a | s)

class PolicyModel:

  def __init__(self, ft, D, hidden_layer_sizes_mean=[], hidden_layer_sizes_var=[]):

  # starting learning rate and other hyperparams

  lr = 10e-3

  mu = 0.

  decay = 0.999

    # save inputs for copy

    self.ft = ft

    self.D = D

    self.hidden_layer_sizes_mean = hidden_layer_sizes_mean

    self.hidden_layer_sizes_var = hidden_layer_sizes_var

Далее мы создаём слои для средних значений. Не забывайте, что среднее значение ничем не ограничено, поэтому функция активации последнего слоя остаётся такой же:

    ##### model the mean #####

    self.mean_layers = []

    M1 = D

    for M2 in hidden_layer_sizes_mean:

      layer = HiddenLayer(M1, M2)

      self.mean_layers.append(layer)

      M1 = M2

    # final layer

    layer = HiddenLayer(M1, 1, lambda x: x, use_bias=False, zeros=True)

    self.mean_layers.append(layer)

Затем мы создаём слои для дисперсии. Не забывайте, что дисперсия должна быть больше нуля, поэтому функцией активации последнего слоя является функция softplus:

    ##### model the variance #####

    self.var_layers = []

    M1 = D

    for M2 in hidden_layer_sizes_var:

      layer = HiddenLayer(M1, M2)

      self.var_layers.append(layer)

      M1 = M2

    # final layer

    layer = HiddenLayer(M1, 1, T.nnet.softplus, use_bias=False, zeros=False)

    self.var_layers.append(layer)

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

    # get all params for gradient later

    params = []

    for layer in (self.mean_layers + self.var_layers):

      params += layer.params

    cashes = [theano.shared(np.ones_like(p.get_value())*0.1) for p in params]

    velocities = [theano.shared(p.get_value()*0) for p in params]

    self.params = params

Следующий этап – объявление, как и раньше, входных данных, преимуществ и действий. Полезной является функция get_output, поскольку получение выходных данных из моделей среднего значения и дисперсии одинаково. Кроме того, мы несколько сглаживаем дисперсию для облегчения исследования.

    # inputs and targets

    X = T.matrix(‘X’)

    actions = T.vector(‘actions’)

    advantages = T.vector(‘advantages’)

    # calculate output and cost

    def get_output(layers):

      Z = X

      for layer in layers:

        Z = layer.forward(Z)

      return Z.flatten()

    mean = get_output(self.mean_layers)

    var = get_output(self.var_layers) + 10e-5 # smoothing

Далее мы создаём функцию для вычисления логарифма плотности распределения вероятностей для распределения Гаусса – формула, с которой уже все должны быть знакомы:

    def log_pdf(actions, mean, var):

      k1 = T.log(2*np.pi*var)

      k2 = (actions – mean)**2 / var

    return -0.5*(k1 + k2)

    log_probs = log_pdf(actions, mean, var)

    cost = -T.sum(advantages * log_probs + 0.1*T.log(2*np.pi.var)) + 1.0*mean.dot

И наконец, мы компилируем функции train_op и predict_op. Обратите внимание, что теперь predict_op возвращает и среднее значение, и дисперсию.

    self.train_op = theano.function(

      inputs=[X, actions, advantages],

      updates=updates,

      allow_input_downcast=True

    )

    # alternatively, we could create a RandomStream and sample from

    # the Gaussian using Theano code

    self.predict_op = theano.function(

      inputs=[X],

      outputs=[mean, var],

      allow_input_downcast=True

    )

После этого мы определяем функцию predict, возвращающуюся, разумеется, и среднее значение, и дисперсию:

  def predict(self, X):

    X = np.atleast_2d(X)

    X = self.ft.transform(X)

    return self.predict_op(X)

Затем у нас идёт функция sample_action. Эта часть несколько отличается от виденного прежде. Вначале мы должны вызвать функцию predict, чтобы получить среднее значение и дисперсию гауссового распределения. Учитывайте, что X содержит только один пример. Функция predict возвращает две вещи – список средних значений и список дисперсий, поэтому первое полученное среднее значение мы индексируем в виде [0][0], а первую полученную дисперсию – в виде [1][0]. Далее мы генерируем выборку из стандартного нормального распределения, масштабируем его по стандартному отклонению и добавляем среднее значение. Поскольку среда допускает только действия, находящиеся в диапазоне между -1 и 1, нам нужно «обрезать» её, чтобы быть уверенными, что она попадает в данный диапазон.

  def sample_action(self, X):

    pred = self.predict(X)

    mu = pred[0][0]

    v = pred[1][0]

    a = np.random.randn()*np.sqrt(v) + mu

    return min(max(a, -1), 1)

Следующими у нас идут несколько функций, связанные с процедурой восхождения по склону. Вначале идёт функция copy, создающая новую модель стратегии с теми же аргументами конструктора, что были изначально; затем вызывается функция copy_from, копирующая параметры из одной модели в другую.

  def copy(self):

    clone = PolicyModel(self.ft, self.D, self.hidden_layer_sizes_mean, self.hidden_layer_sizes_mean)

    clone.copy_from(self)

    return clone

Далее идёт функция copy_from. Поскольку мы ранее сохранили параметры как переменную экземпляра, сейчас нам нужно лишь перебрать их. Можно воспользоваться полезными функциями get_value и set_value, чтобы получить численные значение каждого параметра.

  def copy_from(self, other):

    # self is being copied from other

    for p, q in zip(self.params, other.params):

      v = q.get_value()

      p.set_value(v)

Следом идёт функция perturb_params, помогающая нам в восхождении по склону. В цикле мы перебираем все параметры и с вероятностью 0,1 присваиваем новое случайное значение параметру, в противном случае берём текущее значение параметра и добавляем к нему некоторый шум:

  def perturb_params(self):

    for p in self.params:

      v = p.get_value()

      noise = np.random.randn(*v.shape) / np.sqrt(v.shape[0]) * 5.0

      if np.random.random() < 0.1:

        # with probability 0.1 start completely from scratch

        p.set_value(noise)

      else:

        p.set_value(v + noise)

Далее идёт функция play_one, очень простая.

def play_one(env, pmodel, gamma):

  observation = env.reset()

  done = False

  totalreward = 0

  iters = 0

  while not done and iters < 2000:

    # if we reach 2000, just quit, don’t want this going forever

    # the 200 limit seems a bit early

    action = pmodel.sample_action(observation)

    # oddly, the mountain car environment requires the action to be in

    # an object where the actual action is stored in object[0]

    observation, reward, done, info = env.step([action])

    totalreward += reward

    iters += 1

  return totalreward

После этого идёт функция play_multiple_episodes. В неё просто проигрывается ряд эпизодов и возвращается среднее вознаграждение. Не забывайте, что нам нужно сыграть несколько эпизодов, чтобы проверить настройки каждого параметра и уменьшить дисперсию. Кроме того, нам нужно проверить, насколько хорошо наша модель работает с сотней эпизодов, поскольку именно таким образом о ней судят в OpenAI Gym. Плюс здесь у нас есть аргумент print_iters, благодаря которому выводится среднее значение после каждого эпизода, чтобы можно было увидеть результат, не дожидаясь окончания всей сотни эпизодов.

def play_multiple_episodes(env, T, pmodel, gamma, print_iters=False):

  totalrewards = np.empty(T)

  for i in range(T):

    totalrewards[i] = play_one(env, pmodel, gamma)

    if print_iters:

      print(i, “avg so far:”, totalrewards[:(i+1)].mean())

  avg_totalrewards = totalrewards.mean()

  print(“avg totalrewards:”, avg_totalrewards)

  return avg_totalrewards

Затем идёт функция random_search. Она очень схожа на функцию random_search, виденную ранее. Сначала мы инициируем переменную totalrewards в виде пустого списка, где будут собираться вознаграждения для каждого значения параметра. Затем инициируется переменная best_avg_totalreward, помогающая отслеживать лучшие найденные на данный момент параметры. После неё идёт переменная best_pmodel, которая будет действительной моделью, содержащей лучшие параметры, а после неё – переменная с количеством эпизодов для проверки каждого параметра; я обнаружил, что трёх будет достаточно. Затем мы 100 раз выполняем цикл; я нашёл, что этого достаточно, чтобы найти хорошие настройки всех параметров. В цикле мы делаем копию лучшей модели, назвав её tmp_pmodel, затем привносим возмущения в её параметры, отыгрываем несколько эпизодов с этими настройками и получаем среднее вознаграждение, добавляя его в список totalrewards. Если полученное среднее вознаграждение лучше, чем виденные до сих пор, то новая модель становится лучшей моделью, после чего заканчиваем цикл, возвращая как список общих вознаграждений, так и обнаруженную лучшую модель.

def random_search(env, pmodel, gamma):

  totalrewards = []

  best_avg_totalreward = float(‘-inf’)

  best_pmodel = pmodel

  num_episodes_per_param_test = 3

  for t in xrange(100):

    tmp_pmodel = best_pmodel.copy()

    tmp_pmodel.perturb_params()

    avg_totalrewards = play_multiple_episodes(

      env,

      num_episodes_per_param_test,

      tmp_pmodel,

      gamma

    )

    totalrewards.append(avg_totalrewards)

    if avg_totalrewards > best_avg_totalreward:

      best_pmodel = tmp_pmodel

  return totalrewards, best_pmodel

Далее идёт функция main. В основном, всё ожидаемо: мы создаём среду, преобразователь признаков и модель, а также блок кода для monitor. Затем мы выполняем случайных поиск, выводим максимальное общее вознаграждение и, конечно, хотим проверить эти параметры на последовательность, а потому отыгрываем 100 эпизодов и выводим их результаты. В конце мы строим график общего вознаграждения как функции времени. Обратите внимание, что мы не строим график функции cost_to_go. Это связано с тем, что данный метод вовсе не требует функции ценности, хотя формально, имея стратегию, мы можем использовать оценку стратегии для вычисления функции ценности. Если вам будет интересно, оставляю это для вас в качестве упражнения.

def main():

  env = gym.make(‘MountainCarContinuous-v0’)

  ft = FeatureTransformer(env, n_components=100)

  D = ft.dimensions

  pmodel = PolicyModel(ft, D, [], [])

  gamma = 0.99

  if ‘monitor’ in sys.argv:

    filename = os.path.basename(__file__).split(‘.’)[0]

    monitor_dir = ‘./’ + filename + ‘_’ + str(datetime.now())

    env = wrappers.Monitor(env, monitor_dir)

  totalrewards, pmodel = random_search(env, pmodel, gamma)

  print(“max reward:”, np.max(totalrewards))

  # play 100 episodes and check the average

  avg_totalrewards = play_multiple_episodes(env, 100, pmodel, gamma, print_iters=True)

  print(“avg reward over 100 episodes with best models:”, avg_totalrewards)

  plt.plot(totalrewards)

  plt.title(“Rewards”)

  plt.show()

if __name__ == ‘__main__’:

  main()

Итак, запустим программу и посмотрим, что у нас получится.

Непрерывная задача о машине на склоне в Tensorflow

Реализуем параметризованную стратегию в библиотеке Tensorflow для непрерывной версии задачи о машине на склоне. Если вы не хотите писать код самостоятельно, хотя я настоятельно рекомендую сделать именно так, соответствующий этой лекции файл называется pg_tf_random.py и находится в папке mountaincar.

Вначале у нас идут все привычные импорты; следом идёт класс HiddenLayer, хотя в данной мы не будем использовать его, а применим линейную модель с ядрами радиальных базисных функций. Но если хотите, предлагаю отключить её и использовать обычную нейронную сеть. Обратите внимание, что в HiddenLayer я сохраняю параметры, хотя в Tensorflow нам обычно это не требуется; однако я хочу, чтобы были, чтобы можно было сделать копию сети позже.

# https://deeplearningcourses.com/c/deep-reinforcement-learning-in-python

# https://www.udemy.com/deep-reinforcement-learning-in-python

from __future__ import print_function, division

from builtins import range

# Note: you may need to update your version of future

# sudo pip install -U future

import gym

import os

import sys

import numpy as np

import tensorflow as tf

import matplotlib.pyplot as plt

from gym import wrappers

from datetime import datetime

from q_learning import plot_running_avg, FeatureTransformer

# so you can test different architectures

class HiddenLayer:

  def __init__(self, M1, M2, f=tf.nn.tanh, use_bias=True, zeros=False):

    if zeros:

      W = np.zeros((M1, M2)).astype(np.float32)

      self.W = tf.Variable(W)

    else:

      self.W = tf.Variable(tf.random_normal(shape=(M1, M2)))

    self.params = [self.W]

    self.use_bias = use_bias

    if use_bias:

      self.b = tf.Variable(np.zeros(M2).astype(np.float32))

      self.params.append(self.b)

    self.f = f

  def forward(self, X):

    if self.use_bias:

      a = tf.matmul(X, self.W) + self.b

    else:

      a = tf.matmul(X, self.W)

    return self.f(a)

Далее у нас идёт класс PolicyModel. Он похож на предыдущие свои версии, однако с некоторыми дополнительными функциями, предназначенными для копирования. Вначале мы копируем все аргументы конструктора, чтобы позже можно было сделать их копии:

# approximates pi(a | s)

class PolicyModel:

  def __init__(self, ft, D, hidden_layer_sizes_mean=[], hidden_layer_sizes_var=[]):

    # save inputs for copy

    self.ft = ft

    self.D = D

    self.hidden_layer_sizes_mean = hidden_layer_sizes_mean

    self.hidden_layer_sizes_var = hidden_layer_sizes_var

Затем мы создаём слои для среднего значения. Не забывайте, что среднее значение ничем не ограничено, поэтому функция активации последнего слоя остаётся такой же:

    ##### model the mean #####

    self.mean_layers = []

    M1 = D

    for M2 in hidden_layer_sizes_mean:

      layer = HiddenLayer(M1, M2)

      self.mean_layers.append(layer)

      M1 = M2

    # final layer

    layer = HiddenLayer(M1, 1, lambda x: x, use_bias=False, zeros=True)

    self.mean_layers.append(layer)

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

    ##### model the variance #####

    self.var_layers = []

    M1 = D

    for M2 in hidden_layer_sizes_var:

      layer = HiddenLayer(M1, M2)

      self.var_layers.append(layer)

      M1 = M2

    # final layer

    layer = HiddenLayer(M1, 1, tf.nn.softplus, use_bias=False, zeros=False)

    self.var_layers.append(layer)

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

    # gather params

    self.params = []

    for layer in (self.mean_layers + self.var_layers):

      self.params += layer.params

Следующий этап – как и прежде, объявление входных данных, преимуществ и действий. Полезной является функция get_output, поскольку получение выходных данных из моделей среднего значения и дисперсии одинаково. Кроме того, мы несколько сглаживаем дисперсию для облегчения исследования.

    # inputs and targets

    self.X = tf.placeholder(tf.float32, shape=(None, D), name=’X’)

    self.actions = tf.placeholder(tf.float32, shape=(None,), name=’actions’)

    self.advantages = tf.placeholder(tf.float32, shape=(None,), name=’advantages’)

    def get_output(layers):

      Z = self.X

      for layer in layers:

        Z = layer.forward(Z)

      return tf.reshape(Z, [-1])

    # calculate output and cost

    mean = get_output(self.mean_layers)

    std = get_output(self.var_layers) + 10e-5 # smoothing

После этого мы создаём случайный нормальный, или гауссов, объект. В нём есть полезные функции для выборки и вычисления логарифма плотности распределения вероятностей, хотя прямо сейчас они нам и не нужны. Все оптимизаторы помечены как комментарии, однако по-прежнему находятся здесь. Формально нам тут не нужна никакая подгонка, поскольку мы совершаем восхождение по склону, однако теоретически их можно объединить. Я оставляю эту возможность на ваше усмотрение. И в конце мы  компилируем функцию predict_op. Обратите внимание, что мы должны «обрезать» значения так, чтобы они были между -1 и 1. Это связано с тем, что среда позволяет совершать действия, которые находятся только в этом диапазоне.

    # note: the ‘variance’ is actually standard deviation

    norm = tf.contrib.distributions.Normal(mean, std)

    self.predict_op = tf.clip_by_value(norm.sample(), -1, 1)

    # log_probs = norm.log_prob (self.actions)

    # cost = -tf.reduce_sum(self.advantages * selected_probs)

    # self.cost = cost

    # self.train_op = tf.train.AdamOptimizer(10e-2).minimize(cost)

    # self.train_op = tf.train.AdagradOptimizer(10e-2).minimize(cost)

    # self.train_op = tf.train.MomentumOptimizer(10e-5, momentum=0.9).minimize(cost)

    # self.train_op = tf.train.GradientDescentOptimizer(10e-5).minimize(cost)

Далее мы определяем функцию set_session, которая является простым установщиком. Дело в том, что все наши модели проще использовать в одном и том же сеансе.

  def set_session(self, session):

    self.session = session

Затем идёт функция init_vars. Она необходима в связи с тем, что Tensorflow требует инициировать переменные перед их использованием. Мы не можем воспользоваться инициализатором глобальных переменных, так как если мы вызовем его в самом начале, более поздние сети ещё не будут существовать. Потом же вызывать его мы тоже не можем, поскольку тогда он сбросит все значения переменных до некоторых случайных величин и мы потеряем наши лучшие параметры. Таким образом, мы должны инициировать переменные только при создании сети и только те из них, которые являются частью сети.

  def init_vars(self):

    init_op = tf.variables_initializer(self.params)

    self.session.run(init_op)

  # def partial_fit(self, X, actions, advantages):

  #   X = np.atleast_2d(X)

  #   X = self.ft.transform(X)

  #   actions = np.atleast_1d(actions)

  #   advantages = np.atleast_1d(advantages)

  #   self.session.run(

  #     self.train_op,

  #     feed_dict={

  #       self.X: X,

  #       self.actions: actions,

  #       self.advantages: advantages,

  #     }

  #   )

Далее у нас идут функции predict и sample_action. Они очень просты и всего лишь вызывают уже созданные нами вещи.

  def predict(self, X):

    X = np.atleast_2d(X)

    X = self.ft.transform(X)

    return self.session.run(self.predict_op, feed_dict={self.X: X})

  def sample_action(self, X):

    p = self.predict(X)[0]

    # print(“action:”, p)

    return p

После этого идут несколько функций, относящихся к процедуре восхождения на склон. Вначале идёт функция copy, создающая новую модель стратегии PolicyModel с теми же аргументами конструктора, что и прежде. Обратите внимание, что тут тоже нужно установить сессию и инициировать переменные вновь созданной модели. Затем вызывается функция copy_from, копирующая параметры из одной модели в другую.

  def copy(self):

    clone = PolicyModel(self.ft, self.D, self.hidden_layer_sizes_mean, self.hidden_layer_sizes_mean)

    clone.set_session(self.session)

    clone.init_vars() # tf will complain if we don’t do this

    clone.copy_from(self)

    return clone

Следом идёт функция copy_from. Поскольку мы ранее сохранили параметры как переменную экземпляра, теперь нам нужно лишь перебрать их. Для этого используется session.run, чтобы получить значения и assign, чтобы присвоить их. Но это операторы только библиотеки Tensorflow, поэтому запускать их надо в сессии.

  def copy_from(self, other):

    # collect all the ops

    ops = []

    my_params = self.params

    other_params = other.params

    for p, q in zip(my_params, other_params):

      actual = self.session.run(q)

      op = p.assign(actual)

      ops.append(op)

    # now run them all

    self.session.run(ops)

Далее у нас идёт функция perturb_params, помогающая нам в восхождении по склону. В цикле мы перебираем все параметры и с вероятностью 0,1 присваиваем полностью новое случайное значение параметру, в противном случае берём текущее значение параметра и добавляем к нему некоторый шум:

  def perturb_params(self):

    ops = []

    for p in self.params:

      v = self.session.run(p)

      noise = np.random.randn(*v.shape) / np.sqrt(v.shape[0]) * 5.0

      if np.random.random() < 0.1:

        # with probability 0.1 start completely from scratch

        op = p.assign(noise)

      else:

        op = p.assign(v + noise)

      ops.append(op)

    self.session.run(ops)

Затем идёт функция, play_one очень простая.

def play_one(env, pmodel, gamma):

  observation = env.reset()

  done = False

  totalreward = 0

  iters = 0

  while not done and iters < 2000:

    # if we reach 2000, just quit, don’t want this going forever

    # the 200 limit seems a bit early

    action = pmodel.sample_action(observation)

    # oddly, the mountain car environment requires the action to be in

    # an object where the actual action is stored in object[0]

    observation, reward, done, info = env.step([action])

    totalreward += reward

    iters += 1

  return totalreward

После этого идёт функция play_multiple_episodes. В неё просто проигрывается ряд эпизодов и возвращается среднее вознаграждение. Не забывайте, что нам нужно сыграть несколько эпизодов, чтобы проверить настройки каждого параметра и уменьшить дисперсию. Кроме того, нам нужно проверить, насколько хорошо наша модель работает с сотней итераций, поскольку именно таким образом о ней судят в OpenAI Gym. Плюс здесь у нас есть аргумент print_iters, благодаря которому выводится среднее значение после каждого эпизода, чтобы можно было увидеть результат, не дожидаясь окончания всей сотни эпизодов:

def play_multiple_episodes(env, T, pmodel, gamma, print_iters=False):

  totalrewards = np.empty(T)

  for i in range(T):

    totalrewards[i] = play_one(env, pmodel, gamma)

    if print_iters:

      print(i, “avg so far:”, totalrewards[:(i+1)].mean())

  avg_totalrewards = totalrewards.mean()

  print(“avg totalrewards:”, avg_totalrewards)

  return avg_totalrewards

Затем идёт функция random_search. Она очень схожа на функцию random_search, виденную ранее. Сначала мы инициируем переменную totalrewards в виде пустого списка, где будут собираться вознаграждения для каждого значения параметра. Затем инициируется переменная best_avg_totalreward, помогающая отслеживать лучшие найденные на данный момент параметры. После неё идёт переменная best_pmodel, которая будет действительной моделью, содержащей лучшие параметры, а после неё – переменная с количеством эпизодов для проверки каждого параметра; я обнаружил, что трёх будет достаточно. Затем мы 100 раз выполняем цикл; я нашёл, что этого достаточно, чтобы найти хорошие настройки гиперпараметров. В цикле мы делаем копию лучшей модели, назвав её tmp_pmodel, затем привносим возмущения в её параметры, отыгрываем несколько эпизодов с этими настройками и получаем среднее вознаграждение, добавляя его в список totalrewards. Если полученное среднее вознаграждение лучше, чем виденные до сих пор, то новая модель становится лучшей моделью, после чего заканчиваем цикл, возвращая как список общих вознаграждений, так и обнаруженную лучшую модель.

def random_search(env, pmodel, gamma):

  totalrewards = []

  best_avg_totalreward = float(‘-inf’)

  best_pmodel = pmodel

  num_episodes_per_param_test = 3

  for t in range(100):

    tmp_pmodel = best_pmodel.copy()

    tmp_pmodel.perturb_params()

    avg_totalrewards = play_multiple_episodes(

      env,

      num_episodes_per_param_test,

      tmp_pmodel,

      gamma

    )

    totalrewards.append(avg_totalrewards)

    if avg_totalrewards > best_avg_totalreward:

      best_pmodel = tmp_pmodel

      best_avg_totalreward = avg_totalrewards

  return totalrewards, best_pmodel

Далее идёт функция main. В основном, всё ожидаемо: мы создаём среду, преобразователь признаков и модель, а также создаём сессию, устанавливаем её и инициируем переменные в модели. Кроме того, есть блок кода для мониторинга. Затем мы выполняем случайных поиск, выводим максимальное общее вознаграждение и, конечно, хотим проверить эти параметры на последовательность, а потому отыгрываем 100 эпизодов и выводим их результаты. В конце мы строим график общего вознаграждения как функции времени. Обратите внимание, что мы не строим график функции cost_to_go. Это связано с тем, что данный метод вовсе не требует функции ценности, хотя формально, имея стратегию, мы можем использовать оценку стратегии для вычисления функции ценности. Если вам будет интересно, оставляю это для вас в качестве упражнения.

def main():

  env = gym.make(‘MountainCarContinuous-v0’)

  ft = FeatureTransformer(env, n_components=100)

  D = ft.dimensions

  pmodel = PolicyModel(ft, D, [], [])

  # init = tf.global_variables_initializer()

  session = tf.InteractiveSession()

  # session.run(init)

  pmodel.set_session(session)

  pmodel.init_vars()

  gamma = 0.99

  if ‘monitor’ in sys.argv:

    filename = os.path.basename(__file__).split(‘.’)[0]

    monitor_dir = ‘./’ + filename + ‘_’ + str(datetime.now())

    env = wrappers.Monitor(env, monitor_dir)

  totalrewards, pmodel = random_search(env, pmodel, gamma)

  print(“max reward:”, np.max(totalrewards))

  # play 100 episodes and check the average

  avg_totalrewards = play_multiple_episodes(env, 100, pmodel, gamma, print_iters=True)

  print(“avg reward over 100 episodes with best models:”, avg_totalrewards)

  plt.plot(totalrewards)

  plt.title(“Rewards”)

  plt.show()

if __name__ == ‘__main__’:

  main()

Запустим программу и посмотрим, что у нас получится.

Андрей Никитенко
Андрей Никитенко
Задать вопрос эксперту
Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: