OpenAI Gym и базовые методы обучения с подкреплением

Руководство по OpenAI Gym

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

В этой статье у нас будет руководство по основам OpenAI Gym.

Предполагается, что на данный момент вы уже установили его, воспользовавшись инструкциями из лекции «Где взять код». Что мы сделаем в этой лекции – так это подсоединимся к среде и сыграем эпизод, используя только случайные действия. Тут не будет никакого обучения, поскольку мы хотим лишь лично познакомиться с API и выяснить, где можно найти всё необходимое для обучения вроде состояний, действий и вознаграждений. Если вы хотите посмотреть весь код целиком, то соответствующий файл в репозитарии называется gym_tutorial.py.

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

import gym

Если у вас не получилось, значит, вы неправильно установили библиотеку.

Следующее, что нам нужно, – это получить нашу среду. Это делает функция gym.make:

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

Обратите внимание, что в данном примере мы получаем среду для тележки с шестом. Как вы знаете, это среда, где задача состоит в том, чтобы удерживать шест на тележке. Разумеется, Gym имеет и другие среды. Один из способов получить полный список сред – просто перейти на их сайт и щёлкнуть на ссылку Environments. При этом вы перейдёте по следующему адресу:

https://gym.openai.com/envs

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

Имея среду, мы можем запустить эпизод. Так и сделаем. Первое, что нужно сделать перед запуском эпизода, это запустить функцию reset:

env.reset()

Она помещает нас в начальное состояние. Если вы это сделаете в окне Ipyton, то заметите, что она кое-то возвращает:

array([-0.04533731, -0.03231478, -0.01469216,  0.04151   ])

Это массив. Но что это за массив? Выглядит он как массив из четырёх чисел с плавающей запятой. Как вы можете догадаться, это начальное состояние. Вас может заинтересовать, каков физический смысл этих чисел. Тут документация довольно скудна, но, к счастью, для первых двух сред, которые мы будем рассматривать, эта информация доступна в Википедии, причём, как ни странно, она не имеет ссылки на страницу среды openai.com. Страница Википедии, посвящённая тележке с шестом, доступна по следующему адресу в репозитарии Github:

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

Там же можно найти ссылки и на другие страницы Википедии.

Вы увидите, что четыре числа представляют положение тележки, её скорость, угол шеста и его скорость. Вы также можете получить их минимальное и максимальное значения. И, конечно же, ни один из этих URL-адресов не нужно вводить вручную – вы можете скопировать их из скрипта Python.

Часть этой информации можно получить и с помощью консоли. Если вы введёте команду

env.observation_space

то в ответ получите объект

Box(4,)

Присвоим это значение переменной box. Если вы наберёте

box.

и нажмёте Tab, то увидите список атрибутов и функций, которые можно вызвать. Так, можно вызвать функцию contains, чтобы проверить, является ли вектор частью наблюдаемого пространства. С помощью функций high и low можно увидеть максимальное и минимальное значения, которые может содержать каждый элемент. Можно также получить пример из пространства состояний, что может оказаться полезным.

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

env.action_space

то увидим, что она возвращает объект Discrete  с параметром 2:

Discrete(2)

Если вы введёте команду

env.action_space.

и нажмёте Tab, то вновь увидите ряд функций и атрибутов, которые имеет объект Discrete. Опять-таки, у нас есть методы contains и sample, а также атрибут n – он показывает, сколько действий есть в пространстве действий. Действия всегда нумеруются начиная с нуля и далее, наподобие того, как мы рассматриваем классы в обучении с учителем.

Мы наконец-то готовы начать проигрывание эпизода. Теперь, когда мы знаем, каковы состояния и действия, как же выполнить действие, которое даст нам вознаграждение и следующее состояние? Интересующая нас функция называется env.step. В качестве аргумента она принимает один параметр – действие, и возвращает четыре значения: наблюдение, вознаграждение, значение флага done и словарь info. Как правило, словарь info игнорируется, поскольку служит лишь для отладки. Он может содержать полезную информации, которая бы помогла нашему агенту обучиться, но, по правилам OpenAI Gym, использовать её в своих представлениях не разрешается.

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

