1. Policy Gradient
Policy gradient는 DQN처럼 value function을 근사한 다음 주어진 상태의 value function 값에 따라 어떤 행동을
취할지 결정하는, 즉 implict poilcy를 가지는 다른 강화학습 알고리즘들과 달리 policy를 직접 함수로 나타낸 뒤 학
습하는 강화학습 알고리즘이다.
Policy Gradient 논문 을 따라가면서 이런 방식의 접근이 어떤 점에서 어려운지, 어떻게 해결했는지 알아보고
CartPole에 대해 구현한 코드를 살펴보자.
Policy Gradient를 적용하기 위한 제약조건은 단순하다. Policy를 근사하는 함수를 라고 할 때, 는 미분 가능해야
한다. Gradient 값을 근사해서 policy를 학습하는 것이 주 요지이므로 이는 당연한 제약이라 할 수 있다. Neural
Network 또한 미분 가능하기 때문에 Policy Gradient의 function approximator로 사용할 수 있다.
Policy function 는 상태 s, 행동 a, 매개변수 를 받아 상태 s에서 행동 a를 할 확률을 돌려주는 함수이다. 즉
이다. 실제로 어떤 행동을 할 지는 이 확률에 따라서 stochastic하게 고른다.
이제 우리는 policy가 얼마나 좋은지 측정할 수 있는 함수, 즉 loss function을 정의한 뒤 이를 통해 policy를 업데이
트 할 것이다. loss function에는 두가지 형태가 있는데, 여기서는 출발 상태가 로 고정 됐을 때의 loss function인
로 예시를 들겠다. 로 정의하면
가 성립한다. 직관적인 의미를 생각해보면 는 시간에 따른 비율을 고려한 보
상의 기대값의 합이고, 는 policy를 따라 행동했을 때 어떤 상태 s에서 머무르고 있을 확률을 뜻한다.
이제 해야 할 일은 값에 따라 를 gradient descent로 업데이트 해주면 된다. 하지만, 우리는 무한히 시뮬레이
션을 해 볼 수 없기 때문에 도 계산할 수 없고, 각 (상태, 행동) 쌍의 가치를 나타내는 도 알지 못한다. 이것을 알
고 있다면 그냥 Q-learning policy를 사용하면 될 일이다. 그러면 이걸 어떻게 계산해야 할까?
REINFORCE: Monte Carlo Policy Gradient
REINFORCE는 몬테카를로를 통해 위의 gradient 값을 근사하는 알고리즘 이름으로, Policy gradient 논문 이전에
발표된 것이다. 미분값이 , 즉 policy의 state distribution을 따르므로
라고 볼 수 있다. 여기서 expectation은 state, 즉 에 대해 취해진 것이다. 마찬가지로, 분자와 분모에 를 곱
해주면 가 되는 것을 알 수 있다.
그런데, 우리가 실제로 한번의 시뮬레이션을 통해 얻은 reward 합이 라고 하면, 함수의 정의에 의해
가 성립한다. 즉 로 고쳐 쓸 수 있고, 는 모두 우리가 직접 계산할 수
있는 값이기 때문에 충분한 샘플링을 거쳐 값을 근사할 수 있게 된다. 즉 다음 업데이트를 반복해주면 policy가
점점 좋아진다.
그런데 이렇게 업데이트를 진행할 경우, 이론상에선 문제가 없지만 실제 구현할 때에는 행동을 한 번 할 때마다 미
분값과 그 때 forward propagation 값을 저장해두어야 한다는 단점이 있다. 따라서 인 점을 활용해
loss function을 로 두면 기존 tensorflow나 pytorch 등의 라이브러리로 쉽게 구현할 수 있다.
2. 알고리즘을 정리하면 다음과 같다.
구현 (tensorflow)
텐서플로우를 사용해 policy gradient를 CartPole에 대해 구현한 코드를 살펴보자.
pg라는 클래스를 만들어 CartPole 환경과 텐서플로우 세션, 그리고 policy를 나타낼 neural network를 만들어준
다.
pg의 멤버 변수를 모두 세팅한 후, 실험은 위와 같은 코드로 진행할 것이다.
class pg:
def __init__(self):
self.env = gym.make('CartPole-v0')
self.sess = tf.Session()
self.net = self.build_policy()
self.sess.run(tf.global_variables_initializer())
def main():
agent = pg()
agent.train()
if __name__ == '__main__':
main()
'''
CartPole의 observation을 state로 받아서
각 행동의 unnormalized prob.을 내보내는 네트워크.
[N, 4] -> [N, 2]
reward : Sutton book의 REINFORCE 중 각 time step 별 G를 모은 리스트. reward[t] == time step t
에서의 G
action : 각 time step 별 선택됐던 action은 1, 아니면 0
'''
def build_policy(self):
state = tf.placeholder(tf.float32, shape=(None, 4))
3. 위의 코드는 실제 policy를 나타낼 neural network를 구성하는 코드다. 각 라인별로 살펴보면,
한 에피소드 동안 쌓인 상태/보상/취했던 행동을 입력으로 받아서
64개의 유닛을 가진 히든레이어 하나 짜리 DNN을 만들고, 출력을 softmax 한 값을 각 행동을 취할 확률로 삼는다.
reward = tf.placeholder(tf.float32, shape=(None, 1))
action = tf.placeholder(tf.float32, shape=(None, 2))
layer = tf.layers.dense(state, 64, activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
logit = tf.layers.dense(layer, 2, activation=None,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
log_prob = tf.nn.log_softmax(logit)
prob = tf.nn.softmax(logit)
loss = tf.multiply(log_prob, action)
loss = tf.reduce_sum(loss, axis=1, keepdims=True)
loss = tf.multiply(loss, reward)
loss = -tf.reduce_sum(loss) +
tf.reduce_sum(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES))
optimize = tf.train.AdamOptimizer(LR).minimize(loss) # 1e-2 / 1e-4
return {
'optimize': optimize, 'loss': loss, 'log_prob': log_prob,
'state': state, 'reward': reward, 'action': action,
'prob': prob
}
state = tf.placeholder(tf.float32, shape=(None, 4))
reward = tf.placeholder(tf.float32, shape=(None, 1))
action = tf.placeholder(tf.float32, shape=(None, 2))
layer = tf.layers.dense(state, 64, activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
logit = tf.layers.dense(layer, 2, activation=None,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
log_prob = tf.nn.log_softmax(logit)
prob = tf.nn.softmax(logit)
loss = tf.multiply(log_prob, action)
loss = tf.reduce_sum(loss, axis=1, keepdims=True)
loss = tf.multiply(loss, reward)
loss = -tf.reduce_sum(loss) +
tf.reduce_sum(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES))
4. 위의 식에 따라 loss를 계산한다. 이 때 주의할 점은 우리가 정의한 는 최소화의 대상이 아니라 최대화의 대상이기
때문에, tf.reduce_sum(loss)에 -1을 곱해줘야 한다.
이제 optimizer를 설정해주고 각 오퍼레이터를 사용할 수 있게 리턴해준다.
이제 실제 학습을 어떻게 하는 지 알아보자.
optimize = tf.train.AdamOptimizer(LR).minimize(loss) # 1e-2 / 1e-4
return {
'optimize': optimize, 'loss': loss, 'log_prob': log_prob,
'state': state, 'reward': reward, 'action': action,
'prob': prob
}
def train(self):
gamma = 0.9
history = []
for episode in range(1, 10000 + 1):
#render = True if episode % 100 == 1 else False
render = False
obs, done, state, reward, action, G = self.env.reset(), False, [], [], [], []
while not done:
if render:
self.env.render()
prob = self.sess.run(self.net['prob'], feed_dict={
self.net['state']: np.array(obs).reshape((-1, 4))
})[0]
act = np.random.choice(list(range(2)), p=prob)
state.append(obs)
obs, rew, done, _ = self.env.step(act)
rew = 0.0 if abs(rew) < 1e-5 else (1.0 if rew > 0 else -1.0)
action.append(act)
reward.append(rew)
for t in range(len(reward)):
reward[t] = reward[t] * pow(gamma, t)
for t in range(len(reward)):
G.append(np.sum(reward[t:]))
state = np.reshape(np.array(state), (-1, 4))
reward = np.reshape(np.array(G), (-1, 1))
reward = (reward - np.mean(reward)) / (np.std(reward) + 1e-5)
action = np.eye(2)[np.array(action).reshape(-1)] # one-hot
action = np.reshape(np.array(action), (-1, 2))
self.sess.run(self.net['optimize'], feed_dict={
self.net['state']: state, self.net['reward']: reward, self.net['action']: action
})
history.append(reward.shape[0])
if episode % 100 == 0:
5. 총 10000번의 에피소드(한 번 죽거나 성공하는게 한 에피소드)동안 학습을 진행한다.
각 에피소드가 시작될 때마다 초기화를 해주고,
현재 상태에 대한 확률 계산 -> 확률에 따른 행동 선택 -> 행동 수행 -> 결과 관찰 을 반복한다.
그리고 저장한 reward 값에 discount factor를 적용시켜준다. 즉 시간에 따른 감가상각을 반영해준다.
이제 python list로 저장된 상태 / 보상 / 행동을 numpy 로 가공한 뒤, optimize에 인자로 넘겨줘 policy를 업데이트
한다.
구현 (pytorch)
print('%dth try : %d step, avg %s' % (episode, reward.shape[0],
np.mean(history[-200:])))
render = False
obs, done, state, reward, action, G = self.env.reset(), False, [], [], [], []
while not done:
if render:
self.env.render()
prob = self.sess.run(self.net['prob'], feed_dict={
self.net['state']: np.array(obs).reshape((-1, 4))
})[0]
act = np.random.choice(list(range(2)), p=prob)
state.append(obs)
obs, rew, done, _ = self.env.step(act)
rew = 0.0 if abs(rew) < 1e-5 else (1.0 if rew > 0 else -1.0)
action.append(act)
reward.append(rew)
for t in range(len(reward)):
reward[t] = reward[t] * pow(gamma, t)
for t in range(len(reward)):
G.append(np.sum(reward[t:]))
state = np.reshape(np.array(state), (-1, 4))
reward = np.reshape(np.array(G), (-1, 1))
reward = (reward - np.mean(reward)) / (np.std(reward) + 1e-5)
action = np.eye(2)[np.array(action).reshape(-1)] # one-hot
action = np.reshape(np.array(action), (-1, 2))
self.sess.run(self.net['optimize'], feed_dict={
self.net['state']: state, self.net['reward']: reward, self.net['action']: action
})
6. pytorch로 구현한 버전도 살펴보자.
import random
import gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical
L2_REG = 1e-2
LR = 1e-2
'''
CartPole의 observation을 state로 받아서
각 행동의 unnormalized prob.을 내보내는 네트워크.
[N, 4] -> [N, 2]
reward : Sutton book의 REINFORCE 중 각 time step 별 G를 모은 리스트. reward[t] == time step t에서
의 G
action : 각 time step 별 선택됐던 action은 1, 아니면 0
'''
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(4, 32)
self.fc2 = nn.Linear(32, 2)
self.history = []
def forward(self, x):
state, reward, action = x
n = state.size()[0]
layer = F.relu(self.fc1(state))
logit = self.fc2(layer)
log_prob = F.log_softmax(logit, dim=1)
loss = log_prob * action
loss = torch.sum(loss, 1, True)
loss = loss * reward
loss = torch.sum(loss)
return -loss
def predict(self, x):
with torch.no_grad():
x = F.relu(self.fc1(x))
x = self.fc2(x)
res = F.softmax(x, dim=1)
return res
#return log_prob
class pg:
7. def __init__(self):
self.env = gym.make('CartPole-v0')
self.net = Net()
self.optimizer = optim.RMSprop(self.net.parameters(), lr=LR, weight_decay=L2_REG)
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(self.optimizer, mode='max')
def train(self):
gamma = 0.99
history = []
for episode in range(1, 10000 + 1):
render = True if episode % 100 == 0 and episode else False
obs, done, state, reward, action, G = self.env.reset(), False, [], [], [], []
while not done:
if render:
self.env.render()
prob = self.net.predict(torch.from_numpy(np.array(obs).reshape((-1, 4))
).float()).detach().numpy()[0]
act = np.random.choice(list(range(2)), p=prob)
state.append(obs)
obs, rew, done, _ = self.env.step(act)
action.append(act)
reward.append(rew)
G = [0.0 for _ in range(len(reward))]
Gr = 0.0
for i in range(len(reward)-1, -1, -1):
G[i] = Gr = reward[i] + gamma * Gr
state = np.reshape(np.array(state), (-1, 4))
reward = np.reshape(np.array(G), (-1, 1))
reward = (reward - np.mean(reward)) / (np.std(reward) + 1e-5)
action = np.eye(2)[np.array(action).reshape(-1)] # one-hot
action = np.reshape(np.array(action), (-1, 2))
state = torch.from_numpy(state).float()
reward = torch.from_numpy(reward).float()
action = torch.from_numpy(action).float()
self.optimizer.zero_grad()
loss = self.net([state, reward, action])
loss.backward()
self.optimizer.step()
if episode and episode % 50 == 0:
self.scheduler.step(np.mean(history[-200:]))
history.append(reward.shape[0])
if episode % 50 == 0:
print('%dth try : %d step, avg %s, lr %s' % (episode, reward.shape[0],
np.mean(history[-200:]), self.optimizer.param_groups[0]['lr']))
def main():
8. 텐서플로우 버전과 구조는 거의 비슷하다. 다른 점은
pytorch는 learning rate scheduling이 간편하기 때문에 적용했다는 점과,
optimize를 operator로 빼는 것이 아닌 gradient flush -> back propagation -> apply optimizer를 명시적으로 해
준다는 점이다. Learning rate scheduler의 mode가 max인 이유는 metric, 즉 평균 에피소드 길이를 최대화 하는
방향으로 학습해야하기 때문이다.
실제로 pytorch 버전을 실행시키면
900 에피소드 만에 학습이 완료되는 것을 볼 수 있다.
https://imgur.com/a/bkdaytw 에서 실제 학습 결과를 볼 수 있다.
agent = pg()
agent.train()
if __name__ == '__main__':
main()
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(self.optimizer, mode='max')
self.optimizer.zero_grad()
loss = self.net([state, reward, action])
loss.backward()
self.optimizer.step()
$ python torch_pg.py
?[33mWARN: gym.spaces.Box autodetected dtype as <class 'numpy.float32'>. Please provide explicit
dtype.?[0m
50th try : 200 step, avg 97.68, lr 0.01
100th try : 200 step, avg 137.47, lr 0.01
150th try : 200 step, avg 149.85333333333332, lr 0.01
200th try : 200 step, avg 161.53, lr 0.01
250th try : 200 step, avg 184.48, lr 0.01
300th try : 200 step, avg 188.565, lr 0.01
350th try : 178 step, avg 176.955, lr 0.01
400th try : 143 step, avg 172.125, lr 0.01
450th try : 200 step, avg 171.25, lr 0.01
500th try : 200 step, avg 171.86, lr 0.01
550th try : 200 step, avg 189.815, lr 0.01
600th try : 200 step, avg 183.51, lr 0.01
650th try : 200 step, avg 187.015, lr 0.01
700th try : 200 step, avg 177.83, lr 0.01
750th try : 200 step, avg 177.83, lr 0.01
800th try : 200 step, avg 189.825, lr 0.01
850th try : 200 step, avg 189.825, lr 0.01
900th try : 200 step, avg 200.0, lr 0.01
9. 참고문헌
[1] Richard S. Sutton, David McAllester, Satinder Singh, Yishay Mansou. Policy Gradient Methods for
Reinforcement Learning with Function Approximation.
[2] Sutton, R. S., Barto, A. G. Reinforcement Learning: An Introduction. 2nd edition draft.