Радиальные базисные функции (RBF)

Нейронная сеть радиальных базисных функций

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

В этой статье и продолжение темы OpenAI Gym, мы обсудим сети радиальных базисных функций (RBF), поскольку, как вы увидите, они могут оказаться полезными в обучении с подкреплением.

RBFradial basis function – переводится как радиальная базисная функция. Такие сети позволяют нам перейти к следующему этапу использования приближений функций. Сети радиальных базисных функций можно представить двумя способами. Первый состоит в том, сеть радиальных базисных функций – это на самом деле линейная модель, в которой мы вначале произвели извлечение признаков, а признаки стали ядрами радиальных базисных функций. Вскоре мы обсудим, что такое ядра радиальных базисных функций. Второй способ представления сети радиальных базисных функций – в виде нейронной сети с одним скрытым слоём и радиальными базисными функциями в качестве функции активации.

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

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

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

Возникает хороший вопрос: как же выбрать c? Ещё один хороший вопрос: сколько c нужно выбрать? Выбираемое количество c, по сути, является количеством скрытых узлов в скрытом слое. Каждый скрытый узел соответствует отдельной радиальной базисной функции с отдельным центром. Эти центры ещё иногда называют экземплярами. Есть несколько разных способов выбрать экземпляры. Так, в методе опорных векторов (SVM) количество экземпляров равно количеству единиц учебных данных и в действительности они, собственно, и есть единицами учебных данных. В этом заключается одна из причин, по которой метод опорных векторов потерял популярность: они не масштабируются по размерам сегодняшних данных. Поскольку каждый учебный пример превращается в экземпляр, обучение методом опорных векторов имеет сложность порядка O(N2), а прогнозирование – сложность O(N), где N – размер обучающей выборки. Но это важная часть истории глубокого обучения, поскольку некогда считалось, что метод опорных векторов превосходит нейронные сети.

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

env.observation_space.sample()

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

К счастью, хоть это и в наших силах, мы не будем писать никакого кода для преобразования ядер радиальных базисных функций, поскольку такая возможность уже представлена в библиотеке Sci-Kit Learn. Кроме того, наша реализация была бы излишне медленной. Реализация в Sci-Kit Learn называется RBFSampler и применяет алгоритм Монте-Карло, что позволяет намного ускорить вычисления. Как обычно, тут применяется стандартный интерфейс Sci-Kit Learn: мы создаём экземпляр, вызываем функцию fit и с этого момента можем использовать функцию transform:

sampler = RBFSampler()

sampler.fit(raw_data)

features = sampler.transform(raw_data)

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

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

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

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

Пройдёмся по некоторым деталям практической реализации алгоритма. Вы могли заметить, что ядро радиальной базисной функции имеет параметр масштабирования, похожий на дисперсию в гауссовом распределении. Поскольку мы в действительности не знаем, какой масштаб хороший – или, возможно, несколько масштабов – в Sci-Kit Learn есть инструмент, позволяющий нам использовать несколько ядер радиальных базисных функций с различными масштабами одновременно. Он называется FeatureUnion и, по сути, позволяет нам использовать несколько преобразований признаков любого типа, не только радиальных базисных функций, а затем объединить отдельные векторы признаков для создания одного большого набора векторов признаков.

Кроме того, мы будем приводить данные к стандартному виду, прежде чем применять ядра радиальных базисных функций. Sci-Kit Learn имеет и для этого свой класс, который называется StandardScaler.

Ещё одна возможность использовать Sci-Kit Learn – компонент линейной регрессии с градиентным спуском. Библиотека Sci-Kit Learn также уже имеет для этого класс, который называется SGDRegressor, и мы его также будем использовать. В отличие от типичных моделей Sci-Kit Learn, вместо вызова функции fit мы будем использовать функцию partial_fit, позволяющую выполнять лишь один шаг градиентного спуска – именно то, что нам нужно.

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

input = transform(env.reset()), target = 0

model.partial_fit(input, target)

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

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

from sklearn.linear_model import SGDRegressor

model = SGDRegressor

model.partial_fit([[0,0]], [0])

model.predict([[0, 0]])

Out[87]: array([ 0.])   # логично

model.predict([[0, 1]])

Out[88]: array([ 0.])   # а?

model.predict([[1, 0]])

Out[89]: array([ 0.])   # а?

model.predict([[1, 1]])

Out[90]: array([ 0.])   # а?

model.predict([[99, 99]])

Out[91]: array([ 0.])   # а?

Первым делом мы импортируем SGDRegressor и инициируем его экземпляр. После этого мы совмещаем входной вектор (0, 0) с целевой переменной 0. Далее мы делаем прогноз для (0, 0) и получаем 0, что и следовало ожидать. Затем мы можем попробовать некоторые другие прогнозы. Вы увидите, что не имеет значения, какие входные данные мы пробуем подставить – (0, 1), (1, 0), (1, 1) или даже (99, 99), – прогноз всё равно равен нулю. Это явно было бы не так, если бы нас был статичный вектор размерности 2.

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

И наконец, как я упоминал ранее, следующим рассматриваемым нами окружением будет машина на склоне. Если вы не знаете, что это такое, то можете посетить соответствующие страницы Википедии и документации:

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

https://gym/openai.com/envs/ MountainCar-v0

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

Одна из функций, которая будет отражаться на графике, называется cost-to-go. Название странное, но это все лишь отрицательное значение функции ценности. Так она называется в книге Саттона и Барто. Поскольку пространство состояний имеет всего две переменные, это позволяет нам построить функцию cost-to-go в виде трёхмерного графика, что мы и сделаем в коде.

Сети радиальных базисных функций с машиной на склоне (код)

Используем Q-обучение с сетью радиальных базисных функций для решения задачи о машине на склоне. Если вы не хотите писать код самостоятельно, хотя я настоятельно рекомендую сделать именно так, соответствующий файл в репозитарии называется q_learning.py и находится в папке mountaincar.

Вначале у нас идут импорты, большинство из которых вам уже знакомы. Тут есть ряд вещей из Sci-Kit Learn, о которых упоминалось на предыдущей лекции:

from __future__ import print_function, division

from builtins import range

import gym

import os

import sys

import numpy as np

import matplotlib

import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D

from gym import wrappers

from datetime import datetime

from sklearn.pipeline import FeatureUnion

from sklearn.preprocessing import StandardScaler

from sklearn.kernel_approximation import RBFSampler

from sklearn.linear_model import SGDRegressor

Я вставил параметры по умолчанию для класса SGDRegressor, чтобы вы могли видеть, какие используются гиперпараметры. Как вы можете видеть, мы используем функцию квадрата ошибок с L2-регуляризацией, коэффициентом обучения 10-4 и обратную шкалу этого коэффициента, что значит, что он уменьшается на величину 1/t:

# SGDRegressor defaults:

# loss=’squared_loss’, penalty=’l2′, alpha=0.0001,

# l1_ratio=0.15, fit_intercept=True, n_iter=5, shuffle=True,

# verbose=0, epsilon=0.1, random_state=None, learning_rate=’invscaling’,

# eta0=0.01, power_t=0.25, warm_start=False, average=False

Далее у нас идёт класс FeatureTransformer, выполняющий все преобразования признаков, о которых упоминалось в предыдущей лекции. В конструкторе мы вначале собираем 10 000 примеров из пространства состояний. Затем мы приводим наблюдения к стандартизованному виду, так чтобы имели среднее значение 0 и дисперсию 1. Далее мы создаём FeatureUnion для четырёх выборок радиальных базисных функций с разными дисперсиями. Количество компонент, подставляемых в конструктор, означает количество экземпляров. Далее мы подгоняем выборки радиальных базисных функций к отмасштабированным данным и, наконец, создаём переменные экземпляров для scaler и RBFSampler, чтобы использовать их в следующей далее функции transform:

class FeatureTransformer:

  def __init__(self, env):

    observation_examples = np.array([env.observation_space.sample() for x in range(10000)])

    scaler = StandardScaler()

    scaler.fit(observation_examples)

    # Used to converte a state to a featurizes represenation.

    # We use RBF kernels with different variances to cover different parts of the space

    featurizer = FeatureUnion([

            (“rbf1”, RBFSampler(gamma=5.0, n_components=n_components)),

            (“rbf2”, RBFSampler(gamma=2.0, n_components=n_components)),

            (“rbf3”, RBFSampler(gamma=1.0, n_components=n_components)),

            (“rbf4”, RBFSampler(gamma=0.5, n_components=n_components))

            ])

    example_features = featurizer.fit_transform(scaler.transform(observation_examples))

    self.scaler = scaler

    self.featurizer = featurizer

  def transform(self, observations):

    # print “observations:”, observations

    scaled = self.scaler.transform(observations)

    # assert(len(scaled.shape) == 2)

    return self.featurizer.transform(scaled)

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

# Holds one SGDRegressor for each action

class Model:

  def __init__(self, env, feature_transformer, learning_rate):

    self.env = env

    self.models = []

    self.feature_transformer = feature_transformer

    for i in range(env.action_space.n):

      model = SGDRegressor(learning_rate=learning_rate)

      model.partial_fit(feature_transformer.transform( [env.reset()] ), [0])

      self.models.append(model)

Далее у нас идёт метод predict. Он преобразует состояние в вектор признаков и делает прогноз ценностей – по одной на каждое действие. Ответ возвращается в виде массива Numpy. Обратите внимание, что мы подставляем s в список до того, как вызываем функцию transform. Это связано с тем, что по соглашению входные данные в Sci-Kit Learn должны быть двухмерными. Одиночное же состояние является одномерным, поэтому приходится преобразовывать его в матрицу размерности NxD, где N = 1.

  def predict(self, s):

    X = self.feature_transformer.transform([s])

    assert(len(X.shape) == 2)

    return np.array([m.predict(X)[0] for m in self.models])

Затем у нас идёт функция update, которая также преобразует входное состояние в вектор признаков. Обратите внимание, что функция partial_fit вызывается только для модели, соответствующей выбранному действию, а также что мы подставляем G в список. Это связано с тем, что Sci-Kit Learn ожидает, что целевые переменные будут одномерными объектами, тогда как G является лишь скалярной величиной.

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

    X = self.feature_transformer.transform([s])

    assert(len(X.shape) == 2)

    self.models[a].partial_fit(X, [G])

И наконец у нас идёт функция sample_action, выполняющая эпсилон-жадный алгоритм, с которым, я уверен, вы уже хорошо знакомы:

  def sample_action(self, s, eps):

    # eps = 0

    # Technically, we don’t need to do epsilon-greedy

    # because SGDRegressor predicts 0 for all states

    # until they are updated. This works as the

    # “Optimistic Initial Values” method, since all

    # the rewards for Mountain Car are -1.

    if np.random.random() < eps:

      return self.env.action_space.sample()

    else:

      return np.argmax(self.predict(s))

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

# returns a list of states_and_rewards, and the total reward

def play_one(model, eps, gamma):

  observation = env.reset()

  done = False

  totalreward = 0

  iters = 0

  while not done and iters < 10000:

    action = model.sample_action(observation, eps)

    prev_observation = observation

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

    # update the model

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

    model.update(prev_observation, action, G)

    totalreward += reward

    iters += 1

  return totalreward

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

def plot_cost_to_go(env, estimator, num_tiles=20):

  x = np.linspace(env.observation_space.low[0], env.observation_space.high[0], num=num_tiles)

  y = np.linspace(env.observation_space.low[1], env.observation_space.high[1], num=num_tiles)

  X, Y = np.meshgrid(x, y)

  # both X and Y will be of shape (num_tiles, num_tiles)

  Z = np.apply_along_axis(lambda _: -np.max(estimator.predict(_)), 2, np.dstack([X, Y]))

  # Z will also be of shape (num_tiles, num_tiles)

  fig = plt.figure(figsize=(10, 5))

  ax = fig.add_subplot(111, projection=’3d’)

  surf = ax.plot_surface(X, Y, Z,

    rstride=1, cstride=1, cmap=matplotlib.cm.coolwarm, vmin=-1.0, vmax=1.0)

  ax.set_xlabel(‘Position’)

  ax.set_ylabel(‘Velocity’)

  ax.set_zlabel(‘Cost-To-Go == -V(s)’)

  ax.set_title(“Cost-To-Go Function”)

  fig.colorbar(surf)

  plt.show()

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

def plot_running_avg(totalrewards):

  N = len(totalrewards)

  running_avg = np.empty(N)

  for t in range(N):

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

  plt.plot(running_avg)

  plt.title(“Running Average”)

  plt.show()

Затем у нас идёт раздел main. Мы начинаем с инициации среды, Model и FeatureTransformer. Значение γ установлено равным 0,9, однако вы можете попробовать разные значения. Вновь-таки, я добавил фрагмент кода, чтобы можно было сохранить результаты на диск, если набрать monitor в виде аргумента командной строки. Затем мы делаем цикл для 300 эпизодов. В цикле мы вызываем функцию play_one и уменьшаем ε в геометрической прогрессии. Когда всё сделано, мы выводим результаты и строим графики, о которых говорилось ранее.

if __name__ == ‘__main__’:

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

  ft = FeatureTransformer(env)

  model = Model(env, ft, “constant”)

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

  totalrewards = np.empty(N)

  for n in range(N):

    # eps = 1.0/(0.1*n+1)

    eps = 0.1*(0.97**n)

    # eps = 0.5/np.sqrt(n+1)

    totalreward = play_one(model, eps, gamma)

    totalrewards[n] = totalreward

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

  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)

  # plot the optimal state-value function

  plot_cost_to_go(env, model)

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

Сети радиальных базисных функций и тележка с шестом (теория)

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

Первая модификация, которую мы сделаем, – это создадим собственный SGDRegressor. Это должно дать вам некоторую практику в построении моделей с градиентным спуском на случай, если вы его уже подзабыли. С целью удостовериться, что он имеет тот же API, что и SGDRegressor библиотеки Sci-Kit Learn, нам потребуется создать функции partial_fit и predict. Заметьте, это означает, что мы не будем использовать метод оптимистических начальных значений, так как прогнозы будут определены весовыми коэффициентами.

Важной является следующая модификация. Напомню, что в коде для машины на склоне мы брали выборку из пространства наблюдений, чтобы получить экземпляры для наших ядер радиальных базисных функций. Это хорошо для машины на склоне, поскольку максимальное и минимальное значения для каждой переменной состояния являются небольшими. В случае же тележки с шестом это не так – напомню, что скорость тут может приближаться к бесконечности. К сожалению, функция sample() из OpenAI Gym делает лишь равномерную выборку из всего пространства состояний, которая не является репрезентативной по отношению к состояниям, в которых мы окажемся в реальных эпизодах. Это значит, что данная выборка влечёт за собой плохие экземпляры. Вместо этого нам нужно угадать, какими будут правдоподобные диапазоны значений. Или, если вы хотите сделать всё как следует, то следует сыграть несколько тысяч эпизодов и сделать выборку на основе полученных значений. Так как пространство состояний различно, нам также понадобятся и разные масштабы для ядер радиальных базисных функций, и я призывают вас проверить их самостоятельно, прежде чем смотреть мой код.

