Глубокое Q-обучение

Введение в глубокое Q-обучение

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

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

Однако вначале обсудим ряд практических вопросов. Следует отметить, что на самом деле обучение нашего агента будет занимать очень много времени. Почему так? Прежде всего рассмотрим размер входных данных, то есть пространство состояний. Тележка с шестом имеет четыре входящих величины, а машина на склоне – две. Если же вы взглянете на пространство состояний игры Breakout от Atari, то увидите, что это поле размером 210x160x3. Предположим, мы уберём 3, обратив наш экран в чёрно-белый. Теперь у нас поле «лишь» 210×160. При этом если вы знакомы с типичными разрешениями мониторов, то для вас интуитивно очевидно, что 210×160 – это экран очень скромных размеров, намного меньше, чем в типичной видеоигре. В то же время 210*160 равно 33 600, так что в нейронной сети мы переходим от 4 входных величин к 33 тысячам. Ввиду этого обучение займёт гораздо больше времени, поскольку схемы, которые необходимо изучить, куда сложнее. Так что приготовьтесь потратить несколько недель для одного лишь агента и в одной игре, если вы используете только свой центральный процессор.

Для некоторых моделей, даже с использованием GPU, обучение займёт неделю и более. Давайте сделаем быстрый подсчёт. Предположим, мы используем p2.xlarge, работа которого стоит 90 центов в час. Сколько часов в неделе? 24*7, то есть 168 часов. Умножьте это на 90 центов и получите более 150 долларов, и это лишь один пример обучения. Мне, как правило, нравится проверять различные настройки гиперпараметров, так что если мы захотим проверить 10 различных настроек гиперпараметров, то получим 1 500 долларов. Само собой разумеется, что это нереально для большинства слушателей этого курса.

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

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

Методы глубокого Q-обучения

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

Начнём с того, что вы, возможно, слышали термин «глубокая Q-сеть» и задавались вопросом, чем она отличается от обычной глубокой нейронной сети. В действительности – ничем не отличается от всего виденного ранее. На самом деле это просто обычная нейронная сеть, используемая для Q-обучения или, другими словами, для приближённого представления функции ценности действий. Но есть одна важная тонкость, с которой мы уже встречались в нашей реализации приближения функции для Q и которая состоит в том, что вместо того, чтобы преобразовывать пару состояние-действие в признак, мы преобразуем лишь состояние во входной признак и имеем несколько выходных узлов, каждый из которых представляет ценность различных действий. Это и есть глубокая Q-сеть или, сокращённо, DQN.

Следующий приём, который мы используем, называется воспроизведением опыта. Ранее я уже упоминал о нём несколько раз. Так, метод Монте-Карло можно рассматривать как крошечный шаг в воспроизведении опыта, поскольку когда мы используем этот метод, мы получаем набор кортежей из состояний, действий и отдач, которые затем одновременно используются для обучения приближённого представления функции после окончания эпизода. При воспроизведении опыта мы пойдём дальше и познакомимся с так называемым буфером воспроизведения. Размер буфера выбирается программистом и рассматривается как гиперпараметр. В буфере воспроизведения мы храним 4-кортежи из состояния, действия, вознаграждения и следующего состояния. Для обучения глубокой Q-сети мы случайным образом выбираем мини-пакет из буфера воспроизведения и используем его в качестве обучающих данных. Буфер в основном действует в порядке очереди, на жаргоне компьютерщиков это значит «первым пришёл – первым ушёл», а потому буфер всегда содержит самые свежие 4-кортежи.

Благодаря взятию случайных выборок из прошлого опыта мы получаем лучшее представление истинного распределения данных. Это лучше чем используемое по умолчанию обновление Q, где могут быть скрытые шаблоны и корреляции, способные повлиять на обучение нейронной сети. Представьте, что мы пытаемся классифицировать изображения животных, однако в учебных данных вместо равномерного распределения всех животных в первом пакете идут одни лишь коты, во втором пакете – одни лишь собаки и так далее. Работать это будет так себе.

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

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

Решением является введение ещё одной глубокой Q-сети, которая называется целевой сетью и отвечающая, как вы можете догадаться, за создание целевой переменной для метода временных разниц. Целевая сеть, по сути, является копией «главной» Q-сети, но обновляется не так часто. К примеру, можно сделать целевую сеть константой для 100 временных шагов, продолжая обновлять глубокую Q-сеть, и только через каждые 100 шагов копировать параметры главной Q-сети в целевую. Это помогает стабилизировать целевые переменные.

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

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

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

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

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

Глубокое Q-обучение для тележки с шестом в Tensorflow

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

Вначале идут все наши обычные импорты, после которых идёт класс HiddenLayer:

