Deep learning

[Reinforcement Learning / learn article] Q-Learning

JaykayChoi 2017. 2. 23. 23:45

2015년 Deepmind 팀에서 네이처에 발표한 Human-level control through deep reinforcement learning 을 공부하기 위해 

기초 지식인 Q-Learning 을 공부하기 위해 자료를 찾던 중 

https://medium.com/emergent-future/simple-reinforcement-learning-with-tensorflow-part-0-q-learning-with-tables-and-neural-networks-d195264329d0#.3q19hd14u

위 블로그를 찾게 되어 이 분의 코드를 이용하여 공부를 해봤습니다.


강화 학습(Reinforcement Learning)은 행동에 따른 보상을 지급하여 좋은 점수를 획득하기 위한 행동을 하게 만드는 학습법으로 학습을 시키기위해 어떤 환경 안에서 정의된 에이전트가 현재의 상태를 인식하여, 선택 가능한 행동들 중 보상을 최대화하는 행동 혹은 행동 순서를 선택하는 방법이라고 합니다.

https://ko.wikipedia.org/wiki/%EA%B0%95%ED%99%94_%ED%95%99%EC%8A%B5


예를 들어 이 포스팅에서 사용할 OpenAI gym에서 제공하는 FrozenLake 라는 게임은 꽁꽁 얼어 있는 강바닥이라는 환경에서 에이전트가 출발점에서 목표점까지 구멍에 빠지지 않고 이동하는 것이 목적인 게임입니다. 게임 환경은 아래와 같이 정의되며 시작점(S)에서 구멍(H)을 피해 얼어있는 강바닥(F)만을 따라 목표점(G)에 도달하는 게임입니다. 

이때 에이전트가 시작점 S에서 어떤 방향으로 이동을 해야될지 알기 위해 Q-Learning 을 통해 학습된 Q-table 의 값에 따라 이동 방향을 결정할 수 있습니다. 

학습된 Q-table 을 이용해 이동 방향을 결정하는 방법은 다음과 같은 식을 통해 수식화 할 수 있습니다.