done = False

while not done:

    observation, reward, done, _ = env.step(env.action_space.sample())

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

Случайный поиск

Поскольку вы только-только познакомились с интерфейсом OpenAI Gym, мы не будем прямо сейчас писать код для чего-то слишком уж сложного. В этой лекции мы лишь добавим ещё немного кода в уже написанный для случайного поиска в пространстве параметров для линейной модели. Другими словами, мы возьмём наше состояние s, умножим его на вектор весовых коэффициентов w, и если результат больше нуля, выполним одно действие, а если меньше нуля – второе.

Сделаем краткий обзор алгоритма в псевдокоде.

for # раз, мы хотим настроить весовые коэффициенты

  new_weights = случайно

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

      играть эпизод

  if средняя длительность эпизода > лучшая на данный момент:

      weights = new_weights

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

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

Если вы не хотите сначала самостоятельно попробовать написать код, хотя я настоятельно рекомендую так и сделать, соответствующий файл в репозитарии курса называется random_search.py и находится в папке cartpole.

Вначале у нас идут основные импорты библиотек – Gym, Numpy, Matplotlib, а также некоторые импорты из библиотеки Future – просто на случай, если вы используете Python 3. Если у вас старая версия библиотеки Future, эти импорты могут не работать, но вы можете обновить её с помощью команды sudo pip install -U future.

from __future__ import print_function, division

from builtins import range

import gym

import numpy as np

import matplotlib.pyplot as plt

Первая наша функция принимает в качестве аргументов состояние s и вектор весовых коэффициентов w и вычисляет их скалярное произведение. Если скалярное произведение больше нуля, то возвращается значение 1, в противном случае – 0.

def get_action(s, w):

  return 1 if s.dot(w) > 0 else 0

Далее у нас идёт функция для отыгрыша одного эпизода. Аргументами для неё служат среда и набор параметров. Прежде всего мы сбрасываем среду для начала нового эпизода и устанавливаем начальные значения для done и t, в котором будет отслеживаться длительность эпизода. Далее мы входим цикл. Этот цикл ограничен 10 000 итерациями, поскольку случайный поиск действительно может довольно неплохо сработать, и если это в конце концов произойдёт, то цикл займёт очень много времени или, возможно, даже никогда не закончится. Тут есть одна странность. Похоже, что в более поздних версиях Gym нельзя производить более 200 итераций, хотя в ранних – можно. Насколько мне известно, это нигде не было задокументировано, поэтому если вы используете более позднюю версию Gym, то автоматически получите сигнал done по достижении 200 итераций. Внутри цикла мы увеличиваем t на единицу, выбираем действие и выполняем его. Вознаграждения в данном случае мы игнорируем, поскольку все они равны единице. Когда всё выполнено, мы возвращаем значение t. Обратите внимание, что функция env.render() помечена как комментарий. Если её вызвать, то появится всплывающее окно и вы сможете увидеть видео по мере отыгрыша эпизода. Конечно же, это делает выполнение скрипта намного медленнее, поэтому мы так делать не будем. Впрочем, я предлагаю вам включить её, если хотите посмотреть, как это всё выглядит.

def play_one_episode(env, params):

  observation = env.reset()

  done = False

  t = 0

  while not done and t < 10000:

    # env.render()

    t += 1

    action = get_action(observation, params)

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

    if done:

      break

  return t

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

def play_multiple_episodes(env, T, params):

  episode_lengths = np.empty(T)

  for i in range(T):

    episode_lengths[i] = play_one_episode(env, params)

  avg_length = episode_lengths.mean()

  print(“avg length:”, avg_length)

  return avg_length

Следующей идёт функция random_search. Наш поиск будет вестись по 100 случайным векторам параметров, каждый из которых выбран случайным образом из равномерного распределения между -1 и 1. В цикле мы вызываем функцию play_multiple_episodes с t = 100, так что каждый вектор параметров будет проверяться по 100 раз. Возвращается же средняя длительность эпизода для текущего набора параметров, которая добавляется к списку длительностей эпизодов. Если эта средняя длительность лучше, чем текущее лучшее значение, мы сохраняем эти параметры и обновляем лучшую среднюю длительность. В конце возвращаются все средние длительности эпизодов и конечные параметры:

def random_search(env):

  episode_lengths = []

  best = 0

  params = None

  for t in xrange(100):

    new_params = np.random.random(4)*2 – 1

    avg_length = play_multiple_episodes(env, 100, new_params)

    episode_lengths.append(avg_length)

    if avg_length > best:

      params = new_params

      best = avg_length

  return episode_lengths, params

В разделе main мы подключаемся к среде Cartpole, вызываем функцию random_search и затем выводим на экран все средние длительности эпизодов. После этого мы отыгрываем окончательный набор из 100 эпизодов с лучшими параметрами. Как правило, их средняя длительность будет близка к лучшим длительностям эпизодов, выведенных на экран на предыдущем этапе.

if __name__ == ‘__main__’:

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

  episode_lengths, params = random_search(env)

  plt.plot(episode_lengths)

  plt.show()

  # play a final set of episodes

  print(“***Final run with final weights***”)

  play_multiple_episodes(env, 100, params)

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

Сохранение видео

Теперь поговорим о том, как сохранить видео отыгрыша эпизода нашим агентом.

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

Основные изменения, которые необходимо сделать, состоят в следующем. Вначале мы должны импортировать модуль wrappers из библиотеки Gym:

import gym

from gym import wrappers

Затем мы должны «упаковать» среду в объект Monitor:

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

  env = wrappers.Monitor(env, ‘my_unique_dir’)

Заметьте, что мы должны указать объекту Monitor папку для сохранения видео. Кроме этих двух действий, больше ничего делать не надо. Обратите внимание, что мы не вызываем функцию render, однако окно воспроизведения появится в любом случае и покажет нам видео отыгрыша эпизода агентом. Поскольку изменений тут немного, я не буду проходиться по коду. По сути, всё, что мы сделали, – лишь добавили пару строк к файлу random_search.py. Соответствующий файл в репозитарии называется save_a_video.py и находится в папке cartpole.

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

Тележка с шестом с ячейками (теория)

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

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

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

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

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

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

Другая небольшая деталь, о которой хотелось бы упомянуть, прежде чем мы перейдём к коду, состоит в переопределении вознаграждения, установленного по умолчанию. Вознаграждение для тележки с шестом равно +1 за каждую единицу времени. Если использовать Q-обучение с ячейками, как описано выше, такая структура вознаграждений на самом деле не очень хорошо работает. Будет куда лучше, если мы дадим агенту большое отрицательное вознаграждение, скажем, -300 за каждый случай, когда эпизод заканчивается. Это будет побуждать агента избегать достижения такой ситуации. У вас может возникнуть вопрос, является ли изменение вознаграждения по умолчанию удачной мыслью? Я видел споры об этом в Интернете, но в целом полагаю, что изменение вознаграждения – это нормально. В настоящем мире при создании агента для решения новых задач вы в любом случае будете определять вознаграждения – так или иначе именно программист определяет разумную структуру вознаграждений.

Тележка с шестом с ячейками (код)

В завершение стаьи мы используем Q-обучение для решения задачи тележки с шестом с квантованными состояниями. Это значит, что мы собираемся преобразовать каждое состояние в ячейку, так чтобы набор состояний был дискретным и конечным. Чтобы хранить Q-таблицу, мы используем массив. Если вы не хотите писать код самостоятельно, хотя я настоятельно рекомендую сделать именно так, соответствующий файл в репозитарии называется q_learning_bins.py и находится в папке cartpole.

Вначале у нас идут стандартные импорты, а также библиотека Future, чтобы гарантировать, что данный код будет совместим с Python 3.

# ***

from __future__ import print_function, division

from builtins import range

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

#       if builtins is not defined

# sudo pip install -U future

import gym

import os

import sys

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from gym import wrappers

from datetime import datetime