Сети радиальных базисных функций и тележка с шестом (код)

А теперь мы реализуем Q-обучение с сетью радиальных базисных функций для решения задачи тележки с шестом.

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

Вначале у нас идут все обычные импорты – вы можете заметить FeatureUnion, StandardScaler и RBFSampler с прошлого раза. Кроме того, из нашей прошлой программы мы импортируем функцию plot_running_avg:

from __future__ import print_function, division

from builtins import range

import gym

import os

import sys

import numpy as np

import matplotlib.pyplot as plt

from gym import wrappers

from datetime import datetime

from sklearn.pipeline import FeatureUnion

from sklearn.preprocessing import StandardScaler

from sklearn.kernel_approximation import RBFSampler

from q_learning_bins import plot_running_avg

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

class SGDRegressor:

  def __init__(self, D):

    self.w = np.random.randn(D) / np.sqrt(D)

    self.lr = 10e-2

  def partial_fit(self, X, Y):

    self.w += self.lr*(Y – X.dot(self.w)).dot(X)

  def predict(self, X):

    return X.dot(self.w)

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

class FeatureTransformer:

  def __init__(self, env):

    # observation_examples = np.array([env.observation_space.sample() for x in range(10000)])

    # NOTE!! state samples are poor, b/c you get velocities –> infinity

    observation_examples = np.random.random((20000, 4))*2 – 2

    scaler = StandardScaler()

    scaler.fit(observation_examples)

    # Used to converte a state to a featurizes represenation.

    # We use RBF kernels with different variances to cover different parts of the space

    featurizer = FeatureUnion([

            (“rbf1”, RBFSampler(gamma=0.05, n_components=1000)),

            (“rbf2”, RBFSampler(gamma=1.0, n_components=1000)),

            (“rbf3”, RBFSampler(gamma=0.5, n_components=1000)),

            (“rbf4”, RBFSampler(gamma=0.1, n_components=1000))

            ])

    feature_examples = featurizer.fit_transform(scaler.transform(observation_examples))

    self.dimensions = feature_examples.shape[1]

    self.scaler = scaler

    self.featurizer = featurizer

  def transform(self, observations):

    scaled = self.scaler.transform(observations)

    return self.featurizer.transform(scaled)

Затем идёт класс Model. Он почти такой же, как и ранее, разве что конструктор для SGDRegressor отличается, поскольку мы его написали сами. Теперь он принимает как аргумент также размерность весовых коэффициентов. Не забывайте, что у нас один SGDRegressor для каждого действия. Функции predict и update являются альтернативными способами преобразования одномерного массива в двухмерный. Это связано с тем, что в нашем классе SGDRegressor нам нужны массивы Numpy, а не списки.

# Holds one SGDRegressor for each action

class Model:

  def __init__(self, env, feature_transformer):

    self.env = env

    self.models = []

    self.feature_transformer = feature_transformer

    for i in range(env.action_space.n):

      model = SGDRegressor(feature_transformer.dimensions)

      self.models.append(model)

  def predict(self, s):

    X = self.feature_transformer.transform(np.atleast_2d(s))

    return np.array([m.predict(X)[0] for m in self.models])

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

    X = self.feature_transformer.transform(np.atleast_2d(s))

    self.models[a].partial_fit(X, [G])

  def sample_action(self, s, eps):

    if np.random.random() < eps:

      return self.env.action_space.sample()

    else:

      return np.argmax(self.predict(s))

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

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

    prev_observation = observation

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

    if done:

      reward = -200

    # update the model

    next = model.predict(observation)

    assert(len(next.shape) == 1)

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

    model.update(prev_observation, action, G)

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

      totalreward += reward

    iters += 1

  return totalreward

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

def main():

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

  ft = FeatureTransformer(env)

  model = Model(env, ft)

  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 = 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, eps, gamma)

    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: :???: :?: :!: