A3C – продолжение. Итоги курса!

A3C – код, часть 3

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

В данной заключительной лекции мы продолжим изучение алгоритма АЗС, а в самом конце подведем итоги курса! Рассмотрим код файла nets.py. Этот код относительно прост по сравнению с другими файлами нашего примера, поскольку он включает в себя только построение нейронных сетей, а мы уже знаем, как это делается.

Наиболее сложное здесь – заставить сеть стратегии и сеть ценности иметь общие параметры. Поэтому первая наша функция посвящена построению «тела» нейронной сети и называется build_feature_extractor. Вы вряд ли будете в состоянии это утверждать, просто взглянув на код, однако функция позволяет двум сетям совместно использовать данные слои.

Это будет обычная свёрточная нейронная сеть. Первым делом мы отмасштабируем входные данные, чтобы они были в диапазоне между 0 и 1 путём деления на 255. Затем мы пропускаем их через два свёрточных слоя, сглаживаем данные и пропускаем их через один полносвязный, или плотный, слой – в целом довольно простая базовая архитектура.

import tensorflow as tf

def build_feature_extractor(input_):

  # We only want to create the weights once

  # In all future calls we should set reuse = True

  # scale the inputs from 0..255 to 0..1

  input_ = tf.to_float(input_) / 255.0

  # conv layers

  conv1 = tf.contrib.layers.conv2d(

    input_,

    16, # num output feature maps

    8,  # kernel size

    4,  # stride

    activation_fn=tf.nn.relu,

    scope=”conv1″)

  conv2 = tf.contrib.layers.conv2d(

    conv1,

    32, # num output feature maps

    4,  # kernel size

    2,  # stride

    activation_fn=tf.nn.relu,

    scope=”conv2″)

  # image -> feature vector

  flat = tf.contrib.layers.flatten(conv2)

  # dense layer

  fc1 = tf.contrib.layers.fully_connected(

    inputs=flat,

    num_outputs=256,

    scope=”fc1″)

  return fc1

Далее идёт класс PolicyNetwork. В конструкторе аргументами идут количество действий в виде количества выходов num_outputs и константа регуляризации.

class PolicyNetwork:

  def __init__(self, num_outputs, reg=0.01):

    self.num_outputs = num_outputs

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

    # Graph inputs

    # After resizing we have 4 consecutive frames of size 84 x 84

    self.states = tf.placeholder(shape=[None, 84, 84, 4], dtype=tf.uint8, name=”X”)

    # Advantage = G – V(s)

    self.advantage = tf.placeholder(shape=[None], dtype=tf.float32, name=”y”)

    # Selected actions

    self.actions = tf.placeholder(shape=[None], dtype=tf.int32, name=”actions”)

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

    # Since we set reuse=False here, that means we MUST

    # create the PolicyNetwork before creating the ValueNetwork

    # ValueNetwork will use reuse=True

    with tf.variable_scope(“shared”, reuse=False):

      fc1 = build_feature_extractor(self.states)

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

    # Use a separate scope for output and loss

    with tf.variable_scope(“policy_network”):

      self.logits = tf.contrib.layers.fully_connected(fc1, num_outputs, activation_fn=None)

      self.probs = tf.nn.softmax(self.logits)

      # Sample an action

      cdist = tf.distributions.Categorical(logits=self.logits)

      self.sample_action = cdist.sample()

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

      # Add regularization to increase exploration

      self.entropy = -tf.reduce_sum(self.probs * tf.log(self.probs), axis=1)

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

      # Get the predictions for the chosen actions only

      batch_size = tf.shape(self.states)[0]

      gather_indices = tf.range(batch_size) * tf.shape(self.probs)[1] + self.actions

      self.selected_action_probs = tf.gather(tf.reshape(self.probs, [-1]), gather_indices)

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

      self.loss = tf.log(self.selected_action_probs) * self.advantage + reg * self.entropy

      self.loss = -tf.reduce_sum(self.loss, name=”loss”)

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

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

      # training

      self.optimizer = tf.train.RMSPropOptimizer(0.00025, 0.99, 0.0, 1e-6)

      # we’ll need these later for running gradient descent steps

      self.grads_and_vars = self.optimizer.compute_gradients(self.loss)

      self.grads_and_vars = [[grad, var] for grad, var in self.grads_and_vars if grad is not None]

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

class ValueNetwork:

  def __init__(self):

    # Placeholders for our input

    # After resizing we have 4 consecutive frames of size 84 x 84

    self.states = tf.placeholder(shape=[None, 84, 84, 4], dtype=tf.uint8, name=”X”)

    # The TD target value

    self.targets = tf.placeholder(shape=[None], dtype=tf.float32, name=”y”)

    # Since we set reuse=True here, that means we MUST

    # create the PolicyNetwork before creating the ValueNetwork

    # PolictyNetwork will use reuse=False

    with tf.variable_scope(“shared”, reuse=True):

      fc1 = build_feature_extractor(self.states)

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

    # Use a separate scope for output and loss

    with tf.variable_scope(“value_network”):

      self.vhat = tf.contrib.layers.fully_connected(

        inputs=fc1,

        num_outputs=1,

        activation_fn=None)

      self.vhat = tf.squeeze(self.vhat, squeeze_dims=[1], name=”vhat”)

После этого мы вычисляем потери – это просто квадрат ошибки между vhat и целевой переменной.

      self.loss = tf.squared_difference(self.vhat, self.targets)

      self.loss = tf.reduce_sum(self.loss, name=”loss”)

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

      # training

      self.optimizer = tf.train.RMSPropOptimizer(0.00025, 0.99, 0.0, 1e-6)

      # we’ll need these later for running gradient descent steps

      self.grads_and_vars = self.optimizer.compute_gradients(self.loss)

      self.grads_and_vars = [[grad, var] for grad, var in self.grads_and_vars if grad is not None]

И последняя часть этого файла – функция create_networks. Суть такой структуры заключается в том, что для создания сетей стратегии и ценности остальные файла Python должны использовать только эту функцию и никогда не создавать сеть стратегии или сеть ценности по отдельности. Это связано с тем обстоятельством, что сеть стратегии всегда должна создаваться первой, поскольку в ней аргумент reuse устанавливается в значение «ложь», тогда как в сеть ценности reuse имеет значение «истина». Ввиду этого, чтобы избежать двусмысленности, любой внешний код, желающий создать сети стратегии и ценности, должен воспользоваться этой функцией.

# Should use this to create networks

# to ensure they’re created in the correct order

def create_networks(num_outputs):

  policy_network = PolicyNetwork(num_outputs=num_outputs)

  value_network = ValueNetwork()

  return policy_network, value_network

В следующей лекции мы рассмотрим последний файл кода этого примера – worker.py.

A3C – код, часть 4

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

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

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

Вначале у нас идут все обычные импорты библиотек, а также импорт функции create_networks из файла nets.py.

import gym

import sys

import os

import numpy as np

import tensorflow as tf

from nets import create_networks

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

class Step:

  def __init__(self, state, action, reward, next_state, done):

    self.state = state

    self.action = action

    self.reward = reward

    self.next_state = next_state

    self.done = done

Далее идёт класс ImageTransformer. В конструкторе мы определяем операции Tensorflow, которые необходимо выполнить для преобразования изображения. Первый этап – преобразовать изображение в чёрно-белое при помощи функции rgb_to_grayscale. Затем мы обрезаем изображение, чтобы убрать ненужные части, и, наконец, изменяем размер изображения до 84×84 и сжимаем его, чтобы избавиться от избыточных размерностей.

# Transform raw images for input into neural network

# 1) Convert to grayscale

# 2) Resize

# 3) Crop

class ImageTransformer:

  def __init__(self):

    with tf.variable_scope(“image_transformer”):

      self.input_state = tf.placeholder(shape=[210, 160, 3], dtype=tf.uint8)

      self.output = tf.image.rgb_to_grayscale(self.input_state)

      self.output = tf.image.crop_to_bounding_box(self.output, 34, 0, 160, 160)

      self.output = tf.image.resize_images(

        self.output,

        [84, 84],

        method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)

      self.output = tf.squeeze(self.output)

В функции transform мы запускаем вышеописанные операции и возвращаем массив Numpy:

  def transform(self, state, sess=None):

    sess = sess or tf.get_default_session()

    return sess.run(self.output, { self.input_state: state })

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

# Create initial state by repeating the same frame 4 times

def repeat_frame(frame):

  return np.stack([frame] * 4, axis=2)

Следующая функция – shift_frames – принимает состояние со следующим кадром для построения следующего состояния. По сути она лишь удаляет старый кадр из состояния, а затем присоединяет к оставшимся следующий кадр:

# Create next state by shifting each frame by 1

# Throw out the oldest frame

# And concatenate the newest frame

def shift_frames(state, next_frame):

  return np.append(state[:,:,1:], np.expand_dims(next_frame, 2), axis=2)

Следующей идёт функция get_copy_params_op. Когда мы копируем весовые коэффициенты из одной сети в другую, мы можем выразить это в понятиях операций Tensorflow, что данная функция и делает. Мы берём список из двух наборов переменных – исходных переменных src_vars  и переменных назначения dst_vars, а чтобы гарантировать, что они будут появляться в правильном порядке, сортируем их по имени. После этого мы используем функцию assign библиотеки Tensorflow, для представления каждой операции копирования. В конце мы возвращаем список этих операций.

# Make a Tensorflow op to copy weights from one scope to another

def get_copy_params_op(src_vars, dst_vars):

  src_vars = list(sorted(src_vars, key=lambda v: v.name))

  dst_vars = list(sorted(dst_vars, key=lambda v: v.name))

  ops = []

  for s, d in zip(src_vars, dst_vars):

    op = d.assign(s)

    ops.append(op)

  return ops

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

Итак, данная функция в качестве аргументов принимает глобальную и локальную сети. Первым делом мы создаём список локальных градиентов. После этого мы обрезаем их, чтобы максимальной нормой было 5, а затем получаем список переменных, которые поступают из глобальной сети, и объединяем список переменных и соответствующих градиентов в единый список local_grads_global_vars. Теперь данные имеют правильный формат для передачи их в функцию, которая в действительности обновляет весовые коэффициенты, поэтому мы вызываем функцию apply_gradients, которая относится к оптимизатору глобальных сетей, и передаём ей только что созданный список, а также переменную global_step, определённую, если помните, ранее.

def make_train_op(local_net, global_net):

  “””

  Use gradients from local network to update the global network

  “””

  # Idea:

  # We want a list of gradients and corresponding variables

  # e.g. [[g1, g2, g3], [v1, v2, v3]]

  # Since that’s what the optimizer expects.

  # But we would like the gradients to come from the local network

  # And the variables to come from the global network

  # So we want to make a list like this:

  # [[local_g1, local_g2, local_g3], [global_v1, global_v2, global_v3]]

  # First get only the gradients

  local_grads, _ = zip(*local_net.grads_and_vars)

  # Clip gradients to avoid large values

  local_grads, _ = tf.clip_by_global_norm(local_grads, 5.0)

  # Get global vars

  _, global_vars = zip(*global_net.grads_and_vars)

  # Combine local grads and global vars

  local_grads_global_vars = list(zip(local_grads, global_vars))

  # Run a gradient descent step, e.g.

  # var = var – learning_rate * grad

  return global_net.optimizer.apply_gradients(

    local_grads_global_vars,

    global_step=tf.train.get_global_step())

Затем идёт класс Worker. Как уже обсуждалось ранее в файле main.py, он принимает следующие аргументы: name – своё имя, env – объект среды, policy_net и value_net – глобальные сети стратегии и ценности соответственно, global_counter – глобальный счётчик, returns_list – список отдач, discount_factor – коэффициент обесценивания, и max_global_steps – максимальное количество глобальных этапов, которые надо выполнить.

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

# Worker object to be run in a thread

# name (String) should be unique for each thread

# env (OpenAI Gym Environment) should be unique for each thread

# policy_net (PolicyNetwork) should be a global passed to every worker

# value_net (ValueNetwork) should be a global passed to every worker

# returns_list (List) should be a global passed to every worker

class Worker:

  def __init__(

      self,

      name,

      env,

      policy_net,

      value_net,

      global_counter,

      returns_list,

      discount_factor=0.99,

      max_global_steps=None):

    self.name = name

    self.env = env

    self.global_policy_net = policy_net

    self.global_value_net = value_net

    self.global_counter = global_counter

    self.discount_factor = discount_factor

    self.max_global_steps = max_global_steps

    self.global_step = tf.train.get_global_step()

    self.img_transformer = ImageTransformer()

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

    # Create local policy and value networks that belong only to this worker

    with tf.variable_scope(name):

      # self.policy_net = PolicyNetwork(num_outputs=policy_net.num_outputs)

      # self.value_net = ValueNetwork()

      self.policy_net, self.value_net = create_networks(policy_net.num_outputs)

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

    # We will use this op to copy the global network weights

    # back to the local policy and value networks

    self.copy_params_op = get_copy_params_op(

      tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=”global”),

      tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=self.name+’/’))

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

    # These will take the gradients from the local networks

    # and use those gradients to update the global network

    self.vnet_train_op = make_train_op(self.value_net, self.global_value_net)

    self.pnet_train_op = make_train_op(self.policy_net, self.global_policy_net)

И наконец у нас есть атрибуты state, который отслеживает текущее состояние, total_reward, отслеживающий общее вознаграждение, и returns_list, который просто ссылается на глобальную переменную returns_list.

    self.state = None # Keep track of the current state

    self.total_reward = 0. # After each episode print the total (sum of) reward

    self.returns_list = returns_list # Global returns list to plot later

После этого идёт функция run, которая, если помните, должна запускаться в цепочке задач. Как и следовало ожидать, это просто цикл. Итак, вначале мы сбрасываем среду, получаем первый кадр, преобразовываем его и повторяем его же четыре раза, чтобы получить первое состояние. После этого мы входим в цикл. Как видите, мы используем ранее созданный координатор для контроля остановки цикла. В самом цикле мы первым делом копируем весовые коэффициенты из глобальных сетей в локальные, поэтому наш работник всегда будет работать с относительно свежей копией весовых коэффициентов мастер-сетей. Затем мы запускаем N этапов среды, вызывая для этого функцию run_n_steps, которую мы вскоре рассмотрим. Эта функция возвращает две величины: список объект объектов этапов steps, который, если помните, просто содержит только информацию о состояниях, действиях, вознаграждениях и так далее, а также счётчик global_step. После этого мы проверяем, не является ли счётчик run_n_steps больше или равным количеству максимальных этапов, которые мы хотим пройти, и если да, то вызываем запрос координатору stop, после чего возвращаемся для выхода из функции run. Кстати, благодаря этому все остальные работающие параллельно работники также узнают, когда завершать работу. Далее идёт функция update, которая передаёт собранные нами на протяжении N этапов данные, находит градиент и обновляет глобальную сеть. Это и есть основной цикл, который будет работать в каждой цепочке задач.

  def run(self, sess, coord, t_max):

    with sess.as_default(), sess.graph.as_default():

      # Assign the initial state

      self.state = repeat_frame(self.img_transformer.transform(self.env.reset()))

      try:

        while not coord.should_stop():

          # Copy weights from  global networks to local networks

          sess.run(self.copy_params_op)

          # Collect some experience

          steps, global_step = self.run_n_steps(t_max, sess)

          # Stop once the max number of global steps has been reached

          if self.max_global_steps is not None and global_step >= self.max_global_steps:

            coord.request_stop()

            return

          # Update the global networks using local gradients

          self.update(steps, sess)

      except tf.errors.CancelledError:

        return

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

  def sample_action(self, state, sess):

    # Make input N x D (N = 1)

    feed_dict = { self.policy_net.states: [state] }

    actions = sess.run(self.policy_net.sample_action, feed_dict)

    # Prediction is a 1-D array of length N, just want the first value

    return actions[0]

Далее идёт функция get_value_prediction. Аналогично предыдущей функции, которая получает прогноз из сети стратегии, эта, конечно же, получает прогноз из сети ценности. Как и прежде, мы работаем с размером пакета, равным 1.

  def get_value_prediction(self, state, sess):

    # Make input N x D (N = 1)

    feed_dict = { self.value_net.states: [state] }

    vhat = sess.run(self.value_net.vhat, feed_dict)

    # Prediction is a 1-D array of length N, just want the first value

    return vhat[0]