Затем у нас идёт несколько функций, которые помогут преобразовать состояние в уникальное число. Так, функция build_state в виде аргумента принимает список целых чисел и обрабатывает их как строку целых чисел, возвращая целочисленное представление этой строки. Функция to_bin в качестве аргументов принимает значение и массив возможных ячеек и определяет, к какой ячейке принадлежит данное значение:

# turns list of integers into an int

# Ex.

# build_state([1,2,3,4,5]) -> 12345

def build_state(features):

  return int(“”.join(map(lambda feature: str(int(feature)), features)))

def to_bin(value, bins):

  return np.digitize(x=[value], bins=bins)[0]

Далее у нас идёт класс FeatureTransformer. Его предназначение – работать наподобие преобразователя признаков из библиотеки Scikit Learn, где есть функция transform. Заметьте, что он преобразует только одно наблюдение за раз. Ещё одно, на что следует обратить внимание в конструкторе, – что данные верхний и нижний пределы ячеек могут показаться выбранными несколько произвольно. Я провёл небольшое тестирование, однако оно определённо не было исчерпывающим. Если вы действительно хотите знать, какие ячейки следует использовать, будет лучше взять образец из реальных эпизодов, нарисовать гистограмму и посмотреть на полученный диапазон значений. Ещё лучше – использовать ячейки разного размера, так чтобы вероятность попадания в каждую из них была равной. Обратите также внимание на то, что для каждой из границ ячеек используется функция linspace. В параметр размера подставлено число 9, поскольку 9 разбиений дают 10 ячеек. Не забывайте, что всё, что выше максимума или ниже минимума, пойдёт в крайние ячейки.

class FeatureTransformer:

  def __init__(self):

    # Note: to make this better you could look at how often each bin was

    # actually used while running the script.

    # It’s not clear from the high/low values nor sample() what values

    # we really expect to get.

    self.cart_position_bins = np.linspace(-2.4, 2.4, 9)

    self.cart_velocity_bins = np.linspace(-2, 2, 9) # (-inf, inf) (I did not check that these were good values)

    self.pole_angle_bins = np.linspace(-0.4, 0.4, 9)

    self.pole_velocity_bins = np.linspace(-3.5, 3.5, 9) # (-inf, inf) (I did not check that these were good values)

Следующей у нас идёт функция transform, которая преобразует одно наблюдение за раз в целое число. Она использует функции build_state и to_bin, описанные выше.

  def transform(self, observation):

    # returns an int

    cart_pos, cart_vel, pole_angle, pole_vel = observation

    return build_state([

      to_bin(cart_pos, self.cart_position_bins),

      to_bin(cart_vel, self.cart_velocity_bins),

      to_bin(pole_angle, self.pole_angle_bins),

      to_bin(pole_vel, self.pole_velocity_bins),

    ])

Затем у нас идёт класс Model. Аргументами конструктора являются среда и FeatureTransformer для позднейшего использования. Кроме того, в конструкторе находится наша инициализация Q-таблицы. Её форма – количество состояний x количество действий, поскольку Q индексируется по состояниям и действиям. Количество состояний равно 104, поскольку существует 10 ячеек для каждой из 4 переменных состояния. Количество же действий просто равно 2.

class Model:

  def __init__(self, env, feature_transformer):

    self.env = env

    self.feature_transformer = feature_transformer

    num_states = 10**env.observation_space.shape[0]

    num_actions = env.action_space.n

    self.Q = np.random.uniform(low=-1, high=1, size=(num_states, num_actions))

Далее идёт функция predict. Она преобразует состояние в целое число x, которое затем используется для индексирования Q. Обратите внимание, что поскольку Q является двухмерным массивом, то если мы проиндексируем его одним целым числом, то обратно получим одномерный массив. Поэтому здесь мы получаем Q для данного конкретного состояния, но по всем действиям. Это хорошо для Q-обучения, поскольку нам нужно найти максимум.

  def predict(self, s):

    x = self.feature_transformer.transform(s)

    return self.Q[x]

