Начало работы с градиентами политики

Алгоритм REINFORCE - один из первых алгоритмов градиента политики в обучении с подкреплением и отличная отправная точка для перехода к более продвинутым подходам. Градиенты политики отличаются от алгоритмов Q-значения, потому что PG пытаются изучить параметризованную политику вместо оценки Q-значений пар состояние-действие. Таким образом, выход политики представлен как распределение вероятностей по действиям, а не как набор оценок Q-значения. Если что-то из этого сбивает с толку или непонятно, не волнуйтесь, мы разберем это шаг за шагом!

TL;DR

В этом посте мы рассмотрим алгоритм REINFORCE и протестируем его в среде OpenAI CartPole с PyTorch. Мы предполагаем базовое понимание обучения с подкреплением, поэтому, если вы не знаете, что означают состояния, действия, окружающая среда и т.п., ознакомьтесь с некоторыми из ссылок на другие статьи здесь или простых учебников по этой теме здесь.

Политика и ценности действий

Мы можем отличить алгоритмы градиента политики от подходов с Q-значением (например, Deep Q-Networks) в том, что градиенты политики делают выбор действия без ссылки на значения действия. Некоторые градиенты политики узнают оценку значений, чтобы помочь найти лучшую политику, но эта оценка значения не требуется для выбора действия. Выходные данные DQN будут вектором оценок стоимости, а выходные данные градиента политики будут распределением вероятностей по действиям.

Например, представьте, что у нас есть две сети, сеть политик и сеть DQN, которые изучили задачу CartPole с двумя действиями (левым и правым). Если мы передадим каждому состояние s, мы можем получить из DQN следующее:

И это из градиента политики:

DQN дает нам оценки дисконтированных будущих вознаграждений государства, и мы делаем наш выбор на основе этих значений (обычно беря максимальное значение в соответствии с некоторым ϵ-жадным правилом). С другой стороны, градиент политики дает нам вероятность наших действий. В этом случае мы делаем наш выбор, выбирая действие 0 в 28% случаев и действие 1 в 72% случаев. Эти вероятности будут меняться по мере того, как сеть приобретает больше опыта.

Ценности к вероятностям

Чтобы получить эти вероятности, мы используем простую функцию под названием softmax на уровне вывода. Функция представлена ​​ниже:

Это сводит все наши значения к диапазону от 0 до 1 и гарантирует, что сумма всех выходов равна 1 (Σ σ (x) = 1). Поскольку мы используем функцию exp (x) для масштабирования наших значений, самые большие из них имеют тенденцию доминировать и получают большую вероятность, присвоенную им.

Алгоритм REINFORCE

Теперь о самом алгоритме.

Если вы подписались на некоторые предыдущие сообщения, это не должно выглядеть слишком пугающим. Однако для ясности мы все равно рассмотрим его.

Требования довольно просты, нам нужна дифференцируемая политика, для которой мы можем использовать нейронную сеть, и несколько гиперпараметров, таких как размер нашего шага (α), ставка дисконтирования (γ), размер пакета (K) и максимальное количество эпизодов (N). . Оттуда мы инициализируем нашу сеть и запускаем наши эпизоды. После каждого эпизода мы дисконтируем наши награды, которые представляют собой сумму всех дисконтированных наград, начиная с этой награды. Мы будем обновлять нашу политику после завершения каждого пакета (например, серий K). Это помогает обеспечить стабильность во время тренировок.

Потеря полиса (L (θ)) сначала кажется немного сложной, но ее не так уж сложно понять, если присмотреться к ней внимательно. Напомним, что выходом сети политики является распределение вероятностей. То, что мы делаем с π (a | s, θ), - это просто оценка вероятности нашей сети в каждом состоянии. Затем мы умножаем это на сумму дисконтированных вознаграждений (G), чтобы получить ожидаемую ценность сети.

Например, предположим, что мы находимся в состоянии s, сеть разделена между двумя действиями, поэтому вероятность выбора a = 0 составляет 50%, а a = 1 также составляет 50%. Сеть случайным образом выбирает a = 0, мы получаем награду 1, и серия заканчивается (предположим, что коэффициент дисконтирования равен 1). Когда мы возвращаемся и обновляем нашу сеть, эта пара состояние-действие дает нам (1) (0,5) = 0,5, что переводится в ожидаемое значение сети для этого действия, предпринятого в этом состоянии. Отсюда мы берем логарифм вероятности и суммируем все шаги в нашей серии эпизодов. Наконец, мы усредняем это значение и берем градиент этого значения для обновления.

На всякий случай напомню, что цель Cart-Pole - как можно дольше удерживать шест в воздухе. Вашему агенту необходимо определить, толкать ли тележку влево или вправо, чтобы удерживать ее в равновесии, не переступая при этом за края слева и справа. Если у вас еще не установлена ​​библиотека OpenAI, просто запустите pip install gym, и все будет готово. Кроме того, загрузите последнюю версию с pytorch.org, если вы еще этого не сделали.

Идем дальше и импортируем несколько пакетов:

import numpy as np
import matplotlib.pyplot as plt
import gym
import sys
import torch
from torch import nn
from torch import optim
print("PyTorch:\t{}".format(torch.__version__))
PyTorch:	1.1.0

Реализация в PyTorch

После импортирования наших пакетов мы собираемся создать простой класс с именем policy_estimator, который будет содержать нашу нейронную сеть. У него будет два скрытых слоя с функцией активации ReLU и выходом softmax. Мы также дадим ему метод под названием предсказать, который позволяет нам выполнять прямой проход через сеть.

class policy_estimator():
    def __init__(self, env):
        self.n_inputs = env.observation_space.shape[0]
        self.n_outputs = env.action_space.n
        
        # Define network
        self.network = nn.Sequential(
            nn.Linear(self.n_inputs, 16), 
            nn.ReLU(), 
            nn.Linear(16, self.n_outputs),
            nn.Softmax(dim=-1))
    
    def predict(self, state):
        action_probs = self.network(torch.FloatTensor(state))
        return action_probs

Обратите внимание, что вызов метода predict требует, чтобы мы преобразовали наше состояние в FloatTensor, чтобы PyTorch мог с ним работать. На самом деле, сам метод predict в PyTorch несколько излишний, поскольку тензор может быть передан непосредственно нашему network для получения результатов, но я включил его здесь только для ясности.

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

def discount_rewards(rewards, gamma=0.99):
    r = np.array([gamma**i * rewards[i] 
        for i in range(len(rewards))])
    # Reverse the array direction for cumsum and then
    # revert back to the original order
    r = r[::-1].cumsum()[::-1]
    return r — r.mean()

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

Теперь о самом алгоритме REINFORCE.

def reinforce(env, policy_estimator, num_episodes=2000,
              batch_size=10, gamma=0.99):
    # Set up lists to hold results
    total_rewards = []
    batch_rewards = []
    batch_actions = []
    batch_states = []
    batch_counter = 1
    
    # Define optimizer
    optimizer = optim.Adam(policy_estimator.network.parameters(), 
                           lr=0.01)
    
    action_space = np.arange(env.action_space.n)
    ep = 0
    while ep < num_episodes:
        s_0 = env.reset()
        states = []
        rewards = []
        actions = []
        done = False
        while done == False:
            # Get actions and convert to numpy array
            action_probs = policy_estimator.predict(
                s_0).detach().numpy()
            action = np.random.choice(action_space, 
                p=action_probs)
            s_1, r, done, _ = env.step(action)
            
            states.append(s_0)
            rewards.append(r)
            actions.append(action)
            s_0 = s_1
            
            # If done, batch data
            if done:
                batch_rewards.extend(discount_rewards(
                    rewards, gamma))
                batch_states.extend(states)
                batch_actions.extend(actions)
                batch_counter += 1
                total_rewards.append(sum(rewards))
                
                # If batch is complete, update network
                if batch_counter == batch_size:
                    optimizer.zero_grad()
                    state_tensor = torch.FloatTensor(batch_states)
                    reward_tensor = torch.FloatTensor(
                        batch_rewards)
                    # Actions are used as indices, must be 
                    # LongTensor
                    action_tensor = torch.LongTensor(
                       batch_actions)
                    
                    # Calculate loss
                    logprob = torch.log(
                        policy_estimator.predict(state_tensor))
                    selected_logprobs = reward_tensor * \  
                        torch.gather(logprob, 1, 
                        action_tensor).squeeze()
                    loss = -selected_logprobs.mean()
                    
                    # Calculate gradients
                    loss.backward()
                    # Apply gradients
                    optimizer.step()
                    
                    batch_rewards = []
                    batch_actions = []
                    batch_states = []
                    batch_counter = 1
                    
                avg_rewards = np.mean(total_rewards[-100:])
                # Print running average
                print("\rEp: {} Average of last 100:" +   
                     "{:.2f}".format(
                     ep + 1, avg_rewards), end="")
                ep += 1
                
    return total_rewards

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

Несколько моментов по реализации, всегда убедитесь, что ваши выходные данные из PyTorch преобразуются обратно в массивы NumPy, прежде чем передавать значения в env.step() или функции, такие как np.random.choice(), чтобы избежать ошибок. Кроме того, мы используем torch.gather(), чтобы отделить фактические предпринятые действия от вероятностей действий, чтобы убедиться, что мы правильно вычисляем функцию потерь, как описано выше. Наконец, вы можете изменить окончание, чтобы алгоритм останавливался после «решения» среды вместо выполнения заданного количества шагов (CartPole решается после среднего балла 195 или более для 100 последовательных эпизодов).

Чтобы запустить это, нам просто нужно несколько строк кода, чтобы собрать все воедино.

env = gym.make('CartPole-v0')
policy_est = policy_estimator(env)
rewards = reinforce(env, policy_est)

Изложив результаты, мы видим, что он работает неплохо!

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

Посмотрите, что вы можете сделать с этим алгоритмом в более сложных условиях!