개요
이전 포스팅에서는 인공신경망의 개념과 학습 방법에 대해서 알아봤습니다. 저희는 이 인공신경망을 이용한 코드를 작성하기 위해서 인공신경망을 구현해놓은 딥러닝 프레임워크를 사용하면 됩니다.
현재 가장 널리 쓰이는 딥러닝 프레임워크는 텐서플로 2.0입니다. 텐서플로 2.0 내부에는 인공신경망을 훨씬 더 직관적이고 효율적인 코드로 설계할 수 있게 도와주는 케라스 모듈이 포함돼 있습니다.
이 책에서는 텐서플로 2.1 버전과 케라스 모듈을 사용하여 코드를 작성하였습니다. 이번 포스팅에서는 새로운 그리드월드 예제에서 인공신경망을 이용하여 최적 정책을 학습하는 딥살사에 대해서 코드 예시와 함께 알아보도록 하겠습니다.
딥살사
새로운 그리드월드 예제는 아래와 같습니다. 이제 장애물인 초록색 삼각형 3개가 한 타임스템마다 한 칸씩 오른쪽으로 움직이다가 벽에 부딪힐 경우 반대 방향으로 움직이는 과정이 반복됩니다.
에이전트가 장애물을 만나면 -1의 보상을, 도착점인 파란색에 도달하면 +1의 보상을 준다는 것은 같습니다.
이전 그리드월드 예제와 가장 크게 달라진 점은 바로 에이전트가 학습하기 위해서 필요한 상태의 정의입니다.
이제 3개의 장애물이 계속 움직이기 때문에 장애물의 속도나 상대적 위치를 정의해야 장애물을 피해 목표점까지 가는 학습을 할 수 있을 것입니다.
정리하면 이 문제에서 정의하는 상태는 다음과 같습니다.
1. 에이전트에 대한 도착지점의 상대 위치 x, y
2. 도착지점의 라벨
3. 에이전트에 대한 장애물의 상대 위치 x, y
4. 장애물의 라벨
5. 장애물의 속도
장애물이 총 3개이므로 3, 4, 5 상태는 3배를 해주면 12개이고 1, 2 상태를 합치면 상태는 총 15개의 원소를 가지게 됩니다.
이렇게 상태가 많아졌기 때문에 저희가 배웠던 살사나 큐러닝으로는 이 예제를 풀기 어렵습니다. 따라서 저희는 이 예제를 풀기 위해서 딥살사 알고리즘을 사용합니다.
이름을 보면 알겠지만 딥살사(Deep Learning + SARSA)는 저희가 배웠던 살사 알고리즘에서 인공신경망을 사용하여 발전시킨 것입니다.
살사에서는 큐함수를 업데이트시키면서 테이블 형태로 저장했다면 딥살사는 큐함수를 인공신경망으로 근사하여 경사하강법으로 인공신경망을 업데이트시키는 것이죠.
오차함수로 구한 오차와, 오차의 편미분값으로 계산한 오차 기여도를 곱한 만큼 업데이트 하는 것이 경사하강법이기 때문에 오차함수에 대한 정의가 필요합니다.
업데이트 값 ∝ 오차 x 오차 기여도
이때 오차함수로는 지난 포스팅에서 언급한 MSE를 사용합니다. MSE의 식은 다음과 같았습니다.
오차 = $($정답 - 예측$)^2$
하지만 지난 포스팅에서 예로 든 지도학습과는 다르게 강화학습은 정답이 따로 존재하지 않습니다. 그렇다면 오차함수의 정답과 예측에는 어떤 값을 넣어야 할까요?
이는 살사의 큐함수 업데이트 식에서 찾을 수 있습니다. 살사의 큐함수 업데이트 식은 다음과 같았습니다.
$$Q(S_t,A_t) \leftarrow Q(S_t,A_t) + α(R_{t+1} + γQ(S_{t+1},A_{t+1})) - Q(S_t,A_t))$$
업데이트 과정을 그림으로 표현하면 다음과 같죠.
즉 $R_{t+1} + γQ(S_{t+1},A_{t+1})$이 정답의 역할을 하고 $Q(S_t,A_t)$이 예측에 해당하는 것을 알 수 있습니다.
이를 이용해 오차함수 MSE 식을 완성시키면 다음과 같습니다.
MSE = $($정답 - 예측$)^2$ = $(R_{t+1} + γQ_θ(S_{t+1},A_{t+1})) - Q_θ(S_t,A_t))^2$
이때 $Q$가 아니라 $Q_θ$로 표기한 것은 $θ$를 매개변수로 가지는 인공신경망을 통해 표현한 큐함수라는 뜻입니다.
이 오차함수만 정의한다면 케라스를 이용하여 인공신경망을 업데이트할 수 있습니다. 그럼 이제 새로운 그리드월드 예제에 딥살사 알고리즘을 적용한 코드를 살펴봅시다.
딥살사 코드 설명
환경의 정보를 사용하기 위한 Env 클래스, 텐서플로 라이브러리를 사용하기 위해 tensorflow를 tf로 import 해줍니다.
또한 인공신경망의 층으로 완전 연결 층을 사용하기 위한 Dense와 모델을 업데이트하기 위한 옵티마이저로 Adam을 사용하기 위해서 이들 또한 케라스 모듈에서 import 해줍니다.
from environment import Env
import tensorflow as tf
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import Adam
<에이전트와 환경의 상호작용 코드>
# 현재 상태에 대한 행동 선택
action = agent.get_action(state)
# 선택한 행동으로 환경에서 한 타임스텝 진행 후 샘플 수집
next_state, reward, done = env.step(action)
next_state = np.reshape(next_state, [1, state_size])
next_action = agent.get_action(next_state)
# 샘플로 모델 학습
agent.train_model(state, action, reward, next_state,
next_action, done)
score += reward
state = next_state
딥살사는 살사 알고리즘에서 인공신경망을 사용하여 발전시킨 것이라고 했습니다. 따라서 환경과 상호작용하여 학습을 진행시키는 과정은 살사와 동일합니다.
즉 딥살사 또한 [현재 상태에서 행동을 선택 → 환경으로부터 보상과 다음 상태를 받음 → 다음 상태에서 다시 행동을 선택 → 지금까지 모인 샘플 $<S_t,A_t,R_{t+1},S_{t+1},A_{t+1}>$로 학습 진행] 까지의 과정이 동일합니다.
다른 점이라고 한다면 살사 알고리즘에서는 큐함수 테이블을 이용했다면 딥살사는 인공신경망을 사용한다는 점입니다.
딥살사의 인공신경망은 현재 그리드월드 예제에서 정의한 15개의 상태 원소가 입력으로 들어가 30개의 유닛을 가진 2개의 은닉층을 지나서 각 행동에 대한 큐함수를 출력으로 내놓습니다. 출력층의 유닛은 상, 하, 좌, 우, 제자리의 큐함수로 5개의 유닛이 존재하겠죠.
이를 그림으로 나타내면 다음과 같습니다.
<인공신경망 모델 정의>
# 딥살사 인공신경망
class DeepSARSA(tf.keras.Model): # tf.keras.Model을 상속
def __init__(self, action_size):
super(DeepSARSA, self).__init__()
self.fc1 = Dense(30, activation='relu') #은닉층(완전 연결 층), 유닛 = 30, 활성함수 = relu
self.fc2 = Dense(30, activation='relu') #은닉층(완전 연결 층), 유닛 = 30, 활성함수 = relu
self.fc_out = Dense(action_size) #출력층(완전 연결 층), 유닛 = 5(action_size), 활성함수 = 선형함수
def call(self, x):
x = self.fc1(x)
x = self.fc2(x)
q = self.fc_out(x)
return q
모델을 정의할 때는 tf.keras.Model을 상속받는 클래스를 선언한 후에 모델의 입력층과 은닉층, 출력층을 정의하고, call 함수에 입력으로 들어온 데이터가 층들을 어떻게 지날 것이지 작성해야 합니다.
은닉층의 활성함수는 ReLU 함수이고 출력층의 활성함수는 선형 함수로 선언합니다. 출력이 각 행동에 대한 큐함수이고 큐함수는 0과 1 사이의 값이 아니므로 출력층의 활성함수는 선형 함수로 선언해야합니다.
<행동 선택>
# 그리드월드 예제에서의 딥살사 에이전트
class DeepSARSAgent:
def __init__(self, state_size, action_size):
# 상태의 크기와 행동의 크기 정의
self.state_size = state_size
self.action_size = action_size
# 딥살사 하이퍼 파라메터
self.discount_factor = 0.99
self.learning_rate = 0.001
self.epsilon = 1.
self.epsilon_decay = .9999
self.epsilon_min = 0.01
self.model = DeepSARSA(self.action_size)
self.optimizer = Adam(lr=self.learning_rate)
# 입실론 탐욕 정책으로 행동 선택
def get_action(self, state):
if np.random.rand() <= self.epsilon:
return random.randrange(self.action_size)
else:
q_values = self.model(state)
return np.argmax(q_values[0])
행동 선택도 살사와 마찬가지로 ε-탐욕 정책을 사용합니다.
0~1 사이의 랜덤한 값이 설정한 epsilon 값보다 작다면 무작위로 행동을 선택하고, epsilon 값보다 크다면 큐함수의 최댓값에 해당하는 행동을 선택합니다.
self.model = DeepSARSA(self.action_size) 이므로 model(state)의 출력은 현재 상태에서 각 행동에 대한 큐함수 값입니다.
케라스에서는 기본적으로 미니배치의 형태로 입력과 출력이 나온다고 가정하기 때문에 model(state)의 출력은 [[ ], [ ], [ ], [ ], [ ]]와 같은 형태로 나오기 때문에 q_values에 [0]을 붙인 후 최댓값을 반환합니다.
- 미니배치란 인공지능이 학습을 할 때 거대한 양의 데이터를 한꺼번에 학습하지 않고 단위 별로 쪼개서 하는데, 이 때의 쪼개진 데이터의 뭉치를 말합니다.
<인공신경망 학습>
# <s, a, r, s', a'>의 샘플로부터 모델 업데이트
def train_model(self, state, action, reward, next_state, next_action, done):
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
# 학습 파라메터
model_params = self.model.trainable_variables # 모델 안의 미분 가능한 모든 변수
with tf.GradientTape() as tape: # with문 안에서 일어난 계산이 tape에 기록됨
tape.watch(model_params)
predict = self.model(state)[0]
one_hot_action = tf.one_hot([action], self.action_size)
predict = tf.reduce_sum(one_hot_action * predict, axis=1)
# done = True 일 경우 에피소드가 끝나서 다음 상태가 없음
next_q = self.model(next_state)[0][next_action]
target = reward + (1 - done) * self.discount_factor * next_q
# MSE 오류 함수 계산
loss = tf.reduce_mean(tf.square(target - predict))
# 오류함수를 줄이는 방향으로 모델 업데이트
grads = tape.gradient(loss, model_params) # model_params에 대해서 오류함수를 미분
self.optimizer.apply_gradients(zip(grads, model_params)) # 그레이디언트를 통해 모델 업데이트
살사나 큐러닝과 다르게 딥살사에서는 ε-탐욕 정책의 epsilon 값을 시간에 따라 감소시키도록 하였습니다.
에이전트가 초반에는 에이전트가 탐험을 자주 하다가 이후 학습이 충분히 이뤄진 후에는 예측대로 움직일 수 있게 만든 것이죠.
이제 환경으로부터 받은 샘플을 이용해서 오류함수가 최소가 되도록 인공신경망을 업데이트해야 합니다. 인공신경망은 오류함수로 오류를 계산한 뒤 오류에 대한 편미분값을 이용하여 경사하강법으로 업데이트되었습니다.
업데이트 과정을 코드로 어떻게 표현했는지 살펴봅시다.
편미분을 해야하기 때문에 모델 안의 미분 가능한 모든 변수를 model_params에 저장하였습니다.
tf.GradientTape() 함수는 모델에 입력부터 출력까지의 계산 과정을 저장하는 함수인데 이를 with as를 통해 tape라는 이름으로 선언하면 with문 안에서 일어난 계산이 tape에 기록됩니다.
이후 tape를 통해서 model_params에 대해서 오류함수를 미분하면 편미분값을 구할 수 있습니다. 이후 편미분 값을 이용해서 경사하강법으로 모델을 업데이트합니다.
이 예제에서는 경사하강법 중 하나인 케라스의 Adam이라는 옵티마이저로 업데이트를 하였습니다.
tf.one_hot 함수를 이용하면 각 행동에 대한 큐함수를 저장한 predict에서 실제로 한 행동만 1의 값을 가지는 벡터를 만듭니다.
즉 [q_좌, q_우, q_상, q_하, q_제자리]이 predict에 저장되어있고 에이전트가 실제로 한 행동이 '우'라면 one_hot_action은 [0, 1, 0, 0, 0]의 벡터가 됩니다.
이후 predict과 one_hot_action을 곱하면 실제 행동에 대한 모델의 출력만 남길 수 있습니다. 실제 행동에 대한 예측 값을 구했다면 이제는 목표를 계산하고 아래의 MSE 식에 따라서 오류함수를 계산합니다.
MSE = $($정답 - 예측$)^2$ = $(R_{t+1} + γQ_θ(S_{t+1},A_{t+1})) - Q_θ(S_t,A_t))^2$
딥살사의 전체 코드는 아래 링크에서 확인할 수 있습니다.
https://github.com/rlcode/reinforcement-learning-kr-v2
딥살사의 실행 및 결과
딥살사를 실행시키면 에이전트가 목표에 도달할 때마다 터미널 창에는 다음과 같이 에피소드, 점수, 앱실론 값이 출력됩니다.
앱실론 값이 에피소드가 지남에 따라 계속해서 감소하는 것을 확인할 수 있습니다.
에피소드 별 점수를 pylab을 이용하여 그래프로도 출력되게 하였는데 250 에피소드를 진행했을 때의 그래프는 다음과 같습니다.
score는 장애물에 부딪히면 (-1)점 목표에 도달하면 (+1)점을 주기 때문에 최댓값이 1점이며 에이전트가 학습할수록 점수가 1점에 수렴하는 것을 그래프로 확인할 수 있습니다.
만약 앱실론의 감소율을 높인다면 매 에피소드 별 에이전트가 탐험할 확률이 감소하여 더 빠르게 학습할 수 있지만 자칫하면 잘못된 학습을 할 수도 있기 때문에 앱실론을 어느 정도의 속도로 감소시킬지 정하는 것은 중요합니다.
지금까지 저희가 배운 강화학습 알고리즘은 가치함수를 기반으로 행동을 선택하고 가치함수를 업데이트하면서 학습을 하는 가치 기반 강화학습이었습니다.
하지만 가치함수를 토대로 행동을 선택하지 않고 상태에 따라 바로 행동을 선택하면서 학습할 수 있는데, 이를 정책 기반 강화학습이라고 합니다.
그리고 이 정책 기반 강화학습의 대표적인 예가 REINFORCE 알고리즘입니다. 다음 포스팅에서는 정책 기반 강화학습과 REINFORCE 알고리즘을 코드 예시와 함께 살펴보겠습니다. 읽어주셔서 감사합니다~!!
http://www.yes24.com/Product/Goods/44136413
※ 이 글은 위의 책 내용을 바탕으로 작성한 글입니다.
'강화학습 > 파이썬과 케라스로 배우는 강화학습(스터디)' 카테고리의 다른 글
[강화학습] 12 - DQN 알고리즘(Cartpole) (0) | 2023.01.11 |
---|---|
[강화학습] 11 - REINFORCE 알고리즘 (1) | 2023.01.08 |
[강화학습] 09 - 인공신경망 (0) | 2022.12.30 |
[강화학습] 08 - 큐러닝(QLearning) (2) | 2022.12.28 |
[강화학습] 07 - 살사(SARSA) (4) | 2022.12.27 |