Следующей идёт функция update. Её аргументами являются состояние, действие и целевая отдача. Вначале мы преобразуем состояние в целое число, а затем обновляем Q, используя градиентный спуск:

  def update(self, s, a, G):

    x = self.feature_transformer.transform(s)

    self.Q[x,a] += 10e-3*(G – self.Q[x,a])

Затем у нас идёт функция sample_action. Она реализует эпсилон-жадный алгоритм, то есть с некоторой малой вероятностью ε мы выбираем случайное действие, в противном случае – выбираем лучшее возможное действие, используя нашу текущую оценку Q:

  def sample_action(self, s, eps):

    if np.random.random() < eps:

      return self.env.action_space.sample()

    else:

      p = self.predict(s)

      return np.argmax(p)

Далее идёт функция play_one. Она принимает в качестве аргументов экземпляр model, переменную eps для эпсилон-жадного алгоритма и переменную gamma, являющуюся коэффициентом обесценивания. Сначала мы сбрасываем среду, чтобы можно было перейти к начальному состоянию и инициировать некоторые переменные, а затем входим в цикл. Вновь-таки, он ограничен 10 000 раз, однако если вы используете более новую версию Gym, то автоматически получите сигнал done после 200 единиц времени. Внутри цикла мы выбираем действие из model, присваиваем переменной prev_observation текущее наблюдение и выполняем действие, после чего добавляем вознаграждение к общему вознаграждению. Если мы получили сигнал done, это значит, что шест упал или что мы достигли предела (в более новых версиях Gym). Если предел в 200 единиц времени не достигнут, то мы устанавливаем большое отрицательное вознаграждение -300. После этого обновляем model, используя уравнение Q-обучения и целевую отдачу G. Когда цикл заканчивается, мы возвращаем общее вознаграждение.

def play_one(model, eps, gamma):

  observation = env.reset()

  done = False

  totalreward = 0

  iters = 0

  while not done and iters < 10000:

    action = model.sample_action(observation, eps)

    prev_observation = observation

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

    totalreward += reward

    if done and iters < 199:

      reward = -300

    # update the model

    G = reward + gamma*np.max(model.predict(observation))

    model.update(prev_observation, action, G)

    iters += 1

  return totalreward

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

def plot_running_avg(totalrewards):

  N = len(totalrewards)

  running_avg = np.empty(N)

  for t in range(N):

    running_avg[t] = totalrewards[max(0, t-100):(t+1)].mean()

  plt.plot(running_avg)

  plt.title(“Running Average”)

  plt.show()

И наконец мы переходим к разделу main. Прежде всего мы инициируем среду env, FeatureTransformer и gamma. Затем у нас идёт возможность настроить monitor. Всё, что нужно, чтобы заставить его работать, – это подставить строку monitor в виде аргумента командной строки. Далее мы играем 10 000 эпизодов. Значение ε устанавливаем равным , чтобы оно не снижалось чересчур быстро. Мы отыгрываем эпизод, отмечаем общее вознаграждение и выводим наш прогресс через каждые 100 этапов. После того, как цикл закончится, мы выводим на экран наше среднее вознаграждение за последние 100 этапов и общее число этапов. Затем выводятся все общие вознаграждения. Этот график будет очень изменчивым, поэтому будет трудно увидеть, насколько хорошо в среднем действует наш агент. В связи с этим следующим выводится скользящее среднее:

if __name__ == ‘__main__’:

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

  ft = FeatureTransformer()

  model = Model(env, ft)

  gamma = 0.9

  if ‘monitor’ in sys.argv:

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

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

    env = wrappers.Monitor(env, monitor_dir)

  N = 10000

  totalrewards = np.empty(N)

  for n in range(N):

    eps = 1.0/np.sqrt(n+1)

    totalreward = play_one(model, eps, gamma)

    totalrewards[n] = totalreward

    if n % 100 == 0:

      print(“episode:”, n, “total reward:”, totalreward, “eps:”, eps)

  print(“avg reward for last 100 episodes:”, totalrewards[-100:].mean())

  print(“total steps:”, totalrewards.sum())

  plt.plot(totalrewards)

  plt.title(“Rewards”)

  plt.show()

  plot_running_avg(totalrewards)

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

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

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: