이 장에서는 심층 강화 학습(deep reinforcement learning)에서 가장 중요한 기술인 정책 그래디언트(policy gradient)와 심층 Q-네트워크(deep-Q-networks, DQN)를 마르코프 결정 과정(Markov decision process, MDP)과 함께 소개합니다.
그 다음 최신 알고리즘을 매우 간단하게 구축할 수 있는 TF-Agents 라이브러리를 알아봅니다. 이 라이브러리를 사용해 유명한 아타리 게임인 브레이크아웃(Breakout)을 플레이하는 에이전트를 훈련합니다.
보상을 최적화하기 위한 학습
강화 학습에서 에이전트(agent)는 관측(observation)을 하고 주어진 환경(environment)에서 행동(action)을 합니다. 그 결과로 보상(reward)을 받습니다. 에이전트는 보상이 최대가 되도록 학습합니다.
다음은 강화 학습에서의 몇 가지 사례입니다.
- 에이전트는 실제 로봇일 수 있습니다. 환경은 실제 세상이고 카메라, 터치 센서 등으로 환경을 관찰합니다. 행동은 모터를 구동하기 위해 시그널을 전송하는 것이고, 목적지에 무사히 빨리 도착하면 양의 보상을 받습니다.
- 에이전트가 목표 온도를 맞추어 에너지를 절약하면 양의 보상을 얻고, 사람이 온도를 조작할 필요가 생기면 음의 보상을 받는 스마트 온도 조절기일 수 있습니다.
양의 보상이 전혀 없을 수도 있습니다. 예를 들어 에이전트가 미로 속을 움직인다면 매 타임 스텝마다 음의 보상을 받기 때문에 가능한 한 빨리 탈출구를 찾는 것이 좋을 것입니다.
강화 학습은 잘 들어맞는 작업 사례가 많습니다. 자율주행 자동차, 추천 시스템, 웹 페이지 광고를 배치하는 등의 작업이 있을 수 있습니다.
정책 탐색
에이전트의 행동을 결정하는 알고리즘을 정책(policy)이라고 합니다. 예를 들어, 관측을 입력으로 받고 행동을 출력하는 신경망이 정책이 될 수 있습니다.
정책으로 어떤 알고리즘도 사용할 수 있습니다. 예를 들어 30분 동안 수집한 먼지의 양을 보상으로 받는 로봇 청소기의 경우를 생각해봅시다. 특정 확률로 랜덤하게 움직이는 것처럼 정책에 무작위성이 포함된 것을 확률적 정책(stochastic policy)이라고 합니다. 로봇은 어떤 장소라도 먼지를 수집하려고 이상한 궤적을 만들어낼 것입니다.
여기에는 변경 가능한 두 개의 정책 파라미터(policy parameter)가 있습니다. 로봇이 전진할 확률 $p$와 각도 범위 $r$입니다. 이 파라미터를 튜닝해볼 수 있고, 가장 성능이 좋은 학습 알고리즘을 사용해볼 수 있습니다.
정책 탐색(policy search)과정에서 무작정 대입을 할때, 정책 공간(policy space)이 매우 크면 좋은 파라미터 조합을 찾는 것은 매우 힘듭니다.
정책 공간을 탐색하는 다른 방법으로 유전 알고리즘(genetic algorithm)이 있습니다. 예를 들어 1세대 정책 100개를 랜덤하게 생성한 후 성능이 좋은 20개를 살려서 각각 자식 정책 4개를 생산합니다. 이 자식 정책은 부모를 복사한 것에 약간의 무작위성을 더한 것입니다. 살아남은 정책과 그 자식은 2세대를 구성합니다. 이런 식으로 좋은 정책을 찾을 때까지 여러 세대에 걸쳐 반복합니다.
- 유전자풀(gene pool)에 다양성을 보존하기 위해 성능이 낮은 정책도 살아 남을 여지를 주는 것이 나을 때도 있습니다.
- 부모가 하나만 있는 경우 무성 생식, 둘 이상인 경우 유성 생식이라고 합니다. 자식의 게놈(genome, 일련의 정책 파라미터)은 부모의 게놈 일부분을 사용해 랜덤하게 구성됩니다.
또 다른 방법은 정책 파라미터에 대한 보상의 그레디언트를 평가해서 높은 보상의 방향을 따라 파라미터를 수정하는 것입니다. 이를 경사 상승법(gradient ascent)이라 합니다. 경사 하강법과 반대입니다. 즉, 목적 함수의 최소화가 아닌 최대화가 목적입니다. 이 방법을 정책 그레디언트(policy gradeint, PG)라 합니다.
예를 들면, p를 조금 증가시키고 30분 동안 수집한 먼지 양이 증가했는지 평가한 후 먼지의 양이 많아졌다면 p를 조금 증가시키고 그렇지 않다면 p를 감소시킵니다. 여기에서는 널리 알려진 PG 알고리즘을 텐서플로를 사용해 구현합니다. 하지만 그 전에 에이전트가 활동할 환경을 만들어야합니다.
OpenAI 짐
강화 학습에서 어려운 점은 에이전트를 훈련할 작업 환경을 마련해야한다는 점입니다. 실제 환경에서 훈련하는 것은 어렵고 느리기 때문에 훈련을 위한 최소한의 시뮬레이션 환경이 필요합니다. PyBullet, MuJoGo 같은 3D 물리 시뮬레이션 라이브러리를 사용할 수 있습니다.
OpenAI 짐(https://gym.openai.com)은 다양한 종류의 시뮬레이션 환경(아타리 게임, 보드 게임, 2D와 3D 물리 시뮬레이션 등)을 제공하는 툴킷입니다. 이를 사용해 에이전트를 훈련하고, 이들을 비교하거나 새로운 RL 알고리즘을 개발할 수 있습니다.
virtualenv를 사용해 독립된 환경을 만들었다면 툴킷을 설치하기 전에 먼저 이 환경을 활성화해야 합니다. 그 다음 OpenAI 짐을 설치합니다. (virtualenv 환경을 사용하지 않는다면 --user 옵션을 사용하거나 관리자 권한이 필요합니다.)
!apt-get install -y xvfb x11-utils
!pip install -U gym[all]==0.17.* pyvirtualdisplay==0.2.* PyOpenGL==3.1.* PyOpenGL-accelerate==3.1.*
from pyvirtualdisplay import Display
import matplotlib.pyplot as plt
import gym
display = Display(visible=False, size=(400, 300))
display.start()
시스템에 따라 Mesa OpenGL 유틸리티 (GLU) 라이브러리를 설치해야할 수도 있습니다. (가령, Ubuntu 18.04에서는 apt install libglu1-mesa를 실행해야 합니다.) 이 라이브러리는 첫 번째 환경을 렌더링할 때 필요합니다. 그 다음 파이썬 셸이나 주피터 노트북을 열고 make() 함수로 환경을 만듭니다.
gym.envs.registry.all() 함수로 환경의 전체 목록을 얻을 수 있습니다.
import gym
env = gym.make('CartPole-v1')
obs = env.reset() # obs = array([-0.02756298, 0.01237163, -0.02811164, -0.00753396], dtype=float32)
환경을 만든 후 반드시 reset() 메서드로 초기화해야합니다. 이 메서드는 첫 번째 관측을 반환합니다. 관측은 환경마다 종류가 다르지만 CartPole은 4개의 실수를 담은 1D nadrray입니다. 이는 ([카트의 수평 위치(0.0=중앙), 카트의 속도(양수는 우측 방향), 막대의 각도(0.0=수직), 막대의 각속도(양수는 시계 방향)])를 의미합니다.
render() 메서드를 호출해 환경을 출력해보겠습니다. 윈도우에서는 먼저 VcXsrv나 Xming 같은 X 서버를 설치해야합니다.
env.render() # True
Tip
만약 클라우드에 있는 가상 머신처럼 헤드리스 서버(headless server) 즉, 스크린이 없는 서버를 사용한다면 렌더링에 실패할 것입니다. 이를 해결하는 유일한 방법은 Xvfb, Xdummy와 같은 가짜 X 서버를 사용하는 것입니다.
우분투나 데비안에서 apt install xvfb로 Xvfb를 설치하고 xvfb-run-s "-screen 0 1400x900x24" python3 명령으로 파이썬을 실행할 수 있습니다. 또는, Xvfb와 이를 감싸는 pyvirtualdisplay 라이브러리(https://homl.info/pyvd)를 설치하고 프로그램 시작 부분에서 pyvirtualdisplay.Display(visible=0, size=(1400, 900)).start()를 실행합니다.
render() 메서드에서 반환된 렌더링된 이미지를 넘파이 배열로 받으려면 mode='rgb_array'로 지정합니다. (특이하게 CartPole 환경은 스크린으로도 렌더링을 출력합니다.)
img = env.render(mode='rgb_array')
img.shape # (400, 600, 3)
이 환경에서 어떤 행동이 가능한지 확인해보겠습니다.
env.action_space # Discrete(2)
Discrete(2)는 가능한 행동이 정수 0과 1이라는 것을 의미합니다. 0은 왼쪽 가속, 1은 오른쪽 가속입니다.
# prev_obs = array([-0.02652299, -0.37604043, -0.02982154, 0.53776395], dtype=float32
action=0 # 왼쪽으로 가속
obs, reward, done, info = env.step(action)
# obs = array([-0.0340438 , -0.5707307 , -0.01906627, 0.8209033 ], dtype=float32)
# reward = 1.0
# done = False
# info = {}
앞서서 막대가 왼쪽(obs[2]<0)으로 기울어져 있어서 카트를 왼쪽으로 가속해보겠습니다.
step() 메서드는 주어진 행동을 실행하고 4가지 값을 반환합니다.
- obs: 새로운 관측값, (obs[1]<0) 카트가 왼쪽으로 움직입니다. (obs[2]<0) 여전히 왼쪽으로 기울어져 있지만, 각속도가(obs[3]>0) 양수이므로 다음 스텝 후에는 오른쪽으로 기울어질 가능성이 큽니다.
- reward: 보상, CartPole 환경에서는 어떤 행동을 해도 매 스텝마다 1.0의 보상을 받습니다.
- done: 에피소드가 끝났는지 여부입니다. True면 reset() 함수를 호출해야합니다.
- info: 다른 환경에서는 이 딕셔너리에 디버깅, 훈련에 유용한 추가정보가 담길 수 있습니다. 예를 들면, 어떤 게임에서는 에이전트 생명이 몇 번 남았는지 알려줄 수 있습니다.
환경을 다 사용했다면 close() 메서드를 호출해 자원을 반납해야 합니다.
간단한 정책을 하드코딩해보겠습니다. 이 정책은 막대가 왼쪽으로 기울어지면 카트를 왼쪽으로 가속하고, 오른쪽으로 기울어지면 오른쪽으로 가속합니다. 이 정책으로 에피소드 500번을 실행해서 얻은 평균 보상을 확인해보겠습니다.
def basic_policy(obs):
angle = obs[2]
return 0 if angle < 0 else 1
totals = []
for episode in range(500):
episode_rewards=0
obs = env.reset()
for step in range(200):
action = basic_policy(obs)
obs, reward, done, info = env.step(action)
episode_rewards += reward
if done:
break
totals.append(episode_rewards)
# 결과
np.mean(totals), np.std(totals), np.min(totals), np.max(totals)
# (41.946, 8.91353375491449, 24.0, 68.0)
500번 시도해도 이 정책은 막대를 쓰러뜨리지 않고 68 타임 스텝보다 많이 진행하지 못했습니다. https://github.com/ageron/handson-ml2에 있는 시뮬레이션을 보면 시간이 갈 수록 점점 더 크게 왼쪽, 오른쪽으로 진동하다가 막대가 기울어집니다. 신경망으로 더 좋은 정책을 만들 수 있는지 알아보겠습니다.
신경망 정책
신경망 정책을 만들어봅시다. 앞서 만든 하드 코딩한 정책과 마찬가지로 이 신경망은 관측을 입력 받고 실행할 행동을 출력합니다. 정확히 말하면 각 행동에 대한 확률을 추정합니다. 가능한 행동이 2개 이므로 하나의 출력 뉴런만 있으면 됩니다.
가장 점수가 높은 행동(가령 0일 확률이 0.7%라면 0을 선택)을 하는게 아니라 신경망이 확률을 기반으로 랜덤하게 행동을 선택하는 이유는 에이전트가 새로운 행동을 탐험(exploring)하는 것과 잘 할 수 있는 행동을 활용(exploiting)하는 것 사이에 균형을 맞추게 됩니다.
환경을 모두 관측할 수 없다면, 과거의 행동과 관측도 고려해야 합니다. 예를 들어, 이 환경이 카트의 속도는 빼고 위치만 알려준다면 현재의 속도를 추정하기 위해 이전의 관측도 고려해야 합니다. 또한, 관측에 잡음이 있을 경우, 가장 가능성 있는 현재의 상태를 추정하기 위해 지난 관측 몇 개를 사용하는 것이 좋습니다. CartPole 문제는 아주 간단한 문제여서 관측에 잡음이 없고 환경에 대한 완전한 상태를 담습니다.
tf.keras를 사용해 신경망 정책을 구현하는 코드입니다.
import tensorflow as tf
from tensorflow import keras
n_inputs = 4 # == env.observation_space.shape[0]
model = keras.models.Sequential([
keras.layers.Dense(5, activation='elu', input_shape=[n_inputs]),
keras.layers.Dense(1, activation='sigmoid')
])
가능한 행동이 2개 이상이면 행동마다 하나의 출력 뉴런을 두고 softmax 활성화 함수를 사용해야합니다. 자 이제 이 신경망 정책을 어떻게 훈련해야 할까요?
행동 평가: 신용 할당 문제
에이전트가 얻을 수 있는 가이드는 보상 뿐입니다. 보상은 일반적으로 드물고 지연되어 나타납니다. 예를 들어 에이전트가 100 스텝 동안 막대의 균형을 유지했다면, 100번의 행동 중 어떤 행동이 좋고 나쁜지 알 수 있을까요? 모든 책임이 마지막 행동에 있는 것은 당연히 아닙니다. 이를 신용 할당 문제(credit assignment problem)라고 합니다.
흔히 사용하는 전략은 각 단계마다 할인 계수(discount factor $γ$)를 곱한 보상을 모두 합해서 행동을 평가합니다. 할인된 보상의 합을 대가(return)이라고 부릅니다.
할인 계수가 0에 가까울 수록 미래의 보상은 덜 중요해집니다. 가령 에이전트가 오른쪽으로 3번 이동하기로 결정하고, 첫 번째 스텝에서 10, 두 번째에서 0, 세 번째에서 -50의 보상을 받았을 때 $γ=0.8$을 사용하면 첫 번째 행동의 이득은 $10+γ*0+γ^2*(-50)=-22$가 됩니다. 반대로 1에 가까울 수록 먼 미래의 보상이 현재의 보상만큼 중요하게 고려됩니다. 전형적인 $γ=0.9~0.99$ 사이입니다. 가령, $γ=0.95$면 13 스텝 뒤에서 받는 보상은 당장의 보상에 비해 절반 정도의 가치가 됩니다.($ 0.95^{13} ≈ 0.5 $)
카트폴 환경에서는 행동의 효과가 매우 짧은 기간 안에 나타나므로 $γ=0.95$가 적절해 보입니다.
우리는 가능한 다른 행동과 비교해서 각 행동이 얼마나 좋고 나쁜지를 추정해야 합니다. 이를 행동 이익(action advantage)이라고 부릅니다. 이렇게 하려면 많은 에피소드를 실행하고 모든 행동의 대가(return)를 평균을 빼고 표준편차로 나누어 정규화해야 합니다. 그런 후 행동 이익이 음수인 행동은 나쁘고, 양수인 행동은 좋다고 가정할 수 있습니다.
정책 그레디언트
PG 알고리즘은 높은 보상을 얻는 방향의 그레디언트를 따르도록 정책의 파라미터를 최적화하는 알고리즘입니다. 인기 있는 PG 알고리즘 중 하나는 REINFORCE 알고리즘입니다.
많이 사용하는 REINFORCE 알고리즘 방식은 아래와 같습니다.
- 먼저 신경망 정책이 여러 번에 걸쳐 게임을 플레이하고, 매 스텝마다 선택된 행동이 높은 가능성을 갖도록 그레디언트를 계산합니다. PG는 신경망의 출력을 $\hat{p}$라고 할 때, $\log \left(\hat{p}\right)$가 커지는 방향으로 그레디언트를 업데이트합니다. 하지만, 이 단계에서는 아직 그레디언트를 적용하지 않습니다.
- 에피소드를 몇 번 실행한다음 각 행동의 이익을 계산합니다.
- 한 행동의 이익이 양수이면, 미래에 선택될 가능성이 높도록 앞서 계산한 그레디언트를 적용합니다. 음수인 경우 반대의 그레디언트를 적용합니다. 각 그레디언트 벡터와 그에 상응하는 행동의 이익을 곱하면 됩니다.
- 마지막으로 모든 결과 그레디언트 벡터를 평균 내어 경사 하강법 스텝을 수행합니다.
먼저 한 스텝을 진행할 함수가 필요합니다. 손실과 그레디언트를 계산하기 위해 어떤 행동을 선택하더라도 옳은 행동이라고 가정하겠습니다. 이렇게 구한 그레디언트를 잠시 저장했다가 이 행동이 좋은지 나쁜지 판명된 후에 조정하겠습니다.
def play_one_step(env, obs, model, loss_fn):
with tf.GradientTape() as tape:
left_proba = model(obs[np.newaxis]) # obs.shape=(4,), obs[np.newaxis].shape=(1, 4)
# tf.random.uniform([1, 1])
# = <tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.2835908]], dtype=float32)>
action = (tf.random.uniform([1, 1]) > left_proba)
y_target = tf.constant([[1.]]) - tf.cast(action, tf.float32)
loss = tf.reduct_mean(loss_fn(y_target, left_proba))
grads = tape.gradient(loss, model.trainable_variables)
obs, reward, done, info = env.step(int(action[0, 0].numpy()))
return obs, reward, done, grads
- GradientTape 블록 안에서 하나의 관측값을 받은 모델이 왼쪽으로 이동할 확률을 출력합니다. 모델은 배치를 기대하므로 샘플 하나가 들어있는 배치가 되도록 크기를 바꿔줍니다.
- 그 다음 0~1 사이의 랜덤한 실수를 샘플링해서 left_proba보다 크다면 True, 아니면 False를 반환합니다. 이를 숫자로 바꾸면 left_proba의 확률로 0 또는 1을 출력합니다.
- 왼쪽으로 이동할 타깃 확률을 정의합니다. $1-행동$은 왼쪽으로 이동할 이상적인 확률로 정의합니다
- 그 다음 손실함수로 손실을 구하고 테이프를 사용해 손실 가능한 함수의 그레디언트를 계산합니다.
- 마지막으로 선택한 행동을 플레이하고 새로운 obs, reward, done, 계산한 grads를 반환합니다.
play_one_step() 함수를 사용해 여러 에피소드를 플레이하고, 전체 보상과 각 에피소드와 그 에피소드의 각 스텝에서의 그레디언트를 담은 배열을 반환하는 함수를 만들어보겠습니다.
def play_multiple_episodes(env, n_episodes, n_max_steps, model, loss_fn):
all_rewards = []
all_grads = []
for episode in range(n_episodes):
current_rewards = []
current_grads = []
obs = env.reset()
for step in range(n_max_steps):
obs, reward, done, grad = play_one_step(env, obs, model, loss_fn)
current_rewards.append(reward)
current_grads.append(grad)
if done:
break
all_rewards.append(current_rewards)
all_grads.append(current_grads)
return all_rewards, all_grads
코드 속 grad는 그레이언트 튜플 하나를 담고 있는데, 튜플 속 원소는 훈련 가능한 각 변수마다 그레디언트 텐서 하나를 포함합니다.
REINFORCE 알고리즘은 play_multiple_episodes() 함수로 여러 번(n_episode번) 플레이합니다. 그 다음 각 에피소드에서 스텝에 따른 보상을 할인하고 정규화합니다. 이 과정을 돕는 함수 2개를 아래에 정의하겠습니다.
def discount_rewards(rewards, discount_factor):
discounted = np.array(rewards)
for step in range(len(rewards)-2, -1, -1): # (len(rewards)-2)에서 0까지 -1씩 뺀다.
discounted[step] += discounted[step+1]*discount_factor
# discounted[len(rewards)-2] = discounted[len(rewards)-2] + discounted[len(rewards)-1]*discount_factor
# discounted[len(rewards)-1] = discounted[len(rewards)-1] + discounted[len(rewards)]*discount_factor
# 따라서 discounted[len(rewards)-2] = discounted[len(rewards)-2] + discounted[len(rewards)-1]*discount_factor + discounted[len(rewards)]*discount_factor^2
return discounted
한 스텝에서 먼 스텝일 수록 할인 계수가 많이 곱해지며, 모든 뒷 스텝이 더해집니다. 소수점 1자리는 버리는 것 같다.
def discount_and_normalize_rewards(all_rewards, discount_factor):
all_discounted_rewards = [discount_rewards(rewards, discount_factor)
for rewards in all_rewards]
flat_rewards = np.concatenate(all_discounted_rewards)
reward_mean = flat_rewards.mean()
reward_std = flat_rewards.std()
return [(discounted_rewards - reward_mean) / reward_std
for discounted_rewards in all_discounted_rewards]
discount_rewards() & np.concatenate
a = discount_rewards([1, 2, 3, 4], 0.9) # array([7, 7, 6, 4])
b = discount_rewards([5, 6, 7, 8], 0.9) # array([21, 18, 14, 8])
np.concatenate([a, b]) # array([ 7, 7, 6, 4, 21, 18, 14, 8])
discount_and_normalize_rewards 함수는 모든 에피소드의 각 타임 스텝을 모두 더해 보상의 평균, 표준편차를 구합니다. 그 다음 각 에피소드의 보상 리스트를 정규화합니다.
discount_rewards([10, 0, -50], discount_factor=0.8)
# array([-22, -40, -50])
discount_and_normalize_rewards([[10, 0, -50], [10, 20]], discount_factor=0.8)
# [array([-0.28435071, -0.86597718, -1.18910299]), array([1.26665318, 1.0727777 ])]
discount_rewards 함수는 위 그림 처럼 결과가 나왔습니다. discount_and_normalize_rewards() 함수는 두 에피소드의 각 행동에 대해 정규화된 행동 이익을 반환한 것을 확인할 수 있습니다. 위 결과로는 첫 번째 에피소드의 행동은 모두 나쁜 것으로 간주됩니다. 반면 두 번째 에피소드의 행동은 모두 좋은 것으로 간주됩니다.
epochs=150, 각 epoch는 10 episodes를 실행하고 n_max_steps=200입니다. discount_factor=0.95를 적용합니다.
n_iterations=150
n_episodes_per_update=10
n_max_steps=200
discount_factor=0.95
일반적인 학슙를 0.01인 Adam optimizer를 쓰고 이진 분류기(왼쪽, 오른쪽 2가지 행동만 가능)를 훈련하므로 이진 크로스 엔트로피 손실 함수를 사용합니다.
optimizer = keras.optimizers.Adam(lr=0.01)
loss_fn = keras.losses.binary_crossentropy
이제 훈련 반복을 만들어 실행해보겠습니다.
for iteration in range(n_iterations):
all_rewards, all_grads = play_multiple_episodes(
env, n_episodes_per_update, n_max_steps, model, loss_fn)
all_final_rewards = discount_and_normalize_rewards(all_rewards, discount_factor)
all_mean_grads = []
for var_index in range(len(model.trainable_variables)):
mean_grads = tf.reduce_mean(
[final_reward * all_grads[episode_index][step][var_index]
for episode_index, final_rewards in enumerate(all_final_rewards)
for step, final_reward in enumerate(final_rewards)], axis=0
)
all_mean_grads.append(mean_grads)
optimizer.apply_gradients(zip(all_mean_grads, model.trainable_variables))
- 각 epoch에서 play_multiple_episodes() 함수를 호출합니다. 이 함수는 10번 게임을 플레이하고 각 에피소드와 스텝에 대한 모든 보상과 그레디언트를 리스트의 리스트 형태로 반환합니다.
- 그 다음 discount_and_normalize_rewards() 함수를 호출해 각 행동의 정규화된 이익(final_reward)을 계산해 각 행동이 실제로 얼마나 좋은지 나쁜지 알려줍니다. final_reward는 각 행동이 얼마나 좋은지 나쁜지 알려줍니다.
- 그 다음 각 훈련 가능한 변수를 순회하며 모든 에피소드와 모든 스텝에 대한 (각 변수의 그레디언트)*(그에 상응하는 final_reward)를 평균합니다. (final_reward가 평균적으로 음수이면 가중치를 줄이고, final_reward가 평균적으로 양수이면 가중치를 키웁니다.?)
- 이 평균 gradient를 optimizer에 적용합니다. 모델의 훈련 가능한 변수가 변경되고 아마 정책이 조금 더 나아질 것입니다.
이 코드는 신경망 정책을 훈련시킵니다. 에피소드마다 거의 200에 가까운 평균 보상을 얻을 것입니다. 이 환경의 기본 최댓값입니다! 성공입니다.
Tip
연구자들은 에이전트가 초기에 환경에 대해 아무것도 알지 못할 때 잘 작동하는 알고리즘을 찾으려고 노력합니다. 하지만, 강화 학습에 대한 논문을 연구하는 사람이 아니라면 사전 지시을 많이 주입하는 게 좋습니다. 예를 들어 막대가 가능한 수직으로 서 있는 것이 좋으므로 막대의 각도에 비례해 음의 보상을 추가할 수 있스빈다. 이는 보상을 더 많이 만들고 훈련 속도를 높입니다. 이미 검증된 좋은 정책을 가지고 있다면 (예를 들면 하드코딩된 정책) 검증된 정책을 따라하는 신경망을 훈련하고 나서 정책 그레디언트를 사용해 모델의 성능을 높이는 것이 좋을 것입니다.
방금 훈련한 이 간단한 PG 알고리즘이 CartPole 문제를 풀었습니다. 하지만, 더 크고 복잡한 문제에는 잘 적용되지 않습니다. 사실 샘플 효율성(sample efficiency)이 좋지 못합니다. 아주 긴 시간 동안 게임을 플레이해야 정책을 많이 개선할 수 있습니다. 각 행동의 이익을 추정하기 위해 많은 에피소드를 실행해야하기 때문입니다. 그러나 엑터-크리틱(actor-critic) 알고리즘 같은 더 강력한 알고리즘이 존재합니다. 뒷 장에서 간단히 소개합니다.
잘 알려진 다른 PG 알고리즘을 살펴보겠습니다. PG 알고리즘이 보상을 증가시키기 위해 정책을 직접적으로 최적화하는 반면 이제 살펴볼 알고리즘은 덜 직접적입니다. 즉, 에이전트가 각 상태에 대한 할인된 미래의 대가를 추정하도록 학습됩니다. 그리고 이를 활용해 어떻게 행동할지 결정합니다. 이 알고리즘을 이해하기 위해 먼저 마르코프 결정 과정(Markov decision process, MDP)을 알아봅시다.
마르코프 결정 과정
마르코프 연쇄(Markov chain)는 메모리가 없는 확률 과정(stochastic process)입니다. 즉, 과거의 상태와는 상관 없이 상태 $s$에서 $s'$로 전이하는 확률이 ($s$, $s'$) 쌍에 의존합니다.
상태 $s_0$에서 시작한다고 가정하면, 상태 $s_0$에 남을 확률은 70%입니다. $s_0$를 떠나면 다시 이 상태로 돌아오지 못합니다. 여러 상태를 떠돌아 다니다가 상태 $s_3$에 도달하면 영원히 그 상태에 남게 됩니다. 이 상태를 종료 상태(terminal state)라고 합니다. 마르코프 연쇄는 다양한 역학 관계를 모델링할 수 있어서 열역학, 화학, 통계 등 많은 분야에서 사용됩니다.
마르코프 결정 과정은 마르코프 연쇄에서 여러 행동 중 하나를 선택할 수 있고, 전이 확률이 행동에 따라 달라집니다. 또한 어떤 상태 전이는 보상(음수 or 양수)을 반환합니다. 에이전트의 목적은 시간이 지남에 따라 보상을 최대화하는 정책을 찾는 것입니다.
그림 18-8을 보면 상태 $s_0$에서 선택할 수 있는 행동은 $a_0$~$a_2$ 중 하나를 선택할 수 있습니다. 만약 $a_2$를 행동한다면 80%확률로 다시 상태 $s_0$로 되돌아가거나 20%확률로 상태 $s_1$로 가게될 것입니다. 위 그림에서 그려진 MDP를 보고 시간이 지남에 따라 가장 많은 보상을 받는 전략을 세울 수 있나요? 애매한 부분은 상태 $s_1$에서 그냥 머물러야 할지($a_0$), 아니면 불속으로 가야 할지($a_2$) 확실하지 않은 것입니다.
밸먼은 상태 $s$에서 최적의 상태 가치 $V*\left(s\right)$(state value)를 추정하는 방법을 찾았습니다. 이 값은 상태 $s$에 도달했을 때 최적으로 행동한다면 평균적으로 얻을 수 있는 할인된 미래 보상의 합입니다. 벨먼은 에이전트가 최적으로 행동하면 벨먼 최적 방정식(Bellman optimality equation)이 적용된다는 것을 입증했습니다.
[벨먼 최적 방정식, Bellman optimality equation]
$$V*\left(s\right)=\max _a^{ }\sum _{s'}^{\ }T\left(s,\ a,\ s'\right)\left[R\left(s,\ a,\ s'\right)+\gamma V*\left(s'\right)\right]\ \ \ \ \ \ 모든\ s에\ 대해$$
- $T\left(s,\ a,\ s'\right)$는 에이전트가 행동 $a$를 선택했을 때 상태 $s$에서 $s'$로 전이될 확률입니다. [그림 18-8]에서 $T\left(s_2,\ a_1,\ s_0\right)$=0.8입니다.
- $R\left(s,\ a,\ s'\right)$는 에이전트가 행동 $a$를 선택해서 상태 $s'$으로 이동했을 때 받는 보상입니다. [그림 18-8]에서 $R\left(s_2,\ a_1,\ s_0\right)$=40입니다.
- $γ$는 할인 계수입니다.
벨먼 최적 방정식을 사용해 모든 상태에서 최적의 상태 가치를 추정하는 알고리즘을 만들 수 있습니다. 이 알고리즘을 가치 반복(value iteration) 알고리즘이라고 하며, 아래 식을 사용해 반복적으로 업데이트합니다. 놀랍게도 충분한 시간이 주어지면 추정값이 최적의 정책에 대응되는 최적의 상태 가치에 수렴하는 것이 보장됩니다.
[가치 반복 알고리즘]
$$V_{k+1}\left(s\right)\ \gets \ \max _a^{ }\sum _{s'}^{\ }T\left(s,\ a,\ s'\right)\left[R\left(s,\ a,\ s'\right)+\gamma V_k\left(s'\right)\right]\ \ \ \ \ \ 모든\ s에\ 대해$$
이 식에서 $V_k\left(s\right)$는 알고리즘의 k번째 반복에서 상태 s의 추정 가치입니다.
NOTE
이 알고리즘은 동적 계획법(dynamic programming)의 한 예로 복잡한 문제를 다루기 쉬운 하위 문제로 나누어 반복적으로 해결합니다.
벨만은 최적의 상태 가치 $V*\left(s\right)$를 통해 상태 $s$에서 최적의 행동$a$를 알려주는 최적의 상태-행동 가치(state-action value)인 Q-가치(Q-value)를 추정하는 알고리즘을 발견했습니다.
상태-행동 ($s$, $a$) 쌍에 대한 최적의 Q-가치 $Q*\left(s,\ a\right)$는 상태 $s$에 도달해서 행동 $a$를 선택했을 때 평균적으로 기대할 수 있는 할인된 미래 보상의 합입니다. 여기서 에이전트가 이 행동 이후에 최적으로 행동할 것이라고 가정합니다.
[Q-가치 반복 알고리즘]
$$Q_{k+1}\left(s,\ a\right)\ \gets \ \sum _{s'}^{\ }T\left(s,\ a,\ s'\right)\left[R\left(s,\ a,\ s'\right)+\gamma \cdot \max _{a'}Q_k\left(s',\ a'\right)\right]\ \ \ \ \ \ 모든\ \left(s,\ a\right)에\ 대해$$
여기에서도 Q-가치의 추정을 0으로 초기화한 후 Q-가치 반복(Q-value iteration) 알고리즘을 사용해 업데이트합니다.
최적의 정책 $\pi *\left(s\right)$은 에이전트가 상태 $s$에 도달했을 때 가장 높은 Q-값을 가진 행동을 선택하는 것입니다.
$$\pi *\left(s\right)=\arg \max _a^{ }Q*\left(s,\ a\right)$$
이 알고리즘을 [그림 18-8]에 표현된 MDP에 적용해보겠습니다. 먼저 MDP를 정의해야 합니다.
transition_probabilities = [ # shape=[s, a, s']
[[0.7, 0.3, 0.0], [1.0, 0.0, 0.0], [0.8, 0.2, 0.0]],
[[0.0, 1.0, 0.0], None, [0.0, 0.0, 1.0]],
[None, [0.8, 0.1, 0.1], None]]
rewards = [ # shape=[s, a, s']
[[+10, 0, 0], [0, 0, 0], [0, 0, 0]],
[[0, 0, 0], [0, 0, 0], [0, 0, -50]],
[[0, 0, 0], [+40, 0, 0], [0, 0, 0]]]
possible_actions = [[0, 1, 2], [0, 2], [1]]
예를 들어 행동 $a_1$에서 $s_2$ → $s_0$으로 전이할 확률은 transition_probabilities[2][1][0]을 참조합니다. (0.8입니다.) 비슷흐개 보상은 rewards[2][1][0](+40)을 참조합니다. $s_2$에서 가능한 행동의 리스트는 possible_actions[2]를 참조합니다.(이 경우 행동 $a_1$만 가능합니다.)
그 다음 모든 Q-가치를 0으로 초기화하겠습니다. (불가능한 행동은 제외합니다. 이 행동의 Q-가치는 -∞로 설정합니다.)
Q_values = np.full((3, 3), -np.inf) # 불가능한 행동에 대해서는 -np.inf
for state, actions in enumerate(possible_actions):
Q_values[state, actions] = 0.0 # 모든 가능한 행동에 대해서
이제 Q-가치 반복 알고리즘을 실행해보겠습니다. 이 코드는 모든 상태와 모든 가능한 행동에 대해 모든 Q-가치에 [Q-가치 반복 알고리즘]을 반복 적용합니다.
gamma = 0.90 # 할인 계수
for iteration in range(50):
Q_prev = Q_values.copy()
for s in range(3):
for a in possible_actions[s]:
Q_values[s, a] = np.sum([
transition_probabilities[s][a][sp]
*(rewards[s][a][sp] + gamma*np.max(Q_prev[sp]))
for sp in range(3) ])
이게 다입니다! 결과 Q-가치는 다음과 같습니다.
Q_values
'''
array([[18.91891892, 17.02702702, 13.62162162],
[ 0. , -inf, -4.87971488],
[ -inf, 50.13365013, -inf]])
'''
예를 들어, 에이전트가 상태 $s_0$에 있고 행동 $a_1$을 선택했을 때 할인된 미래 보상의 기대 합은 약 17.0입니다.
각 상태에서 가장 높은 Q-가치를 갖는 행동을 확인해봅시다.
np.argmax(Q_values, axis=1) # 각 상태에 대해 최적의 행동, 행 중에 큰 열
# array([0, 0, 1])
이 것은 할인 계수 0.9를 사용했을 때 이 MDP를 위한 최적의 정책입니다. 상태 $s_0$에서는 행동 $a_0$을 선택하고, 상태 $s_1$에서는 행동 $a_0$(제자리에 있기)를 선택하고, 상태 $s_2$에서는 행동 $a_1$(유일한 행동)을 선택합니다. 재미있는 점은 할인 계수는 0.95로 높이면 최적의 정책이 바뀐다는 점입니다. 즉, 상태 $s_1$에서 최선의 행동은 $a_2$가 됩니다. (불을 통과합니다!) 이는 미래 보사엥 더 가치를 둘 수록 행복을 위해 당장의 고통을 견디려 하기 때문입니다.
시간차 학습
독립적인 행동으로 이루어진 강화 학습 문제는 마르코프 결정 과정으로 모델링될 수 있지만 초기에 에이전트는 전이 확률에 대해 알지 못합니다.(즉, $T\left(s,\ a,\ s'\right)$를 모릅니다.) 또한, 보상이 얼마나 되는지 알지 못합니다.($R\left(s,\ a,\ s'\right)$를 모릅니다.) 보상에 대해 알기 위해서는 적어도 한 번은 각 상태와 전이를 경험해야 합니다. 또한, 전이 확률에 대해 신뢰할 만한 추정을 얻으려면 여러 번 경험을 해야 합니다.
NOTE
가치 반복 알고리즘 같이 MDP의 전이 확률과 보상에 대한 모델을 알고 있는 경우를 모델-기반 강화 학습(model-based RL)이라고 하며, 그 반대의 경우를 모델-프리 강화 학습(model-free RL)이라고 합니다. 시간차 학습과 Q-러닝은 대표적인 모델-프리 강화 학습 알고리즘입니다.
시간차 학습(temporal difference learning, TD 학습)은 가치 반복 알고리즘과 비슷하지만, 에이전트가 MDP에 대해 일부 정보만 알고 있을 때 다룰 수 있도록 변형한 것입니다. 일반적으로 에이전트는 초기에 가능한 상태, 행동만 알고 다른 것은 모른다고 가정합니다. 에이전트는 탐험 정책(exploration policy)을 (예를 들면 완전히 랜덤한 정책을) 사용해 MDP를 탐험합니다. 탐험이 진행될 수록 TD 학습 알고리즘이 실제로 관측된 전이와 보상에 근거하여 상태 가치의 추정값을 업데이트 합니다.
[TD 학습 알고리즘]
$$V_{k+1}\left(s\right)\ \gets \ \left(1-\alpha \right)V_k\left(s\right)+\alpha \left(r+\gamma \cdot V_k\left(s'\right)\right)$$
또는 아래와 같이 쓸 수 있습니다.
$$V_{k+1}\left(s\right)\ \gets \ V_k\left(s\right)+\alpha \delta _k\left(s,\ r,\ s'\right)\ \ \ \ 여기에서\ \ \ \ \delta _k\left(s,\ r,\ s'\right)=r+\gamma \cdot V_k\left(s'\right)-V_k\left(s\right)$$
- $\alpha $는 학습률입니다.(예를 들어 0.01)
- $r+\gamma \cdot V_k\left(s'\right)$는 TD 타깃이라 부릅니다.
- $\delta _k\left(s,\ r,\ s'\right)$는 TD 오차라고 부릅니다.
$a\gets _{\alpha }b$ 표기법을 사용해 더 간단히 쓸 수 있습니다.
$a\gets _{\alpha }b$는 $a_{k+1}\ \gets \ \left(1-\alpha \right)\cdot a_k+\alpha \cdot b_k$를 뜻합니다. 따라서 아래 식과 같이 쓸 수 있습니다.
$$V\left(s\right)\ \gets _{\alpha }\ r+\gamma \cdot V\left(s'\right)$$
TIP
TD 학습은 확률적 경사 하강법(SGD)과 비슷한 점이 많습니다. 특히 한 번에 하나의 샘플을 다루는 점이 같습니다. SGD와 같이 학습률을 점진적으로 줄여가야 올바르게 수렴할 수 있습니다.(그렇지 않으면 최적의 Q-가치 주변을 오갈 것입니다.)
각 상태 $s$에서 이 알고리즘은 에이전트가 이 상태를 떠났을 때 얻을 수 있는 당장의 보상과 (최적으로 행동한다고 가정하여) 나중에 기대할 수 있는 보상을 더한 이동 평균을 저장합니다.
NOTE ?
11.1.3절 '배치 정규화'에서 소개한 것과 같은 지수 이동 평균이지만 계수가 반대입니다. 배치 정규화에서 소개한 momentum은 1에 가까운 값이고 여기서 학습률 α는 0에 가깝습니다.
Q-러닝
시간차 학습과 비슷하게 Q-러닝(Q-learning) 알고리즘은 초기에 전이 확률과 보상을 알지 못하는 상황에서 Q-가치 반복 알고리즘을 적용한 것입니다.
[Q-러닝 알고리즘]
$$Q\left(s,\ a\right)\ \gets _{\alpha }\ r+\gamma \cdot \max _{a'}Q\left(s',\ a'\right)$$
Q-러닝은 에이전트가 플레이(예를 들면 랜덤하게)하는 것을 보고 점진적으로 Q-가치 추정을 향상하는 방식으로 작동합니다. 최적의 정책은 가장 높은 Q-가치를 가지는 행동을 선택하는 것입니다.(즉, 탐욕적 정책입니다.)
각 상태-행동 (s, a) 쌍마다 상태 $s$에서 행동 $a$를 했을 때 에이전트가 받을 수 있는 보상 $r$과 기대할 수 있는 할인된 미래 보상의 합을 더한 이동 평균을 저장합니다. 미래 보상의 합을 추정하기 위해서는 타깃 정책이 이후에 최적으로 행동한다고 가정하고 다음 상태 $s'$에 대한 Q-가치 추정의 최댓값을 선택합니다.
Q-러닝 알고리즘을 구현해보겠습니다. 에이전트가 환경을 탐색할 수 있도록 에이전트가 (s, a)를 할 때, 다음 상태와 보상을 받을 수 있는 스텝 함수를 만듭니다.
def step(state, action):
probas = transition_probabilities[state][action]
next_state = np.random.choice([0, 1, 2], p=probas) # 확률에 맞게 전이
reward = rewards[state][action][next_state]
return next_state, reward
이제 에이전트의 탐색 정책을 구현해보겠습니다. 이 상태 공간은 매우 작아서 단순한 랜덤 정책으로 충분히 모든 상태와 모든 행동을 여러 번 실행할 수 있습니다.
def exploration_policy(state):
return np.random.choice(possible_actions[state])
앞에서와 같이 Q-가치를 초기화한 후 학습률 감쇠(11장에서 소개한 거듭제곱 기반 스케줄링, power scheduling)를 사용해 Q-러닝 알고리즘을 실행합니다.
alpha0 = 0.05 # 초기 학습률
decay = 0.005 # 학습률 감쇠
gamma = 0.90 # 할인 계수
state = 0 # 초기 상태
for iteration in range(10000):
action = exploration_policy(state)
next_state, reward = step(state, action)
next_value = np.max(Q_values[next_state])
alpha = alpha0 / (1 + iteration * decay)
Q_values[state, action] *= 1 - alpha
Q_values[state, action] += alpha * (reward + gamma * next_value)
state = next_state
Q-러닝 알고리즘이 최적의 Q-가치에 수렴하겠지만 많은 반복과 하이퍼파라미터 튜닝이 필요합니다.
위 그림에서 볼 수 있듯이 Q-가치 반복 알고리즘은 20번 반복 이전에 수렴하지만, Q-러닝 알고리즘은 대략 8000번이나 반복해야 수렴합니다. 전이 확률이나 보상을 알지 못하면 최적의 정책을 찾는 데 확실히 더 어렵습니다!
훈련된 정책을 반드시 실행에 사용하지 않기 때문에 Q-러닝 알고리즘을 오프-폴리시(off-policy) 알고리즘이라고 합니다. 위 코드에서 실행한 정책(탐험 정책)은 완전히 랜덤한 정책입니다. Q-러닝이 에이전트가 무작위하게 행동하는 것을 바라보는 것만으로도 최적의 정책을 학습하는 능력이 있다는 것은 조금 놀랍습니다.
PG 알고리즘(정책 그레디언트 알고리즘)은 온-폴리시(on-policy) 알고리즘입니다. 훈련된 정책은 항상 가장 높은 Q-가치를 가진 행동을 선택하는 것입니다. PG 알고리즘은 훈련된 정책을 사용해 환경을 탐험합니다.
탐험 정책
Q-러닝은 탐험 정책이 MDP를 충분히 탐험해야 동작합니다. 완전한 랜덤 정책이 결국 모든 상태와 전이를 여러 번 경험한다는 것을 보장하지만, 극단적으로 오랜 시간이 걸립니다.
ε-그리디 정책(ε-greedy policy)은 각 스텝에서 ε 확률로 렌덤하게 행동하는 것입니다. (이 말은 1-ε 확률로 그 순간 가장 Q-가치가 높은 행동을 선택한다는 것입니다.) ε-그리디 정책은 완전한 랜덤 정책에 비해 Q-가치 추정이 더 많이 향상되기 때문에 관심 있는 부분을 살피는 데 더 많은 시간을 할애합니다. 그럼에도 여전히 ε 확률로 알려지지 않은 지역을 방문하는 데 일정 시간을 사용할 것입니다. ε 값은 높게 시작해서 점점 감소되는게 일반적입니다. (ex, ε = 1.0 → 0.05)
ε-그리디 정책처럼 탐험의 가능성에 의존하지 않고 이전에 많이 시도하지 않았던 행동을 시도하도록 하는 탐험 정책이 있습니다. 아래 식처럼 Q-가치 추정에 보너스를 추가하는 방식으로 구현합니다.
[탐험 함수를 사용한 Q-러닝]
$$Q\left(s,\ a\right)\ \gets _{\alpha }\ \ r+\gamma \cdot \max _{a'}f\left(Q\left(s',\ a'\right),\ N\left(s',\ a'\right)\right)$$
- $N\left(s',\ a'\right)$는 상태 $s'$에서 행동 $a'$를 선택한 횟수를 카운트합니다.
- $f\left(Q,\ N\right)$은 $f\left(Q,\ N\right)\ =\ Q+\frac{\varkappa }{\left(1+N\right)}$와 같은 탐험 함수(exploration function)입니다. $\varkappa $는 에이전트가 알려지지 않은 곳에 얼마나 흥미를 둘지 나타내는 하이퍼파라미터입니다.
근사 Q-러닝과 심층 Q-러닝
Q-러닝의 주요 문제는 많은 상태와 행동을 가진 대규모 MDP에 적용하기 어렵다는 것입니다. 미스 팩맨을 플레이하는 에이전트를 훈련하려면 지구에 있는 원자 수보다 더 많은 상태에 대해 Q-가치에 대한 추정값을 기록해야합니다.
해결책은 상태-행동 (s, a) 상의 Q-가치를 근사하는 함수 $Q_{\theta }\left(s,\ a\right)$를 적절한 개수의 파라미터를 사용하여 찾는 것입니다. $θ$는 파라미터를 담은 파라미터 벡터입니다. 이를 근사 Q-러닝(approximate Q-learning)이라고 합니다. 파라미터는 상태에서 직접 뽑아낸 특성들을 선형 조합하는 방식으로 구성됩니다. (예를 들면 가장 가까운 유령까지의 거리, 유령의 방향 등)
복잡한 문제에서 훨씬 더 나은 결과를 내는 심층 Q-네트워크(deep Q-network, DQN)를 사용해 근사 Q-러닝을 구현한 것을 심층 Q-러닝(deep Q-learning)이라 합니다.
DQN을 어떻게 훈련할 수 있을까요? 주어진 상태-행동 쌍 (s, a)에 대해 DQN이 계산한 근사 Q-가치를 생각해보겠습니다. 가능한 [Q-러닝 알고리즘]에서 우측 항($r+\gamma \cdot \max _{a'}^{ }Q\left(s',\ a'\right)$)과 가깝게 만들어져야 합니다. 여기서 미래의 할인된 가치($\gamma \cdot \max _{a'}^{ }Q\left(s',\ a'\right)$)를 추정하기 위해서는 다음 상태 $s'$와 모든 가능한 행동 $a'$에 대해 DQN을 실행하면 됩니다. 이 결과로 얻은 모든 가능한 행동의 미래의 근사 Q-가치 중 가장 높은 것을 고르고 할인을 적용하면 됩니다. 이를 통해 아래와 같이 (s, a)에 대한 타깃 Q-가치 $Q_{t\arg et}\left(s,\ a\right)$를 얻을 수 있습니다.
[타깃 Q-가치]
$$Q_{t\arg et}\left(s,\ a\right)\ =\ r+\gamma \cdot \max _{a'}^{ }Q_{\theta }\left(s',\ a'\right)$$
이 타깃 Q-가치로 경사 하강법을 훈련해 훈련 단계를 수행할 수 있습니다. 구체적으로 말하면 추정된 Q-가치 $Q\left(s,\ a\right)$와 타깃 Q-가치 사이의 제곱 오차를 최소화합니다.(알고리즘이 큰 오차에 민감하지 않도록 후버 손실을 사용하기도 합니다.)
CartPole 환경 문제를 해결하기 위해 어떻게 이를 구현하는지 알아보겠습니다.
심층 Q-러닝 구현하기
DQN은 ($s$, $a$) → Q-value를 출력하는 신경망입니다. 이보다는 $s$를 입력받고 가능한 모든 $a$에 대해 approximate Q-value를 각각 출력하는게 구현이 쉽습니다. CartPole 환경을 풀기 위해 간단한 DQN을 만들어보겠습니다.
# deep Q-learning
env = gym.make('CartPole-v0')
input_shape = [4] # == env.observation_space.shape
n_outputs = 2 # == env.action_space.n
model = keras.models.Sequential([
keras.layers.Dense(32, activation='elu', input_shape=input_shape),
keras.layers.Dense(32, activation='elu'),
keras.layers.Dense(n_outputs)
])
탐험 정책으로 ε-그리디 정책을 사용하겠습니다.
def epsilon_greedy_policy(state, epsilon=0):
if np.random.rand() < epsilon:
return np.random.randint(n_outputs)
else:
Q_values = model.predict(state[np.newaxis])
return np.argmax(Q_values[0])
replay buffer(memory)를 사용해 탐험했던 모든 경험을 저장해 랜덤하게 훈련 배치를 샘플링합니다. 최근 경험에만 의존하는 문제를 해결해줍니다. DNN에서 iid특성 살펴보기?
from collections import deque
replay_buffer = deque(maxlen=2000)
deque는 linked list로 각 원소가 앞 뒤 원소를 가르키고 있습니다. 매우 큰 재생 버퍼가 필요한 경우 순한 버퍼(circular buffer를 사용하면 됩니다.
각 경험은 원소 5개로 구성됩니다. ($s$, $a$, $r$, $s'$, $done$)입니다. done은 에피소드가 종료되었는지를 가르키는 boolean입니다.
재생 버퍼에서 경험을 랜덤하게 샘플링하는 함수를 만들겠습니다.
def sample_experiences(batch_size):
indices = np.random.randint(len(replay_buffer), size=batch_size)
batch = [replay_buffer[index] for index in indices]
states, actions, rewards, next_states, dones = [
np.array([experience[field_index] for experience in batch]) # experience는
for field_index in range(5)
]
return states, actions, rewards, next_states, dones
ε-그리디 정책을 사용해 한 스텝을 플레이하고 반환된 경험을 재생 버퍼에 저장하는 함수를 만들겠습니다.
def play_one_step(env, state, epsilon):
action = epsilon_greedy_policy(state, epsilon)
next_state, reward, done, info = env.step(action)
replay_buffer.append((state, action, reward, next_state, done))
return next_state, reward, done, info
마지막으로 재생 버퍼에서 경험 배치를 샘플링하고 이 배치에서 경사하강법을 한 스텝 수행하여 DQN을 훈련하는 함수를 만들겠습니다.
batch_size=32
discount_factor=0.95
optimizer = keras.optimizers.Adam(lr=1e-2)
loss_fn = keras.losses.mean_squared_error
def training_step(batch_size):
experiences = sample_experiences(batch_size)
states, actions, rewards, next_states, dones = experiences
next_Q_values = model.predict(next_states) # 행동 a를 해서 전이한 s'
# DQN이 예측한 s'의 모든 a'에 대한 Q가치
max_next_Q_values = np.max(next_Q_values, axis=1) # 가장 큰 Q(s', a')-value
target_Q_values = (rewards +
(1-dones)*discount_factor*max_next_Q_values) # Q_target
target_Q_values = target_Q_values.reshape(-1, 1)
mask = tf.one_hot(actions, n_outputs)
with tf.GradientTape() as tape:
all_Q_values = model(states) # s에서 모든 a에 대한 Q-가치
Q_values = tf.reduce_sum(all_Q_values*mask, axis=1, keepdims=True) # 실제로 경험에서 한 행동의 Q가치
loss = tf.reduce_mean(loss_fn(target_Q_values, Q_values)) # (Q_target - Q_values)의 MSE
grads = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
Q_values 설명
# all_Q_values
tf.Tensor(
[[26.245909 26.85122 ]
[26.322636 23.703789 ]
[23.970129 26.578217 ]
...
# mask
tf.Tensor(
[[1. 0.]
[1. 0.]
[0. 1.]
...
# Q_values
tf.Tensor(
[[26.245909 ]
[26.322636 ]
[26.578217 ]
...
모델 훈련은 간단합니다.
for episode in range(600):
obs = env.reset()
for step in range(200):
epsilon = max(1-episode/500, 0.01)
obs, reward, done, info = play_one_step(env, obs, epsilon)
if done:
break
if episode > 50: # episode가 50보다 크면 timestep 마다 계속 훈련
training_step(batch_size)
훈련 없이 에피소드를 50번 플레이하는 이유는 재생 버퍼가 채워질 시간을 주기 위해서입니다. 충분한 시간을 주지 않으면 다양성이 부족해집니다. 이로써 심층 Q-러닝 알고리즘을 구현했습니다.
[그림 18-10]
[그림 18-10]은 각 에피소드에서 에이전트가 얻은 총 보상을 보여줍니다. 300번의 에피소드 동안 전혀 발전이 없다가 갑자기 300번 쯤에서 성능(보상의 합)이 200까지 치솟습니다. 300에서 에피소드가 몇 개 지나면 배운 것을 완전히 잊어버리고 성능이 다시 25 아래로 떨어집니다. 이를 최악의 망각(catastrophic forgetting)이라고 부릅니다. 이는 모든 RL 알고리즘이 직면하는 큰 문제입니다.
환경의 한 부분에서 학습한 것이 다른 부분에서 앞서 학습한 것을 망가뜨릴 수 있습니다. 경험끼리 크게 연관되며 학습 환경이 계속 바뀌는 것은 경사 하강법에 잘 맞지 않습니다.
재생 버퍼의 크기를 늘리거나 학습률을 감소하는 것은 도움이 될 수 있습니다. 훈련에 잘 맞는 하이퍼파라미터(은닉층의 수, 층의 뉴런 개수 등)과 랜덤 시드의 조합을 찾을 수 있습니다.
NOTE_
강화 학습은 어렵기로 유명합니다. 대부분 훈련이 불안정하고 하이퍼파라미터 값과 랜덤 시드의 선택에 민감하기 때문입니다. (https://homl.info/rlhard)
구글은 강화 학습을 사용해 데이터 센터의 비용을 최적화합니다.
손실 그래프는 모델의 성능을 재는 데 좋지 않습니다. 손실이 내려가더라도 에이전트가 엉망으로 행동할 수 있습니다. 예를 들어 에이전트가 환경의 작은 지역에 갇힐 때 일어날 수 있습니다. DQN이 이 영역에 과대적합되기 시작할 것입니다.
반대로 손실이 올라가도 에이전트가 더 잘 수행할 수 있습니다. 예를 들어 DQN이 Q-value를 과소 평가 했다가 점차 예측을 올바르게 하면 보상이 증가하고 보다 더 잘 수행될 것입니다. 동시에 DQN이 타깃을 더 크게 설정해서 손실은 증가할 수 있습니다.
심층 Q-러닝의 변종
p.753
출처: 핸즈온 머신러닝 2판
사진 출처: 핸즈온 머신러닝 2판
'핸즈온 머신러닝 2판' 카테고리의 다른 글
19장 대규모 텐서플로 모델 훈련과 배포 (0) | 2022.02.13 |
---|---|
이진 오토인코더를 이용한 해싱 (0) | 2022.02.06 |
LSTM autoencoder (0) | 2022.02.04 |
17장 오토인코더와 GAN을 사용한 표현 학습과 생성적 학습 (2) | 2022.02.03 |
16장 RNN과 어텐션을 사용한 자연어 처리 (1) | 2022.01.26 |