Q(s,a) = r + γ(max(Q(s’,a’))

s: state

a: action

r: reward

γ: maximum discounted

s’: next state

a’: next action

Q(s,a) 는 Q-table에 상태(s)와 행동(a)을 인자로 넣어 얻어진 Q value 이며 이 값을 r + γ(max(Q(s’,a’)) 으로 업데이트 함으로써  Q-table 이 학습되게 됩니다. max(Q(s’,a’))은 미래의 보상들 중 최대값이고 여기에 γ 을 곱하고 현재의 보상(r)을 더한 값입니다. 여기에 γ 을 곱하는 이유는 똑같은 이동 경로라도 최소의 경로를 얻기 위함입니다. γ 은 보통 0~1 사이의 값을 사용하게 되는데 이는 경로가 길어짐에 따라 γ 의 영향을 많이 받아 보상값이 줄어들게 됩니다. Q-table 은 학습이 전혀 되기 전에는 의미가 없는 값이겠지만 학습이 되면서 점차 테이블에 값이 쌓이기 시작하면 실제로 의미있는 값을 전달할 수 있을 것입니다. 

이런 것을 Q-table 이라 한 이유는 각 state 마다 취하는 액션에 따른 reward 값이 기록되어 있기 때문입니다. 이 환경의 경우 4*4 의 각 state에 액션 4종류에 따른 값이 테이블에 기입되어 있을 것입니다. (16*4)

다음은 Q-table 을 이용한 Q-Learning 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import gym
import numpy as np
 
env = gym.make('FrozenLake-v0')
 
#Initialize table with all zeros
= np.zeros([env.observation_space.n,env.action_space.n])
# Set learning parameters
lr = .85
= .99
num_episodes = 2000
#create lists to contain total rewards and steps per episode
#jList = []
rList = []
for i in range(num_episodes):
    #Reset environment and get first new observation
    s = env.reset()
    rAll = 0
    d = False
    j = 0
    #The Q-Table learning algorithm
    while j < 99:
        j+=1
        #Choose an action by greedily (with noise) picking from Q table
        a = np.argmax(Q[s,:] + np.random.randn(1,env.action_space.n)*(1./(i+1)))
        #Get new state and reward from environment
        s1,r,d,_ = env.step(a)
        #Update Q-Table with new knowledge
        Q[s,a] = Q[s,a] + lr*(r + y*np.max(Q[s1,:]) - Q[s,a])
        rAll += r
        s = s1
        if d == True:
            break
    #jList.append(j)
    rList.append(rAll)
 
 
print "Score over time: " +  str(sum(rList)/num_episodes)
 
print "Final Q-Table Values"
print Q
cs


우선 gym 을 import 합니다. gym은 OpenAI 에서 공개한 toolkit 으로 Reinforcement Learning 을 위한 다양한 게임 환경을 제공합니다. OpenAI은 gym에 이어 Universe 도 공개하는 등 인공지능을 오픈소스화하기 위해 설립된 미국의 비영리 연구기관으로 기계 학습을 공부하는데 많은 도움을 받을 수 있습니다.

다음으로 FrozenLake 게임 환경을 사용하기 위해 환경을 만듭니다. FrozenLake 게임은 이동을 방해하는 게임 요소도 가지고 있는데 예를 들어 우측 이동 키를 입력해도 일정 랜덤한 확률에 따라 이동이 방해되어 이동이 안되거나 미끄러져 다른 곳으로 이동하는 게임 요소독 가지고 있습니다. 

env = gym.make('FrozenLake-v0')
cs



다음으로 Q-table 을 만들고 0으로 값을 채웁니다. 이때 테이블 배열의 크기는 환경에서 가지고 올 수 있습니다.

= np.zeros([env.observation_space.n,env.action_space.n])
cs



이제 학습을 위해 2천번의 반복을 행하고 그 반복동안 결과를 배열에 저장합니다.

num_episodes = 2000
 
rList = []
 
for i in range(num_episodes):
cs


학습의 시작에 앞서 환경을 초기화하고 반환값으로 에이전트(옵져버)의 첫 state 값을 반환받습니다.

= env.reset()
cs



이제 학습을 시작하는데 각 학습 당 최대 수행횟수를 정하고 만약 목표점에 도달할 경우 해당 학습을 종료시키기 위해 d(done) 변수값을 이용합니다.

= False
 
= 0
 
    while j < 99:
 
        j+=1
 
 
cs


이제 액션을 구하기 위해 아래와 같은 코드를 실행합니다. 여기서 바로 Q-table 에서 위에서 설명한 Q(s,a) = r + γ(max(Q(s’,a’)) 식을 이용해 바로 액션을 구하는 것이 아니라 greedily (with noise) 방법을 이용해 학습이 더욱 잘 되도록 합니다. 이런 노이즈를 주는 이유는 FrozenLake 게임만 봐도 시작점에서 목표점까지 도달하는 방법은 여러 가지 경로가 있을 수 있는데 이런 노이즈가 없을 경우 처음 한 번 목표점에 도달할 경우 그 경로의 reward 값이 높기 때문에 해당 경로로만 계속 이동하게 됩니다. 이 경우 다른 더 좋은 경로가 있는지 확인을 하지 못하기 때문에 노이즈를 줘서 이미 알려진 좋은 경로뿐 아니라 다른 경로로도 시도를 하게 만들어 줄 수 있습니다. 여기서 사용한 방법은 max(Q(s’,a’)) 을 구할 때 얻어진 값에 랜덤값을 더하는 방법입니다. 단, 이때 1./(i+1) 값을 곱해줘서 학습 초반에는 랜덤값을 많이 더해주고 학습 후반에는 랜덤값을 조금만 주는 방법을 사용했습니다. 

= np.argmax(Q[s,:] + np.random.randn(1,env.action_space.n)*(1./(i+1)))
cs



액션값을 얻었기 때문에 아래와 같이 스탭을 수행하고 다음 state, reward, done, info 를 순서대로 얻게 됩니다.

s1,r,d,_ = env.step(a)
cs



s1 과 r 을 얻었기 때문에 이 값으로 아래와 같이 테이블을 업데이트 합니다. lr은 learning rate 로 위에서 0.85로 값을 초기화하였고 y는 maximum discounted 으로 0.99값을 넣었습니다. 여기서 바로 테이블을 업데이트하는 것이 아니라 learning rate을 사용하는 이유는 FrozenLake 환경이 랜덤한 바람과 미끄러짐등의 랜덤 요소가 있는 환경이기 때문에 얻어진 값을 그대로 학습하지 않고 learning rate 을 넣고 학습을 하면 학습효과가 좋아집니다.

Q[s,a] = Q[s,a] + lr*(r + y*np.max(Q[s1,:]) - Q[s,a])
cs



이렇게 Q-table 을 이용한 훈련을 할 경우 성공 확률이 50% 정도가 나오게 됩니다. 썩 좋은 결과는 아니지만 일반적인 사람이 직접 플레이한 결과보다는 좋은 결과입니다.



다음으로 FrozenLake 같은 단순한 게임이 아닌 테이블로 표현하기 힘든 큰 배열이 필요한 환경에는 Q-table 대신에 neural network 을 이용하여 강화학습을 시도할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import gym
import numpy as np
import random
import tensorflow as tf
import matplotlib.pyplot as plt
 
 
env = gym.make('FrozenLake-v0')
 
tf.reset_default_graph()
 
#These lines establish the feed-forward part of the network used to choose actions
inputs1 = tf.placeholder(shape=[1,16],dtype=tf.float32)
= tf.Variable(tf.random_uniform([16,4],0,0.01))
Qout = tf.matmul(inputs1,W)
predict = tf.argmax(Qout,1)
 
#Below we obtain the loss by taking the sum of squares difference between the target and prediction Q values.
nextQ = tf.placeholder(shape=[1,4],dtype=tf.float32)
loss = tf.reduce_sum(tf.square(nextQ - Qout))
trainer = tf.train.GradientDescentOptimizer(learning_rate=0.1)
updateModel = trainer.minimize(loss)
 
 
init = tf.global_variables_initializer()
 
# Set learning parameters
= .99
= 0.1
num_episodes = 2000
#create lists to contain total rewards and steps per episode
jList = []
rList = []
with tf.Session() as sess:
    sess.run(init)
    for i in range(num_episodes):
        #Reset environment and get first new observation
        s = env.reset()
        rAll = 0
        d = False
        j = 0
        #The Q-Network
        while j < 99:
            j+=1
            #Choose an action by greedily (with e chance of random action) from the Q-network
            a,allQ = sess.run([predict,Qout],feed_dict={inputs1:np.identity(16)[s:s+1]})
            if np.random.rand(1< e:
                a[0= env.action_space.sample()
            #Get new state and reward from environment
            s1,r,d,_ = env.step(a[0])
            #Obtain the Q' values by feeding the new state through our network
            Q1 = sess.run(Qout,feed_dict={inputs1:np.identity(16)[s1:s1+1]})
            #Obtain maxQ' and set our target value for chosen action.
            maxQ1 = np.max(Q1)
            targetQ = allQ
            targetQ[0,a[0]] = r + y*maxQ1
            #Train our network using target and predicted Q values
            _,W1 = sess.run([updateModel,W],feed_dict={inputs1:np.identity(16)[s:s+1],nextQ:targetQ})
            rAll += r
            s = s1
            if d == True:
                #Reduce chance of random action as we train the model.
                e = 1./((i/50+ 10)
                break
        jList.append(j)
        rList.append(rAll)
print "Percent of succesful episodes: " + str(sum(rList)/num_episodes) + "%"
 
plt.plot(rList)
plt.plot(jList)
 
plt.show()
cs


neural network 을 구성하기위한 tensorflow 와 결과를 그래프로 보기위해 matplotlib.pyplot 을 import 합니다.

import tensorflow as tf
 
import matplotlib.pyplot as plt
cs


먼저 액션을 선택하기 위해 사용되는 neural network의 feed forward 부분을 구현해야 됩니다.

input 을 state 의 수에 따라 1*16 배열의 placeholder 으로 선언합니다. 

inputs1 = tf.placeholder(shape=[1,16],dtype=tf.float32)
cs



weight 는 결과적으로 1*4 크기의 배열로 얻기 위해 16*4 의 크기의 배열로 만듭니다. 이때 초기화는 0~0.01 사이의 값으로 랜덤하게 넣는 방법을 사용했습니다.

= tf.Variable(tf.random_uniform([16,4],0,0.01))
cs



inputs1 와 W의 곱으로 Qout 을 만들고 1*4 중 가장 큰 값을 predict 으로 만듭니다. 

Qout = tf.matmul(inputs1,W)
 
predict = tf.argmax(Qout,1)
cs



다음으로 레이블 역할을 하는 nextQ 을 placeholder 으로 만들고 다른 neural network와 마찬가지로 (y - y_)의 제곱의 평균 loss 를 최소화 하는 방향으로 trainer 모델을 만듭니다. 

Loss = ∑(Q-target - Q)²

MNIST 에서 레이블은 각 이미지에 따른 정답이지만 여기서는 Q-table와 같이 Q(s,a) = r + γ(max(Q(s’,a’)) 에서 얻어진 Q(s,a)가 레이블이 될 것입니다.

nextQ = tf.placeholder(shape=[1,4],dtype=tf.float32)
 
loss = tf.reduce_sum(tf.square(nextQ - Qout))
 
trainer = tf.train.GradientDescentOptimizer(learning_rate=0.1)
 
updateModel = trainer.minimize(loss)
cs



이제 환경을 만들고 반복하며 backpropagation 을 이용하는 방법으로 훈련을 시작합니다.

먼저 predict 을 통해 하나의 allQ 와 allQ 중 가장 커 선택된 값인 a를 구합니다. 

여기서도 greedily 하게 랜덤 요소를 주었는데 이번에는 일정 확률로 무작위값으로 a 을 만드는 방법을 사용했습니다. 역시 Q-table 소스와 마찬가지로 초반에는 높은 확률로 랜덤값을 사용하고 학습이 진행될수록 낮은 확률로 랜덤값을 사용하게 되어있습니다. (뒷쪽 코드에서 e 값을 정의) 

그리고 1*16 배열의 inputs1 에 값을 넣을 때 np.identity(16)[s:s+1] 와 같은 방법을 사용했습니다. np.identity(16) 을 사용하면 16*16의 배열을 반환하게 되는데 이 배열의 첫 번째 인자는 (1,0,0,0...,0) 와 같은 배열이고 두 번째 인자는 (0,1,0,0....,0) 와 같은 배열입니다. 따라서 np.identity(16)[s:s+1] 을 하게되면 s번째 인자가 1이고 나머지 값이 0인 배열을 얻을 수 있게 되고 그 값을 inputs1에 넣음으로써 에이전트의 현재 위치를 넣은 Qout을 얻을 수 있게 됩니다.

다음으로 a의 경우 tensorflow의 sess.run을 통해 얻어진 값이기 때문에 tensor의 기본적의 특징상 배열로 구성된 값이기 때문에 env.action_space.sample() 으로 랜덤하게 action을 구할 때도 a가 아닌 a[0] 에 값을 넣게 됩니다.

a,allQ = sess.run([predict,Qout],feed_dict={inputs1:np.identity(16)[s:s+1]})
 
            if np.random.rand(1< e:
 
                a[0= env.action_space.sample()
cs



이제 얻은 a를 넣어 step을 수행해 다음 state, reward, done, info 를 얻습니다.

s1,r,d,_ = env.step(a[0])
cs


이제 얻어진 값으로 weight 을 학습시키기 위해 새로운 상태 s1을 넣어서 Q1 을 구해서

Q1 = sess.run(Qout,feed_dict={inputs1:np.identity(16)[s1:s1+1]})
cs



Q(s,a) = r + γ(max(Q(s’,a’))을 만듭니다. allQ, targetQ 는 위에서 s 을 넣고 얻어진 1*4의 배열이기 때문에 targetQ[0,a[0]] 는 Q(s,a) 을 의미하고 s1을 넣어 구한 Q1 의 max 값과 r 을 곱한 r + y*maxQ1 이 r + γ(max(Q(s’,a’)) 이 됩니다.

maxQ1 = np.max(Q1)
 
targetQ = allQ
 
targetQ[0,a[0]] = r + y*maxQ1
cs


이제 얻어진 값들로 학습을 시키고 만약 d이 true일 경우 목표점에 도달한 경우이므로 현 에피소드를 중지시키고 다음 에피소드를 반복합니다.

_,W1 = sess.run([updateModel,W],feed_dict={inputs1:np.identity(16)[s:s+1],nextQ:targetQ})
            rAll += r
            s = s1
            if d == True:
                #Reduce chance of random action as we train the model.
                e = 1./((i/50+ 10)
                break
cs


위와 같은 Q-Network 로 학습을 할 경우 Q-table 과 같은 방법을 사용했기 때문에 비슷한 정확도가 나오게 됩니다. 이제 여기서 정확도를 더 높이기 위해서는 Deep Q-Network (DQN) 과 같은 방법을 사용해야 될 것입니다.