강화학습/파이썬과 케라스로 배우는 강화학습(스터디)

[강화학습] 16 - Breakout DQN

고집호랑이 2023. 1. 29. 07:30

개요 

앞의 카트폴 예제는 화면으로 학습할 필요가 없었기 때문에 DQN 알고리즘에서 컨볼루션 신경망이 아닌 간단한 인공신경망을 사용했습니다.

 

브레이크아웃에서의 DQN 알고리즘은 카트폴에서의 DQN 알고리즘처럼 리플레이 메모리와 타깃신경망이 사용되면서 컨볼루션 신경망이 사용되기 때문에 추가로 알아야 할 점이 있습니다.

 

알아야 할 추가 사항

브레이크아웃 게임 이미지는 아래 그림과 같이 색상 정보를 포함하여 210 × 160 × 3(RGB)의 크기를 가집니다.

 

브레이크아웃 입력 이미지

 

하지만 학습 자체에는 현재 점수, 목숨 개수와 색상 정보는 필요가 없습니다. 따라서 계산량을 줄여 학습 속도를 높이기 위해서 아래 그림처럼 이미지를 흑백으로 만들고 불필요한 부분을 잘라 84 × 84 × 1의 크기로 만들어줍니다. 

 

전처리 과정

 

이 과정을 전처리라고 합니다. 하지만 이렇게 전처리 과정을 거친 하나의 이미지만을 입력으로 넣으면 공이 위로 가고 있는지 아래로 떨어지고 있는지 등 정확한 현재 상태를 알 수 없기 때문에 제대로 된 학습을 할 수 없습니다. 

 

따라서 공이 어느 방향으로 움직이는지에 대한 정보를 얻기 위해서 4개의 연속된 이미지를 입력으로 받아들입니다.

 

이때 연속된 이미지가 비슷하여 공의 움직임에 차이가 없을 수 있으므로 프레임 스킵을 이용하여 4개 중 3개의 화면은 건너뛰고 나머지 하나의 화면을 실제로 사용합니다. 프레임 스킵은 오픈에이아이 짐에서 제공하는 브레이크아웃 환경이 자동으로 해줍니다. 

 

프레임 스킵

 

이렇게 스킵된 화면을 제외한 연속된 4개의 화면이 모델의 입력이 되고 이를 히스토리라고 합니다. 전처리 과정을 거쳐 84 × 84 × 1의 크기를 가진 화면 4개가 입력으로 들어가니 히스토리는 [84, 84, 4]의 크기를 가지게 되겠죠.

 

이것들 이외에는 카트풀에서의 DQN 알고리즘과 동일합니다. 리플레이 메모리를 사용하여 미니 배치로 모델을 업데이트시키고, 타깃신경망을 따로 만들어 오류함수의 타깃에 해당하는 값을 타깃 신경망의 출력으로 구합니다.

 

DQN 코드 설명

모델이 학습하는 전체적인 과정은 카트폴에서의 DQN 알고리즘과 동일합니다. 

 

