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

강화학습 - (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의 학습율($\alpha$), 0.99의 감가율($\gamma$)을 사용합니다.
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
반응형