Краткий обзор

В прошлый раз в нашем руководстве по Keras / OpenAI мы обсудили очень простой пример применения глубокого обучения в контекстах обучения с подкреплением. Оглядываясь назад, это было невероятное зрелище! Если вы посмотрите на данные обучения, то модели случайного шанса обычно будут способны выполнять только 60 шагов в среднем. И все же, обучаясь на этих, казалось бы, очень посредственных данных, мы смогли «превзойти» среду (т. Е. Получить производительность ›200 шагов). Как это возможно?

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

При этом среда, которую мы рассматриваем на этой неделе, значительно сложнее, чем на прошлой неделе: MountainCar.

Более сложные среды

Несмотря на то, что кажется, что мы можем применить ту же технику, которую применяли на прошлой неделе, есть одна важная особенность, которая делает это невозможным: мы не можем генерировать данные для обучения. В отличие от очень простого примера Cartpole, случайные движения часто просто приводят к тому, что испытание заканчивается у нас у подножия холма. То есть у нас есть несколько испытаний, которые в итоге имеют одинаковые значения -200. Это практически бесполезно для использования в качестве обучающих данных. Представьте, что вы были в классе, где независимо от того, какие ответы вы поставили на экзамене, вы получили 0%! Как вы собираетесь извлечь уроки из этого опыта?

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

Предпосылки теории DQN

Q-обучение (которое, кстати, ничего не означает) сосредоточено на создании «виртуальной таблицы», которая учитывает, сколько вознаграждения назначается за каждое возможное действие с учетом текущего состояния среды. Давайте разберем это по шагам:

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

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

Основная проблема описанного мной (поддержание виртуальной таблицы для каждой конфигурации ввода) заключается в том, что это невозможно: у нас есть непрерывное (бесконечное) пространство ввода! Мы могли бы обойти это, дискретизируя входное пространство, но это кажется довольно хакерским решением этой проблемы, с которым мы будем сталкиваться снова и снова в будущих ситуациях. Итак, как нам это обойти? Применяя нейронные сети к ситуации: вот откуда D в DQN!

Агент DQN

Итак, теперь мы свели проблему к поиску способа присвоения различных действий Q-score с учетом текущего состояния. Это ответ на очень естественный первый вопрос, на который нужно ответить при использовании любой NN: каковы входы и выходы нашей модели? Степень математики, которую вам необходимо понять для этой модели, представляет собой следующее уравнение (не волнуйтесь, мы его разберем):

Q, как уже упоминалось, представляет собой значение, оцененное нашей моделью с учетом текущего состояния (состояний) и предпринятых действий (а). Однако цель состоит в том, чтобы определить общую ценность состояния. Что я имею в виду? общее значение - это как немедленное вознаграждение, которое вы получите , так и ожидаемое вознаграждение, которое вы получите в будущем, занимая эту должность. То есть мы хотим учесть тот факт, что стоимость позиции часто отражает не только ее немедленную выгоду, но и будущую выгоду, которую она дает (черт возьми, глубокая). В любом случае, мы дисконтируем будущие вознаграждения, потому что, если я сравниваю две ситуации, в которых я ожидаю получить 100 долларов, одна из двух будет в будущем, я всегда буду соглашаться на настоящую сделку, поскольку положение будущей сделки может измениться между тем, когда Я заключил сделку и когда получу деньги. Гамма-фактор отражает эту амортизированную стоимость для ожидаемой будущей прибыли от государства.

Вот и все: это все, что нам для этого понадобится! Пора действительно перейти к коду!

Реализация агента DQN

Сеть Deep Q вращается вокруг непрерывного обучения, а это означает, что мы не просто собираем кучу данных испытаний / обучения и вводим их в модель. Вместо этого мы создаем обучающие данные с помощью запускаемых нами испытаний и вводим в них эту информацию непосредственно после запуска пробной версии. Если сейчас все это кажется несколько расплывчатым, не волнуйтесь: пора взглянуть на этот код. Код в основном вращается вокруг определения класса DQN, где фактически будет реализована вся логика алгоритма и где мы предоставляем простой набор функций для фактического обучения.

Гиперпараметры DQN

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

class DQN:
    def __init__(self, env):
        self.env     = env
        self.memory  = deque(maxlen=2000)
        
        self.gamma = 0.95
        self.epsilon = 1.0
        self.epsilon_min = 0.01
        self.epsilon_decay = 0.995
        self.learning_rate = 0.01

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

Итак, теперь мы обсуждаем гиперпараметры модели: гамма, эпсилон / эпсилон-распад и скорость обучения. Первый - это коэффициент амортизации будущего вознаграждения (‹1), рассмотренный в предыдущем уравнении, а последний - стандартный параметр скорости обучения, поэтому я не буду обсуждать его здесь. Второй, однако, интересный аспект RL, заслуживающий отдельного обсуждения. В любом виде обучения у нас всегда есть выбор между исследованием и эксплуатацией. Это не ограничивается информатикой или академическими науками: мы делаем это изо дня в день!

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

Эпсилон обозначает ту часть времени, которую мы посвятим исследованиям. То есть в части self.epsilon испытаний, мы просто предпримем случайное действие, а не то, которое мы прогнозировали бы как лучшее в этом сценарии. Как уже говорилось, мы хотим делать это чаще, чем не вначале, до того, как мы сформируем стабилизирующие оценки по этому вопросу, и поэтому инициализируем эпсилон до значения, близкого к 1,0 в начале, и уменьшаем его на некоторую долю

Модели DQN

Был один ключевой момент, который был исключен при инициализации DQN выше: фактическая модель, используемая для прогнозов! Как и в нашем оригинальном руководстве по Keras RL, нам напрямую предоставляются входные и выходные данные в виде числовых векторов. Таким образом, нет необходимости использовать в нашей сети более сложные уровни, кроме полносвязных. В частности, мы определяем нашу модель так:

def create_model(self):
        model   = Sequential()
        state_shape  = self.env.observation_space.shape
        model.add(Dense(24, input_dim=state_shape[0], 
            activation="relu"))
        model.add(Dense(48, activation="relu"))
        model.add(Dense(24, activation="relu"))
        model.add(Dense(self.env.action_space.n))
        model.compile(loss="mean_squared_error",
            optimizer=Adam(lr=self.learning_rate))
        return model

И используйте это, чтобы определить модель и целевую модель (объяснено ниже):

def __init__(self, env):
        self.env     = env
        self.memory  = deque(maxlen=2000)
        
        self.gamma = 0.95
        self.epsilon = 1.0
        self.epsilon_min = 0.01
        self.epsilon_decay = 0.995
        self.learning_rate = 0.01
        self.tau = .05
        self.model = self.create_model()
        # "hack" implemented by DeepMind to improve convergence
        self.target_model = self.create_model()

Тот факт, что существует две отдельных модели, одна для прогнозирования, а другая для отслеживания «целевых значений», определенно противоречит интуиции. Чтобы быть точным, роль модели (self.model) заключается в том, чтобы делать фактические прогнозы относительно того, какое действие следует предпринять, а целевая модель (self.target_model) отслеживает какое действие мы хотим предпринять в нашей модели.

Почему бы просто не создать единственную модель, которая бы сочетала и то, и другое? В конце концов, если что-то предсказывает действия, которые необходимо предпринять, не должно ли это косвенно определять, какую модель мы хотим использовать? На самом деле это одна из тех «странных уловок» в глубоком обучении, которые DeepMind разработал для достижения конвергенции в алгоритме DQN. Если вы используете одну модель, она может (и часто это делает) сходиться в простых средах (таких как CartPole). Но причина того, что она не сходится в этих более сложных средах, заключается в том, как мы обучаем модель: как упоминалось ранее, мы обучаем ее «на лету».

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

Обучение DQN

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

def remember(self, state, action, reward, new_state, done):
        self.memory.append([state, action, reward, new_state, done])

Здесь особо нечего отметить, кроме того, что мы должны сохранить фазу готово для того, как мы позже обновим функцию вознаграждения. Переходя к основной части нашей DQN, у нас есть функция train. Здесь мы используем нашу сохраненную память и активно учимся на том, что видели в прошлом. Мы начинаем с взятия образца из всей нашей памяти. Оттуда мы обрабатываем каждый образец по-разному. Как мы видели в уравнении ранее, мы хотим обновить функцию Q как сумму текущего вознаграждения и ожидаемых будущих вознаграждений (амортизированных на гамма). В случае, если мы находимся в конце испытаний, таких будущих наград нет, поэтому все значение этого состояния - это просто текущая награда, которую мы получили. Однако в нетерминальном состоянии мы хотим увидеть, какое максимальное вознаграждение мы получили бы, если бы смогли предпринять любое возможное действие, из которого мы получаем:

def replay(self):
        batch_size = 32
        if len(self.memory) < batch_size: 
            return
        samples = random.sample(self.memory, batch_size)
        for sample in samples:
            state, action, reward, new_state, done = sample
            target = self.target_model.predict(state)
            if done:
                target[0][action] = reward
            else:
                Q_future = max(
                    self.target_model.predict(new_state)[0])
                target[0][action] = reward + Q_future * self.gamma
            self.model.fit(state, target, epochs=1, verbose=0)

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

def target_train(self):
        weights = self.model.get_weights()
        target_weights = self.target_model.get_weights()
        for i in range(len(target_weights)):
            target_weights[i] = weights[i]
        self.target_model.set_weights(target_weights)

Действие DQN

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

def act(self, state):
        self.epsilon *= self.epsilon_decay
        self.epsilon = max(self.epsilon_min, self.epsilon)
        if np.random.random() < self.epsilon:
            return self.env.action_space.sample()
        return np.argmax(self.model.predict(state)[0])

Обучающий агент

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

def main():
    env     = gym.make("MountainCar-v0")
    gamma   = 0.9
    epsilon = .95
    trials  = 100
    trial_len = 500
    updateTargetNetwork = 1000
    dqn_agent = DQN(env=env)
    steps = []
    for trial in range(trials):
        cur_state = env.reset().reshape(1,2)
        for step in range(trial_len):
            action = dqn_agent.act(cur_state)
            env.render()
            new_state, reward, done, _ = env.step(action)
            reward = reward if not done else -20
            print(reward)
            new_state = new_state.reshape(1,2)
            dqn_agent.remember(cur_state, action, 
                reward, new_state, done)
            
            dqn_agent.replay()
            dqn_agent.target_train()
            cur_state = new_state
            if done:
                break
        if step >= 199:
            print("Failed to complete trial")
        else:
            print("Completed in {} trials".format(trial))
            break

Полный код

Итак, вот полный код, используемый для обучения в среде «MountainCar-v0» с использованием DQN!

Следите за следующим руководством по Keras + OpenAI!

Прокомментируйте и нажмите ❤️ ниже, чтобы выразить поддержку!