에이전트는 <상태에 따른 행동 선택 → 환경으로부터 보상과 다음 상태를 받음 → 샘플(s, a, r, s')을 리플레이 메모리에 저장 → 리플레이 메모리에서 무작위 추출한 32개의 샘플로 학습 → 10000 타임스텝마다 타깃신경망 업데이트> 반복하면서 학습하게 됩니다. 

 

브레이크아웃의 컨볼루션 신경망에 대해서는 이전 포스팅에서 살펴봤으니 생략하고 먼저 컨볼루션 신경망의 입력으로 들어갈 화면을 전처리해줘야 합니다.

 

<화면 전처리>

# 학습속도를 높이기 위해 흑백화면으로 전처리
def pre_processing(observe):
    processed_observe = np.uint8(
        resize(rgb2gray(observe), (84, 84), mode='constant') * 255)
    return processed_observe

환경으로부터 [210, 160, 3]의 크기를 가진 하나의 게임 화면 observe를 받아 불필요한 부분과 색상을 제거하여 [84, 84, 1]의 크기로 만들어줍니다.  

 

<히스토리 생성>

# 프레임을 전처리 한 후 4개의 상태를 쌓아서 입력값으로 사용.
state = pre_processing(observe)
history = np.stack((state, state, state, state), axis=2)
history = np.reshape([history], (1, 84, 84, 4))

#--------------생략--------------

# 각 타임스텝마다 상태 전처리
next_state = pre_processing(observe)
next_state = np.reshape([next_state], (1, 84, 84, 1))
next_history = np.append(next_state, history[:, :, :, :3], axis=3)

전처리된 화면을 state에 저장하고 첫 상태에서는 4개의 화면을 모을 수 없기 때문에 state를 4개 복사하여 하나의 history로 만듭니다.

 

이후 에이전트가 행동을 하고 환경으로부터 다음 게임 화면 observe를 받아 전처리까지 해줬다면, history에 오래된 state는 버리고 전처리를 거친 새로운 next_statenp.append를 이용해 넣어줍니다.

 

<행동 선택>

# 입실론 탐욕 정책으로 행동 선택
def get_action(self, history):
    history = np.float32(history / 255.0)
    if np.random.rand() <= self.epsilon:
        return random.randrange(self.action_size)
    else:
        q_value = self.model(history)
        return np.argmax(q_value[0])

DQN 알고리즘은 가치 기반 강화학습 중 하나로 출력값이 큐함수이기 때문에 ε-탐욕정책을 통해서 행동을 선택합니다.

 

원래 픽셀 값 0~255 사이의 값을 0~1로 만들어 컨볼루션 신경망의 입력으로 넣어 큐함수를 얻은 후, 랜덤으로 선택한 값이 ε보다 크다면 큐함수의 최댓값에 해당하는 행동을 선택합니다.  

 

ε값은 학습을 시작할 때마다 감소합니다.

 

<남은 목숨 판별 및 리플레이 메모리에 저장>

# 선택한 행동으로 환경에서 한 타임스텝 진행
observe, reward, done, info = env.step(real_action)

#----------------생략----------------

if start_life > info['ale.lives']: # 현재 에이전트가 목숨을 잃었는지 판별
    dead = True
    start_life = info['ale.lives'] # 남음 목숨 업데이트
    
#----------------생략----------------

# 샘플 <s, a, r, s'>을 리플레이 메모리에 저장 후 학습
agent.append_sample(history, action, reward, next_history, dead)

행동을 선택했으면 에이전트는 환경으로부터 다음 게임 화면(observe), 보상(reward), 에피소드가 끝났는지에 대한 정보(done), 남은 목숨에 대한 정보(info)를 받습니다.

 

Info 정보 안에 포함된 남은 목숨으로 현재 에이전트가 목숨을 잃었는지를 나타내는 dead를 판별한 후 현재 히스토리(history), 행동(action), 보상(reward), 다음 히스토리(next_history), 목숨을 잃었는지의 정보(dead)를 리플레이 메모리에 하나의 샘플로서 저장합니다. 

 

<학습 시작 조건과 타깃 신경망 업데이트>

# 리플레이 메모리 크기가 정해놓은 수치에 도달한 시점부터 모델 학습 시작
if len(agent.memory) >= agent.train_start:
    agent.train_model()
    # 일정 시간마다 타겟모델을 모델의 가중치로 업데이트
    if global_step % agent.update_target_rate == 0:
        agent.update_target_model()

모델은 리플레이 메모리의 사이즈가 미리 정해놓은 agent.train_start == 50000보다 클 때부터 즉 50000 스텝 이후부터 학습을 시작하고 타깃신경망은 agent.update_target_rate == 10000에 따라서 10000 스텝마다 업데이트를 진행합니다.

 

<컨볼루션 신경망 학습>

# 리플레이 메모리에서 무작위로 추출한 배치로 모델 학습
def train_model(self):
    if self.epsilon > self.epsilon_end:
        self.epsilon -= self.epsilon_decay_step

    # 메모리에서 배치 크기만큼 무작위로 샘플 추출
    batch = random.sample(self.memory, self.batch_size)

    history = np.array([sample[0][0] / 255. for sample in batch],
                       dtype=np.float32)
    actions = np.array([sample[1] for sample in batch])
    rewards = np.array([sample[2] for sample in batch])
    next_history = np.array([sample[3][0] / 255. for sample in batch],
                            dtype=np.float32)
    dones = np.array([sample[4] for sample in batch])

    # 학습 파라메터
    model_params = self.model.trainable_variables
    with tf.GradientTape() as tape:
        # 현재 상태에 대한 모델의 큐함수
        predicts = self.model(history)
        one_hot_action = tf.one_hot(actions, self.action_size)
        predicts = tf.reduce_sum(one_hot_action * predicts, axis=1)

        # 다음 상태에 대한 타깃 모델의 큐함수
        target_predicts = self.target_model(next_history)

        # 벨만 최적 방정식을 구성하기 위한 타깃과 큐함수의 최대 값 계산
        max_q = np.amax(target_predicts, axis=1)
        targets = rewards + (1 - dones) * self.discount_factor * max_q

        # 후버로스 계산
        error = tf.abs(targets - predicts)
        quadratic_part = tf.clip_by_value(error, 0.0, 1.0)
        linear_part = error - quadratic_part
        loss = tf.reduce_mean(0.5 * tf.square(quadratic_part) + linear_part)

        self.avg_loss += loss.numpy()

    # 오류함수를 줄이는 방향으로 모델 업데이트
    grads = tape.gradient(loss, model_params)
    self.optimizer.apply_gradients(zip(grads, model_params))

학습하는 과정은 카트폴의 DQN 알고리즘과 동일하게 리플레이 메모리에서 미니배치 크기(여기선 self.batch_size == 32)만큼의 샘플을 무작위로 추출하여 모델을 학습시켜 줍니다. 

 

또한 타깃 신경망을 이용해서 타깃 값을 구하는 점도 동일하죠. 다른 점은 총 2가지가 있는데, 브레이크아웃의 DQN 알고리즘에서는 오류함수로 MSE가 아닌 후버로스를 사용한다는 점과 옵티마이저에 그레이디언트 클리핑을 사용한다는 점이죠.

 

출처 -&nbsp;https://blog.naver.com/PostView.naver?blogId=jws2218&logNo=221890882708&parentCategoryNo=&categoryNo=12&viewDate=&isShowPopularPosts=true&from=search

 

후버로스란 위의 초록색 그래프로 x 축은 정답과 예측 사이의 에러이며 y축은 후버로스 함수를 통과했을 때의 오류함숫값입니다. 

 

에러가 <-1에서 1 사이의 구간>에서는 2차 함수이고 <그 밖의 구간>에서는 1차 함수인 오류함수입니다. MSE 오류함수는 그래프로 표현하면 위의 파란색 그래프로 모든 구간에서 2차 함수로 표현됩니다. 

 

따라서 MSE 오류함수를 사용하면 큰 에러 값에 대해 업데이트 값이 너무 커져 학습에 방해가 될 수 있습니다. 이를 방지하기 위해서 호버로스 오류함수를 사용하는 것입니다. 

 

옵티마이저에 그레이디언트 클리핑을 사용하는 것도 같은 목적입니다. 

self.optimizer = Adam(self.learning_rate, clipnorm=10.)

위와 같이 clipnorm = 10을 추가해 그레이디언트의 크기가 10을 넘어가지 못하게 하여 모델의 가중치가 너무 급격하게 변화하는 것을 막습니다.

 

<오류 방지>

# 랜덤으로 뽑힌 값 만큼의 프레임동안 움직이지 않음
for _ in range(random.randint(1, agent.no_op_steps)):
    observe, _, _, _ = env.step(1)

브레이크아웃을 플레이해 보면 초반에 공이 날아올 때 왼쪽 아니만 오른쪽 구석으로만 주로 날아오는 것을 확인할 수 있습니다. 

 

이에 따라서 에이전트가 무작정 구석에 붙는 오류가 생길 수 있습니다. 이를 막기 위해서 에이전트는 초반에 1부터 no_op_steps(== 30) 사이 중 무작위 정수를 골라 그 기간 동안 에이전트가 아무것도 하지 않게 만들어줍니다. 

 

브레이크아웃 DQN 실행 및 결과

브레이크아웃을 DQN으로 실제로 학습시키기에는 시간이 너무 오래 걸리므로 책에 있는 결과를 이용하도록 하겠습니다. 

 

에피소드 별 총 보상 그래프

 

다음은 에이전트가 학습하면서 에피소드에 따른 에이전트가 얻은 총 보상 그래프를 텐서보드를 이용해 나타낸 것입니다. 

 

에피소드가 진행됨에 따라 에이전트가 얻는 보상이 커지면서 점수의 위아래 폭이 넓어지는 것을 확인할 수 있습니다. 

 

터널을 뚫는 에이전트

 

5000 에피소드를 학습한 모델을 이용해서 브레이크아웃을 실행한 결과 에이전트가 한쪽 터널을 뚫어 여러 개의 벽돌을 깨는 전략까지 학습했음을 알 수 있습니다. 

 

이런 DQN 알고리즘도 학습 속도가 느리다는 단점과 오프폴리시 강화학습을 사용해야 한다는 단점이 있습니다. 

 

이를 보완하기 위해서 DQN 알고리즘과 다른 방식으로 학습하도록 고안해 낸 것이 A3C 알고리즘입니다. 다음 포스팅에서는 이 A3C 알고리즘에 대해서 설명하도록 하겠습니다. 읽어주셔서 감사합니다~!

 

 

 

http://www.yes24.com/Product/Goods/44136413

 

파이썬과 케라스로 배우는 강화학습 - YES24

“강화학습을 쉽게 이해하고 코드로 구현하기”강화학습의 기초부터 최근 알고리즘까지 친절하게 설명한다!‘알파고’로부터 받은 신선한 충격으로 많은 사람들이 강화학습에 관심을 가지기

www.yes24.com

※ 이 글은 위의 책 내용을 바탕으로 작성한 글입니다.