Градиенты стратегии

Методы градиента стратегии

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

В этой статье курса мы обсудим ещё один способ решения проблемы управления, который называется методами градиента стратегии. А также используем методы для тележки с шестом в Tensorflow и Theano.

До сих пор мы параметризовали функцию ценности, однако вполне вероятно, что можно параметризовать и стратегию. В частности, нам необходимо найти оптимальную стратегию π*. С первого раза это может показаться странной идеей по сравнению с тем, что мы делали ранее. Напомню, что наша текущая стратегия – это итерация по стратегиям: мы итеративно переключаемся между оценкой стратегии, которая означает нахождение функции ценности при заданной текущей стратегии, и совершенствованием стратегии, которое означает «жадное» поведение относительно текущей функции ценности. Мы видели, что они сходятся, так что мы получаем оптимальную функцию ценности, для которой оптимальной стратегией является взятие аргумента максимизации (argmax) для этой оптимальной функции ценности.

Итак, как же выглядит параметризация стратегии? Мы знаем, что стратегия должна быть чем-то вроде вероятности π(a|s). В частности, мы можем оценить каждое действие a, используя линейную модель или любой другой тип модели, а затем, как мы знаем из глубокого обучения, можем использовать функцию софтмакс для преобразования этих оценок действий в вероятности. Это гарантирует, что сумма всех вероятностей будет равна 1.

Чтобы стратегия была оптимальной, она должна иметь какой-то целевой показатель – это то, к чему вы должны были уже привыкнуть в машинном обучении. Большинство рассмотренных нами методов машинного обучения начинаются с попытки оптимизировать некоторую целевую функцию. Если наша модель дифференцируема, то мы можем использовать градиентный спуск или градиентный подъём для достижения нашего целевого показателя. Так как эта часть посвящена методам градиента стратегии, мы, конечно же, используем аналогичный подход по отношению к стратегии. Но возникает важный вопрос: что это должен быть за целевой показатель?

Предположим, мы начинаем с некоторого начального состояния s0. Как вы знаете, нам нужно максимизировать общую отдачу от целого эпизода, равную V(s0). Не забывайте, что функция ценности V также зависит стратегии π, что можно показать явно с помощью индекса Vπ. Целевой показатель стратегии принято обозначать буквой η, что не очень удачно, поскольку мы использовали её в прошлых курсах для других целей. Просто запомните, что в этом курсе η означает целевой показатель стратегии, который обычно ещё называют эффективностью:

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

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

Можно показать, что градиент эффективности принимает следующую форму, зависящую от самого градиента стратегии, и что он сходится:

Это называется теоремой градиента стратегии.

Что мы можем сделать – так это преобразовать это уравнение, умножив и поделив его на π:

Так и сделав, можно видеть, что сумма на самом деле является лишь ещё одним ожидаемым значением по π:

Однако ожидаемое значение ожидаемого значения остаётся ожидаемым значением, так что мы можем переписать уравнение с одним ожидаемым значением:

Далее мы можем использовать тождество из математического анализа, согласно которому градиент логарифма f(x) равен градиенту f(x), делённому на f(x):

И последний этап – понять, что Q в действительности является ожидаемым значением отдачи G, так что мы можем заменить Q на собственно G, поскольку всё это входит в ожидаемое значение:

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

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

Но не забывайте, что библиотеки Tensorflow и Theano будут находить градиенты для нас. В частности, нас интересует только одно выражение, которое мы можем подставить в качестве функции затрат для её оптимизации. Чтобы преобразовать наше выражение в такую форму, мы должны понять, что G является константой, а потому может быть помещено под знак градиента:

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

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

Для понятности: T означает длину эпизода, а индекс tt-й временной шаг эпизода. Поскольку тут включена сумма отдач по всему эпизоду, это метод Монте-Карло.

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

Здесь три члена, влияющих на новое значение: θ – отдача, градиент π и само π. Не забывайте, что π – это вероятность выбора действия a в данном состоянии s и при использовании текущей стратегии.

Вначале рассмотрим G – отдачу. Мы двигаемся в некотором направлении пропорционально значению G – чем оно больше, тем больший делается шаг. Это хорошо, поскольку мы хотим максимизировать наше вознаграждение. Далее посмотрим на π – вероятность выбора действия a. Тут мы двигаемся обратно пропорционально π. Это тоже хорошо, так как если π мало, а отдача велика, то мы можем сделать даже больший шаг в данном направлении. И, наконец, градиент π – вектор, который указывает нам действительное направление движения. Градиент показывает нам направление наибольшего увеличения π.