# ***

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_bins import plot_running_avg

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

# a version of HiddenLayer that keeps track of params

class HiddenLayer:

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

    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)

Далее идёт класс DQN. Аргументами конструктора являются количество входных данных D, количество выходных действий K, размеры скрытых слоёв, значение γ, размер буфера воспроизведения max_experiences, количество опытов для сбора перед обучением min_experiences и размер пакета batch_sz – это количество примеров, которое будет использоваться для обучения. Следующий этап – создание всех слоёв.

class DQN:

  def __init__(self, D, K, hidden_layer_sizes, gamma, max_experiences=10000, min_experiences=100, batch_sz=32):

    self.K = K

    # create the graph

    self.layers = []

    M1 = D

    for M2 in hidden_layer_sizes:

      layer = HiddenLayer(M1, M2)

      self.layers.append(layer)

      M1 = M2

    # final layer

    layer = HiddenLayer(M1, K, lambda x: x)

    self.layers.append(layer)

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

    # collect params for copy

    self.params = []

    for layer in self.layers:

      self.params += layer.params

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

    # inputs and targets

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

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

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

    # calculate output and cost

    Z = self.X

    for layer in self.layers:

      Z = layer.forward(Z)

    Y_hat = Z

    self.predict_op = Y_hat

    selected_action_values = tf.reduce_sum(

      Y_hat * tf.one_hot(self.actions, K),

      reduction_indices=[1]

    )

    cost = tf.reduce_sum(tf.square(self.G – selected_action_values))

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

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

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

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

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

    # create replay memory

    self.experience = {‘s’: [], ‘a’: [], ‘r’: [], ‘s2’: [], ‘done’: []}

    self.max_experiences = max_experiences

    self.min_experiences = min_experiences

    self.batch_sz = batch_sz

    self.gamma = gamma

Следующей идёт функция set_session, которую вы уже видели и знаете, для чего она предназначена.

  def set_session(self, session):

    self.session = session

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

  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)

Далее идёт функция predict, которую вы уже видели ранее:

  def predict(self, X):

    X = np.atleast_2d(X)

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

А следом идёт функция train – её вы ранее не видели. Прежде всего, пока мы не собрали достаточно опытных образцов, мы ничего не делаем. В противном случае мы выбираем набор случайных индексов, равный размеру пакета, далее индексируем словари опыта, чтобы получить состояния, действия, вознаграждения и следующие состояния. Затем эти следующие состояния используются для вычисления целевых переменных Q. Обратите внимание на использование здесь целевой сети. Это же объясняет, зачем нам понадобилось ранее вставлять значение γ. И наконец, мы вызываем train_op с этими данными. В одной из последующих лекций вы увидите более компактный способ выборки пакетов, однако более сложный для разбора.

  def train(self, target_network):

    # sample a random batch from buffer, do an iteration of GD

    if len(self.experience[‘s’]) < self.min_experiences:

      # don’t do anything if we don’t have enough experience

      return

    # randomly select a batch

    idx = np.random.choice(len(self.experience[‘s’]), size=self.batch_sz, replace=False)

    # print(“idx:”, idx)

    states = [self.experience[‘s’][i] for i in idx]

    actions = [self.experience[‘a’][i] for i in idx]

    rewards = [self.experience[‘r’][i] for i in idx]

    next_states = [self.experience[‘s2’][i] for i in idx]

    dones = [self.experience[‘done’][i] for i in idx]

    next_Q = np.max(target_network.predict(next_states), axis=1)

    targets = [r + self.gamma*next_q if not done else r for r, next_q, done in zip(rewards, next_Q, dones)]

    # call optimizer

    self.session.run(

      self.train_op,

      feed_dict={

        self.X: states,

        self.G: targets,

        self.actions: actions

      }

    )

Затем идёт функция add_experience. Она просто добавляет 4-кортеж из состояния, действия, вознаграждения и следующего состояния в список опытов:

  def add_experience(self, s, a, r, s2, done):

    if len(self.experience[‘s’]) >= self.max_experiences:

      self.experience[‘s’].pop(0)

      self.experience[‘a’].pop(0)

      self.experience[‘r’].pop(0)

      self.experience[‘s2’].pop(0)

      self.experience[‘done’].pop(0)

    self.experience[‘s’].append(s)

    self.experience[‘a’].append(a)

    self.experience[‘r’].append(r)

    self.experience[‘s2’].append(s2)

    self.experience[‘done’].append(done)

И наконец идёт функция sample_action, такая же, что и раньше:

  def sample_action(self, x, eps):

    if np.random.random() < eps:

      return np.random.choice(self.K)

    else:

      X = np.atleast_2d(x)

      return np.argmax(self.predict(X)[0])