Следующей идёт функция run_n_steps, использование которой мы видели ранее. Как понятно из названия, эта функция запускает N этапов среды, разве что с одним исключением: если эпизод заканчивается, значение функции возвращается раньше. Вначале нам необходимо инициировать пустой список этапов, а затем выполнить цикл for N раз. В самом цикле for мы используем текущее состояние, передаём его в сеть стратегии и выбираем действие, основанное на этом текущем состоянии. Затем мы выполняем это действие в среде, а обратно получаем следующий кадр, вознаграждение и флаг done. После этого вызывается функция shift_frames, чтобы получить следующее состояние путём добавления текущего кадра в существующее состояние и удаления самого старого кадра. Затем идёт проверка, не получили ли мы конец эпизода, то есть не имеет ли флаг done значение «истина», и если да, то выводим на экран отдачу в этом эпизоде с именами работников, а затем добавляем отдачу в наш глобальный список отдач. Кроме того, с целью облегчения отладки мы будем выводить на экран среднее вознаграждение за последние 100 этапов через каждые 100 этапов. Это должно помочь нам увидеть, улучшает ли свои навыки агент с течением времени, поскольку каждая отдельная отдача может очень отличаться от других. После этого мы сбрасываем переменную total_reward до нуля. Если же флаг done не имеет значения «истина», мы просто добавляем текущее вознаграждение к общему вознаграждению, что позволяет отслеживать общее вознаграждение для каждого эпизода.

  def run_n_steps(self, n, sess):

    steps = []

    for _ in range(n):

      # Take a step

      action = self.sample_action(self.state, sess)

      next_frame, reward, done, _ = self.env.step(action)

      # Shift the state to include the latest frame

      next_state = shift_frames(self.state, self.img_transformer.transform(next_frame))

      # Save total return

      if done:

        print(“Total reward:”, self.total_reward, “Worker:”, self.name)

        self.returns_list.append(self.total_reward)

        if len(self.returns_list) > 0 and len(self.returns_list) % 100 == 0:

          print(“*** Total average reward (last 100):”, np.mean(self.returns_list[-100:]), “Collected so far:”, len(self.returns_list))

        self.total_reward = 0.

      else:

        self.total_reward += reward

Затем мы сохраняем выполненный этап. Для этого создаётся объект step, который, если помните, хранит текущее состояние, действие, вознаграждение, следующее состояние и флаг done, после чего мы добавляем этот этап в наш список этапов.

      # Save step

      step = Step(self.state, action, reward, next_state, done)

      steps.append(step)

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

      # Increase local and global counters

      global_step = next(self.global_counter)

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

      if done:

        self.state = repeat_frame(self.img_transformer.transform(self.env.reset()))

        break

      else:

        self.state = next_state

    return steps, global_step

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

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

Тут мы применим один приём, который включает в себя то, чему мы научились в части, посвящённой методу N шагов. Как вы помните, обычное обучение методом временных разниц для вычисления отдачи использует прогноз на один шаг вперёд. Таким образом, при обучении методом временных разниц мы для вычисления отдачи всегда используем формулу

Отсюда следует, что преимущество равно

Отсюда следует, что преимущество равно

Однако поскольку мы предпринимаем N шагов, то это значит, что мы можем использовать собранные нами N вознаграждений, что делает наш прогноз отдачи более точным. Если представить, что мы предпринимаем три шага, то у нас будут состояния s1, s2 и s3, а также вознаграждения r1, r2 и r3, которые можно использовать для вычисления отдач следующим образом. Отдача для s3 будет равна r3 + V(s4) – тут убран коэффициент γ, чтобы схема была нагляднее. Отдача для s2 будет равна r2 + r3 + V(s4), а отдача для s1 – равна r1 + r2 + r3 +V(s4). Другими словами, нам нужно сделать прогноз только для самого последнего значения ценности. Разумеется, если последнее состояние является конечным, то ценность будет равна нулю, поэтому начальное значение переменной reward мы устанавливаем также равным нулю, в противном случае мы присваиваем ей значение ценности следующего состояния.

  def update(self, steps, sess):

    “””

    Updates global policy and value networks using the local networks’ gradients

    “””

    # In order to accumulate the total return

    # We will use V_hat(s’) to predict the future returns

    # But we will use the actual rewards if we have them

    # Ex. if we have s1, s2, s3 with rewards r1, r2, r3

    # Then G(s3) = r3 + V(s4)

    #      G(s2) = r2 + r3 + V(s4)

    #      G(s1) = r1 + r2 + r3 + V(s4)

    reward = 0.0

    if not steps[-1].done:

      reward = self.get_value_prediction(steps[-1].next_state, sess)