Вы могли заметить, что ранее в этой лекции я упоминал и о приближённом значении V(s), однако до сих пор оно не играло никакой роли. Одна из распространённых модификаций градиента по стратегии состоит в добавлении основы (baseline). В этом случае наша константа, которая была просто G, становится равной разности GV(s), то есть нашего прогноза ценности в состоянии s:

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

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

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

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

Тут возникает естественный вопрос: можно ли превратить всё это из метода Монте-Карло в TD-обучение, чтобы не возникала необходимость дожидаться конца эпизода, прежде чем делать какие-либо обновления? Конечно же, это возможно, и в обучении с подкреплением для этого есть специальное название – метод «субъект-критик». Называется он так потому, что мы рассматриваем стратегию как субъекта, а TD-ошибку, зависящую от оценки ценности, – как критика. В этом случае в обновлениях мы просто заменяем G на одношаговую оценку G:

Теперь, когда мы прошли самое тяжёлое в методе градиента стратегии, давайте обсудим, зачем его стоит использовать. Мы знаем, что метод градиента стратегии даёт вероятностную стратегию. Это может напоминать эпсилон-жадный алгоритм, который также является вероятностным. Впрочем, должно быть ясно, почему метод градиента стратегии является более мощным: в эпсилон-жадном алгоритме все неоптимальные действия имеют одинаковую вероятность выполнения, даже если одно из них кажется лучше, чем другое. С помощью же метода градиента стратегий мы можем прямо смоделировать эту «лучшесть». Например, на самом деле может оказаться, что оптимальным является выполнять первое действие в 90% случаев, второе действие – в 8% случаев, а третье действие – в 2%.

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

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

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

Метод градиента стратегии для тележки с шестом в Tensorflow

Здесь мы реализуем решение задачи о тележке с шестом методом градиента стратегии в библиотеке Tensorflow. Мы используем линейную модель для стратегии и нейронную сеть для функции ценности. Впрочем, абстрагировавшись от слоёв нейронной сети, вы с лёгкостью можете испытать различные архитектуры. Если вы не хотите писать код самостоятельно, хотя я настоятельно рекомендую сделать именно так, соответствующий файл в репозитарии называется pg_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

# Inspired by https://github.com/dennybritz/reinforcement-learning

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 для весовых коэффициентов и назначаем функцию активации.

# so you can test different architectures

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.use_bias = use_bias

    if use_bias:

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

    self.f = f

В функции forward мы вычисляем выход.

  def forward(self, X):

    if self.use_bias:

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

    else:

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

    return self.f(a)

Затем у нас идёт класс PolicyModel. Тут конструктор в качестве аргументов принимает входящий и исходящий размеры, а также список всех размеров скрытого слоя.

Первый блок кода – это создание структуры нейронной сети:

# approximates pi(a | s)

class PolicyModel:

  def __init__(self, D, K, hidden_layer_sizes):

    # create the graph

    # K = number of actions

    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, use_bias=False)

    layer = HiddenLayer(M1, K, tf.nn.softmax, use_bias=False)

    self.layers.append(layer)

Далее мы создаём заполнители для входящих состояний, которые названы X, actions («действия») и advantages («преимущества»). X будет матрицей размерности NxD, тогда как actions и advantages – векторами размерности N. Не забывайте, что actions являются целыми числами, под которыми подразумеваются индексы массивов.

    # inputs and targets

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

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

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

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

    # calculate output and cost

    Z = self.X

    for layer in self.layers:

      Z = layer.forward(Z)

    p_a_given_s = Z

    # action_scores = Z

    # p_a_given_s = tf.nn.softmax(action_scores)

    # self.action_scores = action_scores

    self.predict_op = p_a_given_s