После неё идёт функция play_one. Она почти такая же, как и прежде, разве что с небольшими изменениями. Так, в той части кода, где мы обновляем модель, мы также добавляем add_experience, а функция train теперь в качестве аргумента принимает только целевую сеть, так как все данные хранятся в её буфере опыта. Кроме того, мы используем аргумент copy_period, чтобы указать, насколько часто надо копировать главную сеть в целевую:

def play_one(env, model, tmodel, eps, gamma, copy_period):

  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 = model.sample_action(observation, eps)

    prev_observation = observation

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

    totalreward += reward

    if done:

      reward = -200

    # update the model

    model.add_experience(prev_observation, action, reward, observation, done)

    model.train(tmodel)

    iters += 1

    if iters % copy_period == 0:

      tmodel.copy_from(model)

  return totalreward

Затем идёт раздел main, в значительной мере тот же, что и был прежде, за исключением, конечно же,  того, что теперь мы создаём класс DQN, имеющий другие параметры:

def main():

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

  gamma = 0.99

  copy_period = 50

  D = len(env.observation_space.sample())

  K = env.action_space.n

  sizes = [200,200]

  model = DQN(D, K, sizes, gamma)

  tmodel = DQN(D, K, sizes, gamma)

  init = tf.global_variables_initializer()

  session = tf.InteractiveSession()

  session.run(init)

  model.set_session(session)

  tmodel.set_session(session)

  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 = 500

  totalrewards = np.empty(N)

  costs = np.empty(N)

  for n in range(N):

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

    totalreward = play_one(env, model, tmodel, eps, gamma, copy_period)

    totalrewards[n] = totalreward

    if n % 100 == 0:

      print(“episode:”, n, “total reward:”, totalreward, “eps:”, eps, “avg reward (last 100):”, totalrewards[max(0, n-100):(n+1)].mean())

  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)

if __name__ == ‘__main__’:

  main()

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

Глубокое Q-обучение для тележки с шестом в Theano

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

Вначале идут все наши обычные импорты, после которых идёт класс HiddenLayer, в значительной степени тот же, что и был ранее:

# ***

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_bins import plot_running_avg

# a version of HiddenLayer that keeps track of params

class HiddenLayer:

  def __init__(self, M1, M2, f=T.tanh, use_bias=True):

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

    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)

Далее идёт класс DQN. Аргументами конструктора являются количество входных данных D, количество выходных действий K, размеры скрытых слоёв, значение γ, размер буфера воспроизведения max_experiences, количество опытов для сбора перед обучением min_experiences и размер пакета batch_sz – это количество примеров, которое будет использоваться для обучения.

Прежде всего мы устанавливаем ряд гиперпараметров оптимизатора и приглашаю вас поиграть с ними.

class DQN:

  def __init__(self, D, K, hidden_layer_sizes, gamma, max_experiences=10000, min_experiences=100, batch_sz=32):

    self.K = K

    lr = 10e-3

    mu = 0.

    decay = 0.99

Следующий этап – создание всех слоёв.

    # create the graph

    self.layers = []

    M1 = D

    for M2 in hidden_layer_sizes:

      layer = HiddenLayer(M1, M2)

      self.layers.append(layer)

      M1 = M2

    # final layer

    layer = HiddenLayer(M1, K, lambda x: x)

    self.layers.append(layer)

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

    # collect params for copy

    self.params = []

    for layer in self.layers:

      self.params += layer.params

Затем мы объявляем входные и целевые переменные Theano:

    # inputs and targets

    X = T.matrix(‘X’)

    G = T.vector(‘G’)

    actions = T.ivector(‘actions’)

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

    # calculate output and cost

    Z = X

    for layer in self.layers:

      Z = layer.forward(Z)

    Y_hat = Z

    selected_action_values = Y_hat[T.arange(actions.shape[0]), actions]

    cost = T.sum((G – selected_action_values)**2)

    # create train function

    updates = adam(cost, self.params)

    # compile functions

    self.train_op = theano.function(

      inputs=[X, G, actions],

      updates=updates,

      allow_input_downcast=True

    )

    self.predict_op = theano.function(

      inputs=[X],

      outputs=Y_hat,

      allow_input_downcast=True

    )

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

    # create replay memory

    self.experience = {‘s’: [], ‘a’: [], ‘r’: [], ‘s2’: [], ‘done’: []}

    self.max_experiences = max_experiences

    self.min_experiences = min_experiences

    self.batch_sz = batch_sz

    self.gamma = gamma