Затем мы собираем данные для нашего учебного пакета.

    # Accumulate minibatch samples

    states = []

    advantages = []

    value_targets = []

    actions = []

Поскольку мы умножаем γ для будущих отдач по рекурсии, мы будем перебирать по циклу каждое состояние в обратном порядке. В цикле мы будем обновлять значение отдачи, которую я здесь случайно назвал reward (вознаграждение). Исходя из этого мы можем вычислить преимущество, просто равное отдаче минус V(s). В конце мы добавляем состояние в список состояний, добавляем действие в список действий, преимущество – в список преимуществ и вознаграждение в список целевых переменных ценности.

    # loop through steps in reverse order

    for step in reversed(steps):

      reward = step.reward + self.discount_factor * reward

      advantage = reward – self.get_value_prediction(step.state, sess)

      # Accumulate updates

      states.append(step.state)

      actions.append(step.action)

      advantages.append(advantage)

      value_targets.append(reward)

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

    feed_dict = {

      self.policy_net.states: np.array(states),

      self.policy_net.advantage: advantages,

      self.policy_net.actions: actions,

      self.value_net.states: np.array(states),

      self.value_net.targets: value_targets,

    }

    # Train the global estimators using local gradients

    global_step, pnet_loss, vnet_loss, _, _ = sess.run([

      self.global_step,

      self.policy_net.loss,

      self.value_net.loss,

      self.pnet_train_op,

      self.vnet_train_op,

    ], feed_dict)

    # Theoretically could plot these later

    return pnet_loss, vnet_loss

Итак, это всё, что касается кода. Давайте запустим его и посмотрим, что получится.

Как видите, результате получается не только весьма неплохой, но и за куда меньший промежуток времени, чем в случае глубоких Q-сетей.

A3C – итоги раздела

Настало время подвести итог всему изученному в этой части.

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

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

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

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

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

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

Итоги курса

В заключение мы подведём итоги всему сделанному в этом курсе!

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

Итак, чему же мы научились в этом курсе?

Мы начали курс с обзора некоторых базовых методов как глубокого обучения, так и обучения с подкреплением. Мы рассмотрели марковские процессы принятия решений и три способа их решения: динамическое программирование, метод Монте-Карло и обучение методом временных разниц. Затем мы рассмотрели OpenAI Gym, который предоставляет множество сред для практики в области обучения с подкреплением. Это особенно ценно в связи с тем, что написание кода для наших собственных сред было бы чрезвычайно трудоёмким занятием, совершенно не связанным с обучением с подкреплением как таковым. OpenAI Gym же позволил нам попрактиковаться в изучаемых методах обучения с подкреплением при помощи библиотек Theano и Tensorflow, являющимися очень полезными благодаря своим возможностям автоматического дифференцирования. Они позволили нам создавать как угодно сложные нейронные сети, а уравнения обновления автоматически вычислялись из структуры функции затрат. Кроме того, в этой же части был рассмотрен специальный тип нейронных сетей – так называемые сети радиальных базисных функций. В отличие от обычных глубоких нейронных сетей, эти сети являются «мелкими» и используют фиксированные представления признаков, что означает, что признаки не изучаются при помощи градиентного спуска.

Далее мы рассмотрели методы N шагов и TD(λ). Мы видели, что оба эти способа – это что-то среднее между методом Монте-Карло и обучением методом временных разниц, причём тогда как метод N шагов является дискретным, λ в TD(λ) является непрерывной и находится между 0 и 1. Конечно же, в них нет ничего такого, что делало бы их лучше или хуже изученных нами существующих методов обучения с подкреплением, дело просто в новых гиперпараметрах для выбора.

Далее мы рассмотрели методы градиента стратегии, что дало нам совершенно другой способ решения задач обучения с подкреплением. Вместо того, чтобы проводить жадную или эпсилон-жадную стратегию относительно Q, мы параметризовали саму стратегию, а после этого параметризовали как стратегию, так и функцию ценности состояний V(s). Параметризация стратегии интересна тем, что позволяет легко обрабатывать непрерывные пространства состояний, как мы видели в непрерывной версии задачи о машине на склоне. Единственное, что нам пришлось изменить, – это то, что вместо того, чтобы моделировать выход в виде дискретного распределения, мы моделировали его в виде распределения Гаусса.

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

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

До следующих встреч!

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