Затем мы создаём функцию затрат. Прежде всего, надо отметить, что имеют значения только записи p_a_given_s, соответствующие реально предпринятым действиям. В связи с этим мы должны либо проиндексировать p_a_given_s по действиям, либо создать матрицу прямого кодирования one_hot и умножить на неё. Тут идёт суммирование, однако на самом деле это просто способ избавиться от нулей после умножения на матрицу one_hot. Вы увидите, что в Theano для этого есть более элегантный способ. И в конце мы берём логарифм, поскольку это логарифм от π, возникающий в функции затрат.

    # self.one_hot_actions = tf.one_hot(self.actions, K)

    selected_probs = tf.log(

      tf.reduce_sum(

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

        reduction_indices=[1]

      )

    )

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

    # self.selected_probs = selected_probs

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

    # self.cost = cost

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

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

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

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

Далее у нас идёт функция set_session. Это просто базовый установщик, который позволит нам использовать одну и ту же сессию и для класса PolicyModel, и для класса ValueModel.

  def set_session(self, session):

    self.session = session

Затем идёт функция partial_fit. Сначала мы преобразуем состояния, действия и преимущества в массивы нужного размера, прежде чем вызывать train_op. Затем вызывается train_op.

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

    X = np.atleast_2d(X)

    actions = np.atleast_1d(actions)

    advantages = np.atleast_1d(advantages)

    self.session.run(

      self.train_op,

      feed_dict={

        self.X: X,

        self.actions: actions,

        self.advantages: advantages,

      }

    )

Функция predict вызывает predict_op, а функция sample_action – выбирает действие, исходя из прогноза. Обратите внимание, что здесь у нас не возникает никакой необходимости в эпсилон-жадном алгоритме.

  def predict(self, X):

    X = np.atleast_2d(X)

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

  def sample_action(self, X):

    p = self.predict(X)[0]

    return np.random.choice(len(p), p=p)

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

# approximates V(s)

class ValueModel:

  def __init__(self, D, hidden_layer_sizes):

    # 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, 1, lambda x: x)

    self.layers.append(layer)

    # inputs and targets

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

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

    # calculate output and cost

    Z = self.X

    for layer in self.layers:

      Z = layer.forward(Z)

    Y_hat = tf.reshape(Z, [-1]) # the output

    self.predict_op = Y_hat

    cost = tf.reduce_sum(tf.square(self.Y – Y_hat))

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

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

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

Функции set_session, partial_fit и predict такие же, как и в классе PolicyModel.

  def set_session(self, session):

    self.session = session

  def partial_fit(self, X, Y):

    X = np.atleast_2d(X)

    Y = np.atleast_1d(Y)

    self.session.run(self.train_op, feed_dict={self.X: X, self.Y: Y})

  def predict(self, X):

    X = np.atleast_2d(X)

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

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

def play_one_td(env, pmodel, vmodel, gamma):

  observation = env.reset()

  done = False

  totalreward = 0

  iters = 0

  while not done and iters < 2000:

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

    # the 200 limit seems a bit early

    action = pmodel.sample_action(observation)

    prev_observation = observation

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

    # if done:

    #   reward = -200

    # update the models

    V_next = vmodel.predict(observation)

    G = reward + gamma*np.max(V_next)

    advantage = G – vmodel.predict(prev_observation)

    pmodel.partial_fit(prev_observation, action, advantage)

    vmodel.partial_fit(prev_observation, G)

    if reward == 1: # if we changed the reward to -200

      totalreward += reward

    iters += 1

  return totalreward

В качестве аргументов функция принимает среду, модель стратегии, модель ценности и значение γ. Мы инициируем ряд переменных и не забываем отслеживать состояния, действия и вознаграждения. Это связано с двумя причинами: во-первых, они являются аргументами функции train, а во-вторых, нам нужны вознаграждения, чтобы позже вычислить отдачи.

def play_one_mc(env, pmodel, vmodel, gamma):

  observation = env.reset()

  done = False

  totalreward = 0

  iters = 0

  states = []

  actions = []

  rewards = []

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

  while not done and iters < 2000:

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

    # the 200 limit seems a bit early

    action = pmodel.sample_action(observation)

    states.append(observation)

    actions.append(action)

    rewards.append(reward)

    prev_observation = observation

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

    if done:

      reward = -200

  states.append(observation)

  actions.append(action)

  rewards.append(reward)

    if reward == 1: # if we changed the reward to -200

      totalreward += reward

    iters += 1

Когда цикл заканчивается, мы вычисляем отдачи и преимущества. Необходимо подождать, пока не получим отдачу, чтобы вычислить преимущество, так как преимущество зависит от отдачи. Получив отдачи и преимущества, мы можем вызвать функции partial_fit и из PolicyModel, и из ValueModel.

  returns = []

  advantages = []

  G = 0

  for s, r in zip(reversed(states), reversed(rewards)):

    returns.append(G)

    advantages.append(G – vmodel.predict(s)[0])

    G = r + gamma*G

  returns.reverse()

  advantages.reverse()

  # update the models

  pmodel.partial_fit(states, actions, advantages)

  vmodel.partial_fit(states, returns)

  return totalreward

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

def main():

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

  D = env.observation_space.shape[0]

  K = env.action_space.n

  pmodel = PolicyModel(D, K, [])

  vmodel = ValueModel(D, [10])

  init = tf.global_variables_initializer()

  session = tf.InteractiveSession()

  session.run(init)

  pmodel.set_session(session)

  vmodel.set_session(session)

  gamma = 0.99

  if ‘monitor’ in sys.argv:

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

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

    env = wrappers.Monitor(env, monitor_dir)

  N = 1000

  totalrewards = np.empty(N)

  costs = np.empty(N)

  for n in range(N):

    totalreward = play_one_mc(env, pmodel, vmodel, gamma)

    totalrewards[n] = totalreward

    if n % 100 == 0:

      print(“episode:”, n, “total reward:”, totalreward, “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()

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

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

Метод градиента стратегии для тележки с шестом в Theano

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

Вначале у нас идут все прежние импорты, разве что теперь мы импортируем библиотеку Theano вместо Tensorflow:

# ***

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

# Inspired by https://github.com/dennybritz/reinforcement-learning

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

Затем у нас идёт класс HiddenLayer. У него же параметры, что и в версии для Tensorflow, да и сам он выполняет всё то же самое. Единственное его дополнительное действие – это сохранение параметров. Оно нам нужно, чтобы позже написать выражения для обновления.

# so you can test different architectures

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)

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

# approximates pi(a | s)

class PolicyModel:

  def __init__(self, D, K, hidden_layer_sizes):

    # learning rate and other hyperparams

    lr = 10e-4

    mu = 0.7

    decay = 0999

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

    # create the graph

    # K = number of actions

    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, use_bias=False)

    self.layers.append(layer)

    # get all params for gradient later

    params = []

    for layer in self.layers:

      params += layer.params

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

    # inputs and targets

    X = T.matrix(‘X’)

    actions = T.ivector(‘actions’)

    advantages = T.vector(‘advantages’)

И, наконец, мы вычисляем функцию затрат, ту же самую, что и в версии для Tensorflow. В Theano необходимо определить правило обновления; здесь указаны обновления для алгоритма RMSprop. Вы также можете пометить их как комментарии и снять пометку комментариев с версии с чистым импульсом:

    # calculate output and cost

    Z = X

    for layer in self.layers:

      Z = layer.forward(Z)

    action_scores = Z

    p_a_given_s = T.nnet.softmax(action_scores)

    selected_probs = T.log(p_a_given_s[T.arange(actions.shape[0]), actions])

    cost = -T.sum(advantages * selected_probs)

    # specify update rule

    grads = T.grad(cost, params)

    updates = [(p, p – lr*g) for p, g in zip(params, grads)]

Закончив, мы можем скомпилировать функции train_op и predict_op:

    # compile functions

    self.train_op = theano.function(

      inputs=[X, actions, advantages],

      updates=updates,

      allow_input_downcast=True

    )

    self.predict_op = theano.function(

      inputs=[X],

      outputs=p_a_given_s,

      allow_input_downcast=True

    )

Далее у нас идут функции partial_fit, predict и sample_action, в точности отражающие аналогичные функции для версии в Tensorflow.

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

    X = np.atleast_2d(X)

    actions = np.atleast_1d(actions)

    advantages = np.atleast_1d(advantages)

    self.train_op(X, actions, advantages)

  def predict(self, X):

    X = np.atleast_2d(X)

    return self.predict_op(X)

  def sample_action(self, X):

    p = self.predict(X)[0]

    nonans = np.all(~np.isnan(p))

    assert(nonans)

    return np.random.choice(len(p), p=p)