Затем идёт функция copy_from. Она будет копировать все параметры из входной сети в саму себя. Именно поэтому нам нужно было ранее отслеживать все параметры – потому что теперь мы должны их перебрать, получить их значения и присвоить их соответствующим параметрам в другой сети. К счастью, в Theano есть удобные функции get_value и set_value, которые помогают нам всё это сделать.

  def copy_from(self, other):

    my_params = self.params

    other_params = other.params

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

      actual = q.get_value()

      p.set_value(actual)

Следом идёт функция predict, которую вы уже видели ранее:

  def predict(self, X):

    X = np.atleast_2d(X)

    return self.predict_op(X)

А за ней идёт функция train – её вы ранее не видели. Прежде всего, пока мы не собрали достаточно опытных образцов, мы ничего не делаем. В противном случае мы выбираем набор случайных индексов, равный размеру пакета, далее индексируем словари опыта, чтобы получить состояния, действия, вознаграждения и следующие состояния. Затем эти следующие состояния используются для вычисления целевых переменных Q. Обратите внимание на использование здесь целевой сети. Это же объясняет, зачем нам понадобилось ранее вставлять значение γ. И наконец, мы вызываем train_op с этими данными. В одной из последующих лекций вы увидите более компактный способ выборки пакетов, однако более сложный для разбора.

  def train(self, target_network):

    # sample a random batch from buffer, do an iteration of GD

    if len(self.experience[‘s’]) < self.min_experiences:

      # don’t do anything if we don’t have enough experience

      return

    # randomly select a batch

    idx = np.random.choice(len(self.experience[‘s’]), size=self.batch_sz, replace=False)

    # print(“idx:”, idx)

    states = [self.experience[‘s’][i] for i in idx]

    actions = [self.experience[‘a’][i] for i in idx]

    rewards = [self.experience[‘r’][i] for i in idx]

    next_states = [self.experience[‘s2’][i] for i in idx]

    dones = [self.experience[‘done’][i] for i in idx]

    next_Q = np.max(target_network.predict(next_states), axis=1)

    targets = [r + self.gamma*next_q if not done else r for r, next_q, done in zip(rewards, next_Q, dones)]

    # call optimizer

    self.train_op(states, targets, actions)

Затем идёт функция add_experience. Она просто добавляет 4-кортеж из состояния, действия, вознаграждения и следующего состояния в список опытов:

  def add_experience(self, s, a, r, s2, done):

    if len(self.experience[‘s’]) >= self.max_experiences:

      self.experience[‘s’].pop(0)

      self.experience[‘a’].pop(0)

      self.experience[‘r’].pop(0)

      self.experience[‘s2’].pop(0)

      self.experience[‘done’].pop(0)

    self.experience[‘s’].append(s)

    self.experience[‘a’].append(a)

    self.experience[‘r’].append(r)

    self.experience[‘s2’].append(s2)

    self.experience[‘done’].append(done)

И наконец идёт функция sample_action, такая же, что и раньше:

  def sample_action(self, x, eps):

    if np.random.random() < eps:

      return np.random.choice(self.K)

    else:

      X = np.atleast_2d(x)

      return np.argmax(self.predict(X)[0])

После неё идёт функция play_one. Она почти такая же, как и прежде, разве что с небольшими изменениями. Так, в той части кода, где мы обновляем модель, мы также добавляем add_experience, а функция train теперь в качестве аргумента принимает только целевую сеть, так как все данные хранятся в её буфере опыта. Кроме того, мы используем аргумент copy_period, чтобы указать, насколько часто надо копировать главную сеть в целевую:

def play_one(env, model, tmodel, eps, gamma, copy_period):

  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 = model.sample_action(observation, eps)

    prev_observation = observation

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

    totalreward += reward

    if done:

      reward = -200

    # update the model

    model.add_experience(prev_observation, action, reward, observation, done)

    model.train(tmodel)

    iters += 1

    if iters % copy_period == 0:

      tmodel.copy_from(model)

  return totalreward

Затем идёт раздел main, в значительной мере тот же, что и был прежде, за исключением, конечно же,  того, что теперь мы создаём класс DQN, имеющий другие параметры:

def main():

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

  gamma = 0.99

  copy_period = 50

  D = len(env.observation_space.sample())

  K = env.action_space.n

  sizes = [200,200]

  model = DQN(D, K, sizes, gamma)

  tmodel = DQN(D, K, sizes, gamma)

  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 = 500

  totalrewards = np.empty(N)

  costs = np.empty(N)

  for n in range(N):

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

    totalreward = play_one(env, model, tmodel, eps, gamma, copy_period)

    totalrewards[n] = totalreward

    if n % 100 == 0:

      print(“episode:”, n, “total reward:”, totalreward, “eps:”, eps, “avg reward (last 100):”, totalrewards[max(0, n-100):(n+1)].mean())

  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)

if __name__ == ‘__main__’:

  main()

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

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

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