Loading [MathJax]/jax/output/CommonHTML/jax.js

데이터사이언스/강화학습

강화학습 - (26-2) REINFORCE 코드예제 2

_금융덕후_ 2020. 12. 29. 08:36
728x90
반응형

 

 

강화학습

 

패키지 설치

다음 코드는 세가지 패키지가 선행 되어야 합니다.

sudo apt-get install ffmpeg
pip install gym
pip install gym_minigrid

gym.render() 코드가 에러를 발생할 경우, 다음 패키지를 설치하고:
sudo apt-get install xvfb

주피터 노트북을 다음 명령어를 통해 실행합니다:
xvfb-run -s "-screen 0 1400x900x24" jupyter notebook

In [1]:
import warnings; warnings.filterwarnings('ignore')
import numpy as np
import pandas as pd
import random
import gym
import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torch.optim as optim

%matplotlib inline
 

환경

예제 코드는 그리드월드 예제에서 Deep SARSA 에이전트가 학습하는 코드 입니다.
에이전트가 최종 지점에 도달하면 보상을 받고,
에이전트가 취할 수 있는 행동은 다음 세가지 입니다.

  1. 왼쪽으로 회전
  2. 오른쪽으로 회전
  3. 전진
 

REINFORCE 에이전트

REINFORCE 에이전트 클래스를 만들어 줍니다.
REINFORCE에이전트는 0.001의 학습율(α), 0.99의 감가율(γ)을 사용합니다.
REINFORCE 에이전트는 Policy Gradient를 인공신경망을 사용해 함수 근사를 한 에이전트 입니다.
인공신경망의 레이어 수는 세개, 각각 32개의 노드를 사용합니다.
Softmax정책을 사용하기 때문에 마지막 활성함수는 Softmax함수가 사용됩니다.
옵티마이저는 Adam을 사용합니다.
또한 에이전트는 현재 에피소드의 기록을 저장할 수 있는 메모리를 가지고 있습니다.

In [2]:
class REINFORCE:
    def __init__(self, num_states, num_actions):
        self.num_states = num_states
        self.num_actions = num_actions
        self.alpha = 0.001
        self.gamma = 0.99
        self.pi = nn.Sequential(
            nn.Linear(self.num_states, 32),
            nn.ReLU(),
            nn.Linear(32, 32),
            nn.ReLU(),
            nn.Linear(32, self.num_actions),
            nn.Softmax()
        )
        self.optimizer = optim.Adam(self.pi.parameters(), lr=self.alpha)
        self.memory = []
        ...
 

정책

REINFORCE는 정책을 인공 신경망을 통해 학습합니다.
따라서 정책신경망의 pi에서 Softmax함수에 의해 각 행동을 취할 확률이 나오게 되고,
그 확률을 torch의 Categorical 분포에 넣어줍니다.
그리고 해당 분포에서 샘플을 하게 되면 하나의 행동이 나오게 됩니다.
CartPole과 같은 두개의 행동이 존재하는 문제에서는 Categorical대신 Binomial을 사용해도 무관합니다.

In [2]:
class REINFORCE:
    ...
    
    def act(self, state):
        with torch.no_grad():
            state = torch.FloatTensor(state)
            policy_probs = torch.distributions.Categorical(self.pi(state))

        return policy_probs.sample()
        
 

메모리

REINFORCE는 몬테카를로 학습을 기반으로 하기 때문에, 에피소드의 경험을 저장할 필요가 있습니다.
에이전트가 환경을 통해 경험을 하면 각각의 경험을 메모리에 저장하게 됩니다.

In [2]:
class REINFORCE:
    ...
        
    def append_sample(self, state, action, reward):
        state = torch.FloatTensor(state)
        reward = torch.FloatTensor([reward])
        self.memory.append((state, action, reward))
    ...
 

반환값 계산

몬테카를로 학습은 감가율이 적용된 누적 보상(discounted cumulative reward)을 최대화 합니다.
따라서 에피소드의 마지막 step에서 멀리 떨어져 있을 수록 더 감쇄된 보상으로 인식해야 합니다.

In [2]:
class REINFORCE:
    ...
        
    def calculate_returns(self, rewards):
        returns = torch.zeros(rewards.shape)
        g_t = 0
        for t in reversed(range(0,len(rewards))):
            g_t = g_t * .99 + rewards[t].item()
            returns[t] = g_t
        return returns.detach()
        
    ... 
 

정책 네트워크 학습

감가율이 적용된 누적 보상을 통해 정책 네트워크가 학습됩니다.
이때 주의할 점은, loss가 마이너스 일 수 있다는 점입니다.
에이전트가 학습하는 방식은 경사상승법(gradient ascent)이기 때문입니다.
학습이 끝나면 다음 에피소드를 위해 메모리를 초기화 합니다.

In [2]:
class REINFORCE:
    ...
    def update(self):
        states = torch.stack([m[0] for m in self.memory])
        actions = torch.stack([m[1] for m in self.memory]) 
        rewards = torch.stack([m[2] for m in self.memory])
        
        returns = self.calculate_returns(rewards)
        returns = (returns - returns.mean()) / returns.std()
        self.optimizer.zero_grad()
        policy_log_probs = self.pi(torch.FloatTensor(states)).log()
        policy_loss = torch.cat([-lp[a].unsqueeze(0) * g for a, lp, g in zip(actions, policy_log_probs, returns)])
        policy_loss = policy_loss.sum()
        policy_loss.backward()
        
        self.optimizer.step()
            
        self.memory = []
        return policy_loss.item()
    ...
 

환경 & 에이전트 초기화

환경과 REINFORCE 에이전트를 초기화 합니다.

In [4]:
env = gen_wrapped_env('MiniGrid-Empty-6x6-v0')
obs = env.reset()

agent = REINFORCE(obs.shape[0], 3)
 

에피소드 학습

5000번의 에피소드를 통해 학습합니다.
각 20번의 에피소드마다 리워드 값을 출력합니다.

In [5]:
rewards = []
for ep in range(5000):
    done = False
    obs = env.reset()
    action = agent.act(obs)

    ep_rewards = 0
    while not done:
        next_obs, reward, done, info = env.step(action)
        ep_rewards += reward
        next_action = agent.act(next_obs)

        agent.append_sample(obs, action, reward)
        obs = next_obs
        action = next_action

    pi_loss = agent.update()
    rewards.append(ep_rewards)
    if (ep+1) % 20 == 0:
        print("episode: {}, loss: {:.3f}, rewards: {:.3f}".format(ep+1, pi_loss, ep_rewards))
 
episode: 20, loss: 0.250, rewards: 0.118
episode: 40, loss: 0.000, rewards: 0.000
episode: 60, loss: 0.000, rewards: 0.000
episode: 80, loss: 0.142, rewards: 0.172
episode: 100, loss: 0.000, rewards: 0.000
episode: 120, loss: 0.000, rewards: 0.000
episode: 140, loss: 0.000, rewards: 0.000
episode: 160, loss: -1.303, rewards: 0.100
episode: 180, loss: 0.310, rewards: 0.370
episode: 200, loss: 0.000, rewards: 0.000
...
episode: 4800, loss: -6.360, rewards: 0.424
episode: 4820, loss: -0.382, rewards: 0.838
episode: 4840, loss: 0.000, rewards: 0.000
episode: 4860, loss: -0.212, rewards: 0.802
episode: 4880, loss: -7.828, rewards: 0.568
episode: 4900, loss: -0.441, rewards: 0.856
episode: 4920, loss: 4.281, rewards: 0.550
episode: 4940, loss: 0.100, rewards: 0.766
episode: 4960, loss: 2.619, rewards: 0.370
episode: 4980, loss: -0.460, rewards: 0.856
episode: 5000, loss: -4.992, rewards: 0.316
 

에피소드 시각화

다음은 최종 에피소드의 영상입니다.

In [6]:
show_video()
 
 

에피소드 시각화

다음은 가장 리워드를 높게 받은 에피소드의 영상입니다.

In [6]:
show_video()
 
 

학습 곡선 시각화

다음은 학습 시 받은 보상의 이동 평균(Moving Average)을 시각화 한 그래프 입니다.
비교 분석을 위해 SARSA, ExpectedSARSA, 그리고 Q러닝, 그리고 DeepSARSA의 결과가 포함되었고,
두 기법 모두5번의 시도 중 가장 좋은 그래프를 시각화 하였습니다.

결과에서 볼 수 있는 것과 같이, SARSA기반 에이전트들 보다는 Q러닝 에이전트의 성능이 좋았습니다.
REINFORCE의 경우 학습 초기 성능은 다른 에이전트들보다 떨어지지만,
학습 중반부터 후반까지는 비교된 다른 에이전트들보다 월등한 성능을 보이는 것을 확인할 수 있습니다.

In [7]:
pd.Series(rewards).to_csv('./logs/rewards_reinforce.csv')
In [8]:
sarsa_logs = pd.read_csv('./logs/rewards_sarsa.csv', index_col=False).iloc[:, 1]
q_logs = pd.read_csv('./logs/rewards_qlearning.csv', index_col=False).iloc[:, 1]
exp_sarsa_logs = pd.read_csv('./logs/rewards_expectedsarsa.csv', index_col=False).iloc[:, 1]
deepsarsa_logs = pd.read_csv('./logs/rewards_deepsarsa_gridworld.csv', index_col=False).iloc[:, 1]
reinforce_logs = pd.read_csv('./logs/rewards_reinforce.csv', index_col=False).iloc[:, 1]
In [10]:
plt.figure(figsize=(16, 8))
plt.plot(reinforce_logs.cumsum() / (pd.Series(np.arange(exp_sarsa_logs.shape[0]))+1), label="REINFORCE")
plt.plot(q_logs.cumsum() / (pd.Series(np.arange(q_logs.shape[0]))+1), label="QLearning")
plt.plot(sarsa_logs.cumsum() / (pd.Series(np.arange(sarsa_logs.shape[0]))+1), label="SARSA")
plt.plot(exp_sarsa_logs.cumsum() / (pd.Series(np.arange(exp_sarsa_logs.shape[0]))+1), label="ExpectedSARSA")
plt.plot(deepsarsa_logs.cumsum() / (pd.Series(np.arange(deepsarsa_logs.shape[0]))+1), label="DeepSARSA")
plt.legend()
Out[10]:
<matplotlib.legend.Legend at 0x7f6da35d4978>
 
In [ ]:
 

전체 코드

전체 코드는 다음 깃허브에서 확인할 수 있습니다.

728x90
반응형