Следующим идёт класс ValueModel. Он, как и его предыдущая версия, использует лишь простейший градиентный спуск, поэтому тут у нас лишь коэффициент обучения. Как обычно, мы создаём слои, собираем параметры, вычисляем выход с функцией затрат и определяем правило обновления. Когда всё сделано, мы компилируем train_op и predict_op:

# approximates V(s)

class ValueModel:

  def __init__(self, D, hidden_layer_sizes):

    # constant learning rate is fine

    lr = 10e-5

    # 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, 1, lambda x: x)

    self.layers.append(layer)

    # get all params for gradient later

    params = []

    for layer in self.layers:

      params += layer.params

    # inputs and targets

    X = T.matrix(‘X’)

    Y = T.vector(‘Y’)

    # calculate output and cost

    Z = X

    for layer in self.layers:

      Z = layer.forward(Z)

    Y_hat = T.flatten(Z)

    cost = T.sum((Y – Y_hat)**2)

    # specify update rule

    grads = T.grad(cost, params)

    updates = [(p, p – lr*g) for p, g in zip(params, grads)]

    # compile functions

    self.train_op = theano.function(

      inputs=[X, Y],

      updates=updates,

      allow_input_downcast=True

    )

    self.predict_op = theano.function(

      inputs=[X],

      outputs=Y_hat,

      allow_input_downcast=True

    )

  def partial_fit(self, X, Y):

    X = np.atleast_2d(X)

    Y = np.atleast_1d(Y)

    self.train_op(X, Y)

  def predict(self, X):

    X = np.atleast_2d(X)

    return self.predict_op(X)

# def play_one_td(env, pmodel, vmodel, gamma):

#   observation = env.reset()

#   done = False

#   totalreward = 0

#   iters = 0

#   while not done and iters < 2000:

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

#     # the 200 limit seems a bit early

#     action = pmodel.sample_action(observation)

#     prev_observation = observation

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

#     if done:

#       reward = -200

#     # update the models

#     V_next = vmodel.predict(observation)

#     G = reward + gamma*np.max(V_next)

#     advantage = G – vmodel.predict(prev_observation)

#     pmodel.partial_fit(prev_observation, action, advantage)

#     vmodel.partial_fit(prev_observation, G)

#     if reward == 1: # if we changed the reward to -200

#       totalreward += reward

#     iters += 1

#   return totalreward

Обратите внимание, что поскольку API PolicyModel и ValueModel осталось в точности прежним, функция play_one не нуждается в изменениях.

def play_one_mc(env, pmodel, vmodel, gamma):

  observation = env.reset()

  done = False

  totalreward = 0

  iters = 0

  states = []

  actions = []

  rewards = []

  reward = 0

  while not done and iters < 2000:

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

    # the 200 limit seems a bit early

    action = pmodel.sample_action(observation)

    states.append(observation)

    actions.append(action)

    rewards.append(reward)

    prev_observation = observation

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

    if done:

      reward = -200

    if reward == 1: # if we changed the reward to -200

      totalreward += reward

    iters += 1

  # save the final (s,a,r) tuple

  action = pmodel.sample_action(observation)

  states.append(observation)

  actions.append(action)

  rewards.append(reward)

  returns = []

  advantages = []

  G = 0

  for s, r in zip(reversed(states), reversed(rewards)):

    returns.append(G)

    advantages.append(G – vmodel.predict(s)[0])

    G = r + gamma*G

  returns.reverse()

  advantages.reverse()

  # update the models

  pmodel.partial_fit(states[1:], actions[1:], advantages[1:])

  vmodel.partial_fit(states, returns)

  return totalreward

Пройдя вниз до раздела main, можно заметить, что единственное, что тут нужно изменить, – это убрать создание сессии, поскольку для Theano это не нужно:

def main():

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

  D = env.observation_space.shape[0]

  K = env.action_space.n

  pmodel = PolicyModel(D, K, [])

  vmodel = ValueModel(D, [10])

  gamma = 0.99

  if ‘monitor’ in sys.argv:

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

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

    env = wrappers.Monitor(env, monitor_dir)

  N = 1000

  totalrewards = np.empty(N)

  costs = np.empty(N)

  for n in range(N):

    totalreward = play_one_mc(env, pmodel, vmodel, gamma)

    totalrewards[n] = totalreward

    if n % 100 == 0:

      print(“episode:”, n, “total reward:”, totalreward, “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()

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