강화학습/OpenAI gym

OpenAI gym을 이용해 SARSA 구현하기

APinCan 2020. 3. 21. 20:29

점점 진화해서 OpenAI gym의 FrozenLake-v0를 이용해 SARSA를 구현해보자

얘는 앞의 시간차예측에서 정책을 사용하는 대신 각 action마다 큐함수를 사용하는 것임. 그리고 각 action을 선택할때마다 \( \epsilon \)-greedy action방법을 사용해 action을 선택함.

SARSA의 경우는 온폴리시 시간차제어이기때문에 잘못된 큐함수를 학습할 수도 있음.

 

 

import gym
import numpy as np
import random

EPSILON=0.1
TIMES=1000000
ALPHA=0.1
GAMMA=0.9

이번에도 타임스텝만을 이용해 구현.

새롭게 엡실론이 필요한데 이는 위에 설명한 엡실론 그리디방법때문.

 

env = gym.make("FrozenLake-v0")

어디서 많이 본 얼음호수

 

def action_select(state, q_function):
    actions = q_function[state]
    rand_prob = random.random()
    action = 0
    
    max_val = np.max(actions)
    # max인 인덱스 저장
    max_list = [idx for idx, i in enumerate(actions) if i==max_val]
    # greedy
    if rand_prob >= EPSILON:
        action = random.choice(max_list)
    # non-greedy
    else:
        actions_idx= np.delete([0,1,2,3], max_list)
        if len(actions_idx)==0:
            action = 0
        else:
            action = random.choice(actions_idx)
        
    return action

어디서 많이 본 action_select인데 다만 여기서는 \( \epsilon \)-greedy action 선택 방법을 사용.

일단 내가 greedy하게 action을 선택하기 위해서는 현재 state에서 aciton들의 큐함수 값을 모두 알아야하므로 큐함수와 state를 인자로 전달. 그리고 \( \epsilon \)의 확률로 greedy하지 않은 것을 선택하고 1-\( \epsilon \)의 확률로 greedy한 action을 선택함.

큐함수 중 가장 큰 값을 max_val에 저장하고 가장 큰 값이 하나만 있는게 아닐 수 있으므로 max_list에 큰 값을 갖는 값들의 인덱스를 저장함. 그리고 greedy인지 아닌지 확률을 통해 계산.

greedy인 경우 그냥 max_list에서 하나 꺼내면 되고 non-greedy인 경우 max_list에 있는 것들을 제거함. 근데 그게 만약 빈 리스트이면 그냥 action=0을 줘버리고 다른 경우는 나머지에서 선택함. 그냥 큐함수가 max인 인덱스를 제외시키고 나머지에서 action을 선택한다고 생각하면 됨.

 

q_function = [[0, 0, 0, 0] for i in range(env.observation_space.n)]
observation = env.reset()

for t in range(TIMES):
    action = action_select(observation, q_function)
    current_q = q_function[observation][action]

    next_observation, reward, done, info = env.step(action)
    
    next_action = action_select(next_observation, q_function)   
    next_q = q_function[next_observation][next_action]
    q_function[observation][action] = current_q + ALPHA*(reward + GAMMA*next_q - current_q)
    
    if done:
        observation = env.reset()
        
    observation = next_observation
        
print(np.array(q_function, dtype="U").reshape(4,4,4))

main부분.

일단 q함수를 모두 0으로 초기화하고 환경시작, 타임스텝만큼 for문을 돌림.

현재 state=observation에서 \( \epsilon \)-greedy 방법으로 action을 선택한후 그 action의 큐함수를 current_q에 저장. 그리고 선택한 action으로 한 스텝 진행시킴. 그러면 거기서 next_state와 reward 등의 정보가 나옴.

이 정보를 이용해 얻은 next_state로 또 다시 \( \epsilon \)-greedy방법을 이용해 next_action을 구함. 그리고 next_state와 next_action을 이용해 큐함수를 구함. 

그리고 q함수를 업데이트하는 식을 이용해 큐함수를 업데이트 시킴. 그리고 밑의 결과를 보자.

 

보기 좀 어려우니까 좀 쉽게 보기 위해 함수를 또 하나 만들어보자.

 

def printprint(states):
    states = np.round(states, 9)
    print("      ",states[0][3],"                 ", states[1][3],"                 ",  states[2][3],"                 ", states[3][3])
    print(states[0][0],"   ", states[0][2], " ",
          states[1][0],"   ", states[1][2], " ",
          states[2][0],"   ", states[2][2], " ",
          states[3][0],"   ", states[3][2])
    print("      ",states[0][1],"                  ", states[1][1],"                 ", states[2][1],"                 ", states[3][1])
    print("ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ")

어차피 어떻게 보이느냐가 중요한 것도 아니니 printprint를 대충 눈대중으로 만들고

printprint(q_function[0:4])
printprint(q_function[4:8])
printprint(q_function[8:12])
printprint(q_function[12:16])

다음과 같이 출력해보자

 

100만번
300만번
500만번
천만번

천만번쯤하니 경향이 좀 보이는 듯? 가면 안되는 곳의 큐함수값은 낮고 가야하는 곳은 높아짐.

이상하게 얼음호수의 경계에 있는 큐함수값들이 높은 것을 볼 수 있는데 이는 경계를 나가면 다시 현재 state로 돌아오기 때문에 그런 것 같음. 적어도 장애물을 만나지는 않으니까? 높은 value가 나오는 듯.

 

다음에는 큐러닝을 구현해보자